pict-section-prompteditor 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Steven Velozo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,118 @@
1
+ # pict-section-prompteditor
2
+
3
+ > **[Read the Pict-Section-PromptEditor Documentation](https://fable-retold.github.io/pict-section-prompteditor/)** - interactive docs with the full API reference and a runnable example.
4
+
5
+ A prompt crafting and management section for the [Pict](https://github.com/fable-retold/pict) ecosystem. Prompts are markdown organized by type into segments, word list matrices with weights drive a `{~WordListEntry:Name~}` template expression, and one crafted prompt generates endless concrete variants you can browse and download as a zip.
6
+
7
+ [MIT License](LICENSE)
8
+
9
+ ## Features
10
+
11
+ - Typed prompts with markdown segments (Context / Request / Success Criteria and friends), plus **fixed preamble** segments for locked team standards
12
+ - Weighted word list matrices: `[["Tyrannosaurus", 3], ["Diplodocus", 1]]` draws 75/25, with live share percentages as you curate
13
+ - The `{~WordListEntry:Name~}` template expression (short form `{~WLE:~}`), with an optional miss default: `{~WLE:Name:fallback~}`
14
+ - Generation runs the **real pict template engine**, so every pict expression works inside a prompt
15
+ - Inline preview with reroll, Formatted / Markdown toggle, and a copy button
16
+ - Batch generation with provenance, browsed in file order, downloaded as a zip of `<slug>-<sequence>.md` files
17
+ - Pluggable data backplane: in-memory by default, one `PromptDataProvider` class to back it with your API
18
+ - Opaque `Meta`, `Author` stamping, stable keys, and mutation hooks: the seams a host platform layers ratings and collaboration onto
19
+ - Resizable, collapsible list rail (a pict-section-modal panel) and a searchable insert control (pict-section-picker)
20
+ - Rich segment editing via pict-section-markdowneditor in a modal when CodeMirror is available; plain textareas otherwise
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ npm install pict-section-prompteditor
26
+ ```
27
+
28
+ ## Quick Start
29
+
30
+ ```javascript
31
+ const libPromptEditor = require('pict-section-prompteditor');
32
+
33
+ pict.addView('PromptEditor', Object.assign({}, libPromptEditor.default_configuration,
34
+ {
35
+ DefaultDestinationAddress: '#My-Container',
36
+ Title: 'Team Prompt Workshop',
37
+ CurrentUser: { Key: 42, Name: 'Steven' }
38
+ }), libPromptEditor);
39
+
40
+ let tmpEditor = pict.views['PromptEditor'];
41
+ tmpEditor.render();
42
+
43
+ // Seed through the provider (in-memory by default), then reload the section.
44
+ tmpEditor._provider.createWordList(
45
+ { Name: 'Dinosaurs', Entries: [['Tyrannosaurus', 3], ['Diplodocus', 1]] })
46
+ .then(() => tmpEditor.load());
47
+ ```
48
+
49
+ The section renders three tabs: Prompts (the editor), Word Lists (the matrices), and Generated (the browseable output with a zip download). Write `{~WordListEntry:Dinosaurs~}` in any segment and each generation draws Tyrannosaurus 75% of the time.
50
+
51
+ To back it with your own API instead of memory, implement the twelve-method `PromptDataProvider` contract and pass it as `DataProvider` -- the in-memory provider is the reference implementation.
52
+
53
+ ## Documentation
54
+
55
+ Full documentation is available in the [docs/](./docs/) directory and hosted at [fable-retold.github.io/pict-section-prompteditor](https://fable-retold.github.io/pict-section-prompteditor/):
56
+
57
+ - [Quickstart](./docs/getting-started.md) -- install, mount, seed, generate
58
+ - [Configuration](./docs/configuration.md) -- complete options reference
59
+ - [Architecture](./docs/architecture.md) -- the design, with a diagram
60
+ - [API Reference](./docs/api.md) -- classes, methods, and contracts
61
+ - [Developer Functions](./docs/functions.md) -- a snippet for every exposed function
62
+ - [Examples](./docs/examples.md) -- the Prompt Workshop example application
63
+
64
+ ## Example Applications
65
+
66
+ | Example | Description |
67
+ |---------|-------------|
68
+ | [Prompt Workshop](./example_applications/prompt_workshop/) | The full section in memory: seeded word lists and prompts, inline preview with reroll, batch generation, zip download |
69
+
70
+ Build it:
71
+
72
+ ```bash
73
+ cd example_applications/prompt_workshop
74
+ npm install
75
+ npx quack build && npx quack copy
76
+ # serve dist/ and open index.html — or `npm run example` from the module root
77
+ ```
78
+
79
+ ## API Overview
80
+
81
+ | Export | Description |
82
+ |--------|-------------|
83
+ | *(default)* / `default_configuration` | The section view class and its full default options |
84
+ | `PromptDataProvider` | The data contract a host implements |
85
+ | `InMemoryPromptProvider` | The default provider; deterministic via `Now` / `KeyGenerator` options |
86
+ | `DefaultPromptTypes` / `resolvePromptTypes` / `getPromptType` | The built-in prompt types and their resolution helpers |
87
+ | `PromptCompiler` | `assembleSource`, `generate`, `wordListMap`, `slug`, `generatedFileName` |
88
+ | `PromptZip` | `buildZip`, `downloadBlob` |
89
+ | `PictTemplateWordListEntry` / `weightedPick` | The template expression class and the weighted draw |
90
+ | `normalizeEntries` | Word list entry normalization |
91
+
92
+ View instance methods hosts call: `load()`, `refresh()`, `setReadOnly(bool)`, `setDataProvider(provider)` -- plus every UI action as a drivable method. See [Developer Functions](./docs/functions.md) for snippets.
93
+
94
+ ## Testing
95
+
96
+ ```bash
97
+ npm test
98
+ ```
99
+
100
+ Fifty-seven mocha tests cover the provider contract, the weighted draw and expression, the compiler, zip round-trips, and the view (including focus-preserving edits and multi-instance isolation).
101
+
102
+ ## Related Packages
103
+
104
+ - [pict](https://github.com/fable-retold/pict) -- Core MVC framework
105
+ - [pict-view](https://github.com/fable-retold/pict-view) -- Base view class
106
+ - [pict-section-modal](https://github.com/fable-retold/pict-section-modal) -- Dialogs, toasts, and the resizable rail panel
107
+ - [pict-section-picker](https://github.com/fable-retold/pict-section-picker) -- The searchable insert control
108
+ - [pict-section-markdowneditor](https://github.com/fable-retold/pict-section-markdowneditor) -- The rich segment editor
109
+ - [pict-section-content](https://github.com/fable-retold/pict-section-content) -- Markdown rendering
110
+ - [fable](https://github.com/fable-retold/fable) -- Service provider and dependency injection
111
+
112
+ ## License
113
+
114
+ MIT
115
+
116
+ ## Contributing
117
+
118
+ Pull requests are welcome. For major changes, please open an issue first.
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "pict-section-prompteditor",
3
+ "version": "1.0.0",
4
+ "description": "Prompt crafting and management section for Pict: typed prompts with segments (context, request, success criteria), curated word list matrices with weights, a {~WordListEntry:Name~} template expression for weighted-random generation, batch generation with zip download, and a pluggable data provider that defaults to in-memory AppData.",
5
+ "main": "source/Pict-Section-PromptEditor.js",
6
+ "files": [
7
+ "source"
8
+ ],
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/fable-retold/pict-section-prompteditor.git"
12
+ },
13
+ "scripts": {
14
+ "test": "npx mocha -u tdd -R spec --exit test/*_tests.js",
15
+ "start": "node source/Pict-Section-PromptEditor.js",
16
+ "build": "npx quack build",
17
+ "build:codemirror": "node build/build-codemirror-bundle.js",
18
+ "example": "npx quack examples",
19
+ "docs": "npx quack prepare-docs ./docs"
20
+ },
21
+ "author": "steven velozo <steven@velozo.com>",
22
+ "license": "MIT",
23
+ "dependencies": {
24
+ "jszip": "^3.10.1",
25
+ "pict-section-content": "^1.0.15",
26
+ "pict-section-markdowneditor": "^1.0.17",
27
+ "pict-section-modal": "^1.1.4",
28
+ "pict-section-picker": "^1.4.0",
29
+ "pict-template": "^1.0.15",
30
+ "pict-view": "^1.0.68"
31
+ },
32
+ "devDependencies": {
33
+ "@codemirror/lang-markdown": "^6.5.0",
34
+ "@codemirror/state": "^6.6.0",
35
+ "@codemirror/view": "^6.43.0",
36
+ "chai": "^4.5.0",
37
+ "codemirror": "^6.0.2",
38
+ "esbuild": "^0.28.0",
39
+ "jsdom": "^25.0.1",
40
+ "mocha": "^11.0.1",
41
+ "pict": "^1.0.381",
42
+ "pict-docuserve": "^1.4.19",
43
+ "quackage": "^1.3.0"
44
+ }
45
+ }
@@ -0,0 +1,56 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * pict-section-prompteditor
5
+ *
6
+ * A self-contained Pict section for crafting, curating, and generating AI
7
+ * prompts as a team:
8
+ *
9
+ * - prompts are markdown, organized by prompt type into segments
10
+ * (context / request / success criteria and friends), with fixed-preamble
11
+ * segments for locked team standards
12
+ * - word list matrices ([["Tyrannosaurus", 3], ["Diplodocus", 1]]) drive the
13
+ * {~WordListEntry:Dinosaurs~} template expression by weighted random
14
+ * - any pict template expression works inside a prompt; generation runs the
15
+ * assembled markdown through the real pict template engine
16
+ * - generate as many concrete prompts as you like, browse them rendered,
17
+ * download them as a zip of markdown files
18
+ * - state is in-memory by default; implement PromptDataProvider to map the
19
+ * section onto your own back end
20
+ *
21
+ * The default export is the view class, registered like any pict view:
22
+ *
23
+ * const libPromptEditor = require('pict-section-prompteditor');
24
+ * pict.addView('PromptEditor', Object.assign({}, libPromptEditor.default_configuration,
25
+ * { DefaultDestinationAddress: '#My-Container' }), libPromptEditor);
26
+ * pict.views['PromptEditor'].render();
27
+ */
28
+
29
+ const libPromptEditorView = require('./views/PictView-PromptEditor.js');
30
+ const libProvider = require('./providers/PromptProvider-Base.js');
31
+ const libTypes = require('./types/PromptEditor-DefaultTypes.js');
32
+ const libCompiler = require('./compiler/PromptCompiler.js');
33
+ const libZip = require('./zip/PromptZip.js');
34
+ const libWordListTemplate = require('./templates/Pict-Template-WordListEntry.js');
35
+
36
+ module.exports = libPromptEditorView;
37
+ module.exports.default_configuration = libPromptEditorView.default_configuration;
38
+
39
+ // The data seam: the interface a host implements + the in-memory default.
40
+ module.exports.PromptDataProvider = libProvider.PromptDataProvider;
41
+ module.exports.InMemoryPromptProvider = libProvider.InMemoryPromptProvider;
42
+ module.exports.normalizeEntries = libProvider.normalizeEntries;
43
+
44
+ // Prompt types: the built-ins and the helpers the view uses to resolve them.
45
+ module.exports.DefaultPromptTypes = libTypes.DefaultPromptTypes;
46
+ module.exports.resolvePromptTypes = libTypes.resolvePromptTypes;
47
+ module.exports.getPromptType = libTypes.getPromptType;
48
+
49
+ // The compiler and zip utilities, for hosts generating outside the view.
50
+ module.exports.PromptCompiler = libCompiler;
51
+ module.exports.PromptZip = libZip;
52
+
53
+ // The template expression class (registered automatically by the view; exposed
54
+ // for hosts that want {~WordListEntry:~} active before any section mounts).
55
+ module.exports.PictTemplateWordListEntry = libWordListTemplate;
56
+ module.exports.weightedPick = libWordListTemplate.weightedPick;
@@ -0,0 +1,120 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * PromptCompiler: a prompt record + its type --> markdown.
5
+ *
6
+ * Two stages, deliberately separate:
7
+ *
8
+ * assembleSource() stitches the segments into one markdown document, fixed
9
+ * preambles included, template expressions left intact. This is the SOURCE
10
+ * of a prompt -- what you version, diff, and collaborate on.
11
+ *
12
+ * generate() runs that source through the pict template engine with the
13
+ * word lists in the parse data, so {~WordListEntry:Name~} resolves by
14
+ * weighted random and every other pict expression works too (the prompt
15
+ * record itself is addressable: {~D:Prompt.Title~}). Each call is one
16
+ * concrete generation; call it as many times as you like.
17
+ */
18
+
19
+ /**
20
+ * Filesystem-friendly slug for zip entry names.
21
+ * @param {string} pText
22
+ * @returns {string}
23
+ */
24
+ function slug(pText)
25
+ {
26
+ return String(pText || '').toLowerCase()
27
+ .replace(/[^a-z0-9]+/g, '-')
28
+ .replace(/^-+|-+$/g, '')
29
+ .slice(0, 60) || 'prompt';
30
+ }
31
+
32
+ /**
33
+ * Assemble a prompt's markdown source from its type's segments.
34
+ * @param {object} pPrompt - the prompt record
35
+ * @param {object} pType - the resolved prompt type
36
+ * @param {object} [pOptions] - { IncludeTitleHeading: true, SegmentHeadingLevel: 2 }
37
+ * @returns {string} markdown with template expressions intact
38
+ */
39
+ function assembleSource(pPrompt, pType, pOptions)
40
+ {
41
+ let tmpOptions = pOptions || {};
42
+ let tmpIncludeTitle = (typeof tmpOptions.IncludeTitleHeading === 'undefined') ? true : !!tmpOptions.IncludeTitleHeading;
43
+ let tmpHeading = '#'.repeat(Math.max(1, Math.min(6, Number(tmpOptions.SegmentHeadingLevel) || 2)));
44
+ let tmpSegments = (pType && Array.isArray(pType.Segments)) ? pType.Segments : [];
45
+ let tmpBodies = (pPrompt && pPrompt.Segments) ? pPrompt.Segments : {};
46
+
47
+ let tmpParts = [];
48
+ if (tmpIncludeTitle && pPrompt && pPrompt.Title)
49
+ {
50
+ tmpParts.push('# ' + String(pPrompt.Title).trim());
51
+ }
52
+
53
+ for (let i = 0; i < tmpSegments.length; i++)
54
+ {
55
+ let tmpSegment = tmpSegments[i];
56
+ let tmpBody = tmpSegment.Fixed ? String(tmpSegment.Body || '') : String(tmpBodies[tmpSegment.Key] || '');
57
+ tmpBody = tmpBody.trim();
58
+ if (!tmpBody)
59
+ {
60
+ if (tmpSegment.Optional || tmpSegment.Fixed) { continue; }
61
+ tmpBody = '(nothing written for this segment yet)';
62
+ }
63
+ tmpParts.push(tmpHeading + ' ' + String(tmpSegment.Name || tmpSegment.Key) + '\n\n' + tmpBody);
64
+ }
65
+
66
+ return tmpParts.join('\n\n') + '\n';
67
+ }
68
+
69
+ /**
70
+ * Build the { lowercased name -> Entries } map generate() feeds the
71
+ * {~WordListEntry:~} expression.
72
+ * @param {Array} pWordLists - word list records
73
+ * @returns {object}
74
+ */
75
+ function wordListMap(pWordLists)
76
+ {
77
+ let tmpMap = {};
78
+ (pWordLists || []).forEach((pList) =>
79
+ {
80
+ let tmpName = String(pList.Name || '').trim().toLowerCase();
81
+ if (tmpName) { tmpMap[tmpName] = pList.Entries || []; }
82
+ });
83
+ return tmpMap;
84
+ }
85
+
86
+ /**
87
+ * One concrete generation: parse the assembled source through the pict
88
+ * template engine with the word lists riding in the data record.
89
+ * @param {object} pPict - the pict instance (its template engine does the work)
90
+ * @param {object} pPrompt - the prompt record
91
+ * @param {object} pType - the resolved prompt type
92
+ * @param {Array} pWordLists - word list records in play
93
+ * @param {object} [pOptions] - assembleSource options + { RandomFunction }
94
+ * @returns {string} resolved markdown
95
+ */
96
+ function generate(pPict, pPrompt, pType, pWordLists, pOptions)
97
+ {
98
+ let tmpOptions = pOptions || {};
99
+ let tmpSource = assembleSource(pPrompt, pType, tmpOptions);
100
+ let tmpData =
101
+ {
102
+ Prompt: pPrompt,
103
+ __PromptEditorWordLists: wordListMap(pWordLists),
104
+ __PromptEditorRandom: (typeof tmpOptions.RandomFunction === 'function') ? tmpOptions.RandomFunction : undefined
105
+ };
106
+ return pPict.parseTemplate(tmpSource, tmpData, null, []);
107
+ }
108
+
109
+ /**
110
+ * The zip entry filename for one generated prompt.
111
+ * @param {object} pGenerated - the generated record
112
+ * @returns {string}
113
+ */
114
+ function generatedFileName(pGenerated)
115
+ {
116
+ let tmpSequence = String(Number(pGenerated.Sequence) || 0).padStart(3, '0');
117
+ return slug(pGenerated.PromptTitle) + '-' + tmpSequence + '.md';
118
+ }
119
+
120
+ module.exports = { assembleSource, generate, wordListMap, slug, generatedFileName };
@@ -0,0 +1,302 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * The data seam for pict-section-prompteditor.
5
+ *
6
+ * PromptDataProvider is the interface a host implements to map prompts, word
7
+ * lists, and generated output onto its own back end (a REST API, meadow
8
+ * entities, local storage, whatever). InMemoryPromptProvider is the default:
9
+ * everything lives in a plain object store (usually a node of pict AppData),
10
+ * so the section works with no server at all.
11
+ *
12
+ * Three record families:
13
+ *
14
+ * WordList { Key, Name, Entries: [[word, weight], ...], CreatedAt, UpdatedAt }
15
+ * A curated word matrix. Weights default to 1; an entry's chance of being
16
+ * drawn by {~WordListEntry:Name~} is weight / sum(weights).
17
+ *
18
+ * Prompt { Key, Title, TypeKey, Segments: { segmentKey: markdown },
19
+ * Author, Meta, CreatedAt, UpdatedAt }
20
+ * A crafted prompt. Segments hold the markdown for each of the prompt
21
+ * type's segments, keyed by segment Key. Meta is an opaque object the
22
+ * provider round-trips untouched -- the seam where a host hangs ratings,
23
+ * version pointers, or anything else.
24
+ *
25
+ * Generated { Key, PromptKey, PromptTitle, TypeKey, Markdown, Sequence,
26
+ * Author, GeneratedAt }
27
+ * One concrete generation of a prompt: the assembled markdown with every
28
+ * template expression (word lists included) resolved.
29
+ *
30
+ * All primitives return Promises so a remote provider drops in cleanly.
31
+ */
32
+
33
+ /**
34
+ * Normalize a word list's Entries into [[word, weight], ...] pairs. Accepts
35
+ * pairs, bare strings, or {Word, Weight} objects; weights default to 1 and
36
+ * coerce to a non-negative number.
37
+ * @param {Array} pEntries
38
+ * @returns {Array<Array>}
39
+ */
40
+ function normalizeEntries(pEntries)
41
+ {
42
+ if (!Array.isArray(pEntries)) { return []; }
43
+ let tmpOut = [];
44
+ for (let i = 0; i < pEntries.length; i++)
45
+ {
46
+ let tmpEntry = pEntries[i];
47
+ let tmpWord = '';
48
+ let tmpWeight = 1;
49
+ if (Array.isArray(tmpEntry))
50
+ {
51
+ tmpWord = String(tmpEntry[0] == null ? '' : tmpEntry[0]);
52
+ tmpWeight = (typeof tmpEntry[1] === 'undefined' || tmpEntry[1] === null || tmpEntry[1] === '') ? 1 : Number(tmpEntry[1]);
53
+ }
54
+ else if (tmpEntry && typeof tmpEntry === 'object')
55
+ {
56
+ tmpWord = String(tmpEntry.Word == null ? '' : tmpEntry.Word);
57
+ tmpWeight = (typeof tmpEntry.Weight === 'undefined' || tmpEntry.Weight === null || tmpEntry.Weight === '') ? 1 : Number(tmpEntry.Weight);
58
+ }
59
+ else
60
+ {
61
+ tmpWord = String(tmpEntry == null ? '' : tmpEntry);
62
+ }
63
+ if (!isFinite(tmpWeight) || tmpWeight < 0) { tmpWeight = 1; }
64
+ tmpOut.push([tmpWord, tmpWeight]);
65
+ }
66
+ return tmpOut;
67
+ }
68
+
69
+ class PromptDataProvider
70
+ {
71
+ constructor(pOptions)
72
+ {
73
+ this.options = pOptions || {};
74
+ this._now = (typeof this.options.Now === 'function') ? this.options.Now : Date.now;
75
+ let tmpCounter = 0;
76
+ this._key = (typeof this.options.KeyGenerator === 'function') ? this.options.KeyGenerator : (pPrefix) =>
77
+ {
78
+ tmpCounter++;
79
+ return (pPrefix || 'pe') + '_' + this._now().toString(36) + '_' + tmpCounter.toString(36) + '_' + Math.floor(Math.random() * 1679616).toString(36);
80
+ };
81
+ }
82
+
83
+ /* eslint-disable no-unused-vars */
84
+ // ---- word lists ---------------------------------------------------------
85
+ listWordLists() { return Promise.reject(new Error('listWordLists not implemented')); }
86
+ createWordList(pDraft) { return Promise.reject(new Error('createWordList not implemented')); }
87
+ updateWordList(pKey, pPatch) { return Promise.reject(new Error('updateWordList not implemented')); }
88
+ deleteWordList(pKey) { return Promise.reject(new Error('deleteWordList not implemented')); }
89
+
90
+ // ---- prompts ------------------------------------------------------------
91
+ listPrompts() { return Promise.reject(new Error('listPrompts not implemented')); }
92
+ createPrompt(pDraft) { return Promise.reject(new Error('createPrompt not implemented')); }
93
+ updatePrompt(pKey, pPatch) { return Promise.reject(new Error('updatePrompt not implemented')); }
94
+ deletePrompt(pKey) { return Promise.reject(new Error('deletePrompt not implemented')); }
95
+
96
+ // ---- generated output ---------------------------------------------------
97
+ listGenerated(pPromptKey) { return Promise.reject(new Error('listGenerated not implemented')); }
98
+ createGenerated(pDraft) { return Promise.reject(new Error('createGenerated not implemented')); }
99
+ deleteGenerated(pKey) { return Promise.reject(new Error('deleteGenerated not implemented')); }
100
+ clearGenerated(pPromptKey) { return Promise.reject(new Error('clearGenerated not implemented')); }
101
+ /* eslint-enable no-unused-vars */
102
+
103
+ // ---- conveniences built on the primitives -------------------------------
104
+ /**
105
+ * Load everything the section renders, in one call.
106
+ * @returns {Promise<{WordLists: Array, Prompts: Array, Generated: Array}>}
107
+ */
108
+ loadAll()
109
+ {
110
+ return Promise.all([this.listWordLists(), this.listPrompts(), this.listGenerated()])
111
+ .then((pResults) => ({ WordLists: pResults[0], Prompts: pResults[1], Generated: pResults[2] }));
112
+ }
113
+
114
+ /**
115
+ * Find a word list by Name (trimmed, case-insensitive). The lookup the
116
+ * {~WordListEntry:Name~} expression leans on.
117
+ * @param {string} pName
118
+ * @returns {Promise<object|null>}
119
+ */
120
+ getWordListByName(pName)
121
+ {
122
+ let tmpWanted = String(pName || '').trim().toLowerCase();
123
+ return this.listWordLists().then((pLists) =>
124
+ pLists.find((pList) => String(pList.Name || '').trim().toLowerCase() === tmpWanted) || null);
125
+ }
126
+ }
127
+
128
+ class InMemoryPromptProvider extends PromptDataProvider
129
+ {
130
+ constructor(pOptions)
131
+ {
132
+ super(pOptions);
133
+ // The store is usually a node of pict AppData (so state is observable
134
+ // and survives view re-renders); a fresh object works fine too.
135
+ let tmpStore = (this.options && this.options.Store) ? this.options.Store : {};
136
+ if (!tmpStore.WordLists) { tmpStore.WordLists = {}; }
137
+ if (!tmpStore.Prompts) { tmpStore.Prompts = {}; }
138
+ if (!tmpStore.Generated) { tmpStore.Generated = {}; }
139
+ this.store = tmpStore;
140
+ }
141
+
142
+ _clone(pValue) { return (pValue == null) ? pValue : JSON.parse(JSON.stringify(pValue)); }
143
+
144
+ // ---- word lists ---------------------------------------------------------
145
+ listWordLists()
146
+ {
147
+ let tmpLists = Object.values(this.store.WordLists).map((pList) => this._clone(pList));
148
+ tmpLists.sort((pA, pB) => String(pA.Name).localeCompare(String(pB.Name)));
149
+ return Promise.resolve(tmpLists);
150
+ }
151
+
152
+ createWordList(pDraft)
153
+ {
154
+ let tmpDraft = pDraft || {};
155
+ let tmpName = String(tmpDraft.Name || '').trim();
156
+ if (!tmpName) { return Promise.reject(new Error('A word list needs a Name.')); }
157
+ let tmpNow = this._now();
158
+ let tmpList =
159
+ {
160
+ Key: tmpDraft.Key || this._key('wl'),
161
+ Name: tmpName,
162
+ Entries: normalizeEntries(tmpDraft.Entries),
163
+ CreatedAt: tmpNow,
164
+ UpdatedAt: tmpNow
165
+ };
166
+ this.store.WordLists[tmpList.Key] = tmpList;
167
+ return Promise.resolve(this._clone(tmpList));
168
+ }
169
+
170
+ updateWordList(pKey, pPatch)
171
+ {
172
+ let tmpList = this.store.WordLists[pKey];
173
+ if (!tmpList) { return Promise.reject(new Error('No word list with Key ' + pKey)); }
174
+ let tmpPatch = pPatch || {};
175
+ if (typeof tmpPatch.Name !== 'undefined')
176
+ {
177
+ let tmpName = String(tmpPatch.Name || '').trim();
178
+ if (!tmpName) { return Promise.reject(new Error('A word list needs a Name.')); }
179
+ tmpList.Name = tmpName;
180
+ }
181
+ if (typeof tmpPatch.Entries !== 'undefined') { tmpList.Entries = normalizeEntries(tmpPatch.Entries); }
182
+ tmpList.UpdatedAt = this._now();
183
+ return Promise.resolve(this._clone(tmpList));
184
+ }
185
+
186
+ deleteWordList(pKey)
187
+ {
188
+ delete this.store.WordLists[pKey];
189
+ return Promise.resolve();
190
+ }
191
+
192
+ // ---- prompts ------------------------------------------------------------
193
+ listPrompts()
194
+ {
195
+ let tmpPrompts = Object.values(this.store.Prompts).map((pPrompt) => this._clone(pPrompt));
196
+ tmpPrompts.sort((pA, pB) => (pB.UpdatedAt || 0) - (pA.UpdatedAt || 0));
197
+ return Promise.resolve(tmpPrompts);
198
+ }
199
+
200
+ createPrompt(pDraft)
201
+ {
202
+ let tmpDraft = pDraft || {};
203
+ if (!tmpDraft.TypeKey) { return Promise.reject(new Error('A prompt needs a TypeKey.')); }
204
+ let tmpNow = this._now();
205
+ let tmpPrompt =
206
+ {
207
+ Key: tmpDraft.Key || this._key('pr'),
208
+ Title: String(tmpDraft.Title || 'Untitled prompt'),
209
+ TypeKey: String(tmpDraft.TypeKey),
210
+ Segments: (tmpDraft.Segments && typeof tmpDraft.Segments === 'object') ? this._clone(tmpDraft.Segments) : {},
211
+ Author: tmpDraft.Author || null,
212
+ Meta: (typeof tmpDraft.Meta === 'undefined') ? {} : this._clone(tmpDraft.Meta),
213
+ CreatedAt: tmpNow,
214
+ UpdatedAt: tmpNow
215
+ };
216
+ this.store.Prompts[tmpPrompt.Key] = tmpPrompt;
217
+ return Promise.resolve(this._clone(tmpPrompt));
218
+ }
219
+
220
+ updatePrompt(pKey, pPatch)
221
+ {
222
+ let tmpPrompt = this.store.Prompts[pKey];
223
+ if (!tmpPrompt) { return Promise.reject(new Error('No prompt with Key ' + pKey)); }
224
+ let tmpPatch = pPatch || {};
225
+ if (typeof tmpPatch.Title !== 'undefined') { tmpPrompt.Title = String(tmpPatch.Title); }
226
+ if (typeof tmpPatch.TypeKey !== 'undefined') { tmpPrompt.TypeKey = String(tmpPatch.TypeKey); }
227
+ if (typeof tmpPatch.Segments !== 'undefined') { tmpPrompt.Segments = this._clone(tmpPatch.Segments || {}); }
228
+ if (typeof tmpPatch.Meta !== 'undefined') { tmpPrompt.Meta = this._clone(tmpPatch.Meta); }
229
+ tmpPrompt.UpdatedAt = this._now();
230
+ return Promise.resolve(this._clone(tmpPrompt));
231
+ }
232
+
233
+ deletePrompt(pKey)
234
+ {
235
+ delete this.store.Prompts[pKey];
236
+ // Generated output belongs to its prompt; remove it alongside.
237
+ for (let tmpGenKey of Object.keys(this.store.Generated))
238
+ {
239
+ if (this.store.Generated[tmpGenKey].PromptKey === pKey) { delete this.store.Generated[tmpGenKey]; }
240
+ }
241
+ return Promise.resolve();
242
+ }
243
+
244
+ // ---- generated output ---------------------------------------------------
245
+ listGenerated(pPromptKey)
246
+ {
247
+ let tmpGenerated = Object.values(this.store.Generated)
248
+ .filter((pGen) => !pPromptKey || pGen.PromptKey === pPromptKey)
249
+ .map((pGen) => this._clone(pGen));
250
+ // Ordered like a file listing: the prompt with the most recent
251
+ // generation first, and within a prompt the files in sequence order
252
+ // (001 at the top), matching the names they get in the zip.
253
+ let tmpLatestByPrompt = {};
254
+ tmpGenerated.forEach((pGen) =>
255
+ {
256
+ let tmpStamp = pGen.GeneratedAt || 0;
257
+ if (!(pGen.PromptKey in tmpLatestByPrompt) || tmpStamp > tmpLatestByPrompt[pGen.PromptKey]) { tmpLatestByPrompt[pGen.PromptKey] = tmpStamp; }
258
+ });
259
+ tmpGenerated.sort((pA, pB) =>
260
+ (tmpLatestByPrompt[pB.PromptKey] - tmpLatestByPrompt[pA.PromptKey])
261
+ || String(pA.PromptKey).localeCompare(String(pB.PromptKey))
262
+ || (pA.Sequence || 0) - (pB.Sequence || 0)
263
+ || (pA.GeneratedAt || 0) - (pB.GeneratedAt || 0));
264
+ return Promise.resolve(tmpGenerated);
265
+ }
266
+
267
+ createGenerated(pDraft)
268
+ {
269
+ let tmpDraft = pDraft || {};
270
+ if (!tmpDraft.PromptKey) { return Promise.reject(new Error('Generated output needs a PromptKey.')); }
271
+ let tmpGenerated =
272
+ {
273
+ Key: tmpDraft.Key || this._key('gen'),
274
+ PromptKey: tmpDraft.PromptKey,
275
+ PromptTitle: String(tmpDraft.PromptTitle || ''),
276
+ TypeKey: String(tmpDraft.TypeKey || ''),
277
+ Markdown: String(tmpDraft.Markdown || ''),
278
+ Sequence: Number(tmpDraft.Sequence) || 0,
279
+ Author: tmpDraft.Author || null,
280
+ GeneratedAt: this._now()
281
+ };
282
+ this.store.Generated[tmpGenerated.Key] = tmpGenerated;
283
+ return Promise.resolve(this._clone(tmpGenerated));
284
+ }
285
+
286
+ deleteGenerated(pKey)
287
+ {
288
+ delete this.store.Generated[pKey];
289
+ return Promise.resolve();
290
+ }
291
+
292
+ clearGenerated(pPromptKey)
293
+ {
294
+ for (let tmpKey of Object.keys(this.store.Generated))
295
+ {
296
+ if (!pPromptKey || this.store.Generated[tmpKey].PromptKey === pPromptKey) { delete this.store.Generated[tmpKey]; }
297
+ }
298
+ return Promise.resolve();
299
+ }
300
+ }
301
+
302
+ module.exports = { PromptDataProvider, InMemoryPromptProvider, normalizeEntries };