Compare commits
6 Commits
cloudflare
...
main
Author | SHA1 | Date | |
---|---|---|---|
79f575b9b9 | |||
c994529d01 | |||
3bd171683d | |||
|
6262432b64 | ||
|
e556f72fc7 | ||
|
aea55ee4bb |
203
.env.production
Normal file
203
.env.production
Normal 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=""
|
7483
cloudflare-env.d.ts
vendored
7483
cloudflare-env.d.ts
vendored
File diff suppressed because it is too large
Load Diff
@ -35,6 +35,14 @@ After that, you can return to the blog post and you can read the rest of the blo
|
||||
|
||||
For more details, please check out the documentation: [Blog](https://mksaas.com/docs/blog).
|
||||
|
||||
Test show Tweet in the blog post.
|
||||
|
||||
<XEmbedClient url="https://x.com/mksaascom/status/1960417768505008291" width={500} />
|
||||
|
||||
Test show YouTube video in the blog post.
|
||||
|
||||
<YoutubeVideo url="https://www.youtube.com/embed/xvoeSnlFZJk" width={500} />
|
||||
|
||||
Now the rest of the blog post is premium content.
|
||||
|
||||
<PremiumContent>
|
||||
|
@ -35,6 +35,14 @@ CVV: 567
|
||||
|
||||
更多详情,请参考文档:[博客](https://mksaas.com/docs/blog)。
|
||||
|
||||
测试展示 X 帖子。
|
||||
|
||||
<XEmbedClient url="https://x.com/mksaascom/status/1960417768505008291" width={500} />
|
||||
|
||||
测试展示 YouTube 视频。
|
||||
|
||||
<YoutubeVideo url="https://www.youtube.com/embed/xvoeSnlFZJk" width={500} />
|
||||
|
||||
现在剩下的内容是付费内容。
|
||||
|
||||
<PremiumContent>
|
||||
|
@ -1 +0,0 @@
|
||||
NEXTJS_ENV=development
|
@ -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=""
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
@ -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"
|
||||
|
@ -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,简单且毫不费力"
|
||||
|
@ -18,18 +18,6 @@ const nextConfig: NextConfig = {
|
||||
// removeConsole: process.env.NODE_ENV === 'production',
|
||||
},
|
||||
|
||||
// https://github.com/vercel/next.js/discussions/50177#discussioncomment-6006702
|
||||
// fix build error: Module build failed: UnhandledSchemeError:
|
||||
// Reading from "cloudflare:sockets" is not handled by plugins (Unhandled scheme).
|
||||
webpack: (config, { webpack }) => {
|
||||
config.plugins.push(
|
||||
new webpack.IgnorePlugin({
|
||||
resourceRegExp: /^pg-native$|^cloudflare:sockets$/,
|
||||
})
|
||||
);
|
||||
return config;
|
||||
},
|
||||
|
||||
images: {
|
||||
// https://vercel.com/docs/image-optimization/managing-image-optimization-costs#minimizing-image-optimization-costs
|
||||
// https://nextjs.org/docs/app/api-reference/components/image#unoptimized
|
||||
@ -82,9 +70,3 @@ const withNextIntl = createNextIntlPlugin();
|
||||
const withMDX = createMDX();
|
||||
|
||||
export default withMDX(withNextIntl(nextConfig));
|
||||
|
||||
// https://opennext.js.org/cloudflare/get-started#12-develop-locally
|
||||
import { initOpenNextCloudflareForDev } from '@opennextjs/cloudflare';
|
||||
|
||||
// during local development, to access in any of your server code, local versions of Cloudflare bindings
|
||||
initOpenNextCloudflareForDev();
|
||||
|
@ -1,6 +0,0 @@
|
||||
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
|
||||
|
||||
|
||||
export default defineCloudflareConfig({
|
||||
|
||||
});
|
@ -4,7 +4,6 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"cf-dev": "next dev -p 8787",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"postinstall": "fumadocs-mdx",
|
||||
@ -112,9 +111,9 @@
|
||||
"next-intl": "^4.0.0",
|
||||
"next-safe-action": "^7.10.4",
|
||||
"next-themes": "^0.4.4",
|
||||
"pg": "^8.16.0",
|
||||
"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",
|
||||
@ -122,6 +121,7 @@
|
||||
"react-hook-form": "^7.62.0",
|
||||
"react-remove-scroll": "^2.6.3",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"react-social-media-embed": "^2.5.18",
|
||||
"react-syntax-highlighter": "^15.6.3",
|
||||
"react-tweet": "^3.2.2",
|
||||
"react-use-measure": "^2.1.7",
|
||||
@ -145,7 +145,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@opennextjs/cloudflare": "^1.6.5",
|
||||
"@tailwindcss/postcss": "^4.0.14",
|
||||
"@tanstack/eslint-plugin-query": "^5.83.1",
|
||||
"@types/mdx": "^2.0.13",
|
||||
@ -160,7 +159,6 @@
|
||||
"react-email": "3.0.7",
|
||||
"tailwindcss": "^4.0.14",
|
||||
"tsx": "^4.19.3",
|
||||
"typescript": "^5.8.3",
|
||||
"wrangler": "^4.28.1"
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
4433
pnpm-lock.yaml
generated
4433
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,2 +0,0 @@
|
||||
/_next/static/*
|
||||
Cache-Control: public,max-age=31536000,immutable
|
122
src/ai/image/components/ImageGeneratorByProvider.tsx
Normal file
122
src/ai/image/components/ImageGeneratorByProvider.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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
|
||||
|
@ -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',
|
||||
|
35
src/analytics/posthog-analytics.tsx
Normal file
35
src/analytics/posthog-analytics.tsx
Normal 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>;
|
||||
}
|
49
src/app/[locale]/(marketing)/ai/images/page.tsx
Normal file
49
src/app/[locale]/(marketing)/ai/images/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import defaultMdxComponents from 'fumadocs-ui/mdx';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
import type { MDXComponents } from 'mdx/types';
|
||||
import type { ComponentProps, FC } from 'react';
|
||||
import { XEmbedClient } from './xembed';
|
||||
|
||||
/**
|
||||
* Enhanced MDX Content component that includes commonly used MDX components
|
||||
@ -23,6 +24,7 @@ export function getMDXComponents(components?: MDXComponents): MDXComponents {
|
||||
...defaultMdxComponents,
|
||||
...LucideIcons,
|
||||
// ...((await import('lucide-react')) as unknown as MDXComponents),
|
||||
XEmbedClient,
|
||||
YoutubeVideo,
|
||||
PremiumContent,
|
||||
Tabs,
|
||||
|
16
src/components/docs/xembed.tsx
Normal file
16
src/components/docs/xembed.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import { XEmbed, type XEmbedProps } from 'react-social-media-embed';
|
||||
|
||||
/**
|
||||
* Embedding X Posts in Fumadocs
|
||||
*
|
||||
* https://rjv.im/blog/solution/embed-x-post-in-fuma-docs
|
||||
*/
|
||||
export function XEmbedClient({ ...props }: XEmbedProps) {
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<XEmbed {...props} />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -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'),
|
||||
|
@ -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,
|
||||
|
@ -2,23 +2,17 @@
|
||||
* Connect to PostgreSQL Database (Supabase/Neon/Local PostgreSQL)
|
||||
* https://orm.drizzle.team/docs/tutorials/drizzle-with-supabase
|
||||
*/
|
||||
import { getCloudflareContext } from '@opennextjs/cloudflare';
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import { Pool } from 'pg';
|
||||
import { drizzle } from 'drizzle-orm/postgres-js';
|
||||
import postgres from 'postgres';
|
||||
import * as schema from './schema';
|
||||
|
||||
let db: ReturnType<typeof drizzle> | null = null;
|
||||
|
||||
// https://opennext.js.org/cloudflare/howtos/db#postgresql
|
||||
export async function getDb() {
|
||||
if (db) return db;
|
||||
const { env } = await getCloudflareContext({ async: true });
|
||||
const pool = new Pool({
|
||||
connectionString: env.HYPERDRIVE.connectionString,
|
||||
// You don't want to reuse the same connection for multiple requests
|
||||
maxUses: 1,
|
||||
});
|
||||
db = drizzle({ client: pool, schema });
|
||||
const connectionString = process.env.DATABASE_URL!;
|
||||
const client = postgres(connectionString, { prepare: false });
|
||||
db = drizzle(client, { schema });
|
||||
return db;
|
||||
}
|
||||
|
||||
|
@ -57,13 +57,7 @@ export class StripeProvider implements PaymentProvider {
|
||||
}
|
||||
|
||||
// Initialize Stripe without specifying apiVersion to use default/latest version
|
||||
// https://opennext.js.org/cloudflare/howtos/stripeAPI
|
||||
// When creating a Stripe object, the default http client implementation is based on
|
||||
// node:https which is not implemented on Workers.
|
||||
this.stripe = new Stripe(apiKey, {
|
||||
// Cloudflare Workers use the Fetch API for their API requests.
|
||||
httpClient: Stripe.createFetchHttpClient(),
|
||||
});
|
||||
this.stripe = new Stripe(apiKey);
|
||||
this.webhookSecret = webhookSecret;
|
||||
}
|
||||
|
||||
@ -90,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;
|
||||
|
@ -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',
|
||||
|
@ -1,84 +0,0 @@
|
||||
/**
|
||||
* For more details on how to configure Wrangler, refer to:
|
||||
* https://developers.cloudflare.com/workers/wrangler/configuration/
|
||||
*/
|
||||
{
|
||||
"$schema": "node_modules/wrangler/config-schema.json",
|
||||
"main": ".open-next/worker.js",
|
||||
"name": "mksaas-template",
|
||||
"compatibility_date": "2024-12-30",
|
||||
"compatibility_flags": [
|
||||
// Enable Node.js API
|
||||
// see https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag
|
||||
"nodejs_compat",
|
||||
// This also enables nodejs_compat_v2 as long as compatibility date is 2024-09-23 or later.
|
||||
// https://developers.cloudflare.com/workers/configuration/compatibility-dates/#nodejs-compatibility-flag
|
||||
// Enable improved Node.js API with polyfills and native code
|
||||
// see https://blog.cloudflare.com/zh-cn/more-npm-packages-on-cloudflare-workers-combining-polyfills-and-native-code/
|
||||
// "nodejs_compat_v2",
|
||||
// Enable auto-populating process.env
|
||||
// see https://developers.cloudflare.com/workers/configuration/compatibility-flags/#enable-auto-populating-processenv
|
||||
"nodejs_compat_populate_process_env",
|
||||
// Allow to fetch URLs in your app
|
||||
// see https://developers.cloudflare.com/workers/configuration/compatibility-flags/#global-fetch-strictly-public
|
||||
"global_fetch_strictly_public"
|
||||
],
|
||||
// Minification helps to keep the Worker bundle size down and improve start up time.
|
||||
"minify": true,
|
||||
|
||||
// Enables Workers Trace Events Logpush for a Worker
|
||||
"logpush": true,
|
||||
|
||||
// https://developers.cloudflare.com/workers/wrangler/configuration/#top-level-only-keys
|
||||
// Whether Wrangler should keep variables configured in the dashboard on deploy
|
||||
"keep_vars": true,
|
||||
|
||||
"assets": {
|
||||
"binding": "ASSETS",
|
||||
"directory": ".open-next/assets"
|
||||
},
|
||||
|
||||
// https://developers.cloudflare.com/workers/wrangler/configuration/#observability
|
||||
"observability": {
|
||||
"enabled": true
|
||||
},
|
||||
|
||||
/**
|
||||
* Smart Placement
|
||||
* Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
|
||||
*/
|
||||
// "placement": { "mode": "smart" },
|
||||
|
||||
/**
|
||||
* Bindings
|
||||
* Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including
|
||||
* databases, object storage, AI inference, real-time communication and more.
|
||||
* https://developers.cloudflare.com/workers/runtime-apis/bindings/
|
||||
*/
|
||||
|
||||
/**
|
||||
* Hyperdrive
|
||||
* https://opennext.js.org/cloudflare/howtos/db#hyperdrive-example
|
||||
* https://developers.cloudflare.com/workers/tutorials/postgres/#8-use-hyperdrive-to-accelerate-queries
|
||||
*/
|
||||
"hyperdrive": [
|
||||
{
|
||||
"binding": "HYPERDRIVE",
|
||||
"id": "8ba4508b28cf42f987f3533c1f09433c",
|
||||
"localConnectionString": "postgresql://postgres:postgres@localhost:5432/postgres"
|
||||
}
|
||||
],
|
||||
|
||||
/**
|
||||
* Environment Variables
|
||||
* https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
|
||||
*/
|
||||
"vars": {},
|
||||
|
||||
/**
|
||||
* Note: Use secrets to store sensitive data.
|
||||
* https://developers.cloudflare.com/workers/configuration/secrets/
|
||||
*/
|
||||
|
||||
"kv_namespaces": []
|
||||
}
|
Loading…
Reference in New Issue
Block a user