Fraser commited on
Commit
ececfe6
·
1 Parent(s): e6edab1
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 state={state} onReset={reset} />
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
- state: MonsterWorkflowState;
6
  onReset: () => void;
7
  }
8
 
9
- let { state, onReset }: Props = $props();
 
 
 
10
 
11
  function downloadImage() {
12
- if (!state.monsterImage?.imageUrl) return;
13
 
14
  const link = document.createElement('a');
15
- link.href = state.monsterImage.imageUrl;
16
  link.download = `monster-${Date.now()}.png`;
17
  link.click();
18
  }
19
 
20
  function copyPrompt() {
21
- if (!state.imagePrompt) return;
22
- navigator.clipboard.writeText(state.imagePrompt);
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 state.monsterImage}
31
  <div class="monster-image-container">
32
  <img
33
- src={state.monsterImage.imageUrl}
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>{state.imageCaption || 'No caption available'}</p>
45
  </div>
46
  </div>
47
 
48
  <div class="result-section">
49
  <h4>Monster Concept</h4>
50
  <div class="result-content">
51
- <p>{state.monsterConcept || 'No concept available'}</p>
52
  </div>
53
  </div>
54
 
55
  <div class="result-section">
56
  <h4>Generation Prompt</h4>
57
  <div class="result-content">
58
- <p>{state.imagePrompt || 'No prompt available'}</p>
59
- {#if state.imagePrompt}
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&amp;__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
+ }