Allow manual image count entry

This commit is contained in:
FengLee
2026-05-12 19:54:28 +08:00
parent b9a8521d1b
commit 493ae83d2d
5 changed files with 77 additions and 40 deletions

View File

@@ -45,7 +45,7 @@ Use this guide when the user reports behavior. Start from the symptom row, inspe
| Reference image upload too large or fails | `src/components/create/image-to-image.tsx`, `src/components/create/image-to-video.tsx`, `src/lib/browser-image-compression.ts`, `src/lib/server-image-compression.ts`, `src/app/api/generate/image/route.ts`, `src/app/api/generate/video/route.ts` | Browser compression, `MAX_UPSTREAM_REFERENCE_IMAGE_BYTES`, data URL conversion. |
| 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 or options missing | `src/components/create/text-to-image.tsx`, `src/components/create/image-to-image.tsx` | Use the shared Radix `Select` pattern instead of browser `datalist`; keep the trigger narrow and verify options render in both text-to-image and image-to-image panels. |
| 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. |
| 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. |

View File

@@ -55,6 +55,7 @@ Use this document to jump directly to code before broad searching.
| Image to video | `src/components/create/image-to-video.tsx` | `src/app/api/generation-jobs/route.ts`, `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` |
| 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. |
| 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. |
@@ -166,4 +167,3 @@ Use this document to jump directly to code before broad searching.
| Backup | `scripts/backup-create.sh`, `scripts/backup-list.sh`, `scripts/backup-restore.sh` |
| Admin upgrade runner | `scripts/admin-upgrade-runner.mjs` |
| Boundary checks | `scripts/check-boundaries.sh` |

View File

@@ -0,0 +1,71 @@
'use client';
import { ChevronDown } from 'lucide-react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Input } from '@/components/ui/input';
import { cn } from '@/lib/utils';
const IMAGE_COUNT_OPTIONS = [
{ value: 'auto', label: '自动' },
{ value: '1', label: '1 张' },
{ value: '2', label: '2 张' },
{ value: '3', label: '3 张' },
{ value: '4', label: '4 张' },
] as const;
function normalizeCountValue(value: string): string {
const numeric = value.replace(/[^\d]/g, '');
if (!numeric) return 'auto';
return String(Math.min(10, Math.max(1, Math.floor(Number(numeric)))));
}
interface ImageCountComboboxProps {
value: string;
onChange: (value: string) => void;
className?: string;
}
export function ImageCountCombobox({ value, onChange, className }: ImageCountComboboxProps) {
return (
<div className={cn('relative w-28', className)}>
<Input
aria-label="生成数量"
className="h-10 pr-9 text-center"
inputMode="numeric"
maxLength={2}
placeholder="自动"
value={value === 'auto' ? '' : value}
onBlur={event => onChange(normalizeCountValue(event.currentTarget.value))}
onChange={event => onChange(normalizeCountValue(event.currentTarget.value))}
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
aria-label="选择生成数量"
className="text-muted-foreground hover:text-foreground focus-visible:border-primary/70 focus-visible:ring-primary/30 absolute top-0 right-0 flex h-10 w-9 items-center justify-center rounded-r-md outline-none focus-visible:ring-2"
>
<ChevronDown className="size-4" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="min-w-28">
{IMAGE_COUNT_OPTIONS.map(option => (
<DropdownMenuItem
key={option.value}
onSelect={() => onChange(option.value)}
className={cn(value === option.value && 'bg-accent text-accent-foreground')}
>
{option.label}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}

View File

@@ -45,16 +45,10 @@ import { GenerationErrorPanel, createGenerationError, type GenerationErrorState
import { ExpandablePromptTextarea } from '@/components/create/expandable-prompt-textarea';
import { GenerationLoadingPanel } from '@/components/create/generation-loading-panel';
import { compressImageFileForUpload } from '@/lib/browser-image-compression';
import { ImageCountCombobox } from '@/components/create/image-count-combobox';
import { StylePresetSelector } from '@/components/create/style-preset-selector';
const IMAGE_TO_IMAGE_DRAFT_KEY = 'miaojing:image-to-image-draft';
const IMAGE_COUNT_OPTIONS = [
{ value: 'auto', label: '自动' },
{ value: '1', label: '1 张' },
{ value: '2', label: '2 张' },
{ value: '3', label: '3 张' },
{ value: '4', label: '4 张' },
] as const;
interface RefImage {
id: string;
@@ -655,18 +649,7 @@ export function ImageToImagePanel() {
{/* Count */}
<div className="space-y-2">
<Label></Label>
<Select value={count} onValueChange={setCount}>
<SelectTrigger className="w-28">
<SelectValue placeholder="自动" />
</SelectTrigger>
<SelectContent align="start" className="min-w-28">
{IMAGE_COUNT_OPTIONS.map(option => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<ImageCountCombobox value={count} onChange={setCount} />
</div>
{/* Generate */}

View File

@@ -43,16 +43,10 @@ import { CreationDetailDialog } from '@/components/creation-detail-dialog';
import { GenerationErrorPanel, createGenerationError, type GenerationErrorState } from '@/components/create/generation-error-panel';
import { ExpandablePromptTextarea } from '@/components/create/expandable-prompt-textarea';
import { GenerationLoadingPanel } from '@/components/create/generation-loading-panel';
import { ImageCountCombobox } from '@/components/create/image-count-combobox';
import { StylePresetSelector } from '@/components/create/style-preset-selector';
const TEXT_TO_IMAGE_DRAFT_KEY = 'miaojing:text-to-image-draft';
const IMAGE_COUNT_OPTIONS = [
{ value: 'auto', label: '自动' },
{ value: '1', label: '1 张' },
{ value: '2', label: '2 张' },
{ value: '3', label: '3 张' },
{ value: '4', label: '4 张' },
] as const;
export function TextToImagePanel() {
const { user, accessToken } = useAuth();
@@ -475,18 +469,7 @@ export function TextToImagePanel() {
{/* Count */}
<div className="space-y-2">
<Label></Label>
<Select value={count} onValueChange={setCount}>
<SelectTrigger className="w-28">
<SelectValue placeholder="自动" />
</SelectTrigger>
<SelectContent align="start" className="min-w-28">
{IMAGE_COUNT_OPTIONS.map(option => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<ImageCountCombobox value={count} onChange={setCount} />
</div>
{/* Generate Button */}