Allow manual image count entry
This commit is contained in:
@@ -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. |
|
||||
|
||||
|
||||
@@ -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` |
|
||||
|
||||
|
||||
71
src/components/create/image-count-combobox.tsx
Normal file
71
src/components/create/image-count-combobox.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user