feat: enable automatic live preview updates and improve abacus sizing
- Fix live preview to update automatically when config changes - Use form.Subscribe to properly track form state changes - Increase abacus scale factor from 2.0 to 4.0 for better visibility - Optimize card dimensions (120pt x 160pt) for preview display - Improve SVG CSS handling with proper scaling properties - Add debug logging for preview update troubleshooting 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import { LivePreview } from '@/components/LivePreview'
|
||||
import { GenerationProgress } from '@/components/GenerationProgress'
|
||||
import { DownloadCard } from '@/components/DownloadCard'
|
||||
|
||||
// Complete, validated configuration ready for generation
|
||||
export interface FlashcardConfig {
|
||||
range: string
|
||||
step?: number
|
||||
@@ -39,6 +40,66 @@ export interface FlashcardConfig {
|
||||
format?: 'pdf' | 'html' | 'png' | 'svg'
|
||||
}
|
||||
|
||||
// Partial form state during editing (may have undefined values)
|
||||
export interface FlashcardFormState {
|
||||
range?: string
|
||||
step?: number
|
||||
cardsPerPage?: number
|
||||
paperSize?: 'us-letter' | 'a4' | 'a3' | 'a5'
|
||||
orientation?: 'portrait' | 'landscape'
|
||||
margins?: {
|
||||
top?: string
|
||||
bottom?: string
|
||||
left?: string
|
||||
right?: string
|
||||
}
|
||||
gutter?: string
|
||||
shuffle?: boolean
|
||||
seed?: number
|
||||
showCutMarks?: boolean
|
||||
showRegistration?: boolean
|
||||
fontFamily?: string
|
||||
fontSize?: string
|
||||
columns?: string | number
|
||||
showEmptyColumns?: boolean
|
||||
hideInactiveBeads?: boolean
|
||||
beadShape?: 'diamond' | 'circle' | 'square'
|
||||
colorScheme?: 'monochrome' | 'place-value' | 'heaven-earth' | 'alternating'
|
||||
coloredNumerals?: boolean
|
||||
scaleFactor?: number
|
||||
format?: 'pdf' | 'html' | 'png' | 'svg'
|
||||
}
|
||||
|
||||
// Validation function to convert form state to complete config
|
||||
function validateAndCompleteConfig(formState: FlashcardFormState): FlashcardConfig {
|
||||
return {
|
||||
// Required fields with defaults
|
||||
range: formState.range || '0-99',
|
||||
|
||||
// Optional fields with defaults
|
||||
step: formState.step ?? 1,
|
||||
cardsPerPage: formState.cardsPerPage ?? 6,
|
||||
paperSize: formState.paperSize ?? 'us-letter',
|
||||
orientation: formState.orientation ?? 'portrait',
|
||||
gutter: formState.gutter ?? '5mm',
|
||||
shuffle: formState.shuffle ?? false,
|
||||
seed: formState.seed,
|
||||
showCutMarks: formState.showCutMarks ?? false,
|
||||
showRegistration: formState.showRegistration ?? false,
|
||||
fontFamily: formState.fontFamily ?? 'DejaVu Sans',
|
||||
fontSize: formState.fontSize ?? '48pt',
|
||||
columns: formState.columns ?? 'auto',
|
||||
showEmptyColumns: formState.showEmptyColumns ?? false,
|
||||
hideInactiveBeads: formState.hideInactiveBeads ?? false,
|
||||
beadShape: formState.beadShape ?? 'diamond',
|
||||
colorScheme: formState.colorScheme ?? 'place-value',
|
||||
coloredNumerals: formState.coloredNumerals ?? false,
|
||||
scaleFactor: formState.scaleFactor ?? 0.9,
|
||||
format: formState.format ?? 'pdf',
|
||||
margins: formState.margins
|
||||
}
|
||||
}
|
||||
|
||||
type GenerationStatus = 'idle' | 'generating' | 'success' | 'error'
|
||||
|
||||
interface GenerationResult {
|
||||
@@ -58,7 +119,7 @@ export default function CreatePage() {
|
||||
const [generationResult, setGenerationResult] = useState<GenerationResult | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const form = useForm<FlashcardConfig>({
|
||||
const form = useForm<FlashcardFormState>({
|
||||
defaultValues: {
|
||||
range: '0-99',
|
||||
step: 1,
|
||||
@@ -82,11 +143,14 @@ export default function CreatePage() {
|
||||
}
|
||||
})
|
||||
|
||||
const handleGenerate = async (config: FlashcardConfig) => {
|
||||
const handleGenerate = async (formState: FlashcardFormState) => {
|
||||
setGenerationStatus('generating')
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
// Validate and complete the configuration
|
||||
const config = validateAndCompleteConfig(formState)
|
||||
|
||||
const response = await fetch('/api/generate', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -203,7 +267,10 @@ export default function CreatePage() {
|
||||
shadow: 'card',
|
||||
p: '8'
|
||||
})}>
|
||||
<LivePreview config={form.state.values} />
|
||||
<form.Subscribe
|
||||
selector={(state) => state.values}
|
||||
children={(values) => <LivePreview config={values} />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Generation Status */}
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import { css } from '../../styled-system/css'
|
||||
import { stack, hstack, grid } from '../../styled-system/patterns'
|
||||
import { FlashcardConfig } from '@/app/create/page'
|
||||
import { FlashcardConfig, FlashcardFormState } from '@/app/create/page'
|
||||
import { Eye, RefreshCw } from 'lucide-react'
|
||||
|
||||
interface LivePreviewProps {
|
||||
config: FlashcardConfig
|
||||
config: FlashcardFormState
|
||||
}
|
||||
|
||||
interface PreviewData {
|
||||
@@ -24,10 +24,16 @@ export function LivePreview({ config }: LivePreviewProps) {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// Debug: Log config changes
|
||||
console.log('🔧 LivePreview config changed:', config)
|
||||
|
||||
// Debounced preview generation
|
||||
const debouncedConfig = useDebounce(config, 1000)
|
||||
const debouncedConfig = useDebounce(config, 500)
|
||||
console.log('🕐 Debounced config:', debouncedConfig)
|
||||
|
||||
useEffect(() => {
|
||||
console.log('🚀 useEffect triggered with debouncedConfig:', debouncedConfig)
|
||||
|
||||
const generatePreview = async () => {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
@@ -50,6 +56,9 @@ export function LivePreview({ config }: LivePreviewProps) {
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
console.log('🔍 Preview data received:', data)
|
||||
console.log('🔍 First sample SVG length:', data.samples?.[0]?.front?.length || 'No SVG')
|
||||
console.log('🔍 First sample SVG preview:', data.samples?.[0]?.front?.substring(0, 100) || 'No SVG')
|
||||
setPreviewData(data)
|
||||
} else {
|
||||
throw new Error('Preview generation failed')
|
||||
@@ -117,9 +126,9 @@ export function LivePreview({ config }: LivePreviewProps) {
|
||||
columns: { base: 1, md: 2, lg: 3 },
|
||||
gap: '4'
|
||||
})}>
|
||||
{previewData.samples.map((card, i) => (
|
||||
{previewData.samples.map((card) => (
|
||||
<FlashcardPreview
|
||||
key={i}
|
||||
key={card.number}
|
||||
number={card.number}
|
||||
frontSvg={card.front}
|
||||
backContent={card.back}
|
||||
@@ -145,7 +154,7 @@ export function LivePreview({ config }: LivePreviewProps) {
|
||||
Configuration Summary
|
||||
</h4>
|
||||
<div className={grid({ columns: 2, gap: '3' })}>
|
||||
<ConfigSummaryItem label="Range" value={config.range} />
|
||||
<ConfigSummaryItem label="Range" value={config.range || '0-99'} />
|
||||
<ConfigSummaryItem label="Cards per page" value={config.cardsPerPage?.toString() || '6'} />
|
||||
<ConfigSummaryItem label="Color scheme" value={config.colorScheme || 'place-value'} />
|
||||
<ConfigSummaryItem label="Bead shape" value={config.beadShape || 'diamond'} />
|
||||
@@ -166,6 +175,18 @@ function FlashcardPreview({
|
||||
}) {
|
||||
const [showBack, setShowBack] = useState(false)
|
||||
|
||||
// Reset to front when new SVG data comes in
|
||||
useEffect(() => {
|
||||
if (frontSvg && frontSvg.trim()) {
|
||||
setShowBack(false)
|
||||
}
|
||||
}, [frontSvg])
|
||||
|
||||
// Debug logging (simple)
|
||||
if (!frontSvg || !frontSvg.trim()) {
|
||||
console.warn(`⚠️ No SVG for number ${number}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={css({
|
||||
@@ -229,11 +250,32 @@ function FlashcardPreview({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
})}>
|
||||
{frontSvg ? (
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: frontSvg }}
|
||||
className={css({ maxW: 'full', maxH: 'full' })}
|
||||
/>
|
||||
{frontSvg && frontSvg.trim() ? (
|
||||
<div className={css({
|
||||
maxW: 'full',
|
||||
maxH: 'full',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden'
|
||||
})}>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: frontSvg }}
|
||||
className={css({
|
||||
maxW: 'full',
|
||||
maxH: 'full',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
'& svg': {
|
||||
maxWidth: '100%',
|
||||
maxHeight: '100%',
|
||||
width: 'auto',
|
||||
height: 'auto'
|
||||
}
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<SorobanPlaceholder number={number} />
|
||||
)}
|
||||
@@ -331,23 +373,25 @@ function useDebounce<T>(value: T, delay: number): T {
|
||||
return debouncedValue
|
||||
}
|
||||
|
||||
function getPreviewRange(range: string): string {
|
||||
function getPreviewRange(range: string | undefined): string {
|
||||
// For preview, limit to a few sample numbers
|
||||
if (range.includes('-')) {
|
||||
const [start] = range.split('-')
|
||||
const safeRange = range || '0-99' // Default fallback for undefined range
|
||||
|
||||
if (safeRange.includes('-')) {
|
||||
const [start] = safeRange.split('-')
|
||||
const startNum = parseInt(start) || 0
|
||||
return `${startNum}-${startNum + 2}`
|
||||
}
|
||||
|
||||
if (range.includes(',')) {
|
||||
const numbers = range.split(',').slice(0, 3)
|
||||
if (safeRange.includes(',')) {
|
||||
const numbers = safeRange.split(',').slice(0, 3)
|
||||
return numbers.join(',')
|
||||
}
|
||||
|
||||
return range
|
||||
return safeRange
|
||||
}
|
||||
|
||||
function getMockPreviewData(config: FlashcardConfig): PreviewData {
|
||||
function getMockPreviewData(config: FlashcardFormState): PreviewData {
|
||||
// Mock data for development/fallback
|
||||
return {
|
||||
count: 3,
|
||||
|
||||
@@ -198,7 +198,7 @@ def generate_single_card_json(config_json):
|
||||
if number is None:
|
||||
return json.dumps({'error': 'Missing number parameter'})
|
||||
|
||||
# Build Typst config
|
||||
# Build Typst config optimized for preview display
|
||||
typst_config = {
|
||||
'bead_shape': config.get('beadShape', 'diamond'),
|
||||
'color_scheme': config.get('colorScheme', 'monochrome'),
|
||||
@@ -208,11 +208,11 @@ def generate_single_card_json(config_json):
|
||||
'show_empty_columns': config.get('showEmptyColumns', False),
|
||||
'columns': config.get('columns', 'auto'),
|
||||
'transparent': config.get('transparent', False),
|
||||
'card_width': config.get('cardWidth', '3.5in'),
|
||||
'card_height': config.get('cardHeight', '2.5in'),
|
||||
'card_width': '120pt', # Smaller card for larger abacus
|
||||
'card_height': '160pt', # Smaller card for larger abacus
|
||||
'font_size': config.get('fontSize', '48pt'),
|
||||
'font_family': config.get('fontFamily', 'DejaVu Sans'),
|
||||
'scale_factor': config.get('scaleFactor', 1.0),
|
||||
'scale_factor': config.get('scaleFactor', 4.0), # Much larger scale for preview visibility
|
||||
}
|
||||
|
||||
# Generate in core package directory
|
||||
|
||||
Reference in New Issue
Block a user