prompt.ts 3.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. import type { Spec } from "./types";
  2. import type { EditMode } from "./edit-modes";
  3. import { buildEditUserPrompt, isNonEmptySpec } from "./edit-modes";
  4. /**
  5. * Options for building a user prompt.
  6. */
  7. export interface UserPromptOptions {
  8. /** The user's text prompt */
  9. prompt: string;
  10. /** Existing spec to refine (triggers patch-only mode) */
  11. currentSpec?: Spec | null;
  12. /** Runtime state context to include */
  13. state?: Record<string, unknown> | null;
  14. /** Maximum length for the user's text prompt (applied before wrapping) */
  15. maxPromptLength?: number;
  16. /** Edit modes to offer when refining an existing spec. Default: `["patch"]`. */
  17. editModes?: EditMode[];
  18. /** Wire format. Default: `"json"`. */
  19. format?: "json" | "yaml";
  20. /** Serialise the spec for edits. Defaults to JSON.stringify for json, must be provided for yaml. */
  21. serializer?: (spec: Spec) => string;
  22. }
  23. /**
  24. * Build a user prompt for AI generation.
  25. *
  26. * Handles common patterns that every consuming app needs:
  27. * - Truncating the user's prompt to a max length
  28. * - Including the current spec for refinement (edit mode)
  29. * - Including runtime state context
  30. *
  31. * @example
  32. * ```ts
  33. * // Fresh generation
  34. * buildUserPrompt({ prompt: "create a todo app" })
  35. *
  36. * // Refinement with existing spec
  37. * buildUserPrompt({ prompt: "add a dark mode toggle", currentSpec: spec })
  38. *
  39. * // With multiple edit modes
  40. * buildUserPrompt({ prompt: "change title", currentSpec: spec, editModes: ["patch", "merge"] })
  41. * ```
  42. */
  43. export function buildUserPrompt(options: UserPromptOptions): string {
  44. const {
  45. prompt,
  46. currentSpec,
  47. state,
  48. maxPromptLength,
  49. editModes,
  50. format,
  51. serializer,
  52. } = options;
  53. // Sanitize and optionally truncate the user's text
  54. let userText = String(prompt || "");
  55. if (maxPromptLength !== undefined && maxPromptLength > 0) {
  56. userText = userText.slice(0, maxPromptLength);
  57. }
  58. // --- Refinement mode: currentSpec is provided ---
  59. if (isNonEmptySpec(currentSpec)) {
  60. const editPrompt = buildEditUserPrompt({
  61. prompt: userText,
  62. currentSpec,
  63. config: { modes: editModes ?? ["patch"] },
  64. format: format ?? "json",
  65. serializer,
  66. });
  67. // Append state context if provided
  68. if (state && Object.keys(state).length > 0) {
  69. return `${editPrompt}\n\nAVAILABLE STATE:\n${JSON.stringify(state, null, 2)}`;
  70. }
  71. return editPrompt;
  72. }
  73. // --- Fresh generation mode ---
  74. const parts: string[] = [userText];
  75. if (state && Object.keys(state).length > 0) {
  76. parts.push(`\nAVAILABLE STATE:\n${JSON.stringify(state, null, 2)}`);
  77. }
  78. if (format === "yaml") {
  79. parts.push(
  80. `\nOutput the full spec in a \`\`\`yaml-spec fence. Stream progressively — output elements one at a time.`,
  81. );
  82. } else {
  83. parts.push(
  84. `\nRemember: Output /root first, then interleave /elements and /state patches so the UI fills in progressively as it streams. Output each state patch right after the elements that use it, one per array item.`,
  85. );
  86. }
  87. return parts.join("\n");
  88. }