| <script lang="ts"> |
| import type { PicletWorkflowStep } from '$lib/types'; |
| |
| interface Props { |
| currentStep: PicletWorkflowStep; |
| error?: string | null; |
| } |
| |
| let { currentStep, error = null }: Props = $props(); |
| |
| interface StepInfo { |
| id: PicletWorkflowStep; |
| label: string; |
| description: string; |
| } |
| |
| const steps: StepInfo[] = [ |
| { |
| id: 'upload', |
| label: 'Upload Photo', |
| description: 'Select your image' |
| }, |
| { |
| id: 'captioning', |
| label: 'Image Analysis', |
| description: 'Analyzing your photo' |
| }, |
| { |
| id: 'conceptualizing', |
| label: 'Piclet Design', |
| description: 'Creating concept & lore' |
| }, |
| { |
| id: 'statsGenerating', |
| label: 'Battle Stats', |
| description: 'Generating abilities' |
| }, |
| { |
| id: 'promptCrafting', |
| label: 'Art Planning', |
| description: 'Preparing visual prompt' |
| }, |
| { |
| id: 'generating', |
| label: 'Image Generation', |
| description: 'Creating piclet art' |
| }, |
| { |
| id: 'complete', |
| label: 'Complete', |
| description: 'Your piclet is ready!' |
| } |
| ]; |
| |
| function getStepIndex(step: PicletWorkflowStep): number { |
| return steps.findIndex(s => s.id === step); |
| } |
| |
| function getStepStatus(step: StepInfo): 'completed' | 'current' | 'pending' | 'error' { |
| const currentIndex = getStepIndex(currentStep); |
| const stepIndex = getStepIndex(step.id); |
| |
| if (error && step.id === currentStep) return 'error'; |
| if (stepIndex < currentIndex) return 'completed'; |
| if (stepIndex === currentIndex) return 'current'; |
| return 'pending'; |
| } |
| </script> |
|
|
| <div class="workflow-progress"> |
| <div class="steps-container"> |
| {#each steps as step, i} |
| {@const status = getStepStatus(step)} |
| <div class="step" class:completed={status === 'completed'} class:current={status === 'current'} class:error={status === 'error'}> |
| <div class="step-indicator"> |
| {#if status === 'completed'} |
| <svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor"> |
| <path d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 111.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"/> |
| </svg> |
| {:else if status === 'error'} |
| <svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor"> |
| <path d="M10 2a8 8 0 100 16 8 8 0 000-16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"/> |
| </svg> |
| {:else} |
| <span class="step-number">{i + 1}</span> |
| {/if} |
| </div> |
| <div class="step-content"> |
| <div class="step-label">{step.label}</div> |
| <div class="step-description">{step.description}</div> |
| </div> |
| {#if i < steps.length - 1} |
| <div class="step-connector" class:active={status === 'completed'}></div> |
| {/if} |
| </div> |
| {/each} |
| </div> |
| |
| {#if error} |
| <div class="error-message"> |
| <svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor"> |
| <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"/> |
| </svg> |
| <span>{error}</span> |
| </div> |
| {/if} |
| </div> |
|
|
| <style> |
| .workflow-progress { |
| max-width: 800px; |
| margin: 2rem auto; |
| } |
| |
| .steps-container { |
| display: flex; |
| justify-content: space-between; |
| position: relative; |
| padding: 0 20px; |
| } |
| |
| .step { |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| position: relative; |
| flex: 1; |
| } |
| |
| .step-indicator { |
| width: 40px; |
| height: 40px; |
| border-radius: 50%; |
| background: #e9ecef; |
| color: #6c757d; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| font-weight: 600; |
| margin-bottom: 0.5rem; |
| transition: all 0.3s ease; |
| z-index: 2; |
| } |
| |
| .step.completed .step-indicator { |
| background: #28a745; |
| color: white; |
| } |
| |
| .step.current .step-indicator { |
| background: #007bff; |
| color: white; |
| box-shadow: 0 0 0 4px rgba(0, 123, 255, 0.2); |
| animation: pulse 2s infinite; |
| } |
| |
| .step.error .step-indicator { |
| background: #dc3545; |
| color: white; |
| } |
| |
| .step-number { |
| font-size: 0.9rem; |
| } |
| |
| .step-content { |
| text-align: center; |
| } |
| |
| .step-label { |
| font-weight: 600; |
| color: #333; |
| margin-bottom: 0.25rem; |
| font-size: 0.9rem; |
| } |
| |
| .step-description { |
| font-size: 0.8rem; |
| color: #666; |
| } |
| |
| .step-connector { |
| position: absolute; |
| top: 20px; |
| left: 50%; |
| width: 100%; |
| height: 2px; |
| background: #e9ecef; |
| z-index: 1; |
| } |
| |
| .step-connector.active { |
| background: #28a745; |
| } |
| |
| .error-message { |
| margin-top: 2rem; |
| padding: 1rem; |
| background: #f8d7da; |
| color: #721c24; |
| border-radius: 6px; |
| display: flex; |
| align-items: center; |
| gap: 0.5rem; |
| } |
| |
| @keyframes pulse { |
| 0% { |
| box-shadow: 0 0 0 0 rgba(0, 123, 255, 0.4); |
| } |
| 70% { |
| box-shadow: 0 0 0 10px rgba(0, 123, 255, 0); |
| } |
| 100% { |
| box-shadow: 0 0 0 0 rgba(0, 123, 255, 0); |
| } |
| } |
| |
| @media (max-width: 768px) { |
| .steps-container { |
| flex-direction: column; |
| padding: 0; |
| } |
| |
| .step { |
| flex-direction: row; |
| margin-bottom: 1rem; |
| } |
| |
| .step-indicator { |
| margin-right: 1rem; |
| margin-bottom: 0; |
| } |
| |
| .step-content { |
| text-align: left; |
| flex: 1; |
| } |
| |
| .step-connector { |
| display: none; |
| } |
| } |
| </style> |