Compare commits

...

5 Commits

Author SHA1 Message Date
79f575b9b9 add navi 2025-09-11 00:10:35 +08:00
c994529d01 add ai image generator page 2025-09-11 00:01:48 +08:00
3bd171683d config prmbr image 2025-09-10 22:59:11 +08:00
javayhu
6262432b64 feat: support posthog analytics 2025-09-09 00:23:48 +08:00
javayhu
e556f72fc7 fix: update user customerId handlingto address data inconsistency 2025-09-07 23:46:29 +08:00
16 changed files with 563 additions and 67 deletions

203
.env.production Normal file
View File

@ -0,0 +1,203 @@
# -----------------------------------------------------------------------------
# Application BASE URL
# https://mksaas.com/docs/env#core-configuration
# For production, set to your domain, e.g. https://mksaas.com
# For development, set to http://localhost:3000 or any other port
# -----------------------------------------------------------------------------
NEXT_PUBLIC_BASE_URL="https://image.prmbr.com"
# -----------------------------------------------------------------------------
# Database
# https://mksaas.com/docs/database
# -----------------------------------------------------------------------------
DATABASE_URL="postgresql://neondb_owner:npg_mfEJ5NRrZbd2@ep-long-feather-a1ydksyq-pooler.ap-southeast-1.aws.neon.tech/neondb?sslmode=require&channel_binding=require"
# -----------------------------------------------------------------------------
# Better Auth
# https://mksaas.com/docs/auth
# Generate a random string for the secret key using `openssl rand -base64 32`
# -----------------------------------------------------------------------------
BETTER_AUTH_SECRET="Vi7Tugxf/Mh20VGtsLeZsf6dUULVGCiL7MKloRvLT+c="
# -----------------------------------------------------------------------------
# Github OAuth
# https://mksaas.com/docs/auth#2-configure-github-oauth
# https://www.better-auth.com/docs/authentication/github
# Get Client information from https://github.com/settings/developers
# -----------------------------------------------------------------------------
GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET=""
# -----------------------------------------------------------------------------
# Google OAuth
# https://mksaas.com/docs/auth#3-configure-google-oauth
# https://www.better-auth.com/docs/authentication/google
# Get Client information from https://console.cloud.google.com/apis/credentials
# -----------------------------------------------------------------------------
GOOGLE_CLIENT_ID="1011279317992-1irl2k5mqdso0t12sls0svd0vuuvleto.apps.googleusercontent.com"
GOOGLE_CLIENT_SECRET="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"
# -----------------------------------------------------------------------------
# Email / Newsletter (Resend)
# https://mksaas.com/docs/email
# https://mksaas.com/docs/newsletter
# Get API key and audience id from https://resend.com
# -----------------------------------------------------------------------------
RESEND_API_KEY="re_YGb7jyZk_CxpAWPDPSF53tooCAzoWK6u6"
RESEND_AUDIENCE_ID="c5a0dd69-3a22-4866-b9e6-e4047e83e0d0"
# -----------------------------------------------------------------------------
# Storage (Cloudflare R2 or S3-compatible service of your choice)
# https://mksaas.com/docs/storage
# Cloudflare R2: https://www.cloudflare.com/developer-platform/products/r2
# -----------------------------------------------------------------------------
STORAGE_REGION="auto"
STORAGE_BUCKET_NAME="prmbr-image"
STORAGE_ACCESS_KEY_ID="8da3b416c62642ef69cbb485d5673d58"
STORAGE_SECRET_ACCESS_KEY="d94f673573a595afb11e09d7963484b5f13914bbd01a598debaa1bf2314e1df1"
STORAGE_ENDPOINT="https://209b775a76842f6f305193e41de86be1.r2.cloudflarestorage.com"
STORAGE_PUBLIC_URL="https://image-cdn.prmbr.com"
# -----------------------------------------------------------------------------
# Payment (Stripe)
# https://mksaas.com/docs/payment
# Get Stripe key and secret from https://dashboard.stripe.com
# -----------------------------------------------------------------------------
STRIPE_SECRET_KEY=""
STRIPE_WEBHOOK_SECRET=""
# Pro plan - monthly subscription
NEXT_PUBLIC_STRIPE_PRICE_PRO_MONTHLY=""
# Pro plan - yearly subscription
NEXT_PUBLIC_STRIPE_PRICE_PRO_YEARLY=""
# Lifetime plan - one-time payment
NEXT_PUBLIC_STRIPE_PRICE_LIFETIME=""
# Credit package - basic
NEXT_PUBLIC_STRIPE_PRICE_CREDITS_BASIC=""
# Credit package - standard
NEXT_PUBLIC_STRIPE_PRICE_CREDITS_STANDARD=""
# Credit package - premium
NEXT_PUBLIC_STRIPE_PRICE_CREDITS_PREMIUM=""
# Credit package - enterprise
NEXT_PUBLIC_STRIPE_PRICE_CREDITS_ENTERPRISE=""
# -----------------------------------------------------------------------------
# Configurations
# -----------------------------------------------------------------------------
# Disable image optimization, check out next.config.ts for more details
# -----------------------------------------------------------------------------
DISABLE_IMAGE_OPTIMIZATION=false
# -----------------------------------------------------------------------------
# Run this website as demo website, in most cases, you should set this to false
# -----------------------------------------------------------------------------
NEXT_PUBLIC_DEMO_WEBSITE=false
# -----------------------------------------------------------------------------
# Analytics
# https://mksaas.com/docs/analytics
# -----------------------------------------------------------------------------
# Google Analytics (https://analytics.google.com)
# https://mksaas.com/docs/analytics#google
# -----------------------------------------------------------------------------
NEXT_PUBLIC_GOOGLE_ANALYTICS_ID="G-06MQ7YHDTT"
# -----------------------------------------------------------------------------
# Umami Analytics (https://umami.is)
# https://mksaas.com/docs/analytics#umami
# -----------------------------------------------------------------------------
NEXT_PUBLIC_UMAMI_WEBSITE_ID="ab7e1694-572d-48c9-a76b-8419e8c18111"
NEXT_PUBLIC_UMAMI_SCRIPT="https://umami.frytea.com/script.js"
# -----------------------------------------------------------------------------
# OpenPanel Analytics (https://openpanel.dev)
# https://mksaas.com/docs/analytics#openpanel
# -----------------------------------------------------------------------------
NEXT_PUBLIC_OPENPANEL_CLIENT_ID=""
# -----------------------------------------------------------------------------
# Plausible Analytics (https://plausible.io)
# https://mksaas.com/docs/analytics#plausible
# -----------------------------------------------------------------------------
NEXT_PUBLIC_PLAUSIBLE_DOMAIN=""
NEXT_PUBLIC_PLAUSIBLE_SCRIPT="https://plausible.io/js/script.js"
# -----------------------------------------------------------------------------
# Ahrefs Analytics (https://ahrefs.com)
# https://mksaas.com/docs/analytics#ahrefs
# -----------------------------------------------------------------------------
NEXT_PUBLIC_AHREFS_WEBSITE_ID=""
# -----------------------------------------------------------------------------
# Seline Analytics
# https://mksaas.com/docs/analytics#seline
# -----------------------------------------------------------------------------
NEXT_PUBLIC_SELINE_TOKEN=""
# -----------------------------------------------------------------------------
# DataFast Analytics (https://datafa.st)
# https://mksaas.com/docs/analytics#datafast
# -----------------------------------------------------------------------------
NEXT_PUBLIC_DATAFAST_WEBSITE_ID=""
NEXT_PUBLIC_DATAFAST_DOMAIN=""
# -----------------------------------------------------------------------------
# PostHog Analytics (https://posthog.com)
# https://mksaas.com/docs/analytics#posthog
# -----------------------------------------------------------------------------
NEXT_PUBLIC_POSTHOG_KEY=""
NEXT_PUBLIC_POSTHOG_HOST=""
# -----------------------------------------------------------------------------
# Notification (Discord)
# -----------------------------------------------------------------------------
DISCORD_WEBHOOK_URL=""
# -----------------------------------------------------------------------------
# Notification (Feishu)
# -----------------------------------------------------------------------------
FEISHU_WEBHOOK_URL=""
# -----------------------------------------------------------------------------
# Affiliate
# https://mksaas.com/docs/affiliate
# -----------------------------------------------------------------------------
# Affonso
# https://affonso.com/
# -----------------------------------------------------------------------------
NEXT_PUBLIC_AFFILIATE_AFFONSO_ID=""
# -----------------------------------------------------------------------------
# PromoteKit
# https://www.promotekit.com/
# -----------------------------------------------------------------------------
NEXT_PUBLIC_AFFILIATE_PROMOTEKIT_ID=""
# -----------------------------------------------------------------------------
# Captcha (Cloudflare Turnstile)
# https://mksaas.com/docs/captcha
# -----------------------------------------------------------------------------
NEXT_PUBLIC_TURNSTILE_SITE_KEY=""
TURNSTILE_SECRET_KEY=""
# -----------------------------------------------------------------------------
# Crisp
# https://mksaas.com/docs/chat
# -----------------------------------------------------------------------------
NEXT_PUBLIC_CRISP_WEBSITE_ID=""
# -----------------------------------------------------------------------------
# Cron Jobs
# https://mksaas.com/docs/cronjobs
# -----------------------------------------------------------------------------
CRON_JOBS_USERNAME="prmbr-image-crontab"
CRON_JOBS_PASSWORD="82AwL!Ya^n8ht2u%bE0U*EFI"
# -----------------------------------------------------------------------------
# AI
# https://mksaas.com/docs/ai
# -----------------------------------------------------------------------------
AI_GATEWAY_API_KEY=""
FAL_API_KEY="7eb06562-078d-4a25-a9d8-a97529d8998e:5fc7ddd7ed48b2e669e7a6dee62e7262"
FIREWORKS_API_KEY=""
OPENAI_API_KEY=""
REPLICATE_API_TOKEN=""
GOOGLE_GENERATIVE_AI_API_KEY=""
DEEPSEEK_API_KEY=""
OPENROUTER_API_KEY="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"
# -----------------------------------------------------------------------------
# Web Content Analyzer (Firecrawl)
# https://firecrawl.dev/
# -----------------------------------------------------------------------------
FIRECRAWL_API_KEY=""

View File

@ -132,6 +132,12 @@ NEXT_PUBLIC_SELINE_TOKEN=""
# -----------------------------------------------------------------------------
NEXT_PUBLIC_DATAFAST_WEBSITE_ID=""
NEXT_PUBLIC_DATAFAST_DOMAIN=""
# -----------------------------------------------------------------------------
# PostHog Analytics (https://posthog.com)
# https://mksaas.com/docs/analytics#posthog
# -----------------------------------------------------------------------------
NEXT_PUBLIC_POSTHOG_KEY=""
NEXT_PUBLIC_POSTHOG_HOST=""
# -----------------------------------------------------------------------------

View File

@ -333,6 +333,10 @@
"title": "AI Image",
"description": "Show how to use AI to generate beautiful images"
},
"images": {
"title": "AI Images",
"description": "use AI to generate beautiful images"
},
"chat": {
"title": "AI Chat",
"description": "Show how to use AI to chat with your customers"
@ -1059,6 +1063,10 @@
"title": "AI Image",
"description": "MkSaaS lets you make AI SaaS in days, simply and effortlessly"
},
"AIImagesPage": {
"title": "AI Images Generator",
"description": "Is a AI Image Generator"
},
"AIChatPage": {
"title": "AI Chat",
"description": "MkSaaS lets you make AI SaaS in days, simply and effortlessly"

View File

@ -333,6 +333,10 @@
"title": "AI 图像",
"description": "展示如何使用 AI 生成精美图像"
},
"images": {
"title": "AI 图像生成",
"description": "使用 AI 生成精美图像"
},
"chat": {
"title": "AI 聊天",
"description": "展示如何使用 AI 与客户聊天"
@ -1059,6 +1063,10 @@
"title": "AI 图片",
"description": "MkSaaS 让您在几天内轻松构建您的 AI SaaS简单且毫不费力"
},
"AIImagesPage": {
"title": "AI 图片生成器",
"description": "但模型图片生成测试"
},
"AIChatPage": {
"title": "AI 聊天",
"description": "MkSaaS 让您在几天内轻松构建您的 AI SaaS简单且毫不费力"

View File

@ -113,6 +113,7 @@
"next-themes": "^0.4.4",
"nuqs": "^2.5.1",
"postgres": "^3.4.5",
"posthog-js": "^1.261.7",
"radix-ui": "^1.4.2",
"react": "^19.0.0",
"react-day-picker": "8.10.1",

47
pnpm-lock.yaml generated
View File

@ -272,6 +272,9 @@ importers:
postgres:
specifier: ^3.4.5
version: 3.4.5
posthog-js:
specifier: ^1.261.7
version: 1.261.7
radix-ui:
specifier: ^1.4.2
version: 1.4.2(@types/react-dom@19.0.3(@types/react@19.0.9))(@types/react@19.0.9)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@ -1926,6 +1929,9 @@ packages:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
'@posthog/core@1.0.2':
resolution: {integrity: sha512-hWk3rUtJl2crQK0WNmwg13n82hnTwB99BT99/XI5gZSvIlYZ1TPmMZE8H2dhJJ98J/rm9vYJ/UXNzw3RV5HTpQ==}
'@radix-ui/number@1.1.0':
resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==}
@ -4025,6 +4031,9 @@ packages:
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
engines: {node: '>=18'}
core-js@3.45.1:
resolution: {integrity: sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==}
cors@2.8.5:
resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
engines: {node: '>= 0.10'}
@ -4503,6 +4512,9 @@ packages:
picomatch:
optional: true
fflate@0.4.8:
resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==}
file-entry-cache@8.0.0:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
@ -5627,6 +5639,20 @@ packages:
resolution: {integrity: sha512-cDWgoah1Gez9rN3H4165peY9qfpEo+SA61oQv65O3cRUE1pOEoJWwddwcqKE8XZYjbblOJlYDlLV4h67HrEVDg==}
engines: {node: '>=12'}
posthog-js@1.261.7:
resolution: {integrity: sha512-Fjpbz6VfIMsEbKIN/UyTWhU1DGgVIngqoRjPGRolemIMOVzTfI77OZq8WwiBhMug+rU+wNhGCQhC41qRlR5CxA==}
peerDependencies:
'@rrweb/types': 2.0.0-alpha.17
rrweb-snapshot: 2.0.0-alpha.17
peerDependenciesMeta:
'@rrweb/types':
optional: true
rrweb-snapshot:
optional: true
preact@10.27.1:
resolution: {integrity: sha512-V79raXEWch/rbqoNc7nT9E4ep7lu+mI3+sBmfRD4i1M73R3WLYcCtdI0ibxGVf4eQL8ZIz2nFacqEC+rmnOORQ==}
prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
@ -6355,6 +6381,9 @@ packages:
web-namespaces@2.0.1:
resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
web-vitals@4.2.4:
resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==}
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
@ -7593,6 +7622,8 @@ snapshots:
'@pkgjs/parseargs@0.11.0':
optional: true
'@posthog/core@1.0.2': {}
'@radix-ui/number@1.1.0': {}
'@radix-ui/number@1.1.1': {}
@ -9808,6 +9839,8 @@ snapshots:
cookie@1.0.2: {}
core-js@3.45.1: {}
cors@2.8.5:
dependencies:
object-assign: 4.1.1
@ -10335,6 +10368,8 @@ snapshots:
optionalDependencies:
picomatch: 4.0.2
fflate@0.4.8: {}
file-entry-cache@8.0.0:
dependencies:
flat-cache: 4.0.1
@ -11784,6 +11819,16 @@ snapshots:
postgres@3.4.5: {}
posthog-js@1.261.7:
dependencies:
'@posthog/core': 1.0.2
core-js: 3.45.1
fflate: 0.4.8
preact: 10.27.1
web-vitals: 4.2.4
preact@10.27.1: {}
prelude-ls@1.2.1: {}
prettier@3.4.2: {}
@ -12744,6 +12789,8 @@ snapshots:
web-namespaces@2.0.1: {}
web-vitals@4.2.4: {}
which@2.0.2:
dependencies:
isexe: 2.0.0

View File

@ -0,0 +1,122 @@
'use client';
import { useState } from 'react';
import { useImageGeneration } from '../hooks/use-image-generation';
import {
MODEL_CONFIGS,
type ModelMode,
PROVIDERS,
type ProviderKey,
} from '../lib/provider-config';
import type { Suggestion } from '../lib/suggestions';
import { ImageGeneratorHeader } from './ImageGeneratorHeader';
import { ModelCardCarousel } from './ModelCardCarousel';
import { PromptInput } from './PromptInput';
export function ImageImageGeneratorByProvider({
suggestions,
providerKey,
}: {
suggestions: Suggestion[];
providerKey: ProviderKey;
}) {
const {
images,
timings,
failedProviders,
isLoading,
startGeneration,
activePrompt,
} = useImageGeneration();
const [showProviders, setShowProviders] = useState(true);
const [selectedModels, setSelectedModels] = useState<
Record<ProviderKey, string>
>(MODEL_CONFIGS.performance);
const [mode, setMode] = useState<ModelMode>('performance');
const toggleView = () => {
setShowProviders((prev) => !prev);
};
const handleModeChange = (newMode: ModelMode) => {
setMode(newMode);
setSelectedModels(MODEL_CONFIGS[newMode]);
setShowProviders(true);
};
const handleModelChange = (providerKey: ProviderKey, model: string) => {
setSelectedModels((prev) => ({ ...prev, [providerKey]: model }));
};
// Only map the active provider to its selected model
const providerToModel = {
[providerKey]: selectedModels[providerKey],
} as Record<ProviderKey, string>;
const handlePromptSubmit = (newPrompt: string) => {
const activeProviders: ProviderKey[] = [providerKey];
startGeneration(newPrompt, activeProviders, providerToModel);
setShowProviders(false);
};
return (
<div className="rounded-lg bg-background py-8 px-4 sm:px-6 lg:px-8">
<div className="mx-auto">
{/* header */}
<ImageGeneratorHeader />
{/* input prompt */}
<PromptInput
onSubmit={handlePromptSubmit}
isLoading={isLoading}
showProviders={showProviders}
onToggleProviders={toggleView}
mode={mode}
onModeChange={handleModeChange}
suggestions={suggestions}
/>
{/* models carousel */}
{(() => {
const key = providerKey;
const provider = PROVIDERS[key];
if (!provider) return null;
const imageItem = images.find((img) => img.provider === key);
const imageData = imageItem?.image;
const modelId = imageItem?.modelId ?? selectedModels[key] ?? '';
const timing = timings[key];
const modelProps = {
label: provider.displayName,
models: provider.models,
value: selectedModels[key],
providerKey: key,
onChange: (model: string, pKey: ProviderKey) =>
handleModelChange(pKey, model),
iconPath: provider.iconPath,
color: provider.color,
enabled: true,
image: imageData,
modelId,
timing,
failed: failedProviders.includes(key),
} as const;
return (
<>
<div>
<ModelCardCarousel models={[modelProps]} />
</div>
{activePrompt && activePrompt.length > 0 && (
<div className="text-center mt-8 text-muted-foreground">
{activePrompt}
</div>
)}
</>
);
})()}
</div>
</div>
);
}

View File

@ -78,7 +78,7 @@ export function ImagePlayground({
<div className="rounded-lg bg-background py-8 px-4 sm:px-6 lg:px-8">
<div className="mx-auto">
{/* header */}
{/* <ImageGeneratorHeader /> */}
<ImageGeneratorHeader />
{/* input prompt */}
<PromptInput

View File

@ -11,53 +11,53 @@ export const PROVIDERS: Record<
}
> = {
// https://ai-sdk.dev/providers/ai-sdk-providers/replicate#image-models
replicate: {
displayName: 'Replicate',
iconPath: '/provider-icons/replicate.svg',
color: 'from-purple-500 to-blue-500',
models: [
'black-forest-labs/flux-1.1-pro',
'black-forest-labs/flux-1.1-pro-ultra',
'black-forest-labs/flux-dev',
'black-forest-labs/flux-pro',
'black-forest-labs/flux-schnell',
'ideogram-ai/ideogram-v2',
'ideogram-ai/ideogram-v2-turbo',
'luma/photon',
'luma/photon-flash',
'recraft-ai/recraft-v3',
// 'recraft-ai/recraft-v3-svg', // added by Fox
// 'stability-ai/stable-diffusion-3.5-medium', // added by Fox
'stability-ai/stable-diffusion-3.5-large',
'stability-ai/stable-diffusion-3.5-large-turbo',
],
},
// replicate: {
// displayName: 'Replicate',
// iconPath: '/provider-icons/replicate.svg',
// color: 'from-purple-500 to-blue-500',
// models: [
// 'black-forest-labs/flux-1.1-pro',
// 'black-forest-labs/flux-1.1-pro-ultra',
// 'black-forest-labs/flux-dev',
// 'black-forest-labs/flux-pro',
// 'black-forest-labs/flux-schnell',
// 'ideogram-ai/ideogram-v2',
// 'ideogram-ai/ideogram-v2-turbo',
// 'luma/photon',
// 'luma/photon-flash',
// 'recraft-ai/recraft-v3',
// // 'recraft-ai/recraft-v3-svg', // added by Fox
// // 'stability-ai/stable-diffusion-3.5-medium', // added by Fox
// 'stability-ai/stable-diffusion-3.5-large',
// 'stability-ai/stable-diffusion-3.5-large-turbo',
// ],
// },
// https://ai-sdk.dev/providers/ai-sdk-providers/openai#image-models
openai: {
displayName: 'OpenAI',
iconPath: '/provider-icons/openai.svg',
color: 'from-blue-500 to-cyan-500',
models: [
// 'gpt-image-1', // added by Fox
'dall-e-2',
'dall-e-3',
],
},
// openai: {
// displayName: 'OpenAI',
// iconPath: '/provider-icons/openai.svg',
// color: 'from-blue-500 to-cyan-500',
// models: [
// // 'gpt-image-1', // added by Fox
// 'dall-e-2',
// 'dall-e-3',
// ],
// },
// https://ai-sdk.dev/providers/ai-sdk-providers/fireworks#image-models
fireworks: {
displayName: 'Fireworks',
iconPath: '/provider-icons/fireworks.svg',
color: 'from-orange-500 to-red-500',
models: [
'accounts/fireworks/models/flux-1-dev-fp8',
'accounts/fireworks/models/flux-1-schnell-fp8',
'accounts/fireworks/models/playground-v2-5-1024px-aesthetic',
'accounts/fireworks/models/japanese-stable-diffusion-xl',
'accounts/fireworks/models/playground-v2-1024px-aesthetic',
'accounts/fireworks/models/SSD-1B',
'accounts/fireworks/models/stable-diffusion-xl-1024-v1-0',
],
},
// fireworks: {
// displayName: 'Fireworks',
// iconPath: '/provider-icons/fireworks.svg',
// color: 'from-orange-500 to-red-500',
// models: [
// 'accounts/fireworks/models/flux-1-dev-fp8',
// 'accounts/fireworks/models/flux-1-schnell-fp8',
// 'accounts/fireworks/models/playground-v2-5-1024px-aesthetic',
// 'accounts/fireworks/models/japanese-stable-diffusion-xl',
// 'accounts/fireworks/models/playground-v2-1024px-aesthetic',
// 'accounts/fireworks/models/SSD-1B',
// 'accounts/fireworks/models/stable-diffusion-xl-1024-v1-0',
// ],
// },
// https://ai-sdk.dev/providers/ai-sdk-providers/fal#image-models
fal: {
displayName: 'Fal',

View File

@ -0,0 +1,35 @@
'use client';
import posthog from 'posthog-js';
import { PostHogProvider as PHProvider } from 'posthog-js/react';
import { useEffect } from 'react';
/**
* PostHog Analytics
*
* https://posthog.com
* https://posthog.com/docs/libraries/next-js?tab=PostHog+provider
* https://mksaas.com/docs/analytics#posthog
*/
export function PostHogProvider({ children }: { children: React.ReactNode }) {
const posthogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY;
const posthogHost = process.env.NEXT_PUBLIC_POSTHOG_HOST;
const isProduction = process.env.NODE_ENV === 'production';
const isPostHogEnabled = posthogKey && posthogHost && isProduction;
useEffect(() => {
if (isPostHogEnabled) {
posthog.init(posthogKey, {
api_host: posthogHost,
defaults: '2025-05-24',
});
}
}, [isPostHogEnabled, posthogKey, posthogHost]);
// If PostHog is not enabled, just return children without the provider
if (!isPostHogEnabled) {
return <>{children}</>;
}
return <PHProvider client={posthog}>{children}</PHProvider>;
}

View File

@ -0,0 +1,49 @@
import { ImageGenerator } from '@/ai/image/components/ImageGenerator';
import { ImageImageGeneratorByProvider } from '@/ai/image/components/ImageGeneratorByProvider';
import { ImagePlayground } from '@/ai/image/components/ImagePlayground';
import { getRandomSuggestions } from '@/ai/image/lib/suggestions';
import { constructMetadata } from '@/lib/metadata';
import { getUrlWithLocale } from '@/lib/urls/urls';
import { ImageIcon } from 'lucide-react';
import type { Metadata } from 'next';
import type { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server';
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: Locale }>;
}): Promise<Metadata | undefined> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'Metadata' });
const pt = await getTranslations({ locale, namespace: 'AIImagePage' });
return constructMetadata({
title: pt('title') + ' | ' + t('title'),
description: pt('description'),
canonicalUrl: getUrlWithLocale('/ai/image', locale),
});
}
export default async function AIImagesPage() {
const t = await getTranslations('AIImagesPage');
return (
<div className="min-h-screen bg-muted/50 rounded-lg">
<div className="container mx-auto px-4 py-8 md:py-16">
{/* Header Section */}
<div className="text-center space-y-6 mb-12">
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-primary/10 text-primary text-sm font-medium">
<ImageIcon className="size-4" />
{t('title')}
</div>
</div>
{/* Image Playground Component */}
<div className="max-w-6xl mx-auto">
<ImageImageGeneratorByProvider suggestions={getRandomSuggestions(5)} providerKey='fal' />
</div>
</div>
</div>
);
}

View File

@ -1,5 +1,6 @@
'use client';
import { PostHogProvider } from '@/analytics/posthog-analytics';
import { ActiveThemeProvider } from '@/components/layout/active-theme-provider';
import { QueryProvider } from '@/components/providers/query-provider';
import { TooltipProvider } from '@/components/ui/tooltip';
@ -20,12 +21,12 @@ interface ProvidersProps {
*
* This component is used to wrap the app in the providers.
*
* - PostHogProvider: Provides the PostHog analytics to the app.
* - QueryProvider: Provides the query client to the app.
* - ThemeProvider: Provides the theme to the app.
* - ActiveThemeProvider: Provides the active theme to the app.
* - RootProvider: Provides the root provider for Fumadocs UI.
* - TooltipProvider: Provides the tooltip to the app.
* - PaymentProvider: Provides the payment state to the app.
* - CreditsProvider: Provides the credits state to the app.
*/
export function Providers({ children, locale }: ProvidersProps) {
const theme = useTheme();
@ -53,19 +54,24 @@ export function Providers({ children, locale }: ProvidersProps) {
};
return (
<QueryProvider>
<ThemeProvider
attribute="class"
defaultTheme={defaultMode}
enableSystem={true}
disableTransitionOnChange
>
<ActiveThemeProvider>
<RootProvider theme={theme} i18n={{ locale, locales, translations }}>
<TooltipProvider>{children}</TooltipProvider>
</RootProvider>
</ActiveThemeProvider>
</ThemeProvider>
</QueryProvider>
<PostHogProvider>
<QueryProvider>
<ThemeProvider
attribute="class"
defaultTheme={defaultMode}
enableSystem={true}
disableTransitionOnChange
>
<ActiveThemeProvider>
<RootProvider
theme={theme}
i18n={{ locale, locales, translations }}
>
<TooltipProvider>{children}</TooltipProvider>
</RootProvider>
</ActiveThemeProvider>
</ThemeProvider>
</QueryProvider>
</PostHogProvider>
);
}

View File

@ -103,6 +103,13 @@ export function useNavbarLinks(): NestedMenuItem[] {
href: Routes.AIChat,
external: false,
},
{
title: t('ai.items.images.title'),
description: t('ai.items.images.description'),
icon: <ImageIcon className="size-4 shrink-0" />,
href: Routes.AIImages,
external: false,
}
// {
// title: t('ai.items.video.title'),
// description: t('ai.items.video.description'),

View File

@ -52,7 +52,7 @@ export const websiteConfig: WebsiteConfig = {
},
auth: {
enableGoogleLogin: true,
enableGithubLogin: true,
enableGithubLogin: false,
enableCredentialLogin: true,
},
i18n: {
@ -155,7 +155,7 @@ export const websiteConfig: WebsiteConfig = {
},
},
credits: {
enableCredits: process.env.NEXT_PUBLIC_DEMO_WEBSITE === 'true',
enableCredits: process.env.NEXT_PUBLIC_DEMO_WEBSITE != 'true',
enablePackagesForFreePlan: false,
registerGiftCredits: {
enable: true,

View File

@ -84,10 +84,13 @@ export class StripeProvider implements PaymentProvider {
// Find user id by customer id
const userId = await this.findUserIdByCustomerId(customerId);
// user does not exist, update user with customer id
// in case you deleted user in database, but forgot to delete customer in Stripe
// If no userId found, it means the user record exists (by email) but lacks customerId
// This can happen when user was created before Stripe integration or data got out of sync
// Fix the data inconsistency by updating the user's customerId field
if (!userId) {
console.log('User does not exist, update with customer id (hidden)');
console.log(
'User exists but missing customerId, fixing data inconsistency'
);
await this.updateUserWithCustomerId(customerId, email);
}
return customerId;

View File

@ -40,6 +40,7 @@ export enum Routes {
// AI routes
AIText = '/ai/text',
AIImage = '/ai/image',
AIImages = '/ai/images',
AIChat = '/ai/chat',
AIVideo = '/ai/video',
AIAudio = '/ai/audio',