|
<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> |