Store image style presets with usage ordering

This commit is contained in:
FengLee
2026-05-12 21:10:01 +08:00
parent 901a9ce898
commit 4faace0191
23 changed files with 198 additions and 35 deletions

View File

@@ -44,7 +44,7 @@ Use this table before searching.
| --- | --- | --- |
| Home page, shell, navigation, footer, announcement popup | `src/app/page.tsx`, `src/components/app-shell.tsx`, `src/components/navbar.tsx`, `src/components/site-footer.tsx`, `src/components/announcement-popup.tsx` | `src/lib/site-config.ts`, `src/app/api/site-config/route.ts`, `src/app/api/announcements/route.ts` |
| Create center tabs | `src/app/create/page.tsx` | `src/components/create/*` |
| Text/image generation | `src/components/create/text-to-image.tsx`, `src/components/create/image-to-image.tsx` | `src/app/api/generation-jobs/route.ts`, `src/app/api/generate/image/route.ts`, `src/lib/generation-job-*` |
| Text/image generation | `src/components/create/text-to-image.tsx`, `src/components/create/image-to-image.tsx` | `src/app/api/generation-jobs/route.ts`, `src/app/api/generate/image/route.ts`, `src/lib/generation-job-*`, `src/app/api/style-presets/route.ts`, `src/lib/style-preset-store.ts` |
| Video generation | `src/components/create/text-to-video.tsx`, `src/components/create/image-to-video.tsx` | `src/app/api/generate/video/route.ts` |
| Reverse prompt | `src/components/create/reverse-prompt-panel.tsx` | `src/app/api/generate/reverse-prompt/route.ts`, `src/app/api/generate/suggest-prompt/route.ts` |
| Model/provider visibility | `src/lib/model-config.ts`, `src/lib/model-config-types.ts`, `src/lib/server-api-config.ts` | `src/app/api/model-config/route.ts`, `src/app/api/admin/system-apis/route.ts`, `src/app/api/admin/providers/route.ts`, `src/app/api/user-api-keys/route.ts` |

View File

@@ -35,6 +35,7 @@ All routes are Next.js App Router route handlers under `src/app/api/**/route.ts`
| PUT | `/api/announcements` | Admin | `src/app/api/announcements/route.ts` | Update announcement. Body includes `id` and changed fields. |
| DELETE | `/api/announcements?id=...` | Admin | `src/app/api/announcements/route.ts` | Delete announcement. |
| GET | `/api/model-config` | Public | `src/app/api/model-config/route.ts` | Read managed provider/model configuration for clients. |
| GET | `/api/style-presets` | Public | `src/app/api/style-presets/route.ts` | Returns active image style presets from `image_style_presets`, sorted by usage count. |
| GET | `/api/local-storage/[...path]` | Public by URL | `src/app/api/local-storage/[...path]/route.ts` | Serve local storage object by key. |
| GET | `/api/download?url=...&filename=...` | Public by URL | `src/app/api/download/route.ts` | Download proxy for remote, same-origin, and local-storage URLs. |
@@ -70,7 +71,7 @@ All routes are Next.js App Router route handlers under `src/app/api/**/route.ts`
| Method | Path | Auth | Source | Request | Response/Side Effects |
| --- | --- | --- | --- | --- | --- |
| POST | `/api/generation-jobs` | User | `src/app/api/generation-jobs/route.ts` | `{ type: "image"|"video", payload: {...} }` | Inserts `generation_jobs`, starts worker, returns `202` with `jobId`, `status`, `estimateSeconds`, `eta`. |
| POST | `/api/generation-jobs` | User | `src/app/api/generation-jobs/route.ts` | `{ type: "image"|"video", payload: {...} }` | Inserts `generation_jobs`, starts worker, increments selected image `styleLabel` usage, returns `202` with `jobId`, `status`, `estimateSeconds`, `eta`. |
| GET | `/api/generation-jobs/[id]` | User/admin | `src/app/api/generation-jobs/[id]/route.ts` | Path UUID | Job status/result/error/progress. Owner or admin only. |
| POST | `/api/generate/image` | Trusted internal or resolved user/system API context | `src/app/api/generate/image/route.ts` | Image generation payload; supports prompt, negative prompt, reference images, model/system/custom API config, aspect/size/resolution/count/quality. | Calls SDK or OpenAI/New API-compatible endpoint, persists result to local storage, updates job progress when headers include job ID. |
| POST | `/api/generate/video` | Trusted internal or resolved user/system API context | `src/app/api/generate/video/route.ts` | Video generation payload; supports prompt, reference image, model/system/custom API config, ratio/duration/fps-like params. | Calls SDK or custom endpoint, persists media to local storage. |

View File

@@ -86,6 +86,7 @@ Major groups:
- `profile/*`: user profile and theme.
- `user-api-keys`: user-owned custom API credentials.
- `model-config`: public model/provider config.
- `style-presets`: public DB-backed image style presets ordered by usage count.
- `generate/*`: direct generation, reverse prompt, prompt suggestion.
- `generation-jobs/*`: queued generation job creation/status.
- `creation-history`: user works/history.
@@ -126,6 +127,8 @@ Key source files:
- `src/components/create/image-to-video.tsx`
- `src/lib/generation-job-client.ts`
- `src/app/api/generation-jobs/route.ts`
- `src/app/api/style-presets/route.ts`
- `src/lib/style-preset-store.ts`
- `src/lib/generation-job-worker.ts`
- `src/lib/generation-job-runner.ts`
- `src/app/api/generate/image/route.ts`

View File

@@ -29,7 +29,7 @@ Use this guide when the user reports behavior. Start from the symptom row, inspe
| Footer content missing or not Markdown-rendered | `src/components/site-footer.tsx`, `src/components/site-policy-page.tsx`, `src/lib/site-config.ts`, `src/app/api/site-config/route.ts` | Config response fields, Markdown renderer, fallback defaults, PUT persistence. |
| Policy pages start mid-page after navigation | `src/components/site-policy-page.tsx`, `src/app/about/page.tsx`, `src/app/terms/page.tsx`, `src/app/privacy/page.tsx`, `src/app/help/page.tsx` | Scroll reset behavior and shared policy page wrapper. |
| Site name/logo/favicon not updating | `src/components/site-config-sync.tsx`, `src/components/site-brand.tsx`, `src/app/api/site-config/route.ts`, `src/lib/local-storage.ts` | `site_config` row, base64 image save, generated `/api/local-storage/*` URL. |
| Page content leaves large unused horizontal margins, or wide screens look like the UI was simply enlarged | `src/components/app-shell.tsx`, `src/components/navbar.tsx`, `src/components/site-footer.tsx`, page-level wrappers under `src/app/*/page.tsx`, `src/components/site-policy-page.tsx` | The viewport/background should be `w-full`, but content should use wide adaptive containers such as `max-w-[1680px]` plus responsive padding. Do not fix this by removing all max widths; keep text, tabs, policy articles, and compact controls constrained for readability. |
| Page content leaves large unused horizontal margins, or wide screens look like the UI was simply enlarged | `src/components/app-shell.tsx`, `src/components/navbar.tsx`, `src/components/site-footer.tsx`, page-level wrappers under `src/app/*/page.tsx`, `src/components/site-policy-page.tsx` | The viewport/background can be `w-full`, but product content should keep the original component scale and readable containers such as `max-w-7xl`, `max-w-4xl`, or `max-w-3xl`. Do not fix this by removing all max widths or scaling controls up on wide monitors. |
| Disabled canvas/`画布` appears again in public UI | `src/components/navbar.tsx`, `src/app/canvas/page.tsx`, `docs/codex-miaojing/feature-code-index.md` | Navbar should not include `/canvas`, and `/canvas` should continue to call `notFound()` unless the product explicitly re-enables the legacy canvas feature. |
| Announcement not popping up | `src/components/announcement-popup.tsx`, `src/app/api/announcements/route.ts`, `src/components/app-shell.tsx` | App shell includes popup, active date range, local/session dismissal behavior, GET payload shape. |
| Announcement admin edit fails | `src/components/admin/announcement-tab.tsx`, `src/app/api/announcements/route.ts` | Admin token, required fields, `starts_at`/`expires_at` compatibility. |
@@ -48,6 +48,7 @@ Use this guide when the user reports behavior. Start from the symptom row, inspe
| Generated result previews but does not persist | `src/app/api/generate/image/route.ts`, `src/app/api/generate/video/route.ts`, `src/lib/local-storage.ts`, `src/app/api/creation-history/route.ts` | Media copied to local storage, presigned URL returned, history POST called. |
| Fullscreen/preview/download broken | `src/components/fullscreen-preview.tsx`, `src/components/lightbox.tsx`, `src/components/creation-detail-dialog.tsx`, `src/app/api/download/route.ts` | Dialog state, URL type, download proxy supports local/remote URL. |
| Image generation count dropdown too wide, options missing, or manual count input unavailable | `src/components/create/image-count-combobox.tsx`, `src/components/create/text-to-image.tsx`, `src/components/create/image-to-image.tsx` | Use the shared compact combobox instead of browser `datalist`; verify manual numeric entry and dropdown options in both text-to-image and image-to-image panels. |
| Style presets are hardcoded, missing, or not ordered by usage | `src/components/create/style-preset-selector.tsx`, `src/lib/style-presets-client.ts`, `src/app/api/style-presets/route.ts`, `src/lib/style-preset-store.ts`, `src/app/api/generation-jobs/route.ts` | Presets should come from `image_style_presets`; `generation-jobs` increments `usage_count`; GET `/api/style-presets` should return active presets sorted by usage count. |
| Reverse prompt option missing | `src/components/create/reverse-prompt-panel.tsx`, `src/app/api/generate/reverse-prompt/route.ts` | UI option list and server `outputMode` handling both updated, app rebuilt/restarted if deployed. |
| Prompt optimization fails | `src/app/api/generate/suggest-prompt/route.ts`, `src/lib/server-api-config.ts`, `src/lib/custom-api-fetch.ts` | Text-capable system/custom API, chat response shape, JSON parsing fallback. |
@@ -67,6 +68,7 @@ Use this guide when the user reports behavior. Start from the symptom row, inspe
| --- | --- | --- |
| History missing after generation | `src/lib/creation-history-store.ts`, `src/app/api/creation-history/route.ts`, create panel component | History POST, `works` insert, URL not data URL except reverse prompt placeholder. |
| Published work not in gallery | `src/lib/creation-history-store.ts`, `src/app/api/gallery/publish/route.ts`, `src/app/api/gallery/route.ts`, `src/app/gallery/page.tsx` | `is_public = true`, `status = completed`, media copied to gallery folder, filters. |
| Imported gallery images do not render after production data import | `src/app/api/admin/data-import/route.ts`, `src/lib/local-storage.ts`, `src/app/api/local-storage/[...path]/route.ts`, DB `works.result_url` | Check whether production media files were copied into `LOCAL_STORAGE_DIR`; DB rows alone are not enough for relative `/api/local-storage/*` URLs. |
| Gallery delete does not remove public item | `src/app/api/gallery/route.ts`, admin UI route using it | DELETE unpublishes by setting `is_public = false`, not hard delete. |
| Search/filter/sort wrong | `src/app/api/gallery/route.ts`, `src/app/gallery/page.tsx` | Query params `type`, `limit`, `offset`, `sort`, `q/search`; SQL where/order. |

View File

@@ -8,11 +8,11 @@ Use this document to jump directly to code before broad searching.
| Feature | Primary Files | Notes |
| --- | --- | --- |
| Root layout and providers | `src/app/layout.tsx`, `src/components/app-shell.tsx` | App shell wires navbar, site config sync, visit tracking, theme/account sync, toaster, and full-width page mounting. Use wide adaptive containers inside pages instead of stretching all content to viewport edges. |
| Root layout and providers | `src/app/layout.tsx`, `src/components/app-shell.tsx` | App shell wires navbar, site config sync, visit tracking, theme/account sync, toaster, and full-width page mounting. Keep product content at the original component scale; use centered responsive containers instead of stretching all content to viewport edges. |
| Home page | `src/app/page.tsx` | Landing/dashboard-like public entry. Check site config dependencies when changing brand text. |
| Navbar | `src/components/navbar.tsx`, `src/components/site-brand.tsx` | Navigation, brand display, auth-aware links. User-facing nav intentionally excludes the disabled legacy canvas route. |
| Footer | `src/components/site-footer.tsx` | Uses site config for policy/help/about links and filing text; footer background spans browser width while inner content uses the shared wide adaptive width. |
| Announcement popup | `src/components/announcement-popup.tsx`, `src/app/api/announcements/route.ts` | Frontend popup behavior plus backend announcement CRUD. |
| Footer | `src/components/site-footer.tsx` | Uses site config for policy/help/about links and filing text; footer background spans browser width while inner content keeps the original `max-w-7xl` scale. |
| Announcement popup | `src/components/announcement-popup.tsx`, `src/app/api/announcements/route.ts` | Frontend popup behavior plus backend announcement CRUD. Desktop dialog is intentionally wide (`max-w-5xl`) for long Markdown notices. |
| Site config sync | `src/components/site-config-sync.tsx`, `src/lib/site-config.ts`, `src/app/api/site-config/route.ts` | Site name, tab title, logo, favicon, policy Markdown, filing, membership switch. |
| Visit tracking | `src/components/visit-tracker.tsx`, `src/app/api/site-stats/route.ts` | Public visit counter. |
@@ -56,7 +56,7 @@ Use this document to jump directly to code before broad searching.
| Reverse prompt | `src/components/create/reverse-prompt-panel.tsx` | `src/app/api/generate/reverse-prompt/route.ts`, `src/app/api/generate/suggest-prompt/route.ts` |
| Prompt textarea | `src/components/create/expandable-prompt-textarea.tsx` | Shared prompt input. |
| Image count input/dropdown | `src/components/create/image-count-combobox.tsx` | Shared compact count control for manual image count entry and common dropdown options. |
| Style presets | `src/components/create/style-preset-selector.tsx`, `src/lib/model-config.ts` | Style preset selection and image params. |
| Style presets | `src/components/create/style-preset-selector.tsx`, `src/lib/style-presets-client.ts`, `src/app/api/style-presets/route.ts`, `src/lib/style-preset-store.ts`, `src/lib/model-config.ts` | Style presets are stored in `image_style_presets`, seeded from defaults, sorted by `usage_count`, and incremented from image generation jobs. |
| Loading/error panels | `src/components/create/generation-loading-panel.tsx`, `src/components/create/generation-error-panel.tsx` | Shared generation status UI. |
| Lightbox/fullscreen | `src/components/lightbox.tsx`, `src/components/fullscreen-preview.tsx`, `src/components/creation-detail-dialog.tsx` | Preview, copy, download, share. |
@@ -65,7 +65,7 @@ Use this document to jump directly to code before broad searching.
| Responsibility | Primary Files | Notes |
| --- | --- | --- |
| Client-side job polling | `src/lib/generation-job-client.ts` | Create/poll jobs from create panels. |
| Job creation API | `src/app/api/generation-jobs/route.ts` | Inserts `generation_jobs` and starts worker. |
| Job creation API | `src/app/api/generation-jobs/route.ts` | Inserts `generation_jobs`, starts worker, and increments selected image style preset usage. |
| Job status API | `src/app/api/generation-jobs/[id]/route.ts` | Owner/admin visibility, stale running job handling. |
| Worker loop | `src/lib/generation-job-worker.ts` | Picks and processes queued jobs. |
| Internal runner | `src/lib/generation-job-runner.ts` | Calls `/api/generate/image` or `/api/generate/video` with internal headers. |
@@ -79,8 +79,8 @@ Use this document to jump directly to code before broad searching.
| Feature | Files | Notes |
| --- | --- | --- |
| Built-in model options | `src/lib/model-config.ts`, `src/lib/model-config-types.ts` | Image/video model lists, ratios, sizes, inference helpers. |
| Public model config API | `src/app/api/model-config/route.ts` | Returns model recommendations and provider config to clients. |
| Built-in model options | `src/lib/model-config.ts`, `src/lib/model-config-types.ts` | Image/video model lists, ratios, sizes, inference helpers, and fallback style preset seed labels. Runtime style ordering comes from DB. |
| Public model config API | `src/app/api/model-config/route.ts`, `src/app/api/style-presets/route.ts` | Returns model/provider config plus DB-backed image style presets for clients. |
| User custom API keys | `src/lib/custom-api-store.ts`, `src/app/api/user-api-keys/route.ts`, `src/components/profile/api-key-manager.tsx` | User-owned encrypted API credentials. |
| Admin provider presets | `src/app/api/admin/providers/route.ts`, `src/components/admin/api-management-tab.tsx` | Provider registry, default API URL/model/type. |
| Admin system API configs | `src/app/api/admin/system-apis/route.ts`, `src/lib/server-api-config.ts` | Encrypted shared system API credentials, pricing metadata. |
@@ -148,6 +148,7 @@ Use this document to jump directly to code before broad searching.
| Area | Files |
| --- | --- |
| Database connection pool | `src/storage/database/local-db.ts` |
| Image style preset store | `src/lib/style-preset-store.ts`, `src/app/api/style-presets/route.ts` |
| Schema snapshot | `src/storage/database/shared/schema.ts` |
| Supabase compatibility client | `src/storage/database/supabase-client.ts` |
| Init SQL | `scripts/init-database.sql` |

View File

@@ -335,6 +335,19 @@ CREATE INDEX IF NOT EXISTS api_providers_active_sort_idx ON api_providers (is_ac
CREATE INDEX IF NOT EXISTS model_recommendations_active_type_sort_idx ON model_recommendations (is_active, type, sort_order);
CREATE INDEX IF NOT EXISTS model_recommendations_provider_idx ON model_recommendations (provider_id);
CREATE TABLE IF NOT EXISTS image_style_presets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
label VARCHAR(128) NOT NULL UNIQUE,
prompt TEXT NOT NULL,
usage_count INTEGER NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT true,
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS image_style_presets_active_usage_idx ON image_style_presets (is_active, usage_count DESC, sort_order ASC);
INSERT INTO api_providers (name, default_api_url, default_model, type, website, is_active, sort_order)
VALUES
('硅基流动', 'https://api.siliconflow.cn/v1/images/generations', 'black-forest-labs/FLUX.1-schnell', 'image', 'https://cloud.siliconflow.cn', true, 10),

View File

@@ -19,6 +19,7 @@ export async function GET(request: NextRequest) {
'user_api_keys',
'system_api_configs',
'payment_methods',
'image_style_presets',
'work_likes',
'announcements',
];

View File

@@ -31,6 +31,7 @@ const UUID_ID_TABLES = new Set([
'orders',
'user_api_keys',
'system_api_configs',
'image_style_presets',
'work_likes',
]);
@@ -44,6 +45,7 @@ const TABLE_COLUMNS: Record<string, string[]> = {
orders: ['id', 'user_id', 'order_no', 'product_type', 'product_name', 'amount', 'credits_amount', 'status', 'payment_method', 'paid_at', 'created_at', 'updated_at'],
user_api_keys: ['id', 'user_id', 'provider', 'supplier_name', 'api_url', 'model_name', 'note', 'api_key_encrypted', 'api_key_preview', 'type', 'is_active', 'created_at', 'updated_at'],
system_api_configs: ['id', 'provider', 'name', 'api_url', 'model_name', 'note', 'api_key_encrypted', 'api_key_preview', 'type', 'credits_per_use', 'is_active', 'sort_order', 'created_at', 'updated_at'],
image_style_presets: ['id', 'label', 'prompt', 'usage_count', 'is_active', 'sort_order', 'created_at', 'updated_at'],
payment_methods: ['id', 'type', 'name', 'is_active', 'public_config', 'secret_config_encrypted', 'secret_config_preview', 'created_at', 'updated_at'],
work_likes: ['id', 'user_id', 'work_id', 'created_at'],
};
@@ -62,6 +64,7 @@ const CONFLICT_COLUMNS: Record<string, string[]> = {
orders: ['id'],
user_api_keys: ['id'],
system_api_configs: ['id'],
image_style_presets: ['id'],
payment_methods: ['id'],
work_likes: ['id'],
};

View File

@@ -13,6 +13,7 @@ import {
resolveGenerationJobIdentity,
} from '@/lib/generation-job-estimates';
import { writePlatformLog } from '@/lib/platform-logs';
import { incrementImageStylePresetUsage } from '@/lib/style-preset-store';
export async function POST(request: NextRequest) {
try {
@@ -64,6 +65,11 @@ export async function POST(request: NextRequest) {
],
);
jobId = result.rows[0].id as string;
if (type === 'image' && typeof payload.styleLabel === 'string') {
await incrementImageStylePresetUsage(client, payload.styleLabel).catch(error => {
console.warn('[generation-jobs] style preset usage update failed:', error);
});
}
} finally {
client.release();
}

View File

@@ -0,0 +1,13 @@
import { NextResponse } from 'next/server';
import { STYLE_PRESETS } from '@/lib/model-config';
import { listImageStylePresets } from '@/lib/style-preset-store';
export async function GET() {
try {
const presets = await listImageStylePresets();
return NextResponse.json({ presets });
} catch (err) {
console.error('[style-presets] GET error:', err);
return NextResponse.json({ presets: STYLE_PRESETS });
}
}

View File

@@ -26,7 +26,7 @@ function CreateContent() {
return (
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6">
<TabsList className="mx-auto grid w-full max-w-5xl grid-cols-5">
<TabsList className="grid w-full grid-cols-5 max-w-4xl">
<TabsTrigger value="text2img" className="gap-2">
<Brush className="h-4 w-4" />
<span className="hidden sm:inline"></span>
@@ -74,7 +74,7 @@ function CreateContent() {
export default function CreatePage() {
return (
<div className="min-h-screen bg-background">
<div className="mx-auto w-full max-w-[1680px] px-4 py-8 sm:px-6 lg:px-8 2xl:px-10">
<div className="mx-auto max-w-7xl px-4 sm:px-6 py-8">
<div className="mb-8">
<h1 className="font-serif text-3xl font-bold"></h1>
<p className="mt-2 text-muted-foreground">

View File

@@ -424,7 +424,7 @@ export default function GalleryPage() {
return (
<div className="min-h-screen bg-background">
<div className="mx-auto w-full max-w-[1800px] px-4 py-8 sm:px-6 lg:px-8 2xl:px-10">
<div className="mx-auto max-w-7xl px-4 sm:px-6 py-8">
{/* Header */}
<div className="mb-8">
<div className="flex items-center gap-3">

View File

@@ -107,7 +107,7 @@ export default function HomePage() {
<div className="absolute bottom-0 right-0 w-[400px] h-[400px] bg-primary/3 rounded-full blur-[100px]" />
</div>
<div className="mx-auto w-full max-w-[1440px] px-4 pb-24 pt-20 text-center sm:px-6 lg:px-8">
<div className="mx-auto max-w-7xl px-4 sm:px-6 pt-20 pb-24 text-center">
<Badge variant="secondary" className="mb-6 px-4 py-1.5 text-sm font-medium gap-2">
<SiteLogo className="h-5 w-5 rounded" />
AI多模态创作平台
@@ -157,7 +157,7 @@ export default function HomePage() {
{/* Core Features */}
<section className="py-24 bg-muted/20">
<div className="mx-auto w-full max-w-[1680px] px-4 sm:px-6 lg:px-8 2xl:px-10">
<div className="mx-auto max-w-7xl px-4 sm:px-6">
<div className="text-center mb-16">
<h2 className="font-serif text-3xl sm:text-4xl font-bold"></h2>
<p className="mt-4 text-muted-foreground text-lg">AI创作体验</p>
@@ -193,7 +193,7 @@ export default function HomePage() {
{/* Highlights */}
<section className="py-24">
<div className="mx-auto w-full max-w-[1440px] px-4 sm:px-6 lg:px-8">
<div className="mx-auto max-w-7xl px-4 sm:px-6">
<div className="text-center mb-16">
<h2 className="font-serif text-3xl sm:text-4xl font-bold"></h2>
<p className="mt-4 text-muted-foreground text-lg"></p>
@@ -219,7 +219,7 @@ export default function HomePage() {
{/* Pricing */}
<BillingPlanGuard>
<section className="py-24 bg-muted/20">
<div className="mx-auto w-full max-w-[1680px] px-4 sm:px-6 lg:px-8 2xl:px-10">
<div className="mx-auto max-w-7xl px-4 sm:px-6">
<div className="text-center mb-16">
<h2 className="font-serif text-3xl sm:text-4xl font-bold"></h2>
<p className="mt-4 text-muted-foreground text-lg"></p>
@@ -271,7 +271,7 @@ export default function HomePage() {
{/* CTA */}
<section className="py-24">
<div className="mx-auto w-full max-w-[1120px] px-4 text-center sm:px-6 lg:px-8">
<div className="mx-auto max-w-4xl px-4 sm:px-6 text-center">
<h2 className="font-serif text-3xl sm:text-4xl font-bold"></h2>
<p className="mt-4 text-lg text-muted-foreground">
AI开启你的创作之旅

View File

@@ -411,7 +411,7 @@ export default function ProfilePage() {
if (!mounted) {
return (
<div className="min-h-screen bg-background">
<div className="mx-auto w-full max-w-[1680px] px-4 py-8 sm:px-6 lg:px-8 2xl:px-10">
<div className="mx-auto max-w-7xl px-4 sm:px-6 py-8">
<div className="mb-8">
<div className="flex items-center gap-4">
<div className="h-16 w-16 rounded-full bg-muted animate-pulse" />
@@ -428,7 +428,7 @@ export default function ProfilePage() {
return (
<div className="min-h-screen bg-background">
<div className="mx-auto w-full max-w-[1680px] px-4 py-8 sm:px-6 lg:px-8 2xl:px-10">
<div className="mx-auto max-w-7xl px-4 sm:px-6 py-8">
{/* Profile Header */}
<div className="mb-8">
<div className="flex items-center justify-between">
@@ -518,7 +518,7 @@ export default function ProfilePage() {
{/* Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className={`grid w-full max-w-5xl grid-cols-3 ${membershipEnabled ? 'sm:grid-cols-6' : 'sm:grid-cols-3'}`}>
<TabsList className={`grid w-full grid-cols-3 ${membershipEnabled ? 'sm:grid-cols-6' : 'sm:grid-cols-3'} max-w-3xl`}>
<TabsTrigger value="account" className="gap-1.5"><User className="h-4 w-4" /><span className="hidden sm:inline"></span></TabsTrigger>
{membershipEnabled && <TabsTrigger value="membership" className="gap-1.5"><Crown className="h-4 w-4" /><span className="hidden sm:inline"></span></TabsTrigger>}
{membershipEnabled && <TabsTrigger value="credits" className="gap-1.5"><Coins className="h-4 w-4" /><span className="hidden sm:inline"></span></TabsTrigger>}

View File

@@ -62,7 +62,7 @@ export function AnnouncementPopup() {
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="sm:max-w-lg">
<DialogContent className="w-[calc(100vw-2rem)] !max-w-5xl">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Megaphone className="h-5 w-5 text-primary" />

View File

@@ -15,8 +15,7 @@ import {
IMAGE_QUALITY_OPTIONS,
RESOLUTION_OPTIONS,
IMG2IMG_STYLE_PRESETS,
getImageStylePreset,
isCustomModel,
isCustomModel,
isSystemModel,
getCustomKeyId,
getSystemApiId,
@@ -47,6 +46,7 @@ import { GenerationLoadingPanel } from '@/components/create/generation-loading-p
import { compressImageFileForUpload } from '@/lib/browser-image-compression';
import { ImageCountCombobox } from '@/components/create/image-count-combobox';
import { StylePresetSelector } from '@/components/create/style-preset-selector';
import { useImageStylePresets } from '@/lib/style-presets-client';
const IMAGE_TO_IMAGE_DRAFT_KEY = 'miaojing:image-to-image-draft';
@@ -94,8 +94,9 @@ export function ImageToImagePanel() {
// Lightbox state
const [lightboxSrc, setLightboxSrc] = useState<string | null>(null);
// History detail dialog
// History detail dialog
const [selectedHistoryRecord, setSelectedHistoryRecord] = useState<CreationRecord | null>(null);
const stylePresets = useImageStylePresets(IMG2IMG_STYLE_PRESETS);
const fileInputRef = useRef<HTMLInputElement>(null);
@@ -318,7 +319,10 @@ export function ImageToImagePanel() {
() => inferImageParamsFromPrompt(prompt, { allowOriginalAspectRatio: true }),
[prompt],
);
const selectedStylePreset = useMemo(() => getImageStylePreset(selectedStyleLabel), [selectedStyleLabel]);
const selectedStylePreset = useMemo(
() => stylePresets.find(preset => preset.label === selectedStyleLabel),
[stylePresets, selectedStyleLabel],
);
const creditCount = count === 'auto' ? (inferredImageParams.count ?? 1) : (Number(count) || 1);
const credits = calcImageCredits(selectedModel, resolution, aspectRatio, creditCount);
@@ -569,7 +573,7 @@ export function ImageToImagePanel() {
onValueChange={setPrompt}
/>
<StylePresetSelector
presets={IMG2IMG_STYLE_PRESETS}
presets={stylePresets}
selectedLabel={selectedStyleLabel}
onSelect={setSelectedStyleLabel}
/>

View File

@@ -15,7 +15,6 @@ import {
IMAGE_QUALITY_OPTIONS,
RESOLUTION_OPTIONS,
STYLE_PRESETS,
getImageStylePreset,
isCustomModel,
isSystemModel,
getCustomKeyId,
@@ -45,6 +44,7 @@ import { ExpandablePromptTextarea } from '@/components/create/expandable-prompt-
import { GenerationLoadingPanel } from '@/components/create/generation-loading-panel';
import { ImageCountCombobox } from '@/components/create/image-count-combobox';
import { StylePresetSelector } from '@/components/create/style-preset-selector';
import { useImageStylePresets } from '@/lib/style-presets-client';
const TEXT_TO_IMAGE_DRAFT_KEY = 'miaojing:text-to-image-draft';
@@ -85,6 +85,7 @@ export function TextToImagePanel() {
// History detail dialog
const [selectedHistoryRecord, setSelectedHistoryRecord] = useState<CreationRecord | null>(null);
const stylePresets = useImageStylePresets(STYLE_PRESETS);
const applyPromptDraft = useCallback((draft: unknown) => {
if (!draft || typeof draft !== 'object') return;
@@ -193,7 +194,10 @@ export function TextToImagePanel() {
}, [prompt, user, accessToken, textModelOptions, getCurrentModelLabel]);
const inferredImageParams = useMemo(() => inferImageParamsFromPrompt(prompt), [prompt]);
const selectedStylePreset = useMemo(() => getImageStylePreset(selectedStyleLabel), [selectedStyleLabel]);
const selectedStylePreset = useMemo(
() => stylePresets.find(preset => preset.label === selectedStyleLabel),
[stylePresets, selectedStyleLabel],
);
const creditCount = count === 'auto' ? (inferredImageParams.count ?? 1) : (Number(count) || 1);
const credits = calcImageCredits(selectedModel, resolution, aspectRatio, creditCount);
@@ -389,7 +393,7 @@ export function TextToImagePanel() {
onValueChange={setPrompt}
/>
<StylePresetSelector
presets={STYLE_PRESETS}
presets={stylePresets}
selectedLabel={selectedStyleLabel}
onSelect={setSelectedStyleLabel}
/>

View File

@@ -92,7 +92,7 @@ export function Navbar() {
return (
<header className="sticky top-0 z-50 w-full border-b border-border/50 bg-background/80 backdrop-blur-xl">
<div className="mx-auto flex h-16 w-full max-w-[1680px] items-center justify-between px-4 sm:px-6 lg:px-8 2xl:px-10">
<div className="mx-auto flex h-16 max-w-7xl items-center justify-between px-4 sm:px-6">
{/* Logo */}
<Link href="/" className="flex items-center gap-2.5 group">
<img

View File

@@ -18,7 +18,7 @@ export function SiteFooter() {
return (
<footer className="border-t border-border/50 py-12">
<div className="mx-auto w-full max-w-[1680px] px-4 sm:px-6 lg:px-8 2xl:px-10">
<div className="mx-auto max-w-7xl px-4 sm:px-6">
<div className="flex flex-col gap-5 sm:flex-row sm:items-start sm:justify-between">
<div className="flex flex-col gap-2 text-center sm:text-left">
<div className="flex items-center justify-center gap-2 sm:justify-start">

View File

@@ -21,7 +21,7 @@ export function SitePolicyPage({ kind }: { kind: PolicyPageKind }) {
return (
<main className="min-h-screen bg-background">
<div className="mx-auto flex min-h-screen w-full max-w-[1120px] flex-col px-4 py-10 sm:px-6 lg:px-8">
<div className="mx-auto flex min-h-screen w-full max-w-3xl flex-col px-4 py-10 sm:px-6">
<header className="mb-10 flex items-center justify-between gap-4">
<Link href="/" className="inline-flex items-center gap-2">
<SiteLogo className="h-8 w-8 rounded" />

View File

@@ -252,7 +252,7 @@ export type ImageStylePreset = {
prompt: string;
};
const IMAGE_STYLE_PRESET_LABELS = [
export const IMAGE_STYLE_PRESET_LABELS = [
'写实照片', '动漫插画', '水墨国风', '油画质感', '赛博朋克', '水彩淡雅', '像素复古', '极简线条', '梦幻童话', '暗黑哥特',
'电影写实', '胶片摄影', '宝丽来', '复古港风', '日系清新', '韩系写真', '法式浪漫', '美式复古', '北欧极简', '东方禅意',
'新中式', '国潮插画', '工笔重彩', '宋画雅致', '敦煌壁画', '浮世绘', '漫画分镜', '少女漫画', '少年热血', '欧美漫画',
@@ -268,7 +268,7 @@ const IMAGE_STYLE_PRESET_LABELS = [
'运动视觉', '奢华金色', '透明水晶', '液态金属', '全息镭射', '红外摄影', '航拍视角', '鱼眼镜头', '长曝光', '双重曝光',
] as const;
function buildImage2StylePrompt(label: string): string {
export function buildImage2StylePrompt(label: string): string {
return `Apply a ${label} visual style for an image2 generation model; preserve the user's main subject and composition, refine lighting, color grading, texture, and detail quality, keep clean edges and coherent anatomy, no text or watermark.`;
}

View File

@@ -0,0 +1,83 @@
import type { PoolClient } from 'pg';
import { getDbClient } from '@/storage/database/local-db';
import {
IMAGE_STYLE_PRESET_LABELS,
buildImage2StylePrompt,
type ImageStylePreset,
} from '@/lib/model-config';
export type StoredImageStylePreset = ImageStylePreset & {
id: string;
usageCount: number;
sortOrder: number;
};
export async function ensureStylePresetSchema(client: PoolClient): Promise<void> {
await client.query(`
CREATE TABLE IF NOT EXISTS image_style_presets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
label VARCHAR(128) NOT NULL UNIQUE,
prompt TEXT NOT NULL,
usage_count INTEGER NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT true,
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ
)
`);
await client.query('CREATE INDEX IF NOT EXISTS image_style_presets_active_usage_idx ON image_style_presets (is_active, usage_count DESC, sort_order ASC)');
for (const [index, label] of IMAGE_STYLE_PRESET_LABELS.entries()) {
await client.query(
`INSERT INTO image_style_presets (label, prompt, sort_order)
VALUES ($1, $2, $3)
ON CONFLICT (label) DO UPDATE
SET prompt = COALESCE(NULLIF(image_style_presets.prompt, ''), EXCLUDED.prompt),
sort_order = image_style_presets.sort_order`,
[label, buildImage2StylePrompt(label), (index + 1) * 10],
);
}
}
function mapStylePreset(row: Record<string, unknown>): StoredImageStylePreset {
return {
id: String(row.id),
label: String(row.label || ''),
prompt: String(row.prompt || ''),
usageCount: Number(row.usage_count || 0),
sortOrder: Number(row.sort_order || 0),
};
}
export async function listImageStylePresets(): Promise<StoredImageStylePreset[]> {
const client = await getDbClient();
try {
await ensureStylePresetSchema(client);
const result = await client.query(
`SELECT id, label, prompt, usage_count, sort_order
FROM image_style_presets
WHERE is_active = true
ORDER BY usage_count DESC, sort_order ASC, label ASC`,
);
return result.rows.map(mapStylePreset);
} finally {
client.release();
}
}
export async function incrementImageStylePresetUsage(
client: PoolClient,
label: string | undefined,
): Promise<void> {
const normalized = label?.trim();
if (!normalized) return;
await ensureStylePresetSchema(client);
await client.query(
`INSERT INTO image_style_presets (label, prompt, usage_count, sort_order)
VALUES ($1, $2, 1, 99990)
ON CONFLICT (label) DO UPDATE
SET usage_count = image_style_presets.usage_count + 1,
updated_at = now()`,
[normalized, buildImage2StylePrompt(normalized)],
);
}

View File

@@ -0,0 +1,29 @@
'use client';
import { useEffect, useState } from 'react';
import type { ImageStylePreset } from '@/lib/model-config';
export function useImageStylePresets(fallback: ImageStylePreset[]): ImageStylePreset[] {
const [presets, setPresets] = useState<ImageStylePreset[]>(fallback);
useEffect(() => {
let cancelled = false;
fetch('/api/style-presets')
.then(res => res.ok ? res.json() : null)
.then(data => {
if (cancelled || !Array.isArray(data?.presets)) return;
const next = data.presets.filter((item: unknown): item is ImageStylePreset => {
if (!item || typeof item !== 'object') return false;
const preset = item as Partial<ImageStylePreset>;
return typeof preset.label === 'string' && typeof preset.prompt === 'string';
});
if (next.length > 0) setPresets(next);
})
.catch(() => undefined);
return () => {
cancelled = true;
};
}, []);
return presets;
}