├── src ├── app │ ├── robots.txt │ ├── [lang] │ │ ├── (provide) │ │ │ ├── privacy-policy │ │ │ │ └── page.mdx │ │ │ ├── terms-of-services │ │ │ │ └── page.mdx │ │ │ └── layout.tsx │ │ ├── globals.css │ │ ├── (main) │ │ │ ├── about │ │ │ │ ├── layout.tsx │ │ │ │ └── page.tsx │ │ │ ├── loading.tsx │ │ │ ├── layout.tsx │ │ │ ├── explore │ │ │ │ ├── page.tsx │ │ │ │ └── images-form.tsx │ │ │ ├── contact │ │ │ │ ├── page.tsx │ │ │ │ └── contact-form.tsx │ │ │ ├── lemon │ │ │ │ ├── button.tsx │ │ │ │ └── page.tsx │ │ │ ├── blog │ │ │ │ ├── page.tsx │ │ │ │ └── article │ │ │ │ │ └── [slug] │ │ │ │ │ └── page.tsx │ │ │ ├── error.tsx │ │ │ ├── paddle │ │ │ │ ├── button.tsx │ │ │ │ └── page.tsx │ │ │ ├── page.tsx │ │ │ └── upload-form.tsx │ │ ├── (sign) │ │ │ ├── loading.tsx │ │ │ ├── sign-in │ │ │ │ └── [[...sign-in]] │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── login.tsx │ │ │ ├── sign-up │ │ │ │ └── [[...sign-up]] │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── logout.tsx │ │ │ └── layout.tsx │ │ └── layout.tsx │ ├── api │ │ ├── user │ │ │ └── route.ts │ │ └── home │ │ │ └── route.ts │ └── sitemap.xml ├── actions │ ├── explore.ts │ ├── contact.ts │ └── posts.ts ├── mdx-components.tsx ├── types │ └── responses.ts ├── lib │ ├── lemon-squeezy-client.ts │ ├── vercel-kv-client.ts │ ├── paddle-client.ts │ ├── db-proxy.ts │ ├── definitions.ts │ ├── aws-client-s3.ts │ ├── postgres-client.ts │ ├── gemini-client.ts │ └── baidu-client.ts ├── components │ ├── googletag.tsx │ ├── policy.tsx │ ├── ko-fi.tsx │ ├── nav-context.tsx │ ├── Tooltip.tsx │ ├── mdx-page-remote.tsx │ ├── theme-context.tsx │ ├── theme-icon.tsx │ ├── mdx-page-client.tsx │ ├── loading-skeleton.tsx │ ├── loading-from.tsx │ ├── popup-modal.tsx │ ├── sharebutton.tsx │ ├── clipboard.tsx │ ├── prose-head.tsx │ ├── mdx-cards.tsx │ ├── select-dropdown.tsx │ ├── locale-switcher.tsx │ ├── alerts.tsx │ ├── dialogs.tsx │ ├── footer-logo.tsx │ ├── imagepicker.tsx │ ├── footer-social.tsx │ └── navbar-sticky.tsx ├── i18n-config.ts ├── config │ └── lemonsqueezy.ts ├── middleware.ts ├── scripts │ └── ShiftAndByteBufferMatcher.ts ├── dictionaries.ts └── dictionaries │ └── en.json ├── .eslintrc.json ├── public ├── assets │ ├── avatar.png │ ├── screenshot-1.png │ ├── screenshot-2.png │ ├── screenshot-3.png │ └── language │ │ ├── fr.svg │ │ ├── de.svg │ │ ├── en.svg │ │ ├── ja.svg │ │ ├── zh.svg │ │ └── ko.svg └── posts │ ├── de │ └── prose-v1.mdx │ ├── en │ └── prose-v1.mdx │ ├── es │ └── prose-v1.mdx │ ├── fr │ └── prose-v1.mdx │ ├── ja │ └── prose-v1.mdx │ ├── ko │ └── prose-v1.mdx │ └── zh │ └── prose-v1.mdx ├── next.config.mjs ├── postcss.config.js ├── .gitignore ├── tailwind.config.ts ├── tsconfig.json ├── next.config.js ├── package.json ├── README_zh.md └── README.md /src/app/robots.txt: -------------------------------------------------------------------------------- 1 | User-Agent: * 2 | Allow: * -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /src/app/[lang]/(provide)/privacy-policy/page.mdx: -------------------------------------------------------------------------------- 1 | # ImageAI.QA Privacy Policy 2 | -------------------------------------------------------------------------------- /src/app/[lang]/(provide)/terms-of-services/page.mdx: -------------------------------------------------------------------------------- 1 | # Terms and Conditions for ImageAI.QA -------------------------------------------------------------------------------- /src/app/[lang]/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | -------------------------------------------------------------------------------- /public/assets/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShurshanX/AI-Image-Description/HEAD/public/assets/avatar.png -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/assets/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShurshanX/AI-Image-Description/HEAD/public/assets/screenshot-1.png -------------------------------------------------------------------------------- /public/assets/screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShurshanX/AI-Image-Description/HEAD/public/assets/screenshot-2.png -------------------------------------------------------------------------------- /public/assets/screenshot-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShurshanX/AI-Image-Description/HEAD/public/assets/screenshot-3.png -------------------------------------------------------------------------------- /src/actions/explore.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | export interface IImage { 4 | keys?: string; 5 | imgurl: string; 6 | description: string; 7 | } -------------------------------------------------------------------------------- /src/app/[lang]/(main)/about/layout.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | export default function Layout({ children }: { children: React.ReactNode }) { 5 | return ( 6 | <>{children} 7 | ); 8 | } -------------------------------------------------------------------------------- /src/app/api/user/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | 3 | export async function POST(request: NextRequest) { 4 | 5 | return NextResponse.json(0,{ status: 200 }); 6 | } -------------------------------------------------------------------------------- /src/mdx-components.tsx: -------------------------------------------------------------------------------- 1 | import type { MDXComponents } from 'mdx/types' 2 | 3 | export function useMDXComponents(components: MDXComponents): MDXComponents { 4 | return { 5 | ...components, 6 | } 7 | } -------------------------------------------------------------------------------- /src/app/[lang]/(main)/loading.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { LoadingFull } from "@/components/loading-skeleton"; 3 | export default function Loading() { 4 | // You can add any UI inside Loading, including a Skeleton. 5 | return ; 6 | } -------------------------------------------------------------------------------- /src/app/[lang]/(sign)/loading.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { LoadingFull } from "@/components/loading-skeleton"; 3 | export default function Loading() { 4 | // You can add any UI inside Loading, including a Skeleton. 5 | return ; 6 | } -------------------------------------------------------------------------------- /public/assets/language/fr.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/assets/language/de.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/types/responses.ts: -------------------------------------------------------------------------------- 1 | export declare interface ImageDescriptionResponse { 2 | error_code?: number; 3 | description?: string; 4 | error_msg?: string; 5 | } 6 | 7 | export declare interface Response{ 8 | error_code?: number; 9 | result?: string; 10 | error_msg?: string; 11 | } -------------------------------------------------------------------------------- /src/lib/lemon-squeezy-client.ts: -------------------------------------------------------------------------------- 1 | import 'server-only' 2 | import {getOrder} from "@lemonsqueezy/lemonsqueezy.js"; 3 | import { configureLemonSqueezy } from "@/config/lemonsqueezy"; 4 | 5 | export async function getOrderDetails(orderId: string) { 6 | 7 | configureLemonSqueezy(); 8 | const order = await getOrder(orderId); 9 | return order; 10 | 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/app/[lang]/(main)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { type Locale } from "@/i18n-config"; 2 | 3 | export default async function Layout({ 4 | children, params 5 | }: Readonly<{ 6 | children: React.ReactNode, 7 | params: { lang: Locale } 8 | }>) { 9 | 10 | return ( 11 |
12 | {children} 13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /public/assets/language/en.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/[lang]/(sign)/sign-in/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { SignIn } from "@clerk/nextjs" 3 | import { Locale } from "@/i18n-config"; 4 | import Policy from "@/components/policy"; 5 | import Login from "./login"; 6 | export default function Page({ params: { lang } }: { params: { lang: Locale } }) { 7 | 8 | return ( 9 |
10 | 11 | 12 |
13 | ) 14 | } -------------------------------------------------------------------------------- /src/app/[lang]/(sign)/sign-up/[[...sign-up]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUp } from "@clerk/nextjs" 2 | import { Locale } from "@/i18n-config"; 3 | import Policy from "@/components/policy"; 4 | import Logout from "./logout"; 5 | export default function Page({ params: { lang } }: { params: { lang: Locale } }) { 6 | 7 | return ( 8 |
9 | 10 | 11 |
12 | ) 13 | } -------------------------------------------------------------------------------- /src/lib/vercel-kv-client.ts: -------------------------------------------------------------------------------- 1 | import 'server-only' 2 | import { kv } from '@vercel/kv'; 3 | 4 | export function setKeyWithExpiry(key: string, value: string, expirySeconds: number) { 5 | const expiryTimestamp = expirySeconds * 1000; 6 | 7 | //ex seconds px milliseconds 8 | kv.set(key, value, { px: expiryTimestamp,nx: true }); 9 | } 10 | 11 | 12 | export function getKey(key: string) { 13 | return kv.get(key); 14 | } 15 | 16 | export function deleteKey(key: string) { 17 | kv.del(key); 18 | } 19 | -------------------------------------------------------------------------------- /src/lib/paddle-client.ts: -------------------------------------------------------------------------------- 1 | import 'server-only' 2 | import { Environment, Paddle } from '@paddle/paddle-node-sdk' 3 | 4 | 5 | const paddle = new Paddle(String(process.env.PADDLE_API_KEY), { 6 | environment: process.env.PADDLE_API_ENV ? Environment.sandbox : Environment.production, // or Environment.sandbox for accessing sandbox API 7 | }) 8 | export async function getTransactionDetails(transactionId: string) { 9 | 10 | const transaction = await paddle.transactions.get(transactionId); 11 | return transaction; 12 | 13 | } -------------------------------------------------------------------------------- /src/app/[lang]/(sign)/sign-up/[[...sign-up]]/logout.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { SignUp } from "@clerk/nextjs" 3 | import ThemeContext from "@/components/theme-context"; 4 | import { dark, experimental__simple } from "@clerk/themes"; 5 | import { useContext } from 'react'; 6 | 7 | export default function Logout({ lang }: {lang: string}) { 8 | const { theme } = useContext(ThemeContext); 9 | return ( 10 | 11 | ) 12 | } -------------------------------------------------------------------------------- /public/assets/language/ja.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/components/googletag.tsx: -------------------------------------------------------------------------------- 1 | 2 | import Script from "next/script" 3 | export default function GoogleTag() { 4 | return ( 5 | <> 6 | 7 | 8 | 13 | 14 | 15 | ) 16 | } -------------------------------------------------------------------------------- /.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.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | /src/test 12 | 13 | # next.js 14 | /.next/ 15 | /out/ 16 | 17 | # production 18 | /build 19 | 20 | # misc 21 | .DS_Store 22 | *.pem 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # local env files 30 | .env*.local 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /src/lib/db-proxy.ts: -------------------------------------------------------------------------------- 1 | import 'server-only' 2 | import { sql as vercelSql ,db as verceldb} from '@vercel/postgres'; 3 | import { sql as pgSql, db as pgdb,neonsql} from '@/lib/postgres-client'; 4 | import { PrismaClient } from '@prisma/client'; 5 | 6 | export const prisma = new PrismaClient(); 7 | 8 | export const sql = process.env.POSTGRES_CLIENT_TYPE === 'vercel' ? vercelSql : pgSql 9 | 10 | export const db = process.env.POSTGRES_CLIENT_TYPE === 'vercel' ? verceldb : pgdb 11 | 12 | export const edgesql = neonsql; 13 | 14 | type clientType = "vercel" | "pg" | undefined; 15 | -------------------------------------------------------------------------------- /src/app/[lang]/(sign)/layout.tsx: -------------------------------------------------------------------------------- 1 | import {type Locale } from "@/i18n-config"; 2 | import { LoadingFull } from "@/components/loading-skeleton"; 3 | import {ClerkLoaded, ClerkLoading } from '@clerk/nextjs' 4 | 5 | export default async function Layout({ 6 | children, params 7 | }: Readonly<{ 8 | children: React.ReactNode, 9 | params: { lang: Locale } 10 | }>) { 11 | return ( 12 | <> 13 | 14 | 15 | 16 | 17 | {children} 18 | 19 | 20 | ); 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/app/[lang]/(sign)/sign-in/[[...sign-in]]/login.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { useContext } from 'react'; 3 | import ThemeContext from "@/components/theme-context"; 4 | import { SignIn } from "@clerk/nextjs" 5 | import { Locale } from "@/i18n-config"; 6 | import { dark, experimental__simple } from "@clerk/themes"; 7 | 8 | 9 | export default function Login({lang}:{lang:Locale}) { 10 | const {theme} = useContext(ThemeContext); 11 | 12 | return ( 13 | 14 | ) 15 | } -------------------------------------------------------------------------------- /src/components/policy.tsx: -------------------------------------------------------------------------------- 1 | export default function Policy() { 2 | return ( 3 |
4 |

5 | By continuing, you confirm that you have read, understood,
6 | and agreed to our Privacy Policy and Terms of Use. 7 |

8 |
9 | ) 10 | } -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 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 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 13 | "gradient-conic": 14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 15 | }, 16 | }, 17 | }, 18 | plugins: [require('@tailwindcss/typography')], 19 | darkMode: 'selector', 20 | }; 21 | export default config; 22 | -------------------------------------------------------------------------------- /src/components/ko-fi.tsx: -------------------------------------------------------------------------------- 1 | import Script from "next/script"; 2 | export default function DonateButton() { 3 | return ( 4 | <> 5 | 6 | 14 | 15 | ); 16 | } -------------------------------------------------------------------------------- /src/components/nav-context.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { createContext, useState, Dispatch, SetStateAction } from 'react'; 3 | 4 | interface NavContextType { 5 | value: string; 6 | setValue: Dispatch>; 7 | } 8 | 9 | const NavContext = createContext({ 10 | value: '', 11 | setValue: () => { }, 12 | }); 13 | 14 | export const NavProvider = ({ children,credits }: { children: React.ReactNode,credits:string }) => { 15 | const [value, setValue] = useState(credits); 16 | 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | }; 23 | 24 | export default NavContext; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "src/app/mdx/blog/Test.mdx"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /src/components/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | 2 | interface TooltipProps { 3 | title: string; 4 | children: React.ReactNode; 5 | } 6 | 7 | 8 | export default function Tooltip({ title, children }: TooltipProps){ 9 | return ( 10 |
11 | {children} 12 |
13 | 14 | {title} 15 | 16 |
17 |
18 |
19 | ); 20 | }; -------------------------------------------------------------------------------- /public/assets/language/zh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/app/[lang]/(main)/explore/page.tsx: -------------------------------------------------------------------------------- 1 | import { ImagesForm } from './images-form'; 2 | import { type Locale } from "@/i18n-config"; 3 | import type { Metadata } from "next"; 4 | 5 | export const metadata: Metadata = { 6 | alternates: { canonical: "/en/explore?page=2" }, 7 | other: {next: "/en/explore?page=3",prev: "/en/explore?page=1" }, 8 | 9 | }; 10 | 11 | 12 | export default async function Page({ params, searchParams }: Readonly<{ params: { lang: Locale, }, searchParams: { page:number}}>) { 13 | 14 | const page = searchParams.page ? searchParams.page : 1; 15 | const images: any = []; 16 | 17 | 18 | return ( 19 | 20 |
21 | 22 | 23 | 24 |
25 | ) 26 | } -------------------------------------------------------------------------------- /src/lib/definitions.ts: -------------------------------------------------------------------------------- 1 | process.env.ZOD_LOCALE = "en"; 2 | 3 | import { z } from 'zod' 4 | 5 | export type User = { 6 | error_code: number; 7 | description: string; 8 | error_msg: string; 9 | }; 10 | export const ContactFormSchema = z.object({ 11 | first_name: z.string().min(2, { message: 'First Name must be at least 2 characters long.' }).trim(), 12 | last_name: z.string().min(2, { message: 'Last Name must be at least 2 characters long.' }).trim(), 13 | email: z.string().email({ message: 'Please enter a valid email.' }).trim(), 14 | message: z.string().min(5, { message: 'message must be at least 5 characters long.' }).trim() 15 | }) 16 | 17 | export type FormState = 18 | { 19 | errors?: { 20 | first_name?: string[] 21 | last_name?: string[] 22 | email?: string[] 23 | message?: string[] 24 | } 25 | message?: string 26 | success?: boolean 27 | }| undefined 28 | -------------------------------------------------------------------------------- /src/actions/contact.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | import { ContactFormSchema, FormState } from '@/lib/definitions' 3 | export async function addContactInfo(state: FormState,formData:FormData) { 4 | 5 | const validatedFields = ContactFormSchema.safeParse({ 6 | first_name: formData.get('first_name'), 7 | last_name: formData.get('last_name'), 8 | email: formData.get('email'), 9 | message: formData.get('message'), 10 | }) 11 | 12 | if (!validatedFields.success) { 13 | return { 14 | errors: validatedFields.error.flatten().fieldErrors, 15 | } 16 | } 17 | 18 | const { first_name, last_name, email, message } = validatedFields.data 19 | 20 | const name = first_name + " " + last_name; 21 | 22 | return { 23 | message: 'Your message has been submitted,We value your feedback as it empowers us to enhance our tools.', 24 | success:true 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const withMDX = require('@next/mdx')() 2 | 3 | /** @type {import('next').NextConfig} */ 4 | const nextConfig = { 5 | // Configure `pageExtensions` to include MDX files 6 | pageExtensions: ['js', 'jsx', 'mdx', 'ts', 'tsx'], 7 | // Optionally, add any other Next.js config below 8 | images: { 9 | remotePatterns: [ 10 | { 11 | protocol: 'https', 12 | hostname: 'www.baidu.com', 13 | port: '', 14 | pathname: '/**', 15 | }, 16 | ], 17 | }, 18 | webpack: (config) => { 19 | config.resolve.fallback = { 20 | ...config.resolve.fallback, 21 | fs: false, 22 | path: false, 23 | stream: false, 24 | crypto: false, 25 | net: false, 26 | dns: false, 27 | string_decoder: false, 28 | tls: false, 29 | 'pg-native': false, 30 | pg: false 31 | }; 32 | return config; 33 | } 34 | } 35 | 36 | module.exports = withMDX(nextConfig) -------------------------------------------------------------------------------- /src/i18n-config.ts: -------------------------------------------------------------------------------- 1 | 2 | export const languages = [ 3 | { locale: "en", name: "English" }, 4 | { locale: "ja", name: "日本語" }, 5 | { locale: "zh", name: "中文" }, 6 | { locale: "de", name: "Deutsch" }, 7 | { locale: "es", name: "Español" }, 8 | { locale: "fr", name: "Français" }, 9 | { locale: "ko", name: "한국어" }, 10 | ]as const; 11 | 12 | 13 | export const languagesKV = { 14 | en: "English", 15 | ja: "日本語", 16 | zh: "中文", 17 | de: "Deutsch", 18 | es: "Español", 19 | fr: "Français", 20 | ko: "한국어", 21 | }; 22 | 23 | export const i18n = { 24 | defaultLocale: "en", 25 | locales: languages.map(({ locale }) => locale), 26 | }as const; 27 | 28 | export type Locale = (typeof i18n)["locales"][number]; 29 | 30 | 31 | 32 | import { enUS,frFR,deDE,zhCN,esES,jaJP,koKR } from "@clerk/localizations"; 33 | 34 | export const localizations = { 35 | en: enUS, 36 | fr: frFR, 37 | de: deDE, 38 | zh: zhCN, 39 | es: esES, 40 | ja: jaJP, 41 | ko: koKR 42 | } -------------------------------------------------------------------------------- /src/app/[lang]/(main)/contact/page.tsx: -------------------------------------------------------------------------------- 1 | import { Locale } from "@/i18n-config"; 2 | import { getDictionary,Dictionary } from '@/dictionaries' 3 | import ContactForm from "./contact-form"; 4 | 5 | 6 | export default async function Page({ params: { lang } }: { params: { lang: Locale }} ) { 7 | const dictionary:Dictionary = await getDictionary(lang); 8 | 9 | return ( 10 |
11 | 12 | {/* Contact Container */} 13 |
14 | {/* Contact Title */} 15 |
16 |

{dictionary.contact.title}

17 |

{dictionary.contact.subtitle}

18 |
19 | 20 |
21 |
22 | ); 23 | } 24 | 25 | -------------------------------------------------------------------------------- /src/app/[lang]/(main)/lemon/button.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { useAuth } from "@clerk/nextjs"; 3 | import { useRouter } from "next/navigation"; 4 | interface Props{ 5 | btnlabel:string; 6 | variantId:string; 7 | lang:string; 8 | } 9 | export default function Button({btnlabel,variantId,lang}:Props){ 10 | 11 | const { isLoaded, userId} = useAuth(); 12 | const router = useRouter(); 13 | function onClickHandler() { 14 | if (!isLoaded || !userId) { 15 | router.push(`/${lang}/sign-in`); 16 | return; 17 | } 18 | window.open(`https://imageaiqa.lemonsqueezy.com/buy/${variantId}?checkout[custom][user_id]=${userId}`); 19 | } 20 | 21 | return ( 22 | 23 | ) 24 | } -------------------------------------------------------------------------------- /src/app/[lang]/(main)/blog/page.tsx: -------------------------------------------------------------------------------- 1 | import MdxCards from "@/components/mdx-cards" 2 | import { Locale } from "@/i18n-config"; 3 | import { getAllPostList } from "@/actions/posts"; 4 | import { getDictionary,Dictionary } from '@/dictionaries' 5 | 6 | export default async function Page({ params: { lang } }: { params: { lang: Locale } }) { 7 | 8 | const dictionary:Dictionary = await getDictionary(lang); 9 | 10 | const posts = await getAllPostList(lang); 11 | 12 | return ( 13 | <> 14 |
15 | 16 |
17 |
18 |

19 | {dictionary.blog.title} 20 |

21 | 22 | 23 |
24 |
25 | 26 | ) 27 | } 28 | 29 | -------------------------------------------------------------------------------- /src/config/lemonsqueezy.ts: -------------------------------------------------------------------------------- 1 | import { lemonSqueezySetup } from "@lemonsqueezy/lemonsqueezy.js"; 2 | 3 | /** 4 | * Ensures that required environment variables are set and sets up the Lemon 5 | * Squeezy JS SDK. Throws an error if any environment variables are missing or 6 | * if there's an error setting up the SDK. 7 | */ 8 | export function configureLemonSqueezy() { 9 | const requiredVars = [ 10 | "LEMONSQUEEZY_API_KEY", 11 | "LEMONSQUEEZY_STORE_ID", 12 | "LEMONSQUEEZY_WEBHOOK_SECRET", 13 | ]; 14 | 15 | const missingVars = requiredVars.filter((varName) => !process.env[varName]); 16 | 17 | if (missingVars.length > 0) { 18 | throw new Error( 19 | `Missing required LEMONSQUEEZY env variables: ${missingVars.join(", ")}. Please, set them in your .env file.`, 20 | ); 21 | } 22 | 23 | lemonSqueezySetup({ 24 | apiKey: process.env.LEMONSQUEEZY_API_KEY, 25 | onError: (error) => { 26 | // eslint-disable-next-line no-console -- allow logging 27 | console.error(error); 28 | throw new Error(`Lemon Squeezy API error: ${error.message}`); 29 | }, 30 | }); 31 | } -------------------------------------------------------------------------------- /src/app/[lang]/(main)/blog/article/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import { Locale } from "@/i18n-config"; 2 | import ProseHead from "@/components/prose-head"; 3 | import { getPost } from "@/actions/posts"; 4 | import { MDXRemote } from 'next-mdx-remote/rsc' 5 | import { getDictionary,Dictionary } from '@/dictionaries' 6 | import {RemoteMdxPage} from "@/components/mdx-page-remote"; 7 | export default async function Page({ params: { lang,slug } }: { params: { lang: Locale,slug:string } }) { 8 | 9 | const dictionary:Dictionary = await getDictionary(lang); 10 | const post = await getPost(slug,lang); 11 | 12 | return ( 13 | <> 14 |
15 |
16 |
17 | 18 | 19 |
20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /public/assets/language/ko.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/components/mdx-page-remote.tsx: -------------------------------------------------------------------------------- 1 | import { MDXRemote } from 'next-mdx-remote/rsc' 2 | 3 | 4 | const defaultComponents = { 5 | // h1: (props:any) => ( 6 | //

7 | // {props.children} 8 | //

9 | // ), 10 | // h2: (props:any) => ( 11 | //

12 | // {props.children} 13 | //

14 | // ), 15 | // h3: (props:any) => ( 16 | //

17 | // {props.children} 18 | //

19 | // ), 20 | // strong: (props:any) => ( 21 | // 22 | // {props.children} 23 | // 24 | // ), 25 | } 26 | 27 | type Props = { 28 | mdxSource: string, 29 | components?: any 30 | } 31 | 32 | export function RemoteMdxPage({ mdxSource,components }:Props) { 33 | 34 | return ( 35 |
36 | 37 |
38 | ) 39 | } 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/components/theme-context.tsx: -------------------------------------------------------------------------------- 1 | // ThemeContext.tsx 2 | "use client" 3 | import { createContext, useState, Dispatch, SetStateAction } from 'react'; 4 | import { useEffect,useLayoutEffect } from 'react'; 5 | 6 | 7 | export type Theme = 'light' | 'dark'; 8 | 9 | interface ThemeContextType { 10 | theme: Theme; 11 | setTheme: Dispatch>; 12 | } 13 | 14 | function getLocalStoredTheme():Theme { 15 | let storedTheme:Theme = 'dark'; 16 | if (typeof window !== 'undefined') { 17 | storedTheme = localStorage.getItem('color-theme') as Theme; 18 | } 19 | //console.log("storedTheme:" + storedTheme); 20 | return storedTheme??'dark'; 21 | } 22 | 23 | const ThemeContext = createContext({ 24 | theme: getLocalStoredTheme(), 25 | setTheme: () => {}, 26 | }); 27 | 28 | export const ThemeProvider = ({ children,storedTheme }:{ children: React.ReactNode,storedTheme:string}) => { 29 | 30 | const [theme, setTheme] = useState(storedTheme as Theme); 31 | 32 | const toggleTheme = () => { 33 | setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light')); 34 | }; 35 | 36 | return ( 37 | 38 | {children} 39 | 40 | ); 41 | }; 42 | 43 | export default ThemeContext; -------------------------------------------------------------------------------- /src/app/[lang]/(main)/error.tsx: -------------------------------------------------------------------------------- 1 | 'use client' // Error components must be Client Components 2 | 3 | import { useEffect } from 'react' 4 | 5 | export default function Error({ 6 | error, 7 | reset, 8 | }: { 9 | error: Error & { digest?: string } 10 | reset: () => void 11 | }) { 12 | useEffect(() => { 13 | // Log the error to an error reporting service 14 | console.error(error) 15 | }, [error]) 16 | 17 | return ( 18 |
19 | 27 |
28 | ) 29 | } -------------------------------------------------------------------------------- /src/components/theme-icon.tsx: -------------------------------------------------------------------------------- 1 | 2 | export function ThemeDarkIcon() { 3 | 4 | return( 5 | 8 | ) 9 | } 10 | 11 | export function ThemeLightIcon() { 12 | 13 | 14 | return ( 15 | 18 | ) 19 | } -------------------------------------------------------------------------------- /src/components/mdx-page-client.tsx: -------------------------------------------------------------------------------- 1 | import ReactMarkdown from 'react-markdown'; 2 | 3 | 4 | export const defaultComponents = { 5 | h1: (props:any) => ( 6 |

7 | {props.children} 8 |

9 | ), 10 | h2: (props:any) => ( 11 |

12 | {props.children} 13 |

14 | ), 15 | h3: (props:any) => ( 16 |

17 | {props.children} 18 |

19 | ), 20 | strong: (props:any) => ( 21 | 22 | {props.children} 23 | 24 | ), 25 | p: (props:any) => ( 26 |

27 | {props.children} 28 |

29 | ), 30 | li: (props:any) => ( 31 |

32 | {props.children} 33 |

34 | ) 35 | } 36 | 37 | type Props = { 38 | mdxSource: string, 39 | components?: any 40 | } 41 | 42 | export function ClientMdxPage({ mdxSource,components }:Props) { 43 | 44 | return ( 45 | 46 | {mdxSource} 47 | 48 | ) 49 | } 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/app/[lang]/(provide)/layout.tsx: -------------------------------------------------------------------------------- 1 | import {type Locale } from "@/i18n-config"; 2 | import { getDictionary } from '@/dictionaries' 3 | 4 | 5 | export default async function Layout({ 6 | children, params 7 | }: Readonly<{ 8 | children: React.ReactNode, 9 | params: { lang: Locale } 10 | }>) { 11 | const dictionary = (await getDictionary(params.lang)).policy; 12 | return ( 13 | 14 | <> 15 | 24 |
26 | {children} 27 |
28 | 29 | ); 30 | } -------------------------------------------------------------------------------- /src/components/loading-skeleton.tsx: -------------------------------------------------------------------------------- 1 | export function LoadingFull() { 2 | return ( 3 |
4 |
5 |
6 | ); 7 | }; 8 | 9 | export function LoadingOverlay() { 10 | return ( 11 |
12 |
13 |
14 | {/* Loading 15 | Loading*/} 16 | 18 | 19 | 21 | 22 | 23 |
24 |
25 |
26 | ); 27 | }; -------------------------------------------------------------------------------- /public/posts/de/prose-v1.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | date: 'October 23rd, 2023' 3 | thumbnail: /assets/avatar.jpg 4 | title: How to Think About Security in Next.js 5 | author: Delba de Oliveira 6 | avatar: /assets/avatar.jpg 7 | email: '@timneutkens' 8 | description: React Server Components (RSC) in App Router is a novel paradigm that eliminates much of the redundancy and 9 | potential risks linked with conventional methods. Given the newness, developers and subsequently security 10 | teams may find it challenging to align their existing security protocols with this model. 11 | readTime: 4 12 | --- 13 | 14 | ### Ingredients5 15 | 16 | - 1000 ml water. 17 | 18 | - 10 grams dried kelp. 19 | 20 | - 40 grams bonito flakes. 21 | 22 |
23 | 24 | ### Directions 25 | 26 | - Lightly wipe the surface of the kelp with a tightly wrung cloth. 27 | 28 | - Put water and kelp in a pot. 29 | 30 | - Heat on low heat, and when small bubbles come out from the bottom of the pot (about 7 minutes), remove the kelp. 31 | 32 | - When the kelp soup stock boils, add bonito flakes and simmer on low heat for 1 minute. 33 | 34 | - Set a colander on a bowl, spread a paper towel on it, and strain the soup stock. 35 | 36 | 37 | You can check it out on [YouTube](https://youtu.be/6Lxdp1R40EY). 38 | 39 |
40 | 41 |

Preservation Method

42 | 43 | Transfer the stock to a food storage container and store in the fridge. It can be used for 2-3 days. 44 | 45 |

Tips

46 | 47 | - The white powder on the surface of dried kelp is "umami", so do not remove it. -------------------------------------------------------------------------------- /public/posts/en/prose-v1.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | date: 'October 23rd, 2023' 3 | thumbnail: /assets/avatar.jpg 4 | title: How to Think About Security in Next.js 5 | author: Delba de Oliveira 6 | avatar: /assets/avatar.jpg 7 | email: '@timneutkens' 8 | description: React Server Components (RSC) in App Router is a novel paradigm that eliminates much of the redundancy and 9 | potential risks linked with conventional methods. Given the newness, developers and subsequently security 10 | teams may find it challenging to align their existing security protocols with this model. 11 | readTime: 4 12 | --- 13 | 14 | ### Ingredients5 15 | 16 | - 1000 ml water. 17 | 18 | - 10 grams dried kelp. 19 | 20 | - 40 grams bonito flakes. 21 | 22 |
23 | 24 | ### Directions 25 | 26 | - Lightly wipe the surface of the kelp with a tightly wrung cloth. 27 | 28 | - Put water and kelp in a pot. 29 | 30 | - Heat on low heat, and when small bubbles come out from the bottom of the pot (about 7 minutes), remove the kelp. 31 | 32 | - When the kelp soup stock boils, add bonito flakes and simmer on low heat for 1 minute. 33 | 34 | - Set a colander on a bowl, spread a paper towel on it, and strain the soup stock. 35 | 36 | 37 | You can check it out on [YouTube](https://youtu.be/6Lxdp1R40EY). 38 | 39 |
40 | 41 |

Preservation Method

42 | 43 | Transfer the stock to a food storage container and store in the fridge. It can be used for 2-3 days. 44 | 45 |

Tips

46 | 47 | - The white powder on the surface of dried kelp is "umami", so do not remove it. -------------------------------------------------------------------------------- /public/posts/es/prose-v1.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | date: 'October 23rd, 2023' 3 | thumbnail: /assets/avatar.jpg 4 | title: How to Think About Security in Next.js 5 | author: Delba de Oliveira 6 | avatar: /assets/avatar.jpg 7 | email: '@timneutkens' 8 | description: React Server Components (RSC) in App Router is a novel paradigm that eliminates much of the redundancy and 9 | potential risks linked with conventional methods. Given the newness, developers and subsequently security 10 | teams may find it challenging to align their existing security protocols with this model. 11 | readTime: 4 12 | --- 13 | 14 | ### Ingredients5 15 | 16 | - 1000 ml water. 17 | 18 | - 10 grams dried kelp. 19 | 20 | - 40 grams bonito flakes. 21 | 22 |
23 | 24 | ### Directions 25 | 26 | - Lightly wipe the surface of the kelp with a tightly wrung cloth. 27 | 28 | - Put water and kelp in a pot. 29 | 30 | - Heat on low heat, and when small bubbles come out from the bottom of the pot (about 7 minutes), remove the kelp. 31 | 32 | - When the kelp soup stock boils, add bonito flakes and simmer on low heat for 1 minute. 33 | 34 | - Set a colander on a bowl, spread a paper towel on it, and strain the soup stock. 35 | 36 | 37 | You can check it out on [YouTube](https://youtu.be/6Lxdp1R40EY). 38 | 39 |
40 | 41 |

Preservation Method

42 | 43 | Transfer the stock to a food storage container and store in the fridge. It can be used for 2-3 days. 44 | 45 |

Tips

46 | 47 | - The white powder on the surface of dried kelp is "umami", so do not remove it. -------------------------------------------------------------------------------- /public/posts/fr/prose-v1.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | date: 'October 23rd, 2023' 3 | thumbnail: /assets/avatar.jpg 4 | title: How to Think About Security in Next.js 5 | author: Delba de Oliveira 6 | avatar: /assets/avatar.jpg 7 | email: '@timneutkens' 8 | description: React Server Components (RSC) in App Router is a novel paradigm that eliminates much of the redundancy and 9 | potential risks linked with conventional methods. Given the newness, developers and subsequently security 10 | teams may find it challenging to align their existing security protocols with this model. 11 | readTime: 4 12 | --- 13 | 14 | ### Ingredients5 15 | 16 | - 1000 ml water. 17 | 18 | - 10 grams dried kelp. 19 | 20 | - 40 grams bonito flakes. 21 | 22 |
23 | 24 | ### Directions 25 | 26 | - Lightly wipe the surface of the kelp with a tightly wrung cloth. 27 | 28 | - Put water and kelp in a pot. 29 | 30 | - Heat on low heat, and when small bubbles come out from the bottom of the pot (about 7 minutes), remove the kelp. 31 | 32 | - When the kelp soup stock boils, add bonito flakes and simmer on low heat for 1 minute. 33 | 34 | - Set a colander on a bowl, spread a paper towel on it, and strain the soup stock. 35 | 36 | 37 | You can check it out on [YouTube](https://youtu.be/6Lxdp1R40EY). 38 | 39 |
40 | 41 |

Preservation Method

42 | 43 | Transfer the stock to a food storage container and store in the fridge. It can be used for 2-3 days. 44 | 45 |

Tips

46 | 47 | - The white powder on the surface of dried kelp is "umami", so do not remove it. -------------------------------------------------------------------------------- /public/posts/ja/prose-v1.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | date: 'October 23rd, 2023' 3 | thumbnail: /assets/avatar.jpg 4 | title: How to Think About Security in Next.js 5 | author: Delba de Oliveira 6 | avatar: /assets/avatar.jpg 7 | email: '@timneutkens' 8 | description: React Server Components (RSC) in App Router is a novel paradigm that eliminates much of the redundancy and 9 | potential risks linked with conventional methods. Given the newness, developers and subsequently security 10 | teams may find it challenging to align their existing security protocols with this model. 11 | readTime: 4 12 | --- 13 | 14 | ### Ingredients5 15 | 16 | - 1000 ml water. 17 | 18 | - 10 grams dried kelp. 19 | 20 | - 40 grams bonito flakes. 21 | 22 |
23 | 24 | ### Directions 25 | 26 | - Lightly wipe the surface of the kelp with a tightly wrung cloth. 27 | 28 | - Put water and kelp in a pot. 29 | 30 | - Heat on low heat, and when small bubbles come out from the bottom of the pot (about 7 minutes), remove the kelp. 31 | 32 | - When the kelp soup stock boils, add bonito flakes and simmer on low heat for 1 minute. 33 | 34 | - Set a colander on a bowl, spread a paper towel on it, and strain the soup stock. 35 | 36 | 37 | You can check it out on [YouTube](https://youtu.be/6Lxdp1R40EY). 38 | 39 |
40 | 41 |

Preservation Method

42 | 43 | Transfer the stock to a food storage container and store in the fridge. It can be used for 2-3 days. 44 | 45 |

Tips

46 | 47 | - The white powder on the surface of dried kelp is "umami", so do not remove it. -------------------------------------------------------------------------------- /public/posts/ko/prose-v1.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | date: 'October 23rd, 2023' 3 | thumbnail: /assets/avatar.jpg 4 | title: How to Think About Security in Next.js 5 | author: Delba de Oliveira 6 | avatar: /assets/avatar.jpg 7 | email: '@timneutkens' 8 | description: React Server Components (RSC) in App Router is a novel paradigm that eliminates much of the redundancy and 9 | potential risks linked with conventional methods. Given the newness, developers and subsequently security 10 | teams may find it challenging to align their existing security protocols with this model. 11 | readTime: 4 12 | --- 13 | 14 | ### Ingredients5 15 | 16 | - 1000 ml water. 17 | 18 | - 10 grams dried kelp. 19 | 20 | - 40 grams bonito flakes. 21 | 22 |
23 | 24 | ### Directions 25 | 26 | - Lightly wipe the surface of the kelp with a tightly wrung cloth. 27 | 28 | - Put water and kelp in a pot. 29 | 30 | - Heat on low heat, and when small bubbles come out from the bottom of the pot (about 7 minutes), remove the kelp. 31 | 32 | - When the kelp soup stock boils, add bonito flakes and simmer on low heat for 1 minute. 33 | 34 | - Set a colander on a bowl, spread a paper towel on it, and strain the soup stock. 35 | 36 | 37 | You can check it out on [YouTube](https://youtu.be/6Lxdp1R40EY). 38 | 39 |
40 | 41 |

Preservation Method

42 | 43 | Transfer the stock to a food storage container and store in the fridge. It can be used for 2-3 days. 44 | 45 |

Tips

46 | 47 | - The white powder on the surface of dried kelp is "umami", so do not remove it. -------------------------------------------------------------------------------- /public/posts/zh/prose-v1.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | date: 'October 23rd, 2023' 3 | thumbnail: /assets/avatar.jpg 4 | title: How to Think About Security in Next.js 5 | author: Delba de Oliveira 6 | avatar: /assets/avatar.jpg 7 | email: '@timneutkens' 8 | description: React Server Components (RSC) in App Router is a novel paradigm that eliminates much of the redundancy and 9 | potential risks linked with conventional methods. Given the newness, developers and subsequently security 10 | teams may find it challenging to align their existing security protocols with this model. 11 | readTime: 4 12 | --- 13 | 14 | ### Ingredients5 15 | 16 | - 1000 ml water. 17 | 18 | - 10 grams dried kelp. 19 | 20 | - 40 grams bonito flakes. 21 | 22 |
23 | 24 | ### Directions 25 | 26 | - Lightly wipe the surface of the kelp with a tightly wrung cloth. 27 | 28 | - Put water and kelp in a pot. 29 | 30 | - Heat on low heat, and when small bubbles come out from the bottom of the pot (about 7 minutes), remove the kelp. 31 | 32 | - When the kelp soup stock boils, add bonito flakes and simmer on low heat for 1 minute. 33 | 34 | - Set a colander on a bowl, spread a paper towel on it, and strain the soup stock. 35 | 36 | 37 | You can check it out on [YouTube](https://youtu.be/6Lxdp1R40EY). 38 | 39 |
40 | 41 |

Preservation Method

42 | 43 | Transfer the stock to a food storage container and store in the fridge. It can be used for 2-3 days. 44 | 45 |

Tips

46 | 47 | - The white powder on the surface of dried kelp is "umami", so do not remove it. -------------------------------------------------------------------------------- /src/components/loading-from.tsx: -------------------------------------------------------------------------------- 1 | import { useFormStatus } from "react-dom"; 2 | 3 | 4 | export function LoadingFull() { 5 | const { pending } = useFormStatus(); 6 | return ( 7 |
8 |
9 |
10 | ); 11 | } 12 | 13 | export function LoadingOverlay() { 14 | const { pending } = useFormStatus(); 15 | return ( 16 |
17 |
18 |
19 | {/* Loading 20 | Loading*/} 21 | 23 | 24 | 26 | 27 | 28 |
29 |
30 |
31 | ); 32 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "image-description-ai", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "vercel-build": "prisma generate && next build" 11 | }, 12 | "dependencies": { 13 | "@aws-sdk/client-s3": "^3.572.0", 14 | "@clerk/localizations": "^2.0.0", 15 | "@clerk/nextjs": "^5.0.1", 16 | "@clerk/themes": "^2.1.2", 17 | "@formatjs/intl-localematcher": "^0.5.4", 18 | "@google/generative-ai": "^0.6.0", 19 | "@lemonsqueezy/lemonsqueezy.js": "^2.2.0", 20 | "@mdx-js/loader": "^3.0.1", 21 | "@mdx-js/react": "^3.0.1", 22 | "@next/mdx": "^14.2.1", 23 | "@next/third-parties": "^14.1.4", 24 | "@paddle/paddle-js": "^1.1.0", 25 | "@paddle/paddle-node-sdk": "^1.3.0", 26 | "@prisma/client": "^5.14.0", 27 | "@types/mdx": "^2.0.13", 28 | "@types/negotiator": "^0.6.3", 29 | "@vercel/kv": "^1.0.1", 30 | "@vercel/postgres": "^0.8.0", 31 | "@vercel/speed-insights": "^1.0.10", 32 | "flag-icons": "^7.2.1", 33 | "gray-matter": "^4.0.3", 34 | "negotiator": "^0.6.3", 35 | "next": "14.1.4", 36 | "next-mdx-remote": "^4.4.1", 37 | "pg": "^8.11.5", 38 | "react": "^18", 39 | "react-dom": "^18", 40 | "react-intersection-observer": "^9.10.2", 41 | "react-markdown": "^9.0.1", 42 | "svix": "^1.21.0", 43 | "zod": "^3.23.4" 44 | }, 45 | "devDependencies": { 46 | "@tailwindcss/typography": "^0.5.12", 47 | "@types/node": "^20", 48 | "@types/react": "^18", 49 | "@types/react-dom": "^18", 50 | "autoprefixer": "^10.0.1", 51 | "eslint": "^8", 52 | "eslint-config-next": "14.1.4", 53 | "postcss": "^8", 54 | "prisma": "^5.14.0", 55 | "tailwindcss": "^3.4.3" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | AI Image Description Generator 2 | ================ 3 | 4 | **[English](./README.md)** | **[中文](./README_zh.md)** 5 | 6 | **AI说图解图** 精准提取图片中的主要元素,解读图片创作目的;可用于科研、图表分析、艺术创作中图文互搜领域. 7 | 8 | * 它是基于 **ERNIE 3.5** 或 **GEMINI-1.5-PRO** API; 9 | * 支持7种语言; 10 | * 支持**Lemon Squeezy**支付平台 或 **Paddle Billing**支付平台; 11 | * 集成**clerk.com**用户管理平台; 12 | * 实时数据处理: 流式数据传输支持,利用 Shit-And 模式匹配算法解析 JSON 数据; 13 | * 响应式设计: 适配桌面、平板、手机等设备; 14 | * 支持 **S3[aws-sdk]** 存储,管理您的数据; 15 | * 无限滚动卡片列表**SEO**友好; 16 | * 支持黑暗模式主题; 17 | * 它是基于Next.js构建的全栈 web 应用解决方案; 18 | 19 | 截图与演示 20 | ---------------- 21 | 22 |
23 | 24 | ![AI Image Description Generator Screenshot 1](./public/assets/screenshot-2.png "Screenshot 1") 25 | ![AI Image Description Generator Screenshot 3](./public/assets/screenshot-3.png "Screenshot 3") 26 | ![AI Image Description Generator Screenshot 2](./public/assets/screenshot-1.png "Screenshot 2") 27 | 28 |
29 | 30 | DEMO: [www.imagedescriptiongenerator.xyz](https://imagedescriptiongenerator.xyz/) 31 | 32 | 快速开始 33 | ---------------- 34 | 35 | 步骤1. 安装Node.js 18.17以上版本. 36 | 37 | 步骤2. 运行next.js服务 38 | 39 | ```sh 40 | cd 41 | npm install 42 | npm run dev 43 | ``` 44 | 45 | 步骤3. 打开浏览器, 访问 **** 46 | 47 | 官方网站 48 | ---------------- 49 | 50 | * [www.imagedescriptiongenerator.xyz](https://imagedescriptiongenerator.xyz/) 51 | 52 | 目录结构 53 | ---------------- 54 | 55 | ```text 56 | root // next.js 项目 57 | ├─ public 58 | ├─ src 59 | ├─ app //功能页面 60 | ├─ components // next.js 自定义组件 61 | ├─ dictionaries // 这里增加新的语言支持,JSON文件 62 | ├─ lib // ernie和gemini api服务实现 63 | ``` 64 | 65 | 其它 66 | ---------------- 67 | 68 | 推特: [https://twitter.com/imgdesgen](https://twitter.com/imgdesgen) 69 | 70 | 如果这个项目对你有帮组,请给我买杯咖啡吧! 71 | 72 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/Q5Q1WDG36) 73 | 74 | -------------------------------------------------------------------------------- /src/app/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | https://imagedescriptiongenerator.xyz/en 12 | 2024-04-17T15:27:01+00:00 13 | 1.00 14 | 15 | 16 | https://imagedescriptiongenerator.xyz/en/explore 17 | 2024-04-17T15:27:01+00:00 18 | 0.80 19 | 20 | 21 | https://imagedescriptiongenerator.xyz/en/blog 22 | 2024-04-17T15:27:01+00:00 23 | 0.80 24 | 25 | 26 | https://imagedescriptiongenerator.xyz/en/about 27 | 2024-04-17T15:27:01+00:00 28 | 0.80 29 | 30 | 31 | https://imagedescriptiongenerator.xyz/en/contact 32 | 2024-04-17T15:27:01+00:00 33 | 0.80 34 | 35 | 36 | https://imagedescriptiongenerator.xyz/en/blog/article/prose-v1 37 | 2024-04-17T15:27:01+00:00 38 | 0.64 39 | 40 | 41 | https://imagedescriptiongenerator.xyz/en/blog/article/prose-v2 42 | 2024-04-17T15:27:01+00:00 43 | 0.64 44 | 45 | 46 | https://imagedescriptiongenerator.xyz/en/blog/article/prose-v3 47 | 2024-04-17T15:27:01+00:00 48 | 0.64 49 | 50 | 51 | https://imagedescriptiongenerator.xyz/en/blog/article/prose-v4 52 | 2024-04-17T15:27:01+00:00 53 | 0.64 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/components/popup-modal.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React, {forwardRef,useState, useImperativeHandle } from 'react'; 3 | 4 | export interface ModalRef { 5 | openModal: () => void; 6 | closeModal: () => void; 7 | } 8 | 9 | interface ModalProps { 10 | children: React.ReactNode; 11 | } 12 | const Modal = forwardRef((props, ref) => { 13 | 14 | const [isOpen, setIsOpen] = useState('hidden'); 15 | 16 | useImperativeHandle(ref, () => ({ 17 | openModal: () => setIsOpen('block'), 18 | closeModal: () => setIsOpen('hidden'), 19 | })); 20 | 21 | return ( 22 | 37 | ) 38 | }); 39 | Modal.displayName = 'Modal'; 40 | export default Modal; 41 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest,NextResponse } from 'next/server' 2 | import { match as matchLocale } from '@formatjs/intl-localematcher' 3 | import Negotiator from 'negotiator' 4 | import { i18n } from "@/i18n-config"; 5 | import { clerkMiddleware,createRouteMatcher } from "@clerk/nextjs/server"; 6 | 7 | 8 | const isProtectedRoute = createRouteMatcher([ 9 | '/api/home(.*)', 10 | '/api/user(.*)' 11 | ]); 12 | 13 | // @ts-ignore locales are readonly 14 | const locales: string[] = i18n.locales.map((locale) => locale.toString()); 15 | 16 | // 使用 clerkMiddleware 处理请求 17 | export default clerkMiddleware((auth, request, event) => { 18 | 19 | 20 | if (isProtectedRoute (request)){ 21 | return NextResponse.next(); 22 | } 23 | 24 | const pathname = request.nextUrl.pathname 25 | 26 | 27 | const pathnameHasLocale = i18n.locales.some( 28 | (locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}` 29 | ) 30 | 31 | if (pathnameHasLocale) return NextResponse.next(); 32 | const locale = getLocale(request) 33 | request.nextUrl.pathname = `/${locale}${pathname}` 34 | return NextResponse.redirect(request.nextUrl); 35 | 36 | }, {debug: false}); 37 | 38 | 39 | function getLocale(request: NextRequest): string | undefined { 40 | // Negotiator expects plain object so we need to transform headers 41 | const negotiatorHeaders: Record = {}; 42 | request.headers.forEach((value, key) => (negotiatorHeaders[key] = value)); 43 | 44 | // Use negotiator and intl-localematcher to get best locale 45 | let languages = new Negotiator({ headers: negotiatorHeaders }).languages( 46 | locales, 47 | ); 48 | 49 | const locale = matchLocale(languages, locales, i18n.defaultLocale); 50 | 51 | return locale; 52 | } 53 | 54 | export const config = { 55 | matcher: [ 56 | // Skip all internal paths (_next) 57 | '/((?!_next|robots.txt|sitemap.xml|assets|favicon|posts).*)', 58 | '/', // Run middleware on index page 59 | '/(api|trpc)(.*)' // Run middleware on API routes 60 | ], 61 | } 62 | -------------------------------------------------------------------------------- /src/actions/posts.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | import fs from 'fs' 3 | import path from 'path'; 4 | import matter from 'gray-matter'; 5 | import { serialize } from 'next-mdx-remote/serialize'; 6 | import { MDXRemoteSerializeResult } from 'next-mdx-remote'; 7 | 8 | export interface Post { 9 | slug: string, 10 | frontMatter:FrontMatter, 11 | content:string, 12 | } 13 | 14 | export interface FrontMatter{ 15 | title: string, 16 | author: string, 17 | avatar: string, 18 | twitter: string, 19 | description:string, 20 | date:string, 21 | thumbnail:string, 22 | tags:string[] 23 | } 24 | 25 | export async function getAllPostList(lang:string="en"){ 26 | if(!lang){ 27 | lang = "en"; 28 | } 29 | 30 | const root = process.cwd(); 31 | let files = fs.readdirSync(path.join(root, 'public',"posts",lang)); // get the files 32 | files = files.filter(file => file.split('.')[1] == "mdx"); // filter only the mdx files 33 | const posts = files.map(file => { // for each file extract the front matter and the slug 34 | const fileData = fs.readFileSync(path.join(root, 'public',"posts",lang,file),'utf-8'); 35 | const {data} = matter(fileData); 36 | return { 37 | frontMatter:data as FrontMatter, 38 | slug:file.split('.')[0] 39 | } as Post 40 | }); 41 | return posts; 42 | } 43 | 44 | export async function getPost(slug:string,lang:string="en"){ 45 | 46 | if(!lang){ 47 | lang = "en"; 48 | } 49 | const root = process.cwd(); 50 | let fileData = fs.readFileSync(path.join(root,'public',"posts",lang,`${slug}.mdx`)); // get the files 51 | const {data,content} = matter(fileData); 52 | const Post = { 53 | frontMatter:data as FrontMatter, 54 | content:content 55 | } 56 | return Post; 57 | } 58 | 59 | 60 | export async function getSerializePost(slug:string):Promise{ 61 | 62 | const root = process.cwd(); 63 | let fileData = fs.readFileSync(path.join(root,'public',"posts",`${slug}.mdx`)); // get the files 64 | const {content} = matter(fileData); 65 | const mdxSource = await serialize(content); 66 | return mdxSource; 67 | 68 | } -------------------------------------------------------------------------------- /src/scripts/ShiftAndByteBufferMatcher.ts: -------------------------------------------------------------------------------- 1 | class ShiftAndByteBufferMatcher { 2 | private endMask: number; 3 | private currentMask: number = 0; 4 | private buffer: Uint8Array = new Uint8Array(0); 5 | constructor(endChar: string = '}') { 6 | this.endMask = 1 << endChar.charCodeAt(0); 7 | } 8 | 9 | public async *processStreamingData(dataStream: AsyncIterableIterator): AsyncIterableIterator { 10 | const decoder = new TextDecoder('utf-8'); 11 | for await (const chunk of dataStream) { 12 | // Append the received chunk to the buffer 13 | this.buffer = this.concatArrays(this.buffer, chunk); 14 | 15 | let completeMaskIndex: number | null = null; 16 | while ((completeMaskIndex = this.findEndMask(this.buffer)) !== null) { 17 | const endPosition = completeMaskIndex + 1; 18 | const dataBytes = this.buffer.slice(0, endPosition); 19 | this.buffer = this.buffer.slice(endPosition); // Remove parsed data 20 | const completeData = decoder.decode(dataBytes); 21 | //console.log('found data:', completeData); 22 | yield completeData; // Yield the complete data 23 | } 24 | } 25 | } 26 | 27 | private concatArrays(a: Uint8Array, b: Uint8Array): Uint8Array { 28 | const totalLength = a.length + b.length; 29 | const result = new Uint8Array(totalLength); 30 | result.set(a); 31 | result.set(b, a.length); 32 | return result; 33 | } 34 | 35 | private findEndMask(chunk: Uint8Array): number | null { 36 | for (let i = chunk.length - 1; i >= 0; i--) { 37 | const byte = chunk[i]; 38 | //check if it is a single byte character or the start of a multibyte character 39 | if ((byte & 0x80) === 0 || (byte & 0xC0) === 0xC0) { 40 | const currentMask = 1 << byte; 41 | if ((currentMask & this.endMask) !== 0) { 42 | return i; // find end mask '}' 43 | } 44 | } 45 | } 46 | return null; // not found 47 | } 48 | } 49 | 50 | export default ShiftAndByteBufferMatcher; 51 | -------------------------------------------------------------------------------- /src/app/[lang]/(main)/paddle/button.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { useAuth } from "@clerk/nextjs"; 3 | import { useRouter } from "next/navigation"; 4 | import {initializePaddle,Paddle,Environments,Theme} from "@paddle/paddle-js"; 5 | import { useState,useEffect } from "react"; 6 | import { useContext } from 'react'; 7 | import ThemeContext from "@/components/theme-context"; 8 | 9 | interface Props{ 10 | btnlabel:string; 11 | priceId:string; 12 | lang:string; 13 | token:string; 14 | env:Environments; 15 | email:string; 16 | } 17 | export default function Button({btnlabel,priceId,lang,token,env,email}:Props){ 18 | 19 | const { isLoaded, userId} = useAuth(); 20 | const router = useRouter(); 21 | const [paddle, setPaddle] = useState(); 22 | const {theme} = useContext(ThemeContext); 23 | 24 | useEffect(() => { 25 | initializePaddle( 26 | { 27 | environment: env, 28 | token: token, 29 | eventCallback: function(data) { 30 | //console.log(data); 31 | } , 32 | checkout:{ 33 | settings:{ 34 | displayMode: "overlay", 35 | theme:theme, 36 | locale: lang 37 | } 38 | } 39 | }).then( 40 | (paddleInstance: Paddle | undefined) => { 41 | if (paddleInstance) { 42 | setPaddle(paddleInstance); 43 | } 44 | }, 45 | ); 46 | }, [theme]); 47 | 48 | function openCheckout (){ 49 | paddle?.Checkout.open({ 50 | items: [{ priceId: priceId, quantity: 1 }], 51 | customer: { email: email}, 52 | customData:{userId:String(userId)} 53 | }); 54 | } 55 | 56 | function onClickHandler() { 57 | if (!isLoaded || !userId) { 58 | router.push(`/${lang}/sign-in`); 59 | return; 60 | } 61 | openCheckout(); 62 | } 63 | 64 | return ( 65 | 66 | ) 67 | } -------------------------------------------------------------------------------- /src/lib/aws-client-s3.ts: -------------------------------------------------------------------------------- 1 | import 'server-only' 2 | import { S3Client, 3 | CreateBucketCommand, 4 | ListBucketsCommand, 5 | ListObjectsCommand, 6 | GetObjectCommand, 7 | PutObjectCommand, 8 | DeleteObjectCommand, 9 | } from "@aws-sdk/client-s3"; 10 | 11 | 12 | const ACCOUNT_ID = process.env.CF_R2_ACCOUNT_ID as string; 13 | 14 | const ACCESS_KEY_ID = process.env.CF_R2_ACCESS_KEY_ID as string; 15 | 16 | const SECRET_ACCESS_KEY = process.env.CF_R2_SECRET_ACCESS_KEY as string; 17 | 18 | const BUCKET_NAME = process.env.CF_R2_BUCKET_NAME as string; 19 | 20 | const config ={ 21 | region: "auto", 22 | endpoint: `https://${ACCOUNT_ID}.r2.cloudflarestorage.com`, 23 | credentials: { 24 | accessKeyId: ACCESS_KEY_ID, 25 | secretAccessKey: SECRET_ACCESS_KEY, 26 | }, 27 | } 28 | 29 | const client = new S3Client(config); 30 | 31 | export async function putObject(data: Buffer,Key: string) { 32 | 33 | const input = { // PutObjectRequest 34 | Body: data, // see \@smithy/types -> StreamingBlobPayloadInputTypes 35 | Bucket: BUCKET_NAME, // required 36 | Key: Key, // required 37 | ContentType: 'image/jpeg' 38 | }; 39 | 40 | const command = new PutObjectCommand(input); 41 | const response = await client.send(command); 42 | //console.log(response); 43 | return response.ETag; 44 | } 45 | 46 | // export function listBuckets() { 47 | // const client = new S3Client(config); 48 | // return client.send(new ListBucketsCommand({})); 49 | // } 50 | 51 | // export function createBucket() { 52 | // const client = new S3Client({}); 53 | // return client.send(new CreateBucketCommand({})); 54 | // } 55 | 56 | // export function deleteBucket() { 57 | // const client = new S3Client({}); 58 | // return client.send(new ListBucketsCommand({})); 59 | // } 60 | 61 | // export function getObject() { 62 | // const client = new S3Client({}); 63 | // return client.send(new GetObjectCommand({})); 64 | // } 65 | 66 | export async function deleteObject(key:string) { 67 | const input = { // DeleteObjectRequest 68 | Bucket: BUCKET_NAME, // required 69 | Key: key // required 70 | }; 71 | const command = new DeleteObjectCommand(input); 72 | const response = await client.send(command); 73 | return response; 74 | } 75 | 76 | // export function listObjects() { 77 | // const client = new S3Client({}); 78 | // return client.send(new ListObjectsCommand({})); 79 | // } -------------------------------------------------------------------------------- /src/components/sharebutton.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import Tooltip from "@/components/Tooltip"; 3 | type Props = { 4 | imageUrl: string; 5 | text: string; 6 | } 7 | 8 | export function ShareButton({ imageUrl, text }: Props) { 9 | 10 | const twitterUrl = new URL('https://twitter.com/intent/tweet'); 11 | const twitterParams = new URLSearchParams({ 12 | url: imageUrl, 13 | text: text, 14 | }); 15 | twitterUrl.search = twitterParams.toString(); 16 | 17 | const facebookUrl = new URL('https://www.facebook.com/sharer/sharer.php'); 18 | const facebookParams = new URLSearchParams({ 19 | u: imageUrl, 20 | quote: text, 21 | }); 22 | facebookUrl.search = facebookParams.toString(); 23 | 24 | return ( 25 |
26 | 27 | 28 | 31 | 32 | 33 | 34 | 35 | 38 | 39 | 40 |
41 | ) 42 | } -------------------------------------------------------------------------------- /src/lib/postgres-client.ts: -------------------------------------------------------------------------------- 1 | import 'server-only' 2 | import { Pool } from 'pg'; 3 | import type { 4 | QueryResult, 5 | QueryResultRow, 6 | } from '@neondatabase/serverless'; 7 | 8 | const connectionString = process.env.POSTGRES_URL; 9 | 10 | const pool = new Pool({ 11 | connectionString, 12 | ssl: { 13 | rejectUnauthorized: false 14 | } 15 | }) 16 | 17 | export async function sql( 18 | strings: TemplateStringsArray, 19 | ...values: Primitive[] 20 | ): Promise> { 21 | const [query, params] = sqlTemplate(strings, ...values); 22 | 23 | try { 24 | const result = await pool.query(query, params); 25 | return result as unknown as Promise>; 26 | } catch (error) { 27 | console.error('Database query error:', error); 28 | throw new Error('Failed to execute query'); 29 | } 30 | 31 | } 32 | 33 | export async function neonsql(strings: TemplateStringsArray, ...values: Primitive[]): Promise> { 34 | try { 35 | const url = process.env.POSTGRES_NEON_PUBLIC_API_BASE_URL as string; 36 | const [query, params] = sqlTemplate(strings, ...values); 37 | const response = await fetch(url, { 38 | method: 'POST', 39 | headers: { 40 | 'Content-Type': 'application/json' 41 | }, 42 | body: JSON.stringify({ 43 | query: query, 44 | params: params 45 | }) 46 | }); 47 | if (!response.ok) { 48 | throw new Error(`Failed to execute query: ${await response.text()}`); 49 | } 50 | const payload: QueryResult = await response.json(); 51 | return payload; 52 | } catch (error) { 53 | console.error('Failed to execute query:', error); 54 | throw new Error(`Failed to execute query: ${error?.toString()}`); 55 | } 56 | } 57 | 58 | export const db = pool; 59 | 60 | 61 | export function sqlTemplate( 62 | strings: TemplateStringsArray, 63 | ...values: Primitive[] 64 | ): [string, Primitive[]] { 65 | if (!isTemplateStringsArray(strings) || !Array.isArray(values)) { 66 | throw new Error("Invalid template strings array. Use it as a tagged template: sql`SELECT * FROM users`."); 67 | } 68 | 69 | let query = strings[0] ?? ''; 70 | 71 | for (let i = 1; i < strings.length; i++) { 72 | query += `$${i}${strings[i] ?? ''} `; 73 | } 74 | 75 | return [query.trim(), values]; 76 | } 77 | 78 | function isTemplateStringsArray(strings: unknown): strings is TemplateStringsArray { 79 | return Array.isArray(strings) && 'raw' in strings && Array.isArray(strings.raw); 80 | } 81 | 82 | export type Primitive = string | number | boolean | undefined | null; 83 | 84 | 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | AI Image Description Generator 2 | ================ 3 | 4 | **[English](./README.md)** | **[中文](./README_zh.md)** 5 | 6 | **AI Image Description Generator** accurately extracts the key elements from images and interprets the creative purposes behind them, which can be applied in fields such as scientific research, artistic creation, data chart Analysis, and the mutual search between images and texts. 7 | 8 | * It is based on **ERNIE 3.5** OR **GEMINI-1.5-PRO** API; 9 | * It supports multi languages; 10 | * It Supports **Lemon Squeezy** platform OR **Paddle Billing**; 11 | * Integrating the **clerk.com** User Management Platform; 12 | * Real-time Data Processing: Supports streaming data transmission and utilizes the Shit-And algorithm to parse JSON data. 13 | * Responsive Design: Adapts to desktops, tablets, mobile phones, and other devices. 14 | * S3 storage support: Manage your data with **S3[aws-sdk]** storage. 15 | * Infinite Scrolling Card List SEO-Friendly: Provides an infinite scrolling card list designed for SEO. 16 | * It supports dark mode theme; 17 | * It use Next.js to build full-stack web applications.; 18 | 19 | Screenshots & Demo 20 | ---------------- 21 | 22 |
23 | 24 | ![AI Image Description Generator Screenshot 1](./public/assets/screenshot-2.png "Screenshot 1") 25 | ![AI Image Description Generator Screenshot 3](./public/assets/screenshot-3.png "Screenshot 3") 26 | ![AI Image Description Generator Screenshot 2](./public/assets/screenshot-1.png "Screenshot 2") 27 | 28 |
29 | 30 | DEMO: [www.imagedescriptiongenerator.xyz](https://imagedescriptiongenerator.xyz/) 31 | 32 | Getting Started 33 | ---------------- 34 | 35 | Step 1. Node.js 18.17 or later. 36 | 37 | Step 2. Run the development server 38 | 39 | ```sh 40 | cd 41 | npm install 42 | npm run dev 43 | ``` 44 | 45 | Step 3. Open browser, visit **** 46 | 47 | Official site 48 | ---------------- 49 | 50 | * [www.imagedescriptiongenerator.xyz](https://imagedescriptiongenerator.xyz/) 51 | 52 | Directory Structure 53 | ---------------- 54 | 55 | ```text 56 | root // next.js project 57 | ├─ public 58 | ├─ src 59 | ├─ app //main page 60 | ├─ components // next.js components 61 | ├─ dictionaries // Add new language using the JSON file 62 | ├─ lib // ernie and gemini api service 63 | ``` 64 | 65 | Others 66 | ---------------- 67 | 68 | you can contact me at Twitter: [https://twitter.com/imgdesgen](https://twitter.com/imgdesgen) 69 | 70 | if this project is helpful to you, buy be a coffee. 71 | 72 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/Q5Q1WDG36) 73 | -------------------------------------------------------------------------------- /src/components/clipboard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | 3 | interface ClipboardProps { 4 | className?: string; 5 | value : string; 6 | } 7 | export default function Clipboard({ className, value }: ClipboardProps) { 8 | const [copied, setCopied] = useState(false); 9 | 10 | async function handleCopy(){ 11 | navigator.clipboard.writeText(value) 12 | .then(() => { 13 | setCopied(true); 14 | }) 15 | .catch(err => { 16 | console.error('Failed to copy: ', err); 17 | }); 18 | }; 19 | 20 | useEffect(() => { 21 | if (copied) { 22 | setTimeout(() => { setCopied(false) }, 1500); // Reset 'copied' state after 1.5 seconds 23 | } 24 | }, [copied]); 25 | 26 | return ( 27 | 28 | //
29 | // {children} 30 |
31 | 45 |
46 | {copied ? 'Copied!' : 'Copy to clipboard'} 47 |
48 |
49 |
50 | //
51 | ); 52 | }; -------------------------------------------------------------------------------- /src/lib/gemini-client.ts: -------------------------------------------------------------------------------- 1 | import 'server-only' 2 | import { GoogleGenerativeAI, HarmCategory, HarmBlockThreshold, GenerateContentStreamResult} from "@google/generative-ai"; 3 | import { Response } from "../types/responses"; 4 | const MODEL_NAME = "gemini-1.5-pro-latest"; 5 | const API_KEY = process.env.GOOGLE_GEMINI_API_KEY as string; 6 | 7 | const IMAGE_AI_REBOT = { 8 | role: "user", 9 | parts: [{ text: `作为图像解读专家,你需要对上传的图片进行深入分析,挖掘其中的创作背景、情感表达、作品背后的故事和寓意。` }] 10 | }; 11 | 12 | const safetySettings = [ 13 | { 14 | category: HarmCategory.HARM_CATEGORY_HARASSMENT, 15 | threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, 16 | }, 17 | { 18 | category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, 19 | threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, 20 | }, 21 | { 22 | category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, 23 | threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, 24 | }, 25 | { 26 | category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, 27 | threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, 28 | } 29 | ]; 30 | 31 | const generationConfig = { 32 | temperature: 0.9, 33 | topK: 1, 34 | topP: 1, 35 | maxOutputTokens: 2048, 36 | }; 37 | 38 | const MODEL_PARAMs = { 39 | model: MODEL_NAME, 40 | safetySettings, 41 | generationConfig, 42 | systemInstruction: IMAGE_AI_REBOT 43 | }; 44 | 45 | const REQUEST_OPTIONS = { 46 | apiVersion: "v1beta" 47 | } 48 | 49 | export async function runChatFromGemini(base64Image: string, lang: string = "en") { 50 | 51 | const responseGemini: Response = {}; 52 | try { 53 | const genAI = new GoogleGenerativeAI(API_KEY); 54 | const model = genAI.getGenerativeModel(MODEL_PARAMs, REQUEST_OPTIONS); 55 | 56 | const prompt = lang; 57 | const image = { 58 | inlineData: { 59 | data: base64Image, 60 | mimeType: "image/png", 61 | }, 62 | }; 63 | 64 | const result = await model.generateContent([prompt, image]); 65 | responseGemini.result = result.response.text(); 66 | //console.log(response.text()); 67 | } catch (error) { 68 | responseGemini.error_code = 999999; 69 | responseGemini.error_msg = error as string; 70 | console.error(error); 71 | } 72 | return responseGemini; 73 | } 74 | 75 | 76 | export async function runChatFromGeminiStreamResult(base64Image: string, lang: string = "en") { 77 | 78 | try { 79 | const genAI = new GoogleGenerativeAI(API_KEY); 80 | const model = genAI.getGenerativeModel(MODEL_PARAMs, REQUEST_OPTIONS); 81 | 82 | const prompt = lang; 83 | const image = { 84 | inlineData: { 85 | data: base64Image, 86 | mimeType: "image/png", 87 | }, 88 | }; 89 | return await model.generateContentStream([prompt, image]); 90 | } catch (error) { 91 | console.error(error); 92 | } 93 | 94 | } 95 | 96 | 97 | -------------------------------------------------------------------------------- /src/components/prose-head.tsx: -------------------------------------------------------------------------------- 1 | import { Locale } from "@/i18n-config"; 2 | import { FrontMatter } from "@/actions/posts"; 3 | import { Dictionary } from '@/dictionaries' 4 | 5 | type Props = { 6 | lang: Locale, 7 | frontMatter: FrontMatter, 8 | dictionary: Dictionary["blog"], 9 | } 10 | export default function ProseHead({ lang,frontMatter,dictionary }: Props) { 11 | return ( 12 | <> 13 | 14 | 15 | 16 | {dictionary.back_button_lable} 17 | 18 |

{frontMatter.date}

19 |

{frontMatter.title}

20 | {dictionary.author_lable} 21 | {/* mb-8 border-b-[1px] border-gray-200*/} 22 | 40 | 41 | ) 42 | } -------------------------------------------------------------------------------- /src/lib/baidu-client.ts: -------------------------------------------------------------------------------- 1 | import 'server-only' 2 | import { Response } from "../types/responses"; 3 | const AK = process.env.BAIDU_TRANSLATE_API_KEY; 4 | const SK = process.env.BAIDU_TRANSLATE_SECRET_KEY; 5 | 6 | const ACCESS_TOKEN_URL = 'https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=' + AK + '&client_secret=' + SK 7 | 8 | const TOKEN_HEADERS = { 9 | 'Content-Type': 'application/json', 10 | 'Accept': 'application/json' 11 | } 12 | 13 | const IMAGE2TXT_HEADERS = { 14 | 'Content-Type': 'application/json' 15 | } 16 | const PROMPT = "解读图像中的创作背景、情感以及作品背后的故事与寓意,将图片中的信息转化为简洁、有见地的文字描述。英文返回" 17 | const BASE_IMAGE2TXT_URL = 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/image2text/fuyu_8b?access_token=' 18 | let LAST_TIME = 0; 19 | let ACCESS_TOKEN = '' 20 | const ACCESS_TOKEN_EXPIRES = 2592000; 21 | 22 | 23 | async function getAccessToken() { 24 | let _access_token = '' 25 | try { 26 | const response = await fetch(ACCESS_TOKEN_URL, { 27 | method: 'POST', 28 | headers: TOKEN_HEADERS 29 | }); 30 | const result = await response.json(); 31 | //console.log("result", result); 32 | if (result && result.error) { 33 | throw new Error(result.error_description); 34 | } 35 | if (result.access_token) { 36 | _access_token = result.access_token; 37 | } 38 | } catch (error) { 39 | console.error(error); 40 | } 41 | return _access_token; 42 | } 43 | 44 | 45 | 46 | 47 | export async function runChatFromBaidu(base64:string) { 48 | 49 | 50 | const responseBaidu: Response = {}; 51 | try { 52 | const currentTimeSec = Math.floor(Date.now() / 1000); 53 | if (ACCESS_TOKEN === '' || LAST_TIME === 0 || (currentTimeSec - LAST_TIME) > ACCESS_TOKEN_EXPIRES) { 54 | LAST_TIME = currentTimeSec; 55 | ACCESS_TOKEN = await getAccessToken(); 56 | } 57 | 58 | const IMAGE2TXT_URL = BASE_IMAGE2TXT_URL + ACCESS_TOKEN; 59 | const response = await fetch( IMAGE2TXT_URL, { 60 | method: 'POST', 61 | headers: IMAGE2TXT_HEADERS, 62 | body: JSON.stringify({ 63 | "prompt": PROMPT, 64 | 'image': base64, 65 | }) 66 | 67 | }); 68 | 69 | const result = await response.json(); 70 | if(result.error_code){ 71 | responseBaidu.error_code = result.error_code; 72 | responseBaidu.error_msg = result.error_msg; 73 | }else{ 74 | responseBaidu.result = result.result; 75 | } 76 | 77 | //console.log("payload", content); 78 | } catch (error) { 79 | responseBaidu.error_code = 999999; 80 | responseBaidu.error_msg = error as string; 81 | //console.error(error); 82 | } 83 | return responseBaidu; 84 | } 85 | -------------------------------------------------------------------------------- /src/components/mdx-cards.tsx: -------------------------------------------------------------------------------- 1 | import { Dictionary } from '@/dictionaries' 2 | import { i18n, type Locale } from "@/i18n-config"; 3 | import { MDXRemote } from 'next-mdx-remote/rsc' 4 | import { Post } from "@/actions/posts"; 5 | import Image 6 | from 'next/image'; 7 | import Link from 'next/link'; 8 | type Props = { 9 | lang:Locale, 10 | dictionary: Dictionary["blog"], 11 | posts:Post[] 12 | } 13 | 14 | export default function MdxCards({lang,dictionary,posts}: Props) { 15 | 16 | 17 | return (<> 18 |
19 | 20 | {/* Card */} 21 | {posts.map((item, index) => ( 22 |
23 |
24 |
25 |

{item.frontMatter.date}

26 |
27 |
28 | images descriptions generated 29 |
30 |
31 |
32 |
33 | {/* Title */} 34 | 35 | {item.frontMatter.title} 36 | 37 |
38 |
39 | {/* Text 40 |

41 | {item.postContent} 42 |

43 | */} 44 | 45 |
46 |
47 |
48 | {/* Button */} 49 | {dictionary.more_button_lable} 50 |
51 |
52 | ))} 53 | 54 |
55 | ) 56 | } -------------------------------------------------------------------------------- /src/app/[lang]/(main)/about/page.tsx: -------------------------------------------------------------------------------- 1 | 2 | import Image from "next/image" 3 | import { getDictionary, Dictionary } from '@/dictionaries' 4 | import { Locale } from "@/i18n-config"; 5 | export default async function Page({ params: { lang } }: { params: { lang: Locale } }) { 6 | const dictionary: Dictionary = await getDictionary(lang); 7 | return ( 8 | <> 9 |
10 |
11 |
12 |
13 |

{dictionary.about.about_title}

14 |

15 | {dictionary.about.about_description} 16 |

17 |
18 |

{dictionary.about.contact_title}

19 |

20 | {dictionary.about.contact_description} 21 |

22 |
23 | 24 | 25 | 26 | 27 |

https://twitter.com/imgdesgen

28 |
29 |
30 | 31 | 32 | 33 | 34 |

imgdesgen@gmail.com

35 |
36 |
37 |
38 | aitools 39 |
40 |
41 |
42 |
43 | 44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /src/dictionaries.ts: -------------------------------------------------------------------------------- 1 | import 'server-only' 2 | import type { Locale } from '@/i18n-config' 3 | 4 | const dictionaries = { 5 | en: () => import('@/dictionaries/en.json').then((module) => module.default), 6 | zh: () => import('@/dictionaries/en.json').then((module) => module.default), 7 | de: () => import('@/dictionaries/en.json').then((module) => module.default), 8 | fr: () => import('@/dictionaries/en.json').then((module) => module.default), 9 | ja: () => import('@/dictionaries/en.json').then((module) => module.default), 10 | ko: () => import('@/dictionaries/en.json').then((module) => module.default), 11 | es: () => import('@/dictionaries/en.json').then((module) => module.default), 12 | } 13 | 14 | 15 | export const getDictionary = async (locale: Locale):Promise => 16 | dictionaries[locale]?.() ?? dictionaries.en(); 17 | 18 | 19 | export type Dictionary = { 20 | index: { 21 | title: string; 22 | subtitle: string; 23 | note: string; 24 | func_image_label: string; 25 | func_text_label: string; 26 | func_text_placeholder: string; 27 | func_submit_label: string; 28 | example_title: string; 29 | example_link_label: string; 30 | fqa_title: string; 31 | fqa_ask_1: string; 32 | fqa_answer_1: string; 33 | fqa_ask_2: string; 34 | fqa_answer_2: string; 35 | fqa_ask_3: string; 36 | fqa_answer_3: string; 37 | fqa_ask_4: string; 38 | fqa_answer_4: string; 39 | fqa_note_label: string; 40 | fqa_link_label: string; 41 | donation_label: string; 42 | donation_about: string; 43 | public_label: string; 44 | credits_label: string; 45 | }; 46 | about:{ 47 | about_title: string; 48 | about_description: string; 49 | contact_title: string; 50 | contact_description: string; 51 | }; 52 | contact : { 53 | title: string; 54 | subtitle: string; 55 | first_name: string; 56 | last_name: string; 57 | email: string; 58 | message: string; 59 | submit: string; 60 | }; 61 | navbar:{ 62 | logo_alt: string; 63 | logo_title: string; 64 | home: string; 65 | blog: string; 66 | about: string; 67 | contact: string; 68 | explore: string; 69 | language: string; 70 | pricing: string; 71 | theme_label: string; 72 | locale_label: string; 73 | }; 74 | footer:{ 75 | copyright:string; 76 | rights:string; 77 | privacy:string; 78 | terms:string; 79 | contact:string; 80 | coffee_buy_title:string; 81 | coffee_buy:string; 82 | }; 83 | blog: { 84 | title: string; 85 | more_button_lable: string; 86 | back_button_lable: string; 87 | author_lable: string 88 | }; 89 | policy: { 90 | back_button_lable: string 91 | }; 92 | pricing: { 93 | title: string; 94 | subtitle: string; 95 | pricing_description: string; 96 | ko_fi_tips1: string; 97 | ko_fi_tips2: string; 98 | prices: { 99 | type: string; 100 | variantId: string; 101 | name: string; 102 | price: number; 103 | duration: string; 104 | buy_lable: string; 105 | description: string[]; 106 | }[]; 107 | }; 108 | } -------------------------------------------------------------------------------- /src/components/select-dropdown.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useRef, useEffect } from 'react'; 2 | 3 | interface SelectDropdownProps { 4 | defaultValue?: Option; 5 | onChange?: (value: Option) => void; 6 | options: Option[]; 7 | name: string; 8 | id: string; 9 | } 10 | 11 | interface Option { 12 | value: string; 13 | label: string; 14 | disabled?: boolean; 15 | } 16 | 17 | export function SelectDropdown({ defaultValue, options, name, id, onChange }: SelectDropdownProps) { 18 | 19 | const [option, setOption] = useState(defaultValue ?? options[0]); 20 | const [isOpen, setIsOpen] = useState(false); 21 | const selectRef = useRef(null); 22 | 23 | useEffect(() => { 24 | const handleClickOutside = (event:MouseEvent) => { 25 | if (selectRef.current && !selectRef.current.contains(event.target)) { 26 | setIsOpen(false); 27 | } 28 | }; 29 | 30 | document.addEventListener('mousedown', handleClickOutside); 31 | return () => document.removeEventListener('mousedown', handleClickOutside); 32 | }, [selectRef]); 33 | 34 | function handleOptionClick(option: Option) { 35 | if (onChange) { 36 | onChange(option); 37 | } 38 | setOption(option); 39 | setIsOpen(false); 40 | }; 41 | 42 | return ( 43 |
44 | 45 |
46 | 47 | 48 | setIsOpen(!isOpen)} className="w-full text-sm bg-gray-50 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-900 dark:focus:ring-blue-500 dark:focus:border-blue-500 dark:border-gray-600" /> 49 | 52 |
53 | {isOpen && } 63 |
64 | ) 65 | } -------------------------------------------------------------------------------- /src/app/[lang]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { i18n, type Locale, localizations } from "@/i18n-config"; 2 | import { Inter } from "next/font/google"; 3 | import { getDictionary, Dictionary } from '@/dictionaries' 4 | import { ThemeProvider } from "@/components/theme-context"; 5 | import type { Metadata } from "next"; 6 | import { cookies } from 'next/headers'; 7 | import { dark, experimental__simple } from "@clerk/themes"; 8 | import { ClerkProvider } from '@clerk/nextjs' 9 | import { SpeedInsights } from "@vercel/speed-insights/next" 10 | import FooterSocial from "@/components/footer-social"; 11 | import { GoogleAnalytics } from "@next/third-parties/google"; 12 | import { NavProvider } from "@/components/nav-context"; 13 | import NavbarSticky from "@/components/navbar-sticky"; 14 | 15 | import "./globals.css"; 16 | 17 | const inter = Inter({ subsets: ["latin"] }); 18 | export async function generateStaticParams() { 19 | return i18n.locales.map((locale) => ({ lang: locale })); 20 | } 21 | export const metadata: Metadata = { 22 | title: "Free ai image description generator - 100% Free, No Login", 23 | description: "Exploring the Mysteries Behind the Image Using an AI Image Description Generator Tool.", 24 | twitter: { 25 | card: "summary_large_image", title: "Free ai image description generator - 100% Free, No Login", 26 | description: "Exploring the Mysteries Behind the Image Using an AI Image Description Generator Tool.", 27 | images: ["https://imagedescriptiongenerator.xyz/assets/og-explore.png"] 28 | }, 29 | openGraph: { 30 | type: "website", 31 | url: "https://imagedescriptiongenerator.xyz", 32 | title: "Free ai image description generator - 100% Free, No Login", 33 | description: "Exploring the Mysteries Behind the Image Using an AI Image Description Generator Tool.", 34 | siteName: "imagetodescription.ai", 35 | images: [{ 36 | url: "https://imagedescriptiongenerator.xyz/favicon/assets/og-explore.png", 37 | }] 38 | }, 39 | robots: { index: true, follow: true }, 40 | alternates: { canonical: "https://imagedescriptiongenerator.xyz" }, 41 | other: { "shortcut icon": "favicon.ico" } 42 | }; 43 | 44 | export default async function RootLayout({ 45 | children, params 46 | }: Readonly<{ 47 | children: React.ReactNode, 48 | params: { lang: Locale } 49 | }>) { 50 | const dictionary = await getDictionary(params.lang); 51 | const themeCookie = cookies().get('color-theme'); 52 | const theme = themeCookie ? themeCookie.value : 'dark'; 53 | const credits = "0"; 54 | 55 | return ( 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | {children} 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | ) 74 | } -------------------------------------------------------------------------------- /src/components/locale-switcher.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from 'react'; 3 | import { usePathname, useRouter } from "next/navigation"; 4 | import Link from "next/link"; 5 | import { i18n, type Locale, languages } from "@/i18n-config"; 6 | import { ChangeEvent, useState } from 'react'; 7 | import { Dictionary } from '@/dictionaries' 8 | export default function LocaleSwitcherSelect({ lang }: { lang: Locale }) { 9 | 10 | const router = useRouter(); 11 | const pathName = usePathname(); 12 | const redirectedPathName = (locale: Locale) => { 13 | if (!pathName) return "/"; 14 | const segments = pathName.split("/"); 15 | segments[1] = locale; 16 | return segments.join("/"); 17 | }; 18 | 19 | function onChangeHandler(event: React.ChangeEvent) { 20 | const locale = event.target.value as Locale; 21 | router.push(redirectedPathName(locale)); 22 | } 23 | 24 | return ( 25 |
26 |
27 | 34 | 37 |
38 |
39 | ); 40 | } 41 | 42 | 43 | export function LocaleSwitcherMenus({ lang ,dictionary }: {lang:Locale,dictionary: Dictionary["navbar"] }) { 44 | const [showOrHidden, setMenusState] = useState(false) 45 | const pathName = usePathname() 46 | const redirectedPathName = (locale: Locale) => { 47 | if (!pathName) return "/"; 48 | const segments = pathName.split("/"); 49 | segments[1] = locale; 50 | return segments.join("/"); 51 | }; 52 | return ( 53 | 54 |
55 | 61 | {showOrHidden && 62 |
63 |
64 | {languages.map((lg) => { 65 | return ( 66 |
67 | 72 |
73 | ); 74 | })} 75 |
76 |
77 | } 78 |
79 | 80 | ); 81 | } -------------------------------------------------------------------------------- /src/app/[lang]/(main)/lemon/page.tsx: -------------------------------------------------------------------------------- 1 | import { Locale } from "@/i18n-config"; 2 | import { getDictionary,Dictionary } from '@/dictionaries' 3 | import Button from "./button"; 4 | export default async function page({ params: { lang } }: { params: { lang: Locale } }) { 5 | const dictionary:Dictionary = await getDictionary(lang); 6 | 7 | return ( 8 |
9 |

10 | {dictionary.pricing.title} 11 |

12 |

13 | {dictionary.pricing.subtitle} 14 |

15 |

16 | 17 | {dictionary.pricing.pricing_description} 18 | 19 |

20 |

21 | {dictionary.pricing.ko_fi_tips1}{dictionary.pricing.ko_fi_tips2} ) 22 |

23 |

24 | 25 |

26 | 27 |
28 | {dictionary.pricing.prices.map((price, index) => { 29 | return ( 30 |
31 |
{price.name}
32 |
33 | $ 34 | {price.price} 35 | {price.duration} 36 |
37 |
38 |
    39 | {price.description.map((desc, index) => { 40 | return ( 41 |
  • 42 | 45 | {desc} 46 |
  • 47 | ) 48 | })} 49 | 50 |
51 |
52 |
53 |
55 | 56 |
57 | ) 58 | })} 59 | 60 |
61 |
62 | ) 63 | } -------------------------------------------------------------------------------- /src/components/alerts.tsx: -------------------------------------------------------------------------------- 1 | import React, {forwardRef,useState, useImperativeHandle } from 'react'; 2 | 3 | export interface AlertRef { 4 | openModal: (options: AlertProps) => void; 5 | closeModal: () => void; 6 | } 7 | 8 | interface AlertProps { 9 | type: number; 10 | message: string; 11 | title: string; 12 | } 13 | 14 | const Dialogs = forwardRef((props, ref) => { 15 | const [isOpen, setIsOpen] = useState('hidden'); 16 | const [type, setType] = useState(0); 17 | const [message, setMessage] = useState(''); 18 | const [title, setTitle] = useState(''); 19 | //const ref = useRef(null); 20 | 21 | useImperativeHandle(ref, () => ({ 22 | openModal: (options:AlertProps) => { 23 | setIsOpen('show'); 24 | setType(options.type); 25 | setTitle(options.title); 26 | setMessage(options.message); 27 | //console.log(options); 28 | }, 29 | closeModal: () => setIsOpen('hidden'), 30 | })); 31 | 32 | return ( 33 | 65 | ) 66 | }); 67 | Dialogs.displayName = 'Dialogs'; 68 | export default Dialogs; -------------------------------------------------------------------------------- /src/components/dialogs.tsx: -------------------------------------------------------------------------------- 1 | import React, {forwardRef,useState, useImperativeHandle } from 'react'; 2 | 3 | export interface DialogRef { 4 | openModal: (options: DialogProps) => void; 5 | closeModal: () => void; 6 | } 7 | 8 | interface DialogProps { 9 | type: number; 10 | message: string; 11 | title: string; 12 | } 13 | 14 | const Dialogs = forwardRef((props, ref) => { 15 | const [isOpen, setIsOpen] = useState('hidden'); 16 | const [type, setType] = useState(0); 17 | const [message, setMessage] = useState(''); 18 | const [title, setTitle] = useState(''); 19 | //const ref = useRef(null); 20 | 21 | useImperativeHandle(ref, () => ({ 22 | openModal: (options:DialogProps) => { 23 | setIsOpen('show'); 24 | setType(options.type); 25 | setTitle(options.title); 26 | setMessage(options.message); 27 | //console.log(options); 28 | }, 29 | closeModal: () => setIsOpen('hidden'), 30 | })); 31 | 32 | return ( 33 | 65 | ) 66 | }); 67 | Dialogs.displayName = 'Dialogs'; 68 | export default Dialogs; -------------------------------------------------------------------------------- /src/components/footer-logo.tsx: -------------------------------------------------------------------------------- 1 | import { Dictionary } from '@/dictionaries' 2 | import { type Locale } from "@/i18n-config"; 3 | export default function FooterLogo({ lang, dictionary }: { lang: Locale, dictionary: Dictionary }) { 4 | return ( 5 | 6 | 71 | 72 | ) 73 | } -------------------------------------------------------------------------------- /src/app/[lang]/(main)/paddle/page.tsx: -------------------------------------------------------------------------------- 1 | import { Locale } from "@/i18n-config"; 2 | import { getDictionary,Dictionary } from '@/dictionaries' 3 | import Button from "./button"; 4 | import { currentUser } from '@clerk/nextjs/server'; 5 | import { Environment } from '@paddle/paddle-node-sdk' 6 | 7 | export async function generateMetadata({ params }:{params: { lang: Locale }}) { 8 | return { 9 | alternates: { canonical: `https://imagedescriptiongenerator.xyz/${params.lang}/paddle` } 10 | } 11 | } 12 | 13 | export default async function page({ params: { lang } }: { params: { lang: Locale } }) { 14 | const dictionary:Dictionary = await getDictionary(lang); 15 | const paddle_token = String(process.env.PADDLE_API_AUTH_TOKEN); 16 | const paddle_env = process.env.PADDLE_API_ENV ? Environment.sandbox : Environment.production; //'production' | 'sandbox' 17 | const user = await currentUser(); 18 | let email =""; 19 | if(user && user.emailAddresses){ 20 | email = user.emailAddresses[0]?.emailAddress; 21 | } 22 | 23 | return ( 24 |
25 |

26 | {dictionary.pricing.title} 27 |

28 |

29 | {dictionary.pricing.subtitle} 30 |

31 |

32 | 33 | {dictionary.pricing.pricing_description} 34 | 35 |

36 |

37 | {dictionary.pricing.payment_tips1}{dictionary.pricing.ko_fi_tips2} ) 38 |

39 |

40 | 41 |

42 | 43 |
44 | {dictionary.pricing.prices.map((price, index) => { 45 | return ( 46 |
47 |
{price.name}
48 |
49 | $ 50 | {price.price} 51 | {price.duration} 52 |
53 |
54 |
    55 | {price.description.map((desc, index) => { 56 | return ( 57 |
  • 58 | 61 | {desc} 62 |
  • 63 | ) 64 | })} 65 | 66 |
67 |
68 |
69 |
71 | 72 |
73 | ) 74 | })} 75 | 76 |
77 |
78 | ) 79 | } -------------------------------------------------------------------------------- /src/app/[lang]/(main)/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import Link from "next/link"; 3 | import React, { useState } from 'react'; 4 | import UploadForm from "@/app/\/[lang]/(main)/upload-form"; 5 | import { getDictionary, Dictionary } from '@/dictionaries' 6 | import { Locale } from "@/i18n-config"; 7 | import { useUser } from "@clerk/nextjs"; 8 | import { IImage } from '@/actions/explore'; 9 | 10 | export default async function Home({ params: { lang } }: { params: { lang: Locale } }) { 11 | 12 | const dictionary: Dictionary = await getDictionary(lang); 13 | 14 | const user = await useUser; 15 | 16 | const imagesList:IImage[] = []; 17 | 18 | const fqas = [ 19 | { 20 | question: dictionary.index.fqa_ask_1, 21 | answer: dictionary.index.fqa_answer_1 22 | }, 23 | { 24 | question: dictionary.index.fqa_ask_2, 25 | answer: dictionary.index.fqa_answer_2 26 | }, 27 | { 28 | question: dictionary.index.fqa_ask_3, 29 | answer: dictionary.index.fqa_answer_3 30 | }, 31 | { 32 | question: dictionary.index.fqa_ask_4, 33 | answer: dictionary.index.fqa_answer_4 34 | } 35 | ]; 36 | 37 | const index = fqas.length % 2 === 0 ? fqas.length/2 : fqas.length/2+1; 38 | const leftFqas = fqas.slice(0, index); 39 | const rightFqas = fqas.slice(index); 40 | 41 | return ( 42 |
43 | 44 |
45 | 46 |
47 | 48 | {/* Example Use Cases */} 49 |
50 |
51 |

{dictionary.index.example_title}

52 | 53 |
54 | {imagesList.map((image, index) => ( 55 | 56 |
57 | 58 | {image.keys}/ 59 | 60 |
61 |

{image.description}

62 |
63 |
64 | )) 65 | } 66 |
67 |
68 | {dictionary.index.example_link_label} 69 |
70 |
71 |
72 | 73 | {/* #FQA */} 74 | 75 |
76 |
77 |

{dictionary.index.fqa_title}

78 |
79 | 80 | 81 |
82 |

{dictionary.index.fqa_note_label} 83 | {dictionary.index.fqa_link_label}. 84 |

85 |
86 |
87 |
88 | ); 89 | } 90 | 91 | interface FQA { 92 | question: string; 93 | answer: string; 94 | } 95 | 96 | function FQAPage ({ list}: { list: FQA[]}) { 97 | return ( 98 |
99 | { 100 | list.map((faq, index) => ( 101 |
102 |

103 | 106 | {faq.question} 107 |

108 |

109 | {faq.answer} 110 |

111 |
112 | )) 113 | } 114 |
115 | ) 116 | } -------------------------------------------------------------------------------- /src/dictionaries/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "index": { 3 | "title": "Free AI Image Description Generator", 4 | "subtitle": "Unlock the secrets of images with AI! Upload your artwork,and let AI reveal the hidden details, emotions, and meanings behind it. Artists, designers, or anyone interested, this \"image description generator\" offers a fresh perspective on your creations. Try it now!", 5 | "note": "As this is an early test version, some functions may not be stable. We appreciate your patience and understanding. If you encounter any problems or have any suggestions, please feel free to contact us. Your feedback is valuable to us and will help us improve the tool.", 6 | "func_image_label": "1. UPLOAD AN IMAGE OR PHOTO (MAX 2MB)", 7 | "func_text_label": "2. IMAGE OR PHOTO DESCRIPTION", 8 | "func_text_placeholder": "This is a text description of the image generated by AI recognition.", 9 | "func_submit_label": "Generate Description", 10 | "example_title": "Example Use Cases", 11 | "example_link_label": "Explore All Examples", 12 | "fqa_title": "Frequently asked questions", 13 | "fqa_ask_1": "xxxx?", 14 | "fqa_answer_1": "xxx!", 15 | "fqa_ask_2": "xxx?", 16 | "fqa_answer_2": "xxx", 17 | "fqa_ask_3": "xxx?", 18 | "fqa_answer_3": "xxx", 19 | "fqa_ask_4": "xxx?", 20 | "fqa_answer_4": "xxx", 21 | "fqa_note_label": "Can’t find the answer you’re looking for? Reach out to our ", 22 | "fqa_link_label": "customer support team", 23 | "donation_label": "Buy Me a Coffee", 24 | "donation_about": "xxxx", 25 | "public_label": "I agree to publicly display this content on ImageAI.QA.", 26 | "credits_label": "It will cost credits: 1" 27 | }, 28 | "about": { 29 | "about_title": "About us", 30 | "about_description": "xxxxx", 31 | "contact_title": "Contact Me", 32 | "contact_description": "xxxx" 33 | }, 34 | "contact": { 35 | "title": "Contact Us", 36 | "subtitle": "xxx", 37 | "first_name": "First Name", 38 | "last_name": "Last Name", 39 | "email": "Email address", 40 | "message": "Your Message", 41 | "submit": "Let's Talk" 42 | }, 43 | "navbar": { 44 | "logo_alt": "xxx", 45 | "logo_title": "xxxx", 46 | "home": "Home", 47 | "blog": "Blog", 48 | "about": "About", 49 | "pricing": "Pricing", 50 | "contact": "Contact", 51 | "explore": "Explore", 52 | "language": "Languages", 53 | "theme_label": "Theme", 54 | "locale_label": "Local" 55 | }, 56 | "footer": { 57 | "copyright": "Copyright © 2019 ImageAI.QA", 58 | "rights": "All Rights Reserved", 59 | "privacy": "Privacy Policy", 60 | "terms": "Terms of Service", 61 | "contact": "Contact Us", 62 | "coffee_buy_title": "Enjoy ImageAI.QA?", 63 | "coffee_buy": "Buy Me a Coffee" 64 | }, 65 | "blog": { 66 | "title": "The latest “images descriptions generated” news", 67 | "more_button_lable": "Read More", 68 | "back_button_lable": "Back to Blog", 69 | "author_lable": "Posted by" 70 | }, 71 | "policy": { 72 | "back_button_lable": "Back to Home" 73 | }, 74 | "pricing": { 75 | "title": "AI Image Description Generator Pricing", 76 | "subtitle": "Choose a plan to buy credits, Unleash your creativity with AI Image Description Generator.", 77 | "pricing_description": "Image description generation utilizes a credit-based system, with each image requiring 1 credit.", 78 | "ko_fi_tips1": "☕ ko-fi Tip: Don't forget your ImageAI.QA username in the ko-fi payment message! 😉 ( Forgot? ", 79 | "ko_fi_tips2": "contact us", 80 | "prices": [ 81 | { 82 | "type": "0", 83 | "variantId": "6f78fe41-xxx-46c6-8770-xxx", 84 | "name": "Free", 85 | "price": 0, 86 | "duration": "", 87 | "buy_lable": "Get Started", 88 | "description": [ 89 | "Includes 10 credits for image descriptions", 90 | "Maximum upload size of 2MB per image", 91 | "Normal Generation Speed", 92 | "Powered by the basic model" 93 | ] 94 | }, 95 | { 96 | "type": "1", 97 | "variantId": "097a7cfc-e9da-xxx-b065-xxx", 98 | "name": "Single payment", 99 | "price": 9.9, 100 | "duration": "", 101 | "buy_lable": "Buy plan", 102 | "description": [ 103 | "Includes 100 credits for Image Descriptions", 104 | "Maximum upload size of 5MB per image", 105 | "1 Month Validity", 106 | "Fast Generation Speed", 107 | "Powered by advanced models" 108 | ] 109 | }, 110 | { 111 | "type": "2", 112 | "variantId": "52080244-22f7-xxx-84b2-xxx", 113 | "name": "Pro plan", 114 | "price": 9.9, 115 | "duration": "per user / month", 116 | "buy_lable": "Buy plan", 117 | "description": [ 118 | "Includes 100 credits for Image Descriptions", 119 | "Maximum upload size of 5MB per image", 120 | "1 Month Validity", 121 | "Fast Generation Speed", 122 | "Powered by advanced models" 123 | ] 124 | } 125 | ] 126 | } 127 | } -------------------------------------------------------------------------------- /src/components/imagepicker.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import Image from 'next/image'; 3 | import { ChangeEvent, useState, useRef,ClipboardEvent } from 'react'; 4 | import Dialogs, { AlertRef } from "@/components/alerts"; 5 | 6 | type ImagePickerProps = { 7 | onImageSelected?: (selectedImage: File | null) => void; 8 | member: boolean; 9 | } 10 | 11 | export default function ImagePicker({ onImageSelected,member }: ImagePickerProps) { 12 | 13 | const alertRef = useRef(null); 14 | const [selectedImage, setSelectedImage] = useState(null); 15 | const [isPickerShow, setIsPickerShow] = useState(false); 16 | 17 | //定义一个大小4M 18 | const MAX_FILE_SIZE_IN_BYTES_2M = 2 * 1024 * 1024; // 2 MB 19 | const MAX_FILE_SIZE_IN_BYTES_4M = 4 * 1024 * 1024; // 4 MB 20 | const FILE_SELECT_ERROR_MESSAGE = 'Failed to load the selected image. Please try again.'; 21 | const SIZE_EXCEEDS_LIMIT_MESSAGE = 'The selected file exceeds the maximum allowed size.'; 22 | const MAX_FILE_SIZE_IN_BYTES = member ? MAX_FILE_SIZE_IN_BYTES_4M : MAX_FILE_SIZE_IN_BYTES_2M; 23 | 24 | function handleImageSelect (e: ChangeEvent){ 25 | if (!e.target.files || e.target.files.length === 0) { 26 | setErrorTips(FILE_SELECT_ERROR_MESSAGE); 27 | return; 28 | } 29 | 30 | const file = e.target.files[0]; 31 | if (!file.type.startsWith('image/')) { 32 | setErrorTips(); 33 | return; 34 | } 35 | if (file.size > MAX_FILE_SIZE_IN_BYTES) { 36 | setErrorTips(SIZE_EXCEEDS_LIMIT_MESSAGE); 37 | return; 38 | } 39 | const reader = new FileReader(); 40 | reader.onload = () => { 41 | setSelectedImage(reader.result as string); 42 | setIsPickerShow(true); 43 | if (typeof onImageSelected === 'function') { 44 | onImageSelected(null); 45 | } 46 | }; 47 | reader.onerror = function () { 48 | setErrorTips(FILE_SELECT_ERROR_MESSAGE); 49 | }; 50 | reader.readAsDataURL(file); 51 | }; 52 | 53 | 54 | function setErrorTips(error: string = "select an image") { 55 | 56 | alertRef.current?.openModal({ type: 0, title: "Oops!", message: error }) 57 | } 58 | 59 | function pasteHandeler(e: ClipboardEvent) { 60 | const items = e.clipboardData?.items; 61 | if (!items || !items[0])return; 62 | const file:File = items[0].getAsFile() as File; 63 | if (!file.type.startsWith('image/')) { 64 | setErrorTips(); 65 | return; 66 | } 67 | if (file && file.size > MAX_FILE_SIZE_IN_BYTES) { 68 | setErrorTips(SIZE_EXCEEDS_LIMIT_MESSAGE); 69 | return; 70 | } 71 | 72 | const reader = new FileReader(); 73 | reader.onload = () => { 74 | setSelectedImage(reader.result as string); 75 | setIsPickerShow(true); 76 | if (typeof onImageSelected === 'function') { 77 | onImageSelected(file); 78 | } 79 | } 80 | reader.onerror = function () { 81 | setErrorTips(FILE_SELECT_ERROR_MESSAGE); 82 | }; 83 | reader.readAsDataURL(file); 84 | } 85 | 86 | return ( 87 | <> 88 | 89 |
90 | 92 |
93 | 97 |
98 |
99 |
100 | 106 |
107 | 111 |

or copy and paste

112 |
113 |

PNG, JPG up to {member? '4MB':'2MB'}

114 |
115 | 116 | ); 117 | }; -------------------------------------------------------------------------------- /src/app/api/home/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { runChatFromGeminiStreamResult } from "@/lib/gemini-client"; 3 | import { GenerateContentStreamResult, EnhancedGenerateContentResponse, HarmProbability } from "@google/generative-ai"; 4 | import { auth } from '@clerk/nextjs/server'; 5 | import { ImageDescriptionResponse } from "@/types/responses"; 6 | import { putObject } from '@/lib/aws-client-s3' 7 | 8 | export const dynamic = 'force-dynamic' 9 | export const runtime = 'edge' //nodejs edge 10 | 11 | 12 | const enhancedResponse: EnhancedGenerateContentResponse = { 13 | 14 | text: () => { 15 | return ""; 16 | }, 17 | functionCall: () => { 18 | return undefined; 19 | }, 20 | functionCalls: () => { 21 | return undefined; 22 | }, 23 | 24 | }; 25 | 26 | export async function POST(request: NextRequest) { 27 | 28 | 29 | const formData = await request.formData(); 30 | const file = formData.get('file') as File; 31 | const lang = formData.get('lang') as string; 32 | 33 | const format = file.name.split('.').pop() as string; 34 | const timestamp = Date.now(); 35 | const randomNum = Math.floor(Math.random() * 1000); 36 | const uniqueFileName = `${timestamp}-${randomNum}.${format}`; 37 | 38 | const isPublic = formData.get('public') as string; 39 | 40 | const fileBuffer = await file.arrayBuffer(); 41 | const buffer = Buffer.from(fileBuffer); 42 | const base64 = buffer.toString('base64'); 43 | 44 | const stream = makeGeminiStreamJSON(fetchGeminiItemsJSON(base64, lang, buffer, uniqueFileName, isPublic)); 45 | 46 | const response = new StreamingResponse(stream, { 47 | headers: { 'Content-Type': 'application/json; charset=utf-8' } 48 | }); 49 | return response; 50 | 51 | } 52 | 53 | async function* fetchGeminiItemsJSON(base64: string, lang: string, buffer: Buffer, fileName: string, isPublic: string): AsyncGenerator { 54 | 55 | try { 56 | 57 | if (!base64) { 58 | const error_Response = { error_code: 999999, error_msg: "image is empty" }; 59 | yield error_Response; 60 | return; 61 | } 62 | 63 | // const { userId } = auth(); 64 | // if (!userId) { 65 | // const no_Auth_Response = {error_code: 999998,error_msg: "please login ImageAI.QA"}; 66 | // yield no_Auth_Response; 67 | // return; 68 | // } 69 | 70 | let description = ''; 71 | let keys = ''; 72 | let foundDelimiter = false; 73 | let chunkText; 74 | const stream = await runChatFromGeminiStreamResult(base64, lang) as GenerateContentStreamResult; 75 | for await (const chunk of stream.stream) { 76 | chunkText = chunk.text(); 77 | //console.log(chunk); 78 | for (const candidate of chunk.candidates ?? []) { 79 | for (const safetyRating of candidate.safetyRatings ?? []) { 80 | if (safetyRating.probability === HarmProbability.MEDIUM || safetyRating.probability === HarmProbability.HIGH) { 81 | //console.log(safetyRating); 82 | const error_Response = { 83 | error_code: 999996, 84 | error_msg: `I cannot generate a response. Let's keep our conversation polite and respectful.`, 85 | }; 86 | yield error_Response; 87 | return; 88 | } 89 | } 90 | } 91 | 92 | description += chunkText; 93 | if (!foundDelimiter && findContentEnd(description) !== null) { 94 | foundDelimiter = true; 95 | const parts = description.split('|'); 96 | if (parts.length == 3) { 97 | keys = parts[1]; 98 | description = parts[2]; 99 | chunkText = description; 100 | } 101 | } 102 | if (foundDelimiter) { 103 | const imageResponse: ImageDescriptionResponse = {}; 104 | imageResponse.description = chunkText; 105 | yield imageResponse; 106 | } 107 | 108 | } 109 | 110 | 111 | 112 | if (isPublic) { 113 | 114 | if (description.length > 50) { 115 | const etag = await putObject(buffer, fileName); 116 | } 117 | } 118 | 119 | } catch (error) { 120 | console.error(error); 121 | const error_Response = { error_code: 999997, error_msg: error?.toString() }; 122 | yield error_Response; 123 | } 124 | 125 | } 126 | 127 | 128 | const makeGeminiStreamJSON = >(generator: AsyncGenerator) => { 129 | 130 | const encoder = new TextEncoder(); 131 | return new ReadableStream({ 132 | async start(controller) { 133 | controller.enqueue(encoder.encode("")); 134 | for await (let chunk of generator) { 135 | const chunkData = encoder.encode(JSON.stringify(chunk)); 136 | controller.enqueue(chunkData); 137 | } 138 | controller.close(); 139 | } 140 | }); 141 | } 142 | 143 | class StreamingResponse extends Response { 144 | 145 | constructor(res: ReadableStream, init?: ResponseInit) { 146 | super(res as any, { 147 | ...init, 148 | status: 200, 149 | headers: { 150 | ...init?.headers, 151 | }, 152 | }); 153 | } 154 | } 155 | 156 | 157 | function findContentEnd(buffer: string): number | null { 158 | const stack = []; 159 | for (let i = 0; i < buffer.length; i++) { 160 | const char = buffer[i]; 161 | if (stack.length == 0 && char === '|') { 162 | stack.push(char); 163 | } else if (char === '|' && stack.length > 0 && stack[stack.length - 1] === '|') { 164 | stack.pop(); 165 | } 166 | if (stack.length === 0) { 167 | return i; 168 | } 169 | } 170 | return null; 171 | } -------------------------------------------------------------------------------- /src/app/[lang]/(main)/explore/images-form.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { IImage } from '@/actions/explore'; 3 | import { LoadingFull } from '@/components/loading-from'; 4 | import { useRef, useState, useActionState,useEffect } from "react"; 5 | import { useFormStatus } from "react-dom"; 6 | import { useInView } from 'react-intersection-observer' 7 | import { ClientMdxPage } from '@/components/mdx-page-client'; 8 | 9 | export function ImagesForm({ images,pageParam }: { images: IImage[],pageParam:number }) { 10 | 11 | console.log("init page:"+pageParam); 12 | const[tips,setTips] = useState('Loading...'); 13 | 14 | const [imageList, setImageList] = useState(images); 15 | const [searchText, setSearchText] = useState(''); 16 | const [hasMore, setHasMore] = useState(true); 17 | const [page, setPage] = useState(pageParam); 18 | const { ref, inView } = useInView(); 19 | 20 | async function formAction(formData: FormData) { 21 | 22 | setTips('Loading...'); 23 | setPage(1); 24 | setHasMore(true); 25 | const keys = formData.get('keys') as string; 26 | setSearchText(keys); 27 | const imageList:any = []; 28 | setImageList(imageList); 29 | } 30 | 31 | 32 | 33 | async function loadMore() { 34 | 35 | if (!hasMore) return; 36 | const pageNum = Number(page)+1; 37 | setPage(pageNum); 38 | 39 | const imageList:any = []; 40 | if (!imageList || imageList.length === 0 || imageList.length < 9) { 41 | setHasMore(false); 42 | setTips('No more data'); 43 | } 44 | if(imageList.length > 0){ 45 | setImageList((prevImages)=>([...prevImages,...imageList])); 46 | } 47 | } 48 | 49 | useEffect(() => { 50 | if (inView) { 51 | loadMore() 52 | } 53 | }, [inView]) 54 | 55 | return ( 56 | <> 57 |
58 | 59 | 60 |
61 |
62 | 63 | 64 | 65 | 66 |
67 | 68 |
69 | 70 | 71 | 72 |
73 | {imageList.length>0?imageList.map((image, index) => ( 74 | 75 |
76 | 77 | {image.keys}/ 78 | 79 |
80 | {/*

{image.description}

*/} 81 |
82 | 83 |
84 |
85 |
86 | )):

No results for {`"${searchText}"`}

87 | } 88 |
89 | {tips} 90 |
91 |
92 | 93 | 94 | ) 95 | 96 | } 97 | 98 | function SubmitLoading() { 99 | const { pending } = useFormStatus(); 100 | return ( 101 | 111 | ); 112 | } -------------------------------------------------------------------------------- /src/app/[lang]/(main)/contact/contact-form.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { useFormStatus, useFormState } from "react-dom"; 3 | import { Dictionary } from '@/dictionaries' 4 | import { addContactInfo } from "@/actions/contact"; 5 | import React, { useRef } from "react"; 6 | import { useRouter } from "next/navigation"; 7 | import { useEffect } from "react"; 8 | import Dialogs,{DialogRef} from "@/components/dialogs"; 9 | 10 | 11 | 12 | export default function ContactForm({ dictionary }: { dictionary: Dictionary }) { 13 | 14 | const [state, action] = useFormState(addContactInfo, undefined) 15 | const dialogRef = useRef(null); 16 | 17 | const formRef = useRef(null); 18 | const router = useRouter(); 19 | 20 | 21 | useEffect(() => { 22 | 23 | if (state?.success) { 24 | dialogRef.current?.openModal({type: 1, title: "Success!", message: state?.message}) 25 | formRef.current?.reset(); 26 | router.back(); 27 | }else if (state?.message) { 28 | 29 | dialogRef.current?.openModal({type: 0, title: "Oops!", message: state?.message}) 30 | } 31 | }, [state]); 32 | 33 | 34 | 35 | return ( 36 | <> 37 | {/* Contact Form */} 38 | 39 |
40 |
41 |
42 | 43 | 47 | {state?.errors?.first_name &&

{state.errors.first_name}

} 48 | 49 | 50 |
51 |
52 | 53 | 57 | {state?.errors?.last_name &&

{state.errors.last_name}

} 58 |
59 |
60 |
61 | 62 | 66 | {state?.errors?.email &&

{state.errors.email}

} 67 |
68 |
69 | 70 | 74 | {state?.errors?.message &&

{state.errors.message}

} 75 |
76 | 77 | 78 | 79 | ) 80 | } 81 | 82 | function SubmitLoading({ dictionary }: { dictionary: Dictionary["contact"] }) { 83 | 84 | const { pending } = useFormStatus(); 85 | return ( 86 | 99 | ); 100 | } -------------------------------------------------------------------------------- /src/components/footer-social.tsx: -------------------------------------------------------------------------------- 1 | import { Dictionary } from '@/dictionaries' 2 | import { type Locale } from "@/i18n-config"; 3 | export default function FooterSocial({ lang, dictionary }: { lang: Locale, dictionary: Dictionary }) { 4 | return ( 5 | 6 | 101 | 102 | ) 103 | } -------------------------------------------------------------------------------- /src/app/[lang]/(main)/upload-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useFormStatus } from "react-dom"; 3 | import { useEffect, useRef, useContext } from "react"; 4 | import ImagePicker from "@/components/imagepicker"; 5 | import { useState } from "react"; 6 | import { LoadingOverlay } from "@/components/loading-from"; 7 | import { Dictionary } from '@/dictionaries' 8 | import { Locale } from "@/i18n-config"; 9 | import ReactMarkdown from 'react-markdown'; 10 | import { ImageDescriptionResponse } from "@/types/responses"; 11 | import ShiftAndByteBufferMatcher from "@/scripts/ShiftAndByteBufferMatcher"; 12 | import Dialogs, { DialogRef } from "@/components/dialogs"; 13 | import NavContext from "@/components/nav-context"; 14 | import { useAuth } from "@clerk/nextjs"; 15 | import { RemoteMdxPage } from "@/components/mdx-page-remote"; 16 | import { defaultComponents } from '@/components/mdx-page-client'; 17 | 18 | 19 | export default function UploadForm({ lang, dictionary }: { lang: Locale, dictionary: Dictionary["index"] }) { 20 | 21 | const formRef = useRef(null); 22 | const [imageDesc, setImageDesc] = useState(''); 23 | 24 | const dialogRef = useRef(null); 25 | const { setValue } = useContext(NavContext); 26 | const { userId } = useAuth(); 27 | 28 | const handleImageSelected = (selectedImage: string | null) => { 29 | //console.log('Image selected:', selectedImage); 30 | setImageDesc(''); 31 | }; 32 | 33 | async function formAction(formData: FormData) { 34 | 35 | formData.set("lang", lang); 36 | const it = streamingFetch(`/api/home`, { 37 | method: 'POST', 38 | body: formData, 39 | }) 40 | 41 | for await (let value of it) { 42 | const individualJSONObjects = value.split('{'); 43 | for (const individualJSON of individualJSONObjects) { 44 | if (individualJSON.length > 0) { 45 | try { 46 | //console.log('Parsing one JSON:', individualJSON); 47 | const parsedJSON = JSON.parse(`{${individualJSON}`) as ImageDescriptionResponse; // Add braces for valid parsing 48 | if (parsedJSON.error_code) { 49 | 50 | setErrorTips(parsedJSON.error_msg); 51 | break; 52 | } 53 | setImageDesc((prev) => prev + parsedJSON.description); 54 | 55 | } catch (error) { 56 | console.error('Error parsing JSON:', error); 57 | } 58 | } 59 | } 60 | } 61 | if (userId) { 62 | const credits = "0"; 63 | setValue(credits); 64 | } 65 | 66 | formRef.current?.reset(); 67 | 68 | 69 | function setErrorTips(error: string = "please select image") { 70 | setImageDesc(''); 71 | dialogRef.current?.openModal({ type: 0, title: "Oops!", message: error }) 72 | } 73 | } 74 | 75 | return ( 76 | <> 77 | 78 |
79 | 80 | 81 | 82 |

{dictionary.title}

83 | 84 |

{dictionary.subtitle}

85 | 86 | {userId &&
87 |

{dictionary.credits_label}

88 |
89 | } 90 |
91 | 92 |
93 | 94 |
95 | 96 |
97 |
98 |
99 | 100 |
101 | 102 | 103 | 104 | {imageDesc === '' ? `${dictionary.func_text_placeholder}` : imageDesc} 105 | 106 | {/* 107 | 111 | */} 112 |
113 | {/*

{dictionary.note}

*/} 114 |
115 |
116 | 117 |
118 | 119 |
120 |

{dictionary.donation_about}☕😊    {dictionary.donation_label} ☕️

121 |
122 | 123 |
124 | 125 |
126 |
127 | 128 | 129 | 130 | 131 | ); 132 | } 133 | 134 | function Submit() { 135 | const { pending } = useFormStatus(); 136 | return ( 137 | 138 | ); 139 | } 140 | 141 | 142 | 143 | function SubmitLoading({ dictionary }: { dictionary: Dictionary["index"] }) { 144 | const { pending } = useFormStatus(); 145 | return ( 146 | 153 | ); 154 | } 155 | 156 | async function* streamingFetch(input: RequestInfo | URL, init?: RequestInit) { 157 | 158 | const response = await fetch(input, init) 159 | const bytesStream = convertStreamToIterator(response.body!); 160 | const parserBuff = new ShiftAndByteBufferMatcher(); 161 | for await (const chunk of parserBuff.processStreamingData(bytesStream)) { 162 | yield chunk; 163 | } 164 | } 165 | 166 | 167 | 168 | async function* convertStreamToIterator(stream: ReadableStream): AsyncIterableIterator { 169 | const reader = stream.getReader(); 170 | 171 | for (; ;) { 172 | const { done, value } = await reader?.read() ?? { done: false, value: undefined } 173 | if (done) break; 174 | 175 | try { 176 | yield value 177 | } 178 | catch (e: any) { 179 | console.warn(e.message) 180 | } 181 | 182 | } 183 | } -------------------------------------------------------------------------------- /src/components/navbar-sticky.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { useState, useContext } from 'react'; 3 | import Link from "next/link"; 4 | import { usePathname } from 'next/navigation' 5 | import { Dictionary } from '@/dictionaries' 6 | import { i18n, type Locale, languages, languagesKV } from "@/i18n-config"; 7 | import { SignInButton, UserButton, SignedIn, SignedOut, SignUpButton, useAuth } from "@clerk/nextjs"; 8 | import NavContext from "@/components/nav-context"; 9 | import ThemeContext, { Theme } from "@/components/theme-context"; 10 | import { ThemeDarkIcon, ThemeLightIcon } from '@/components/theme-icon'; 11 | import { dark, experimental__simple } from "@clerk/themes"; 12 | 13 | type Props = { 14 | lang: Locale; 15 | dictionary: Dictionary; 16 | } 17 | 18 | type UserAgent = 'mobile' | 'desktop'; 19 | 20 | 21 | export default function NavbarSticky({ lang, dictionary }: Props) { 22 | 23 | const [collapseMobile, setCollapseMobile] = useState("hidden"); 24 | 25 | const { theme, setTheme } = useContext(ThemeContext); 26 | 27 | const {value, setValue} = useContext(NavContext); 28 | 29 | const { userId } = useAuth(); 30 | 31 | const pathname = usePathname() 32 | 33 | 34 | const links = [ 35 | { name: `${dictionary.navbar.home}`, href: `/${lang}` }, 36 | { name: `${dictionary.navbar.explore}`, href: { pathname: `/${lang}/explore`,query: { page: 1} }}, 37 | { name: `${dictionary.navbar.blog}`, href: `/${lang}/blog` }, 38 | { name: `${dictionary.navbar.about}`, href: `/${lang}/about` }, 39 | { name: `${dictionary.navbar.pricing}`, href: `/${lang}/paddle` }, 40 | // { name: `${dictionary.navbar.pricing}`, href: `/${lang}/lemon` }, 41 | ] 42 | 43 | function handleThemeChange(themeName: Theme) { 44 | 45 | setTheme(themeName); 46 | ThemeChange() 47 | } 48 | 49 | function handleClick(state: string) { 50 | setCollapseMobile(state); 51 | } 52 | 53 | function signUpOrInSwitcher() { 54 | if (!pathname || pathname.search("/sign-in") !== -1) { 55 | 56 | return true; 57 | } 58 | return false; 59 | } 60 | 61 | return ( 62 | 152 | 153 | 154 | ); 155 | } 156 | 157 | 158 | async function* streamingFetch(input: RequestInfo | URL, init?: RequestInit) { 159 | 160 | const response = await fetch(input, init) 161 | yield await response.json(); 162 | } 163 | 164 | function LanguageSwitcher({ lang, userAgent, dictionary }: { lang: Locale, userAgent: UserAgent, dictionary: Dictionary["navbar"] }) { 165 | 166 | const pathname = usePathname() 167 | //是否显示语言选择下拉框 168 | const [showLanguageSwitcher, setShowLanguageSwitcher] = useState("hidden"); 169 | const redirectedPathName = (locale: Locale) => { 170 | if (!pathname) return "/"; 171 | const segments = pathname.split("/"); 172 | segments[1] = locale; 173 | return segments.join("/"); 174 | }; 175 | 176 | if (userAgent === 'desktop') { 177 | 178 | return ( 179 |
180 | 184 |
185 | 199 |
200 |
201 | ) 202 | } else { 203 | 204 | return ( 205 | 206 |
207 | 208 |
209 | {dictionary.locale_label} 210 |
211 | 212 |
213 | 220 | 221 |
222 | 236 |
237 |
238 |
239 | ) 240 | } 241 | } 242 | 243 | function ThemeSwitcher({ dictionary }: { dictionary: Dictionary["navbar"] }) { 244 | const [showThemeSwitcher, setShowThemeSwitcher] = useState("hidden"); 245 | 246 | const { theme, setTheme } = useContext(ThemeContext); 247 | 248 | const themes = [ 249 | { name: `light` }, 250 | { name: `dark` }, 251 | ] 252 | function handleThemeChange(themeName: Theme) { 253 | 254 | setTheme(themeName); 255 | setShowThemeSwitcher("hidden"); 256 | ThemeChange() 257 | } 258 | return ( 259 |
260 |
261 | {dictionary.theme_label} 262 |
263 |
264 | 271 |
272 |
    273 | {themes.map((theme, index) => { 274 | return ( 275 |
  • 276 | 282 |
  • 283 | ); 284 | })} 285 |
286 |
287 |
288 |
289 | 290 | ) 291 | } 292 | 293 | function ThemeChange() { 294 | 295 | if (document.documentElement.classList.contains('dark')) { 296 | document.documentElement.classList.remove('dark'); 297 | document.documentElement.classList.add('light'); 298 | localStorage.setItem('color-theme', 'light'); 299 | document.cookie = `color-theme=light; path=/`; 300 | } else { 301 | document.documentElement.classList.remove('light'); 302 | document.documentElement.classList.add('dark'); 303 | localStorage.setItem('color-theme', 'dark'); 304 | document.cookie = `color-theme=dark; path=/`; 305 | } 306 | } --------------------------------------------------------------------------------