├── 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 |
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 |
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 |
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 |
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 | {btnlabel}
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 |
28 | )
29 | }
--------------------------------------------------------------------------------
/src/components/theme-icon.tsx:
--------------------------------------------------------------------------------
1 |
2 | export function ThemeDarkIcon() {
3 |
4 | return(
5 |
6 |
7 |
8 | )
9 | }
10 |
11 | export function ThemeLightIcon() {
12 |
13 |
14 | return (
15 |
16 |
17 |
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 |
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 |
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 | 
25 | 
26 | 
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 | [](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 | {btnlabel}
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 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
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 | 
25 | 
26 | 
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 | [](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 |
32 | {copied ?
33 |
34 |
35 |
36 | :
37 |
38 |
39 |
40 |
41 |
42 |
43 | }
44 |
45 |
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 |
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 |
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 |
29 |
36 |
37 |
38 |
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 |
53 | {isOpen &&
54 |
55 |
56 | {options.map((option) => (
57 | handleOptionClick(option)} className={`${ option.disabled? 'pointer-events-none text-gray-400 dark:text-gray-500 select-none ': 'text-gray-700 dark:text-gray-200'} inline-flex cursor-pointer w-full px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white`}>
58 | {option.label}
59 | ))
60 | }
61 |
62 |
}
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 |
28 | {languages.map((lg) => {
29 | return (
30 | {lg.name}
31 | );
32 | })}
33 |
34 |
35 |
36 |
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 |
setMenusState(!showOrHidden)}>
56 | {dictionary.language}
57 |
58 |
59 |
60 |
61 | {showOrHidden &&
62 |
63 |
64 | {languages.map((lg) => {
65 | return (
66 |
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 |
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 |
43 |
44 |
45 | {desc}
46 |
47 | )
48 | })}
49 |
50 |
51 |
52 |
53 |
54 |
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 |
7 |
8 |
9 |
16 |
17 |
18 |
HELP AND SUPPORT
19 |
30 |
31 |
45 |
56 |
57 |
58 |
59 |
60 |
© 2024 ImageAI.QA™ . All Rights Reserved.
61 |
62 |
68 |
69 |
70 |
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 |
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 |
59 |
60 |
61 | {desc}
62 |
63 | )
64 | })}
65 |
66 |
67 |
68 |
69 |
70 |
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 |
47 |
48 | {/* Example Use Cases */}
49 |
50 |
51 |
{dictionary.index.example_title}
52 |
53 |
54 | {imagesList.map((image, index) => (
55 |
56 |
57 |
58 |
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 |
104 |
105 |
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 |
94 | {selectedImage && }
96 |
97 |
98 |
99 |
100 |
102 |
105 |
106 |
107 |
109 | Click to Upload
110 |
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 |
71 |
72 |
73 | {imageList.length>0?imageList.map((image, index) => (
74 |
75 |
76 |
77 |
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 |
102 | {pending ?
103 |
104 |
105 | :
106 |
107 |
108 | }
109 | Search
110 |
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 |
78 | >
79 | )
80 | }
81 |
82 | function SubmitLoading({ dictionary }: { dictionary: Dictionary["contact"] }) {
83 |
84 | const { pending } = useFormStatus();
85 | return (
86 |
87 | {
88 | pending &&
89 |
90 |
91 |
92 | }
93 | {dictionary.submit}
94 |
95 | Arrow Right
96 |
97 |
98 |
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 |
7 |
8 |
9 |
55 |
56 |
57 |
HELP AND SUPPORT
58 |
69 |
70 |
84 |
98 |
99 |
100 |
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 |
{dictionary.func_image_label}
94 |
95 |
96 |
97 |
98 |
99 |
{dictionary.func_text_label}
100 |
101 |
102 |
103 |
104 | {imageDesc === '' ? `${dictionary.func_text_placeholder}` : imageDesc}
105 |
106 | {/*
107 |
109 |
110 |
111 | */}
112 |
113 | {/*
{dictionary.note}
*/}
114 |
115 |
116 |
117 |
118 |
{dictionary.public_label}
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 | {pending ? "Submitting" : "Generate Description"}
138 | );
139 | }
140 |
141 |
142 |
143 | function SubmitLoading({ dictionary }: { dictionary: Dictionary["index"] }) {
144 | const { pending } = useFormStatus();
145 | return (
146 |
147 | {pending &&
148 |
149 |
150 | }
151 | {dictionary.func_submit_label}
152 |
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 |
63 |
64 |
65 | {/* */}
66 |
67 |
68 |
Image Describer
69 | {/*
Beta
*/}
70 |
71 |
72 |
73 |
74 | {/* {userId &&
77 | credits:{value}
78 | } */}
79 | {userId &&
80 |
81 | credits:{value}
82 |
83 | }
84 |
87 |
88 |
89 |
90 |
91 |
92 | {/* */}
93 | {signUpOrInSwitcher() ?
94 | Sign Up
95 | : Sign In
96 | }
97 |
98 |
99 |
100 |
101 |
102 | {/* Sign Up */}
103 |
104 |
105 |
Open main menu
106 |
setCollapseMobile(collapseMobile === 'hidden' ? 'show' : 'hidden')} className="inline-flex items-center w-10 h-10 justify-center text-sm text-gray-500 rounded-lg md:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600" aria-controls="navbar-default" aria-expanded="false">
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 | {/* main menu link */}
115 | {/*
*/}
116 |
117 |
118 |
119 | setCollapseMobile('hidden')} className=" w-full text-center text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-600 dark:focus:ring-gray-700">{dictionary.navbar.contact}
120 |
121 |
122 |
123 |
124 |
125 |
126 | {links.map((link, index) => {
127 | return (
128 |
129 | setCollapseMobile('hidden')}
131 | key={link.name}
132 | href={link.href}
133 | className={'block py-2 px-3 text-gray-900 rounded hover:bg-gray-100 md:hover:bg-transparent md:hover:text-blue-700 md:p-0 md:dark:hover:text-blue-500 dark:text-white dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700'}>
134 | {link.name}
135 |
136 |
137 | );
138 | })}
139 |
140 |
141 |
142 | {/* */}
143 | {/* main menu link */}
144 |
145 |
146 |
147 |
handleThemeChange(theme === 'dark' ? 'light' : 'dark')} className='text-gray-900 dark:text-white rounded-lg p-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 dark:hover:text-white'>
148 | {theme === 'dark' ? : }
149 |
150 |
151 |
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 |
setShowLanguageSwitcher(showLanguageSwitcher === 'hidden' ? 'show' : 'hidden')} className="flex w-full items-center font-medium justify-center px-2 py-3 text-sm text-gray-900 dark:text-white rounded-lg cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 dark:hover:text-white">
181 |
182 | {languagesKV[lang]}
183 |
184 |
200 |
201 | )
202 | } else {
203 |
204 | return (
205 |
206 |
207 |
208 |
209 | {dictionary.locale_label}
210 |
211 |
212 |
213 |
setShowLanguageSwitcher(showLanguageSwitcher === 'hidden' ? 'show' : 'hidden')} className="flex w-full border px-3 py-2 items-center font-medium justify-center text-sm text-gray-900 dark:text-white rounded-lg cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 dark:hover:text-white dark:border-gray-700">
214 |
215 | {languagesKV[lang]}
216 |
217 |
218 |
219 |
220 |
221 |
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 |
setShowThemeSwitcher(showThemeSwitcher === 'hidden' ? 'show' : 'hidden')} className="flex w-full border px-3 py-2 items-center font-medium justify-center text-sm text-gray-900 dark:text-white rounded-lg cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 dark:hover:text-white dark:border-gray-700">
265 | {theme === 'light' ? : }
266 | {theme}
267 |
268 |
269 |
270 |
271 |
272 |
273 | {themes.map((theme, index) => {
274 | return (
275 |
276 | handleThemeChange(theme.name as Theme)} className="block w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-600 dark:hover:text-white" role="menuitem">
277 |
278 |
{theme.name === 'light' ? : }
279 | {theme.name}
280 |
281 |
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 | }
--------------------------------------------------------------------------------