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 +21 -0
- package/README.md +118 -0
- package/package.json +45 -0
- package/source/Pict-Section-PromptEditor.js +56 -0
- package/source/compiler/PromptCompiler.js +120 -0
- package/source/providers/PromptProvider-Base.js +302 -0
- package/source/templates/Pict-Template-WordListEntry.js +140 -0
- package/source/types/PromptEditor-DefaultTypes.js +116 -0
- package/source/views/PictView-PromptEditor.js +1465 -0
- package/source/zip/PromptZip.js +65 -0
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 };
|