Merge pull request #41 from Yiling-J/fix-fitting-room-and-extractor-save-14637309535569615021

Fix Fitting Room overflow and add Save as Outfit to Extractor
This commit is contained in:
Yiling-J authored and GitHub committed 2026-02-18 12:22:07 +08:00
commit 69c56d064b
15 files changed
+146 -34

No files matched your search

+1
View File
@@ -38,6 +38,7 @@ COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/bunfig.toml ./bunfig.toml
COPY --from=builder /app/public ./public
COPY --from=builder /app/prisma.config.ts ./prisma.config.ts
COPY --from=builder /app/example-data ./example-data
# Create data directory
RUN mkdir -p data
+1
View File
@@ -10,6 +10,7 @@
"@prisma/adapter-libsql": "7.3.0",
"@prisma/client": "^7.3.0",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-scroll-area": "^1.2.10",
+1 -1
View File
@@ -19,4 +19,4 @@
"path": "01KHNYBPY85F6BZAJXZDSKBQY7.webp"
}
}
]
]
+1 -1
View File
@@ -107,4 +107,4 @@
"path": "01KHQ66H2X62ZFQ8E1WHK0DSFE.webp"
}
}
]
]
+1 -1
View File
@@ -80,4 +80,4 @@
"path": "01KHNY8VTPEQMHJ7X0C31F6YA6.webp"
}
}
]
]
+1 -1
View File
@@ -69,4 +69,4 @@
"01KHQ66H318BPBAE4ZFBY7JHBD"
]
}
]
]
+1 -1
View File
@@ -80,4 +80,4 @@
"path": "01KHNY603BYFF3M1RA5AWYHCD2.webp"
}
}
]
]
+1
View File
@@ -17,6 +17,7 @@
"@prisma/adapter-libsql": "7.3.0",
"@prisma/client": "^7.3.0",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-scroll-area": "^1.2.10",
+34 -1
View File
@@ -34,6 +34,10 @@ export const ExtractorDialog: React.FC<ExtractorDialogProps> = ({
const [results, setResults] = useState<ExtractedAsset[]>([])
const [isRefineComplete, setIsRefineComplete] = useState(false)
// Save as Outfit state
const [saveAsOutfit, setSaveAsOutfit] = useState(false)
const [outfitName, setOutfitName] = useState('')
// Refs for cleanup
const analyzeController = useRef<AbortController | null>(null)
const refineController = useRef<AbortController | null>(null)
@@ -48,6 +52,8 @@ export const ExtractorDialog: React.FC<ExtractorDialogProps> = ({
setSelectedIndices([])
setResults([])
setIsRefineComplete(false)
setSaveAsOutfit(false)
setOutfitName('')
analyzeController.current?.abort()
refineController.current?.abort()
}
@@ -194,7 +200,30 @@ export const ExtractorDialog: React.FC<ExtractorDialogProps> = ({
const handleClose = () => onOpenChange(false)
const handleDone = () => {
const handleDone = async () => {
if (saveAsOutfit) {
if (!outfitName.trim()) {
toast.error('Please enter an outfit name')
return
}
try {
const equipmentIds = results.map((r) => r.id)
const res = await client.outfits.$post({
json: {
name: outfitName,
equipmentIds,
},
})
if (res.ok) {
toast.success('Outfit created')
} else {
toast.error('Failed to create outfit')
}
} catch (e) {
console.error(e)
toast.error('Failed to create outfit')
}
}
onSuccess(results)
handleClose()
}
@@ -234,6 +263,10 @@ export const ExtractorDialog: React.FC<ExtractorDialogProps> = ({
selectedCandidates={candidates.filter((_, i) => selectedIndices.includes(i))}
results={results}
isComplete={isRefineComplete}
saveAsOutfit={saveAsOutfit}
setSaveAsOutfit={setSaveAsOutfit}
outfitName={outfitName}
setOutfitName={setOutfitName}
/>
)}
</div>
+41 -2
View File
@@ -2,24 +2,35 @@ import { CheckCircle2, Loader2 } from 'lucide-react'
import React from 'react'
import { cn } from '../../lib/utils'
import { CandidateAsset, ExtractedAsset } from '../../types'
import { Checkbox } from '../ui/checkbox'
import { Input } from '../ui/input'
import { Label } from '../ui/label'
import { ScrollArea } from '../ui/scroll-area'
interface RefineStageProps {
selectedCandidates: CandidateAsset[]
results: ExtractedAsset[]
isComplete: boolean
saveAsOutfit: boolean
setSaveAsOutfit: (value: boolean) => void
outfitName: string
setOutfitName: (value: string) => void
}
export const RefineStage: React.FC<RefineStageProps> = ({
selectedCandidates,
results,
isComplete,
saveAsOutfit,
setSaveAsOutfit,
outfitName,
setOutfitName,
}) => {
return (
<div className="flex flex-col h-full w-full max-w-6xl mx-auto px-4 py-6">
{/* Grid */}
<ScrollArea className="h-full px-2">
<div className="grid grid-cols-3 gap-2 p-1 h-full pb-6">
<ScrollArea className="flex-1 px-2">
<div className="grid grid-cols-3 gap-2 p-1 pb-6">
{selectedCandidates.map((candidate, index) => {
const isFinished = index < results.length
const isProcessing = index === results.length && !isComplete
@@ -114,6 +125,34 @@ export const RefineStage: React.FC<RefineStageProps> = ({
})}
</div>
</ScrollArea>
{isComplete && (
<div className="flex flex-col gap-4 mt-4 p-4 bg-white rounded-xl border border-slate-200 shadow-sm shrink-0 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="flex items-center space-x-2">
<Checkbox
id="saveOutfit"
checked={saveAsOutfit}
onCheckedChange={(c) => setSaveAsOutfit(c === true)}
/>
<Label
htmlFor="saveOutfit"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Save as Outfit
</Label>
</div>
{saveAsOutfit && (
<Input
placeholder="Outfit Name"
value={outfitName}
onChange={(e) => setOutfitName(e.target.value)}
className="w-full animate-in fade-in zoom-in duration-300"
autoFocus
/>
)}
</div>
)}
<style>{`
@keyframes scanline {
0% {
+30
View File
@@ -0,0 +1,30 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/ui/lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }
+8 -4
View File
@@ -1,14 +1,14 @@
import { InferResponseType } from 'hono/client'
import { Plus, Trash2, Upload } from 'lucide-react'
import { Plus, ScanLine, Trash2, Upload } from 'lucide-react'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import { EQUIPMENT_CATEGORIES } from '../../lib/categories'
import { client } from '../client'
import { CreateOutfitDialog } from '../components/CreateOutfitDialog'
import { EquipmentDetailsDialog } from '../components/EquipmentDetailsDialog'
import { UploadEquipmentDialog } from '../components/UploadEquipmentDialog'
import { ExtractorDialog } from '../components/ExtractorDialog'
import { PageHeader } from '../components/PageHeader'
import { UploadEquipmentDialog } from '../components/UploadEquipmentDialog'
import { VibeCard } from '../components/VibeCard'
import { Badge } from '../components/ui/badge'
import { Button } from '../components/ui/button'
@@ -158,11 +158,15 @@ export default function Equipments() {
actions={
viewMode === 'equipments' ? (
<div className="flex gap-2">
<Button onClick={() => setUploadEquipmentOpen(true)} variant="secondary">
<Button
onClick={() => setUploadEquipmentOpen(true)}
variant="secondary"
className="border"
>
<Upload className="mr-2 h-4 w-4" /> Upload
</Button>
<Button onClick={() => setExtractorOpen(true)}>
<Plus className="mr-2 h-4 w-4" /> Extract Equipment
<ScanLine className="mr-2 h-4 w-4" /> Extractor
</Button>
</div>
) : (
+1
View File
@@ -111,6 +111,7 @@ export default function Expressions() {
}
}}
/>
<style>{`@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }`}</style>
</div>
)
}
+23 -22
View File
@@ -11,11 +11,12 @@ import { PageHeader } from '../components/PageHeader'
import { Button } from '../components/ui/button'
import { Card } from '../components/ui/card'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '../components/ui/dropdown-menu'
import { ScrollArea } from '../components/ui/scroll-area'
// Types - Define locally to avoid complex type extraction if client inference is tricky
// But we should try to use inference
@@ -82,7 +83,7 @@ export default function FittingRoom() {
}
return (
<div className="p-8 pt-2 h-full flex flex-col bg-stone-50/30 relative">
<div className="px-8 pb-8 pt-2 h-[calc(100vh-6rem)] flex flex-col bg-stone-50/30 relative">
<PageHeader
title="Fitting Room"
subtitle="System // FITTING_ROOM"
@@ -121,7 +122,7 @@ export default function FittingRoom() {
<img
src={character.image?.path ? `/files/${character.image.path}` : ''}
alt={character.name}
className="w-full h-full object-cover object-top drop-shadow-2xl hover:scale-105 transition-transform duration-500"
className="w-full h-full object-contain drop-shadow-2xl hover:scale-105 transition-transform duration-500"
/>
<div className="absolute bottom-4 left-4 right-4 bg-white/80 backdrop-blur-md p-4 rounded-xl border border-white/50 shadow-sm">
<h3 className="font-bold text-stone-800 text-lg">{character.name}</h3>
@@ -142,21 +143,21 @@ export default function FittingRoom() {
<span className="text-xs font-mono text-stone-400">{generations.length} LOOKS</span>
</div>
<div className="flex-1 overflow-y-auto p-4 custom-scrollbar">
{generations.length === 0 ? (
<div className="h-full flex flex-col items-center justify-center text-stone-400 gap-4">
<div className="w-16 h-16 rounded-full bg-stone-100 flex items-center justify-center">
<Plus className="w-8 h-8 text-stone-300" />
</div>
<div className="text-center">
<p className="font-medium text-stone-600">No looks generated yet.</p>
<p className="text-xs mt-1">Create your first look to get started.</p>
</div>
<Button variant="outline" onClick={() => setCreateLookOpen(true)}>
Create Look
</Button>
{generations.length === 0 ? (
<div className="flex-1 flex flex-col items-center justify-center text-stone-400 gap-4">
<div className="w-16 h-16 rounded-full bg-stone-100 flex items-center justify-center">
<Plus className="w-8 h-8 text-stone-300" />
</div>
) : (
<div className="text-center">
<p className="font-medium text-stone-600">No looks generated yet.</p>
<p className="text-xs mt-1">Create your first look to get started.</p>
</div>
<Button variant="outline" onClick={() => setCreateLookOpen(true)}>
Create Look
</Button>
</div>
) : (
<ScrollArea className="flex-1 p-4">
<div className="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-4">
{generations.map((gen) => (
<div
@@ -207,8 +208,8 @@ export default function FittingRoom() {
</div>
))}
</div>
)}
</div>
</ScrollArea>
)}
</div>
</div>
+1
View File
@@ -111,6 +111,7 @@ export default function Poses() {
}
}}
/>
<style>{`@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }`}</style>
</div>
)
}