monsters
Browse files- package-lock.json +9 -0
- package.json +3 -0
- src/lib/components/MonsterGenerator/MonsterGenerator.svelte +1 -1
- src/lib/components/MonsterGenerator/MonsterResult.svelte +117 -12
- src/lib/components/MonsterGenerator/todo.txt +67 -0
- src/lib/db/index.ts +16 -0
- src/lib/db/monsters.ts +23 -0
- src/lib/db/schema.ts +9 -0
package-lock.json
CHANGED
|
@@ -7,6 +7,9 @@
|
|
| 7 |
"": {
|
| 8 |
"name": "svelte",
|
| 9 |
"version": "0.0.0",
|
|
|
|
|
|
|
|
|
|
| 10 |
"devDependencies": {
|
| 11 |
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
| 12 |
"@tsconfig/svelte": "^5.0.4",
|
|
@@ -953,6 +956,12 @@
|
|
| 953 |
"node": ">=0.10.0"
|
| 954 |
}
|
| 955 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 956 |
"node_modules/esbuild": {
|
| 957 |
"version": "0.25.6",
|
| 958 |
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz",
|
|
|
|
| 7 |
"": {
|
| 8 |
"name": "svelte",
|
| 9 |
"version": "0.0.0",
|
| 10 |
+
"dependencies": {
|
| 11 |
+
"dexie": "^4.0.11"
|
| 12 |
+
},
|
| 13 |
"devDependencies": {
|
| 14 |
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
| 15 |
"@tsconfig/svelte": "^5.0.4",
|
|
|
|
| 956 |
"node": ">=0.10.0"
|
| 957 |
}
|
| 958 |
},
|
| 959 |
+
"node_modules/dexie": {
|
| 960 |
+
"version": "4.0.11",
|
| 961 |
+
"resolved": "https://registry.npmjs.org/dexie/-/dexie-4.0.11.tgz",
|
| 962 |
+
"integrity": "sha512-SOKO002EqlvBYYKQSew3iymBoN2EQ4BDw/3yprjh7kAfFzjBYkaMNa/pZvcA7HSWlcKSQb9XhPe3wKyQ0x4A8A==",
|
| 963 |
+
"license": "Apache-2.0"
|
| 964 |
+
},
|
| 965 |
"node_modules/esbuild": {
|
| 966 |
"version": "0.25.6",
|
| 967 |
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz",
|
package.json
CHANGED
|
@@ -17,5 +17,8 @@
|
|
| 17 |
"svelte-check": "^4.1.6",
|
| 18 |
"typescript": "~5.8.3",
|
| 19 |
"vite": "^6.3.5"
|
|
|
|
|
|
|
|
|
|
| 20 |
}
|
| 21 |
}
|
|
|
|
| 17 |
"svelte-check": "^4.1.6",
|
| 18 |
"typescript": "~5.8.3",
|
| 19 |
"vite": "^6.3.5"
|
| 20 |
+
},
|
| 21 |
+
"dependencies": {
|
| 22 |
+
"dexie": "^4.0.11"
|
| 23 |
}
|
| 24 |
}
|
src/lib/components/MonsterGenerator/MonsterGenerator.svelte
CHANGED
|
@@ -281,7 +281,7 @@ Assistant:`;
|
|
| 281 |
isProcessing={state.isProcessing}
|
| 282 |
/>
|
| 283 |
{:else if state.currentStep === 'complete'}
|
| 284 |
-
<MonsterResult
|
| 285 |
{:else}
|
| 286 |
<div class="processing-container">
|
| 287 |
<div class="spinner"></div>
|
|
|
|
| 281 |
isProcessing={state.isProcessing}
|
| 282 |
/>
|
| 283 |
{:else if state.currentStep === 'complete'}
|
| 284 |
+
<MonsterResult workflowState={state} onReset={reset} />
|
| 285 |
{:else}
|
| 286 |
<div class="processing-container">
|
| 287 |
<div class="spinner"></div>
|
src/lib/components/MonsterGenerator/MonsterResult.svelte
CHANGED
|
@@ -1,36 +1,81 @@
|
|
| 1 |
<script lang="ts">
|
| 2 |
import type { MonsterWorkflowState } from '$lib/types';
|
|
|
|
| 3 |
|
| 4 |
interface Props {
|
| 5 |
-
|
| 6 |
onReset: () => void;
|
| 7 |
}
|
| 8 |
|
| 9 |
-
let {
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
function downloadImage() {
|
| 12 |
-
if (!
|
| 13 |
|
| 14 |
const link = document.createElement('a');
|
| 15 |
-
link.href =
|
| 16 |
link.download = `monster-${Date.now()}.png`;
|
| 17 |
link.click();
|
| 18 |
}
|
| 19 |
|
| 20 |
function copyPrompt() {
|
| 21 |
-
if (!
|
| 22 |
-
navigator.clipboard.writeText(
|
| 23 |
alert('Prompt copied to clipboard!');
|
| 24 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
</script>
|
| 26 |
|
| 27 |
<div class="result-container">
|
| 28 |
<h3>Your Monster Has Been Created!</h3>
|
| 29 |
|
| 30 |
-
{#if
|
| 31 |
<div class="monster-image-container">
|
| 32 |
<img
|
| 33 |
-
src={
|
| 34 |
alt="Generated Monster"
|
| 35 |
class="monster-image"
|
| 36 |
/>
|
|
@@ -41,22 +86,22 @@
|
|
| 41 |
<div class="result-section">
|
| 42 |
<h4>Original Description</h4>
|
| 43 |
<div class="result-content">
|
| 44 |
-
<p>{
|
| 45 |
</div>
|
| 46 |
</div>
|
| 47 |
|
| 48 |
<div class="result-section">
|
| 49 |
<h4>Monster Concept</h4>
|
| 50 |
<div class="result-content">
|
| 51 |
-
<p>{
|
| 52 |
</div>
|
| 53 |
</div>
|
| 54 |
|
| 55 |
<div class="result-section">
|
| 56 |
<h4>Generation Prompt</h4>
|
| 57 |
<div class="result-content">
|
| 58 |
-
<p>{
|
| 59 |
-
{#if
|
| 60 |
<button class="copy-button" onclick={copyPrompt}>
|
| 61 |
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
| 62 |
<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"/>
|
|
@@ -75,6 +120,26 @@
|
|
| 75 |
</svg>
|
| 76 |
Download Monster
|
| 77 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
<button class="action-button reset" onclick={onReset}>
|
| 79 |
<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
|
| 80 |
<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"/>
|
|
@@ -82,6 +147,10 @@
|
|
| 82 |
Create Another Monster
|
| 83 |
</button>
|
| 84 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
</div>
|
| 86 |
|
| 87 |
<style>
|
|
@@ -197,6 +266,42 @@
|
|
| 197 |
background: #5a6268;
|
| 198 |
}
|
| 199 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
@media (max-width: 768px) {
|
| 201 |
.result-container {
|
| 202 |
padding: 1rem;
|
|
|
|
| 1 |
<script lang="ts">
|
| 2 |
import type { MonsterWorkflowState } from '$lib/types';
|
| 3 |
+
import { saveMonster } from '$lib/db/monsters';
|
| 4 |
|
| 5 |
interface Props {
|
| 6 |
+
workflowState: MonsterWorkflowState;
|
| 7 |
onReset: () => void;
|
| 8 |
}
|
| 9 |
|
| 10 |
+
let { workflowState, onReset }: Props = $props();
|
| 11 |
+
let isSaving = $state(false);
|
| 12 |
+
let isSaved = $state(false);
|
| 13 |
+
let saveError: string | null = $state(null);
|
| 14 |
|
| 15 |
function downloadImage() {
|
| 16 |
+
if (!workflowState.monsterImage?.imageUrl) return;
|
| 17 |
|
| 18 |
const link = document.createElement('a');
|
| 19 |
+
link.href = workflowState.monsterImage.imageUrl;
|
| 20 |
link.download = `monster-${Date.now()}.png`;
|
| 21 |
link.click();
|
| 22 |
}
|
| 23 |
|
| 24 |
function copyPrompt() {
|
| 25 |
+
if (!workflowState.imagePrompt) return;
|
| 26 |
+
navigator.clipboard.writeText(workflowState.imagePrompt);
|
| 27 |
alert('Prompt copied to clipboard!');
|
| 28 |
}
|
| 29 |
+
|
| 30 |
+
async function saveToCollection() {
|
| 31 |
+
if (!workflowState.monsterImage || !workflowState.imageCaption || !workflowState.monsterConcept || !workflowState.imagePrompt) {
|
| 32 |
+
saveError = 'Missing monster data';
|
| 33 |
+
return;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
isSaving = true;
|
| 37 |
+
saveError = null;
|
| 38 |
+
|
| 39 |
+
try {
|
| 40 |
+
// Extract monster name from concept (usually first line or after "Monster Name:")
|
| 41 |
+
let monsterName = 'Unknown Monster';
|
| 42 |
+
const conceptLines = workflowState.monsterConcept.split('\n');
|
| 43 |
+
for (const line of conceptLines) {
|
| 44 |
+
if (line.includes('Monster Name:') || line.includes('**Monster Name:**')) {
|
| 45 |
+
monsterName = line.replace(/\*\*Monster Name:\*\*|Monster Name:/g, '').trim();
|
| 46 |
+
break;
|
| 47 |
+
} else if (line.trim() && !line.includes(':')) {
|
| 48 |
+
// First non-empty line without colon might be the name
|
| 49 |
+
monsterName = line.trim();
|
| 50 |
+
break;
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
await saveMonster({
|
| 55 |
+
name: monsterName,
|
| 56 |
+
imageUrl: workflowState.monsterImage.imageUrl,
|
| 57 |
+
imageCaption: workflowState.imageCaption,
|
| 58 |
+
concept: workflowState.monsterConcept,
|
| 59 |
+
imagePrompt: workflowState.imagePrompt
|
| 60 |
+
});
|
| 61 |
+
|
| 62 |
+
isSaved = true;
|
| 63 |
+
} catch (err) {
|
| 64 |
+
console.error('Failed to save monster:', err);
|
| 65 |
+
saveError = 'Failed to save monster to collection';
|
| 66 |
+
} finally {
|
| 67 |
+
isSaving = false;
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
</script>
|
| 71 |
|
| 72 |
<div class="result-container">
|
| 73 |
<h3>Your Monster Has Been Created!</h3>
|
| 74 |
|
| 75 |
+
{#if workflowState.monsterImage}
|
| 76 |
<div class="monster-image-container">
|
| 77 |
<img
|
| 78 |
+
src={workflowState.monsterImage.imageUrl}
|
| 79 |
alt="Generated Monster"
|
| 80 |
class="monster-image"
|
| 81 |
/>
|
|
|
|
| 86 |
<div class="result-section">
|
| 87 |
<h4>Original Description</h4>
|
| 88 |
<div class="result-content">
|
| 89 |
+
<p>{workflowState.imageCaption || 'No caption available'}</p>
|
| 90 |
</div>
|
| 91 |
</div>
|
| 92 |
|
| 93 |
<div class="result-section">
|
| 94 |
<h4>Monster Concept</h4>
|
| 95 |
<div class="result-content">
|
| 96 |
+
<p>{workflowState.monsterConcept || 'No concept available'}</p>
|
| 97 |
</div>
|
| 98 |
</div>
|
| 99 |
|
| 100 |
<div class="result-section">
|
| 101 |
<h4>Generation Prompt</h4>
|
| 102 |
<div class="result-content">
|
| 103 |
+
<p>{workflowState.imagePrompt || 'No prompt available'}</p>
|
| 104 |
+
{#if workflowState.imagePrompt}
|
| 105 |
<button class="copy-button" onclick={copyPrompt}>
|
| 106 |
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
| 107 |
<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"/>
|
|
|
|
| 120 |
</svg>
|
| 121 |
Download Monster
|
| 122 |
</button>
|
| 123 |
+
<button
|
| 124 |
+
class="action-button save"
|
| 125 |
+
onclick={saveToCollection}
|
| 126 |
+
disabled={isSaving || isSaved}
|
| 127 |
+
>
|
| 128 |
+
{#if isSaving}
|
| 129 |
+
<div class="spinner-small"></div>
|
| 130 |
+
Saving...
|
| 131 |
+
{:else if isSaved}
|
| 132 |
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
|
| 133 |
+
<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"/>
|
| 134 |
+
</svg>
|
| 135 |
+
Saved!
|
| 136 |
+
{:else}
|
| 137 |
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
|
| 138 |
+
<path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"/>
|
| 139 |
+
</svg>
|
| 140 |
+
Save to Collection
|
| 141 |
+
{/if}
|
| 142 |
+
</button>
|
| 143 |
<button class="action-button reset" onclick={onReset}>
|
| 144 |
<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
|
| 145 |
<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"/>
|
|
|
|
| 147 |
Create Another Monster
|
| 148 |
</button>
|
| 149 |
</div>
|
| 150 |
+
|
| 151 |
+
{#if saveError}
|
| 152 |
+
<div class="error-message">{saveError}</div>
|
| 153 |
+
{/if}
|
| 154 |
</div>
|
| 155 |
|
| 156 |
<style>
|
|
|
|
| 266 |
background: #5a6268;
|
| 267 |
}
|
| 268 |
|
| 269 |
+
.action-button.save {
|
| 270 |
+
background: #007bff;
|
| 271 |
+
color: white;
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
.action-button.save:hover:not(:disabled) {
|
| 275 |
+
background: #0056b3;
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
.action-button:disabled {
|
| 279 |
+
opacity: 0.6;
|
| 280 |
+
cursor: not-allowed;
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
.spinner-small {
|
| 284 |
+
width: 16px;
|
| 285 |
+
height: 16px;
|
| 286 |
+
border: 2px solid #ffffff;
|
| 287 |
+
border-top-color: transparent;
|
| 288 |
+
border-radius: 50%;
|
| 289 |
+
animation: spin 0.8s linear infinite;
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
.error-message {
|
| 293 |
+
margin-top: 1rem;
|
| 294 |
+
padding: 0.5rem;
|
| 295 |
+
background: #f8d7da;
|
| 296 |
+
color: #721c24;
|
| 297 |
+
border-radius: 4px;
|
| 298 |
+
text-align: center;
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
@keyframes spin {
|
| 302 |
+
to { transform: rotate(360deg); }
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
@media (max-width: 768px) {
|
| 306 |
.result-container {
|
| 307 |
padding: 1rem;
|
src/lib/components/MonsterGenerator/todo.txt
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Long term I want to turn this into a monster battle game. As part of this I need some kind of internal DB for the game.
|
| 2 |
+
When I worked on a Flutter-based version of this I used Isar DB, is there an equivalent that would work well here?
|
| 3 |
+
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
How could you update the image processing so that pure white is made transparent? Could you store the transparent images in local storage?
|
| 7 |
+
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
Note the "Sign in with Hugging Face" button that is added with my page, can I just auto
|
| 11 |
+
<iframe src="https://fraser-piclets.static.hf.space/index.html?embed=true&__sign=eyJhbGciOiJFZERTQSJ9.eyJyZWFkIjp0cnVlLCJwZXJtaXNzaW9ucyI6eyJyZXBvLmNvbnRlbnQucmVhZCI6dHJ1ZX0sIm9uQmVoYWxmT2YiOnsia2luZCI6InVzZXIiLCJfaWQiOiI1ZjE5NTc4NDkyNWI5ODYzZTI4YWQ2MTAiLCJ1c2VyIjoiRnJhc2VyIiwic2Vzc2lvbklkIjoiNjg3NjU0ZjZhYmI2ZWE2ZTk0OThkNjVmIn0sImlhdCI6MTc1MjY1NzYxMiwic3ViIjoiL3NwYWNlcy9GcmFzZXIvcGljbGV0cyIsImV4cCI6MTc1Mjc0NDAxMiwiaXNzIjoiaHR0cHM6Ly9odWdnaW5nZmFjZS5jbyJ9.vH_qEMDwpCpEapX36n-JPgfj6P7jxGdpwomhT6MIpY-r2OS9Wc1bFsQq0USfbQqKxif2rR9XL7sB8f0ximxCDA" aria-label="static space app" class="space-iframe outline-hidden grow bg-white p-0" allow="accelerometer; ambient-light-sensor; autoplay; battery; camera; clipboard-read; clipboard-write; display-capture; document-domain; encrypted-media; fullscreen; geolocation; gyroscope; layout-animations; legacy-image-formats; magnetometer; microphone; midi; oversized-images; payment; picture-in-picture; publickey-credentials-get; serial; sync-xhr; usb; vr ; wake-lock; xr-spatial-tracking" sandbox="allow-downloads allow-forms allow-modals allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts allow-storage-access-by-user-activation" scrolling="yes" id="iFrameResizer0" style="overflow: auto;"></iframe>
|
| 12 |
+
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
Now I would also like the text generator to produce a JSON object based on the monster concept.
|
| 16 |
+
This will be created with the following schema:
|
| 17 |
+
- name: string
|
| 18 |
+
- description: string
|
| 19 |
+
- rarity: likert (very-low, low, medium, high, very-high)
|
| 20 |
+
- HP: likert
|
| 21 |
+
- defence: likert
|
| 22 |
+
- attack: likert
|
| 23 |
+
- speed: likert
|
| 24 |
+
- special ability: string (passive trait that gives the monster a unique advantage in battle)
|
| 25 |
+
- attack action description: string (deals damage)
|
| 26 |
+
- attack action description: string (buff monsters own stats/status)
|
| 27 |
+
- disparage action description: string (lowers enemy stats/status)
|
| 28 |
+
- special action description: string (powerful action with single use per battle)
|
| 29 |
+
|
| 30 |
+
---
|
| 31 |
+
|
| 32 |
+
```python
|
| 33 |
+
import json
|
| 34 |
+
|
| 35 |
+
from pydantic import BaseModel
|
| 36 |
+
|
| 37 |
+
STRUCTURED_OUTPUT_FORMAT_INSTRUCTIONS = """The output should be formatted as a JSON instance that conforms to the JSON schema below.
|
| 38 |
+
|
| 39 |
+
As an example, for the schema {{"properties": {{"foo": {{"title": "Foo", "description": "a list of strings", "type": "array", "items": {{"type": "string"}}}}}}, "required": ["foo"]}}
|
| 40 |
+
the object {{"foo": ["bar", "baz"]}} is a well-formatted instance of the schema. The object {{"properties": {{"foo": ["bar", "baz"]}}}} is not well-formatted.
|
| 41 |
+
|
| 42 |
+
Here is the output schema:
|
| 43 |
+
```
|
| 44 |
+
{schema}
|
| 45 |
+
```"""
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class StructuredOutputParser(BaseModel):
|
| 49 |
+
pydantic_class: type[BaseModel]
|
| 50 |
+
|
| 51 |
+
def get_schema(self, indent: int = 2) -> str:
|
| 52 |
+
# Copy schema to avoid altering original Pydantic schema.
|
| 53 |
+
schema = self.pydantic_class.model_json_schema()
|
| 54 |
+
# Iterate over fields to remove from the schema
|
| 55 |
+
for field_name in ["title", "type"]:
|
| 56 |
+
if field_name in schema:
|
| 57 |
+
schema.pop(field_name)
|
| 58 |
+
return json.dumps(schema, indent=indent, ensure_ascii=False)
|
| 59 |
+
|
| 60 |
+
def get_format_instructions(self) -> str:
|
| 61 |
+
schema_str = self.get_schema()
|
| 62 |
+
# Ensure json in context is well-formed with double quotes.
|
| 63 |
+
return STRUCTURED_OUTPUT_FORMAT_INSTRUCTIONS.format(schema=schema_str)
|
| 64 |
+
|
| 65 |
+
def parse(self, json_string: str) -> BaseModel:
|
| 66 |
+
return self.pydantic_class.model_validate_json(json_string)
|
| 67 |
+
```
|
src/lib/db/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Dexie, { type Table } from 'dexie';
|
| 2 |
+
import type { Monster } from './schema';
|
| 3 |
+
|
| 4 |
+
export class MonsterDatabase extends Dexie {
|
| 5 |
+
monsters!: Table<Monster>;
|
| 6 |
+
|
| 7 |
+
constructor() {
|
| 8 |
+
super('MonsterGeneratorDB');
|
| 9 |
+
|
| 10 |
+
this.version(1).stores({
|
| 11 |
+
monsters: '++id, name, createdAt'
|
| 12 |
+
});
|
| 13 |
+
}
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export const db = new MonsterDatabase();
|
src/lib/db/monsters.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { db } from './index';
|
| 2 |
+
import type { Monster } from './schema';
|
| 3 |
+
|
| 4 |
+
export async function saveMonster(monsterData: Omit<Monster, 'id' | 'createdAt'>): Promise<number> {
|
| 5 |
+
const monster: Omit<Monster, 'id'> = {
|
| 6 |
+
...monsterData,
|
| 7 |
+
createdAt: new Date()
|
| 8 |
+
};
|
| 9 |
+
|
| 10 |
+
return await db.monsters.add(monster);
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
export async function getAllMonsters(): Promise<Monster[]> {
|
| 14 |
+
return await db.monsters.toArray();
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
export async function getMonster(id: number): Promise<Monster | undefined> {
|
| 18 |
+
return await db.monsters.get(id);
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
export async function deleteMonster(id: number): Promise<void> {
|
| 22 |
+
await db.monsters.delete(id);
|
| 23 |
+
}
|
src/lib/db/schema.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export interface Monster {
|
| 2 |
+
id?: number;
|
| 3 |
+
name: string;
|
| 4 |
+
imageUrl: string;
|
| 5 |
+
imageCaption: string;
|
| 6 |
+
concept: string;
|
| 7 |
+
imagePrompt: string;
|
| 8 |
+
createdAt: Date;
|
| 9 |
+
}
|