diff --git a/AGENTS.md b/AGENTS.md index 662e364..638c678 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -99,6 +99,8 @@ We use `bun:test` for testing. - Fixes must be iterated until **no check causes any other check to fail**. - Do **not** submit intermediate states where some checks pass and others fail, even temporarily. +- **Cleanup Requirement**: Remove all verification related files (scripts, screenshots, `verification/` folder) before submit. + ## Prisma Configuration & Migrations - Schema: `prisma/schema.prisma` @@ -113,6 +115,7 @@ Whenever you modify the `prisma/schema.prisma` file, you **must** generate a cor **Do not create migration SQL files manually.** Instead, follow this workflow: + 1. Ensure you have a local SQLite database configured (e.g., via `.env` setting `DATABASE_URL="file:./dev.db"`). 2. Run the migration command: ```bash diff --git a/src/ui/components/ExtractionFlowDesigner.tsx b/src/ui/components/ExtractionFlowDesigner.tsx new file mode 100644 index 0000000..0444f74 --- /dev/null +++ b/src/ui/components/ExtractionFlowDesigner.tsx @@ -0,0 +1,172 @@ +import { ArrowDown, BoxSelect, FileSearch, Image as ImageIcon, User, Wand2 } from 'lucide-react' +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from './ui/select' + +interface ExtractionFlowDesignerProps { + settings: Record + openaiTextModels: string[] + openaiImageModels: string[] + googleTextModels: string[] + googleImageModels: string[] + onSelect: (key: string, value: string) => void +} + +export function ExtractionFlowDesigner({ + settings, + openaiTextModels, + openaiImageModels, + googleTextModels, + googleImageModels, + onSelect, +}: ExtractionFlowDesignerProps) { + const steps = [ + { + id: 'analyze', + key: 'step_analyze_model', + title: 'Analyze Image', + description: 'Extracts visual features and descriptions from the input image.', + icon: , + type: 'text', + }, + { + id: 'texture', + key: 'step_texture_model', + title: 'Generate Texture', + description: 'Creates a texture sheet based on the analysis.', + icon: , + type: 'image', + }, + { + id: 'bbox', + key: 'step_bounding_box_model', + title: 'Detect Bounding Boxes', + description: 'Identifies regions for individual equipment pieces.', + icon: , + type: 'text', + }, + { + id: 'refine', + key: 'step_refine_model', + title: 'Refine Asset', + description: 'Polishes and isolates each equipment piece.', + icon: , + type: 'image', + }, + { + id: 'generation', + key: 'step_generation_model', + title: 'Character Generation', + description: 'Generates the final character using the equipment.', + icon: , + type: 'image', + }, + ] + + return ( +
+ {/* Vertical Line */} +
+ +
+ {steps.map((step, index) => { + const textModels = + step.type === 'text' + ? { openai: openaiTextModels, google: googleTextModels } + : { openai: openaiImageModels, google: googleImageModels } + + return ( +
+ {/* Icon Bubble */} +
+ {step.icon} +
+ + {/* Card */} +
+
+
+

+ 0{index + 1} // + {step.title} +

+

{step.description}

+
+ +
+ onSelect(step.key, val)} + /> +
+
+
+ + {/* Arrow Connection (except last) */} + {index < steps.length - 1 && ( +
+ +
+ )} +
+ ) + })} +
+
+ ) +} + +function ModelSelect({ + value, + openaiModels, + googleModels, + onSelect, +}: { + value?: string + openaiModels: string[] + googleModels: string[] + onSelect: (val: string) => void +}) { + return ( + + ) +} diff --git a/src/ui/components/PageHeader.tsx b/src/ui/components/PageHeader.tsx new file mode 100644 index 0000000..0acff8f --- /dev/null +++ b/src/ui/components/PageHeader.tsx @@ -0,0 +1,53 @@ +import { Hexagon } from 'lucide-react' +import { ReactNode } from 'react' +import { cn } from '../lib/utils' + +interface PageHeaderProps { + title: ReactNode + subtitle?: ReactNode + actions?: ReactNode + children?: ReactNode + className?: string + sticky?: boolean + topOffset?: string +} + +export function PageHeader({ + title, + subtitle, + actions, + children, + className, + sticky = true, + topOffset = '4rem', // Default to 16 (4rem) below global header +}: PageHeaderProps) { + return ( +
+
+
+ {subtitle && ( +
+ + {subtitle} +
+ )} +

+ + {title} + +

+
+ {actions &&
{actions}
} +
+ {children &&
{children}
} +
+ ) +} diff --git a/src/ui/pages/Characters.tsx b/src/ui/pages/Characters.tsx index 1167c8c..24a9447 100644 --- a/src/ui/pages/Characters.tsx +++ b/src/ui/pages/Characters.tsx @@ -1,9 +1,10 @@ import { InferResponseType } from 'hono/client' -import { Hexagon, Plus } from 'lucide-react' +import { Plus } from 'lucide-react' import { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' import { client } from '../client' import { CreateCharacterDialog } from '../components/CreateCharacterDialog' +import { PageHeader } from '../components/PageHeader' import { VibeCard } from '../components/VibeCard' import { Button } from '../components/ui/button' @@ -60,25 +61,17 @@ export default function Characters() { } return ( -
-
-
-
- - System // Character_List -
-

- - Characters - -

-
-
+
+ setCreateOpen(true)}> Create Character -
-
+ } + className="mb-8" + /> {characters.length === 0 ? (
diff --git a/src/ui/pages/Equipments.tsx b/src/ui/pages/Equipments.tsx index 3440f1f..b599c37 100644 --- a/src/ui/pages/Equipments.tsx +++ b/src/ui/pages/Equipments.tsx @@ -1,11 +1,12 @@ import { InferResponseType } from 'hono/client' -import { Hexagon, Plus, Trash2 } from 'lucide-react' +import { Plus, Trash2 } 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 { ExtractorDialog } from '../components/ExtractorDialog' +import { PageHeader } from '../components/PageHeader' import { VibeCard } from '../components/VibeCard' import { Badge } from '../components/ui/badge' import { Button } from '../components/ui/button' @@ -108,40 +109,35 @@ export default function Equipments() { } return ( -
-
-
-
- +
+ System //{' '} setViewMode('equipments')} > Equipment_List {' '} - | + |{' '} setViewMode('outfits')} > Outfit_List -
-

- - {viewMode === 'equipments' ? 'Equipments' : 'Outfits'} - -

-
-
- {viewMode === 'equipments' ? ( + + } + actions={ + viewMode === 'equipments' ? ( @@ -149,13 +145,12 @@ export default function Equipments() { - )} -
-
- - {/* Category Filter */} - {viewMode === 'equipments' && ( -
+ ) + } + className="mb-8" + > + {/* Category Filter - Sticky inside PageHeader */} + {viewMode === 'equipments' && (
{EQUIPMENT_CATEGORIES.map((cat) => { @@ -179,8 +174,8 @@ export default function Equipments() {
-
- )} + )} + {loading ? (
diff --git a/src/ui/pages/Settings.tsx b/src/ui/pages/Settings.tsx index dab3a36..e9a1264 100644 --- a/src/ui/pages/Settings.tsx +++ b/src/ui/pages/Settings.tsx @@ -1,20 +1,13 @@ -import { useState, useEffect } from 'react' +import { client } from '@/ui/client' +import { ExtractionFlowDesigner } from '@/ui/components/ExtractionFlowDesigner' +import { PageHeader } from '@/ui/components/PageHeader' import { Button } from '@/ui/components/ui/button' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/ui/components/ui/card' import { Input } from '@/ui/components/ui/input' import { Label } from '@/ui/components/ui/label' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/ui/components/ui/card' -import { client } from '@/ui/client' -import { Loader2, Plus, Trash2 } from 'lucide-react' -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, - SelectGroup, - SelectLabel, -} from '@/ui/components/ui/select' import { useToast } from '@/ui/hooks/use-toast' +import { Loader2, Plus, Trash2 } from 'lucide-react' +import { useEffect, useState } from 'react' export default function Settings() { const { toast } = useToast() @@ -121,178 +114,154 @@ export default function Settings() { if (loading) { return ( -
- +
+
) } return ( -
-

Settings

+
+ - {/* OpenAI Provider */} - - - OpenAI Provider - Configure OpenAI API settings and models. - - -
- -
- setSettings({ ...settings, openai_api_key: e.target.value })} - placeholder="sk-..." - /> - +
+ {/* OpenAI Provider */} + + + OpenAI Provider + Configure OpenAI API settings and models. + + +
+ +
+ setSettings({ ...settings, openai_api_key: e.target.value })} + placeholder="sk-..." + className="font-mono text-sm" + /> + +
-
-
- -
- setSettings({ ...settings, openai_base_url: e.target.value })} - placeholder="https://api.openai.com/v1" - /> - +
+ +
+ setSettings({ ...settings, openai_base_url: e.target.value })} + placeholder="https://api.openai.com/v1" + className="font-mono text-sm" + /> + +
+ + + addModel(openaiTextModels, setOpenaiTextModels, 'openai_text_models', m) + } + onRemove={(m) => + removeModel(openaiTextModels, setOpenaiTextModels, 'openai_text_models', m) + } + /> + + addModel(openaiImageModels, setOpenaiImageModels, 'openai_image_models', m) + } + onRemove={(m) => + removeModel(openaiImageModels, setOpenaiImageModels, 'openai_image_models', m) + } + /> + + + + {/* Google Provider */} + + + Google Provider + Configure Google Gemini API settings and models. + + +
+ +
+ setSettings({ ...settings, google_api_key: e.target.value })} + placeholder="AIza..." + className="font-mono text-sm" + /> + +
+
+ + + addModel(googleTextModels, setGoogleTextModels, 'google_text_models', m) + } + onRemove={(m) => + removeModel(googleTextModels, setGoogleTextModels, 'google_text_models', m) + } + /> + + addModel(googleImageModels, setGoogleImageModels, 'google_image_models', m) + } + onRemove={(m) => + removeModel(googleImageModels, setGoogleImageModels, 'google_image_models', m) + } + /> +
+
+ + {/* Extraction Flow Defaults */} +
+
+

Extraction Flow Defaults

+

+ Configure the AI models used for each step of the asset generation pipeline. +

- addModel(openaiTextModels, setOpenaiTextModels, 'openai_text_models', m)} - onRemove={(m) => - removeModel(openaiTextModels, setOpenaiTextModels, 'openai_text_models', m) - } + saveSetting(key, val)} /> - - addModel(openaiImageModels, setOpenaiImageModels, 'openai_image_models', m) - } - onRemove={(m) => - removeModel(openaiImageModels, setOpenaiImageModels, 'openai_image_models', m) - } - /> - - - - {/* Google Provider */} - - - Google Provider - Configure Google Gemini API settings and models. - - -
- -
- setSettings({ ...settings, google_api_key: e.target.value })} - placeholder="AIza..." - /> - -
-
- - addModel(googleTextModels, setGoogleTextModels, 'google_text_models', m)} - onRemove={(m) => - removeModel(googleTextModels, setGoogleTextModels, 'google_text_models', m) - } - /> - - addModel(googleImageModels, setGoogleImageModels, 'google_image_models', m) - } - onRemove={(m) => - removeModel(googleImageModels, setGoogleImageModels, 'google_image_models', m) - } - /> -
-
- - {/* Extraction Flow Defaults */} - - - Extraction Flow Defaults - - Select default models for each step of the extraction process. - - - - {/* Step 1: Analyze Image (Text) */} - saveSetting('step_analyze_model', val)} - /> - {/* Step 2: Texture Sheet (Image) */} - saveSetting('step_texture_model', val)} - /> - {/* Step 3: Bounding Box (Text) */} - saveSetting('step_bounding_box_model', val)} - /> - {/* Step 4: Refine Asset (Image) */} - saveSetting('step_refine_model', val)} - /> - {/* Step: Generation (Image) */} - saveSetting('step_generation_model', val)} - /> - - +
+
) } @@ -318,7 +287,7 @@ function ModelList({ value={newModel} onChange={(e) => setNewModel(e.target.value)} placeholder="Add model (e.g., gpt-4o)" - className="max-w-xs" + className="max-w-xs font-mono text-sm" />
))} {models.length === 0 && ( - No models configured. + No models configured. )}
) } - -function StepSelector({ - label, - value, - openaiModels, - googleModels, - onSelect, -}: { - label: string - value?: string - openaiModels: string[] - googleModels: string[] - onSelect: (val: string) => void -}) { - return ( -
- - -
- ) -}