Fraser's picture
monsters
ececfe6
raw
history blame
8.03 kB
<script lang="ts">
import type { MonsterWorkflowState } from '$lib/types';
import { saveMonster } from '$lib/db/monsters';
interface Props {
workflowState: MonsterWorkflowState;
onReset: () => void;
}
let { workflowState, onReset }: Props = $props();
let isSaving = $state(false);
let isSaved = $state(false);
let saveError: string | null = $state(null);
function downloadImage() {
if (!workflowState.monsterImage?.imageUrl) return;
const link = document.createElement('a');
link.href = workflowState.monsterImage.imageUrl;
link.download = `monster-${Date.now()}.png`;
link.click();
}
function copyPrompt() {
if (!workflowState.imagePrompt) return;
navigator.clipboard.writeText(workflowState.imagePrompt);
alert('Prompt copied to clipboard!');
}
async function saveToCollection() {
if (!workflowState.monsterImage || !workflowState.imageCaption || !workflowState.monsterConcept || !workflowState.imagePrompt) {
saveError = 'Missing monster data';
return;
}
isSaving = true;
saveError = null;
try {
// Extract monster name from concept (usually first line or after "Monster Name:")
let monsterName = 'Unknown Monster';
const conceptLines = workflowState.monsterConcept.split('\n');
for (const line of conceptLines) {
if (line.includes('Monster Name:') || line.includes('**Monster Name:**')) {
monsterName = line.replace(/\*\*Monster Name:\*\*|Monster Name:/g, '').trim();
break;
} else if (line.trim() && !line.includes(':')) {
// First non-empty line without colon might be the name
monsterName = line.trim();
break;
}
}
await saveMonster({
name: monsterName,
imageUrl: workflowState.monsterImage.imageUrl,
imageCaption: workflowState.imageCaption,
concept: workflowState.monsterConcept,
imagePrompt: workflowState.imagePrompt
});
isSaved = true;
} catch (err) {
console.error('Failed to save monster:', err);
saveError = 'Failed to save monster to collection';
} finally {
isSaving = false;
}
}
</script>
<div class="result-container">
<h3>Your Monster Has Been Created!</h3>
{#if workflowState.monsterImage}
<div class="monster-image-container">
<img
src={workflowState.monsterImage.imageUrl}
alt="Generated Monster"
class="monster-image"
/>
</div>
{/if}
<div class="results-grid">
<div class="result-section">
<h4>Original Description</h4>
<div class="result-content">
<p>{workflowState.imageCaption || 'No caption available'}</p>
</div>
</div>
<div class="result-section">
<h4>Monster Concept</h4>
<div class="result-content">
<p>{workflowState.monsterConcept || 'No concept available'}</p>
</div>
</div>
<div class="result-section">
<h4>Generation Prompt</h4>
<div class="result-content">
<p>{workflowState.imagePrompt || 'No prompt available'}</p>
{#if workflowState.imagePrompt}
<button class="copy-button" onclick={copyPrompt}>
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M4 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V2zm2 0v8h8V2H6zM2 6a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2v-2h-2v2H2V8h2V6H2z"/>
</svg>
Copy
</button>
{/if}
</div>
</div>
</div>
<div class="action-buttons">
<button class="action-button download" onclick={downloadImage}>
<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
<path d="M13 8V2H7v6H2l8 8 8-8h-5zM0 18h20v2H0v-2z"/>
</svg>
Download Monster
</button>
<button
class="action-button save"
onclick={saveToCollection}
disabled={isSaving || isSaved}
>
{#if isSaving}
<div class="spinner-small"></div>
Saving...
{:else if isSaved}
<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
<path d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"/>
</svg>
Saved!
{:else}
<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
<path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"/>
</svg>
Save to Collection
{/if}
</button>
<button class="action-button reset" onclick={onReset}>
<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
<path d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.058 7.293a1 1 0 01-1.414 1.414l-2.35-2.35A1 1 0 011 5.648V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.943 13H13a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z"/>
</svg>
Create Another Monster
</button>
</div>
{#if saveError}
<div class="error-message">{saveError}</div>
{/if}
</div>
<style>
.result-container {
max-width: 900px;
margin: 0 auto;
padding: 2rem;
}
h3 {
text-align: center;
color: #333;
margin-bottom: 2rem;
}
.monster-image-container {
text-align: center;
margin-bottom: 3rem;
}
.monster-image {
max-width: 100%;
max-height: 600px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}
.results-grid {
display: grid;
gap: 1.5rem;
margin-bottom: 3rem;
}
.result-section {
background: #f8f9fa;
padding: 1.5rem;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.result-section h4 {
margin: 0 0 1rem 0;
color: #495057;
font-size: 1.1rem;
}
.result-content {
position: relative;
}
.result-content p {
margin: 0;
line-height: 1.6;
color: #333;
}
.copy-button {
position: absolute;
top: -8px;
right: -8px;
background: #007bff;
color: white;
border: none;
padding: 0.4rem 0.8rem;
border-radius: 4px;
font-size: 0.85rem;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.3rem;
transition: background 0.2s;
}
.copy-button:hover {
background: #0056b3;
}
.action-buttons {
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
}
.action-button {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.8rem 1.5rem;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.action-button.download {
background: #28a745;
color: white;
}
.action-button.download:hover {
background: #218838;
}
.action-button.reset {
background: #6c757d;
color: white;
}
.action-button.reset:hover {
background: #5a6268;
}
.action-button.save {
background: #007bff;
color: white;
}
.action-button.save:hover:not(:disabled) {
background: #0056b3;
}
.action-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.spinner-small {
width: 16px;
height: 16px;
border: 2px solid #ffffff;
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.error-message {
margin-top: 1rem;
padding: 0.5rem;
background: #f8d7da;
color: #721c24;
border-radius: 4px;
text-align: center;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@media (max-width: 768px) {
.result-container {
padding: 1rem;
}
.action-buttons {
flex-direction: column;
}
.action-button {
width: 100%;
justify-content: center;
}
}
</style>