@nodaro/prompts 1.27.0 → 1.28.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.
@@ -131,6 +131,17 @@ export interface PickerOption {
131
131
  * duration" — the safe reading, since the composer then behaves as before.
132
132
  */
133
133
  readonly instant?: true
134
+ /**
135
+ * PER-OPTION PARAMETERS — extra node-data fields that apply only while THIS
136
+ * option is picked, each with its own rows (Transitions: a wipe's
137
+ * `wipeDirection`; see `Transition.options`). Same shape as the catalog's
138
+ * `dimensions`, but scoped to the row that declares them: a consumer shows
139
+ * the control while the row is picked and stores the chosen row id under
140
+ * `field` beside the pick. The `auto` row leads each list and means "send
141
+ * nothing"; a row's `promptHint` is the phrase it writes into the option's
142
+ * description, not a clause of its own. Absent on every other option.
143
+ */
144
+ readonly params?: readonly PickerDimension[]
134
145
  /** Only present if the source catalog entry already carries a data icon/emoji/thumbnail field. */
135
146
  readonly icon?: string
136
147
  }
@@ -186,6 +197,18 @@ interface BaseCatalogEntry {
186
197
  readonly adultOnly?: true
187
198
  /** Transitions: see `Transition.instant`. Propagated into the flattened option as `instant: true`. */
188
199
  readonly instant?: boolean
200
+ /** Transitions: see `Transition.options`. Propagated as the option's `params`. */
201
+ readonly options?: ReadonlyArray<{
202
+ readonly field: string
203
+ readonly label: string
204
+ readonly choices: ReadonlyArray<{
205
+ readonly id: string
206
+ readonly label: string
207
+ readonly description: string
208
+ readonly phrase: string
209
+ readonly term: string
210
+ }>
211
+ }>
189
212
  }
190
213
 
191
214
  /**
@@ -215,6 +238,19 @@ function toOptions<T extends BaseCatalogEntry>(
215
238
  if (categoryField) opt.category = e[categoryField] as unknown as string
216
239
  if (e.adultOnly) opt.adultOnly = true
217
240
  if (e.instant) opt.instant = true
241
+ if (e.options?.length) {
242
+ opt.params = e.options.map((o) => ({
243
+ field: o.field,
244
+ label: o.label,
245
+ options: o.choices.map((c) => ({
246
+ id: c.id,
247
+ label: c.label,
248
+ description: c.description,
249
+ promptHint: c.phrase,
250
+ term: c.term,
251
+ })),
252
+ }))
253
+ }
218
254
  return opt as PickerOption
219
255
  })
220
256
  }
@@ -975,8 +1011,25 @@ export type ProjectedPickerCatalog = ProjectedCatalog
975
1011
  function projectOption(o: PickerOption, detail: PickerCatalogDetail, imageUrl?: string): ProjectedPickerOption {
976
1012
  // `term` rides at BOTH detail levels: a thin client renders `label` and
977
1013
  // injects `term`, so compact must carry it — it is what makes compact hint
978
- // mode possible without a second round-trip for the full payload. So does
979
- // `imageUrl` (display data), present only when the option has a picture.
1014
+ // mode possible without a second round-trip for the full payload.
1015
+ const projected = projectOptionFields(o, detail, imageUrl)
1016
+ // Per-option params (a wipe's direction) ride at both detail levels too: an
1017
+ // id-only client needs the rows to offer the control at all.
1018
+ return o.params
1019
+ ? {
1020
+ ...projected,
1021
+ params: o.params.map((d) => ({
1022
+ field: d.field,
1023
+ label: d.label,
1024
+ options: d.options.map((p) => projectOption(p, detail)),
1025
+ })),
1026
+ }
1027
+ : projected
1028
+ }
1029
+
1030
+ function projectOptionFields(o: PickerOption, detail: PickerCatalogDetail, imageUrl?: string): ProjectedPickerOption {
1031
+ // `imageUrl` (display data) rides at both detail levels too, present only
1032
+ // when the option has a picture.
980
1033
  const extras = { ...(o.motion ? { motion: o.motion } : {}), ...(imageUrl ? { imageUrl } : {}) }
981
1034
  return detail === "full"
982
1035
  ? {
@@ -4,6 +4,7 @@
4
4
  * provider-aware prompt truncation (default 5000 chars), and reference image filtering by model support.
5
5
  */
6
6
 
7
+ import { joinSentences } from "./hint-join.js"
7
8
  import { resolveTemplate, applyTemplate } from "./prompt-templates.js"
8
9
  import { NATIVE_NEGATIVE_PROMPT_MODELS, MODELS_WITH_REFERENCE_IMAGE_SUPPORT, imageReferenceLimit, getMaxImagePromptChars, getMaxNegativePromptChars } from "@nodaro/shared"
9
10
  import { getStylePromptHint, isDeniedStyleId } from "./style.js"
@@ -60,7 +61,7 @@ function composeIdentityDescPart(
60
61
  if (c) parts.push(c)
61
62
  const e = elementInjection?.trim()
62
63
  if (e) parts.push(e)
63
- return parts.length > 0 ? `${subject} — ${parts.join(". ")}` : subject
64
+ return parts.length > 0 ? `${subject} — ${joinSentences(parts)}` : subject
64
65
  }
65
66
 
66
67
  /**
@@ -22,7 +22,11 @@
22
22
  * Never mutates its inputs.
23
23
  */
24
24
 
25
- /** The measured separator between the user's prompt and each folded hint. */
25
+ /** The measured separator between the user's prompt and each folded hint.
26
+ * Pieces that already end a sentence are followed by a single space instead
27
+ * (`joinSentences`), so this is the longest separator a join can add. */
28
+ import { joinSentences } from "./hint-join.js"
29
+
26
30
  export const PROMPT_HINT_SEPARATOR = ". "
27
31
 
28
32
  /**
@@ -35,5 +39,7 @@ export const PROMPT_HINT_SEPARATOR = ". "
35
39
  */
36
40
  export function joinPromptHints(userPrompt: string, hints: readonly string[]): string {
37
41
  if (hints.length === 0) return userPrompt
38
- return [userPrompt.trim(), ...hints].filter((p) => p.length > 0).join(PROMPT_HINT_SEPARATOR)
42
+ // Sentence-aware: a prompt or hint that already ends a sentence is followed
43
+ // by a space, not by PROMPT_HINT_SEPARATOR (which made "watermark.. hint").
44
+ return joinSentences([userPrompt.trim(), ...hints])
39
45
  }
@@ -45,7 +45,8 @@
45
45
  * section is written flush-left rather than relying on that collapse to be
46
46
  * harmless.
47
47
  */
48
- import { joinPromptHints, PROMPT_HINT_SEPARATOR } from "./prompt-hint-join.js"
48
+ import { joinPromptHints } from "./prompt-hint-join.js"
49
+ import { joinSentences } from "./hint-join.js"
49
50
  import {
50
51
  renderDirectionHintClauses,
51
52
  type DirectionFamily,
@@ -173,10 +174,7 @@ export function partitionStyleClauses(
173
174
  */
174
175
  export function styleSectionFromClauses(clauses: readonly SlottedPromptClause[]): string {
175
176
  const line = (slot: PromptClauseSlot) =>
176
- clauses
177
- .filter((c) => c.slot === slot)
178
- .map((c) => c.text)
179
- .join(PROMPT_HINT_SEPARATOR)
177
+ joinSentences(clauses.filter((c) => c.slot === slot).map((c) => c.text))
180
178
  const lines = [line("film"), line("scene")].filter((l) => l.length > 0)
181
179
  return lines.length === 0 ? "" : `${STYLE_SECTION_HEADER}\n${lines.join("\n")}`
182
180
  }
@@ -1,5 +1,6 @@
1
1
  import { resolveNodeRefs, readPromptAffixes, type PromptAffixes } from "@nodaro/shared"
2
2
  import { SOCIAL_POST_NODE_TYPES } from "@nodaro/shared"
3
+ import { joinSentences } from "./hint-join.js"
3
4
 
4
5
  export interface ResolvePromptArgs {
5
6
  override?: string
@@ -72,7 +73,8 @@ export function applyPromptAffixes(
72
73
  * override (list fan-out) > first present typed candidate > wired > "".
73
74
  * "present" = non-empty after trim. {Label} refs are resolved on the chosen
74
75
  * branch via the shared resolveNodeRefs. With `appendWired`, the chosen base
75
- * AND the wired value are both emitted (joined ". "). Affixes wrap the result. */
76
+ * AND the wired value are both emitted, joined as sentences (`joinSentences`:
77
+ * ". ", or a space after one that already ends). Affixes wrap the result. */
76
78
  export function resolvePrompt({ override, typed = [], wired, refMap, appendWired, affixes }: ResolvePromptArgs): string {
77
79
  return applyPromptAffixes(resolveCore({ override, typed, wired, refMap, appendWired }), affixes, refMap)
78
80
  }
@@ -83,7 +85,7 @@ function resolveCore({ override, typed = [], wired, refMap, appendWired }: Omit<
83
85
  // so per-item fan-out prompts are unchanged.
84
86
  if (appendWired && !present(override)) {
85
87
  const base = typed.find(present)
86
- return [base, wired].filter(present).map((s) => rr(s, refMap)).join(". ")
88
+ return joinSentences([base, wired].filter(present).map((s) => rr(s, refMap)))
87
89
  }
88
90
  if (present(override)) return rr(override, refMap)
89
91
  for (const t of typed) if (present(t)) return rr(t, refMap)
@@ -92,13 +94,13 @@ function resolveCore({ override, typed = [], wired, refMap, appendWired }: Omit<
92
94
  }
93
95
 
94
96
  /** Compose a final NEGATIVE prompt from a TYPED base + a WIRED (connected
95
- * negative-handle) value: both are emitted, joined ". " (mirrors `appendWired`
97
+ * negative-handle) value: both are emitted, joined as sentences (`joinSentences`, mirrors `appendWired`
96
98
  * for the positive prompt). Pure join — the caller resolves `{label}` refs on
97
99
  * the typed value first, and the wired value is already a resolved output that
98
100
  * the input-resolver has filtered (referenced / Inject-Negative-off dropped).
99
101
  * Generate-image / generate-video only; empty parts are dropped. */
100
102
  export function composeNegative(typed?: string, wired?: string): string {
101
- return [typed, wired].filter(present).join(". ")
103
+ return joinSentences([typed, wired])
102
104
  }
103
105
 
104
106
  /** Ordered typed-candidate fields per node type — the precedence source of
@@ -184,3 +186,19 @@ export function computeLlmChatFields(
184
186
  systemPrompt: resolvePrompt({ typed: [data.systemPrompt as string | undefined], wired: wiredSystemPrompt, refMap }),
185
187
  }
186
188
  }
189
+
190
+ /** generate-script's topic. Both engines call this, so a wired Text node reads
191
+ * the same on a single-node run and a server run.
192
+ *
193
+ * Precedence is override (a list fan-out item) > wired > `data.prompt`. Wired
194
+ * comes BEFORE the typed field, the reverse of `computeNodePrompt`, because
195
+ * `data.prompt` has no editor: the panel's text field is the style guide. A
196
+ * value there was written by the old connect-time `{Label}` auto-fill or by an
197
+ * agent, and it is kept only as the fallback that saved workflows rely on. */
198
+ export function computeScriptTopic(
199
+ data: Record<string, unknown>,
200
+ { override, wired, refMap }: Omit<ComputeNodePromptArgs, "appendWired">,
201
+ ): string {
202
+ const core = [override, wired, data.prompt as string | undefined].find(present)
203
+ return applyPromptAffixes(core === undefined ? "" : rr(core, refMap), readPromptAffixes(data), refMap)
204
+ }