openpencil/apps/web/src/services/ai/design-code-generator.ts
Kayshen Xu 3caaa495bb
V0.5.2 (#82)
* feat(ai): scaffold pen-ai-skills package

* feat(ai): add pen-ai-skills core types

* feat(ai): add Vite plugin for skill file compilation

* feat(ai): add budget module with token estimation and category-priority trimming

* feat(ai): add skill loader with phase filtering and test injection

* feat(ai): add skill resolver with phase filter, intent match, and dynamic injection

* feat(ai): migrate all prompt content to skill files

Extract prompt content from 7 TypeScript source files into 27 Markdown
skill files with proper frontmatter. Each file contains phase, trigger,
priority, budget, and category metadata for the skill resolution engine.

Planning: decomposition, design-type
Generation: jsonl-format, jsonl-format-simplified, schema, layout,
  text-rules, overflow, style-defaults, variables, design-system,
  design-code, design-md
Validation: vision-feedback
Maintenance: local-edit, incremental-add, style-consistency
Domains: landing-page, dashboard, mobile-app, form-ui, cjk-typography
Knowledge: role-definitions, design-principles, icon-catalog,
  copywriting, examples

* feat(ai): add document context memory module

* feat(ai): add generation history memory module

* feat(ai): wire memory loading into skill resolver

Add memory field to ResolveOptions and load documentContext/generationHistory
into AgentContext with per-phase limits (planning:5, maintenance:3, others:0).
Export memory utility functions from package index.

* refactor(ai): use resolveSkills('planning') in orchestrator

Replace ORCHESTRATOR_PROMPT import with resolveSkills() from pen-ai-skills.
The planning phase system prompt is now resolved dynamically from skill
files instead of a static constant.

* refactor(ai): use resolveSkills('generation') in sub-agent

Replace SUB_AGENT_PROMPT + designPrinciples concatenation with
resolveSkills() from pen-ai-skills. The generation phase system prompt
is now resolved dynamically with flag-based skill filtering for
variables and design.md context.

* refactor(ai): use resolveSkills('validation') in design validation

Replace the 70-line inline VALIDATION_SYSTEM_PROMPT constant with a
lazy resolver function that loads the validation prompt from pen-ai-skills
skill files at call time.

* refactor(mcp): use skill registry for design prompt sections

* refactor(ai): remove old prompt files replaced by pen-ai-skills

Delete prompt files whose content has been migrated to the
pen-ai-skills package:
- ai-prompt-sections.ts (section registry, triggers, builders)
- orchestrator-prompts.ts (orchestrator + sub-agent prompts)
- design-system-prompts.ts (design token generation prompt)
- design-code-prompts.ts (HTML/CSS code-gen prompt)
- design-principles/ directory (5 principle files + index)

Consolidate role-definitions/ 8 sub-files into index.ts
(registerRole calls are runtime behavior, must be preserved).

Move buildDesignMdStylePolicy into ai-prompts.ts. Strip migrated
constants (PEN_NODE_SCHEMA, DESIGN_EXAMPLES, ADAPTIVE_STYLE_POLICY,
CHAT_SYSTEM_PROMPT, DESIGN_GENERATOR_PROMPT, etc.) from ai-prompts.ts.

Add pen-ai-skills alias to mcp:compile esbuild script.

* docs: add pen-ai-skills to architecture documentation

* fix(mcp): use h3 v2 createEventStream API for SSE endpoint

event.node.res was removed in h3 v2. Migrate to createEventStream
which handles SSE headers, streaming, and cleanup automatically.

* chore: update package versions and add pen-ai-skills to Dockerfile

- Bump version for multiple packages to 0.5.2, including pen-ai-skills, pen-codegen, pen-core, pen-figma, pen-renderer, pen-sdk, and pen-types.
- Add pen-ai-skills package to Dockerfile for inclusion in the build process.
- Update TypeScript configuration to ensure proper file inclusion.
- Modify GitHub Actions workflow to trigger on version tags.
- Enhance AI-related functionality by integrating resolveAgentModel for dynamic model resolution in chat and generation APIs.

* refactor: clean up unused imports and improve dynamic content handling

- Removed unused imports from various files, including SkillMeta and ResolvedSkill from types.test.ts, and estimateTokens from resolve-skills.ts.
- Updated the dynamic content injection function to use a more concise parameter in the regex replacement callback.

* chore: enhance build and CI workflows with skill generation

- Added a new script to generate the skill registry during post-installation in package.json.
- Updated the GitHub Actions workflows to include steps for compiling the CLI and generating the skill registry, ensuring all necessary components are built and ready for deployment.

---------

Co-authored-by: Fini <fini.yang@gmail.com>
2026-03-24 21:16:28 +08:00

190 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Design Code Generator (Stage 1 of visual reference pipeline).
*
* Generates self-contained HTML/CSS code using the model's strongest design
* capability. The output is a visual reference that guides PenNode generation.
* Design principles are included to ensure consistent visual quality.
*/
import type { DesignSystem } from './ai-types'
import type { AIProviderType } from '@/types/agent-settings'
import { generateCompletion } from './ai-service'
import { getSkillByName } from '@zseven-w/pen-ai-skills'
import { designSystemToPromptContext } from './design-system-generator'
interface CodeGenOptions {
width: number
height: number
model?: string
provider?: AIProviderType
}
/**
* Generate self-contained HTML/CSS code for a design request.
* The code is production-grade and serves as a visual blueprint.
*/
export async function generateDesignCode(
prompt: string,
designSystem: DesignSystem,
options: CodeGenOptions,
): Promise<string> {
const designCodeSkill = getSkillByName('design-code')?.content ?? ''
const principles = getSkillByName('design-principles')?.content ?? ''
// Build the system prompt with principles injected
const systemPrompt = principles
? `${designCodeSkill}\n\n${principles}`
: designCodeSkill
// Build the user prompt with design system context
const dsContext = designSystemToPromptContext(designSystem)
const userPrompt = buildCodeGenUserPrompt(
prompt,
dsContext,
options.width,
options.height,
)
const response = await generateCompletion(
systemPrompt,
userPrompt,
options.model,
options.provider,
)
return extractHtmlFromResponse(response)
}
/**
* Extract the HTML content from an AI response.
* Handles responses with code fences, markdown, or bare HTML.
*/
function extractHtmlFromResponse(response: string): string {
const trimmed = response.trim()
// Check for code fence wrapped HTML
const fenceMatch = trimmed.match(/```(?:html)?\s*\n?([\s\S]*?)\n?```/)
if (fenceMatch) {
const content = fenceMatch[1].trim()
if (content.includes('<!DOCTYPE') || content.includes('<html')) {
return content
}
}
// Check if the response itself starts with HTML
if (trimmed.startsWith('<!DOCTYPE') || trimmed.startsWith('<html')) {
return trimmed
}
// Try to find HTML document in the response
const htmlMatch = trimmed.match(/(<!DOCTYPE[\s\S]*<\/html>)/i)
if (htmlMatch) {
return htmlMatch[1]
}
// Last resort: wrap bare content
return `<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Design</title></head>
<body>${trimmed}</body>
</html>`
}
/**
* Extract a structural summary from HTML for use as sub-agent reference.
* Produces a concise text description of the HTML structure.
*/
export function extractStructureSummary(html: string): string {
const lines: string[] = ['DESIGN REFERENCE STRUCTURE:']
// Extract section-level elements
const sectionPattern = /<(?:section|header|footer|nav|main|div)\s+[^>]*(?:class|id)="([^"]*)"[^>]*>/gi
let match: RegExpExecArray | null
while ((match = sectionPattern.exec(html)) !== null) {
const classOrId = match[1]
if (classOrId && !classOrId.includes('__')) {
lines.push(`- Section: ${classOrId}`)
}
}
// Extract heading content for structure hints
const headingPattern = /<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/gi
while ((match = headingPattern.exec(html)) !== null) {
const level = match[1]
const content = match[2].replace(/<[^>]+>/g, '').trim().slice(0, 60)
if (content) {
lines.push(`- H${level}: "${content}"`)
}
}
// Extract button/CTA text
const buttonPattern = /<(?:button|a)\s+[^>]*class="[^"]*(?:btn|button|cta)[^"]*"[^>]*>([\s\S]*?)<\/(?:button|a)>/gi
while ((match = buttonPattern.exec(html)) !== null) {
const text = match[1].replace(/<[^>]+>/g, '').trim().slice(0, 30)
if (text) {
lines.push(`- CTA: "${text}"`)
}
}
// If we couldn't extract structure, provide a generic summary
if (lines.length <= 1) {
lines.push('(HTML structure extracted — use as visual layout reference)')
}
return lines.join('\n')
}
/**
* Extract the HTML section relevant to a specific subtask label.
* Uses heuristic matching on section/div IDs, classes, and heading content.
*/
export function extractHtmlSection(html: string, subtaskLabel: string): string | null {
const labelLower = subtaskLabel.toLowerCase()
// Try to find a matching section by common keywords
const keywords = labelLower
.replace(/[(].+[)]/g, '')
.split(/[\s,/]+/)
.filter((w) => w.length > 2)
if (keywords.length === 0) return null
// Build a regex to match section containers
const keywordPattern = keywords.map((k) => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')
const sectionRegex = new RegExp(
`<(?:section|div|header|footer|nav)[^>]*(?:class|id)="[^"]*(?:${keywordPattern})[^"]*"[^>]*>[\\s\\S]*?(?=<(?:section|div|header|footer|nav)[^>]*(?:class|id)="|$)`,
'i',
)
const match = sectionRegex.exec(html)
if (match) {
// Truncate to reasonable length for context
const section = match[0].slice(0, 1500)
return `HTML reference for "${subtaskLabel}":\n${section}`
}
return null
}
/**
* Build the user prompt for HTML/CSS code generation.
* Includes the design system tokens and viewport constraints.
*/
function buildCodeGenUserPrompt(
userPrompt: string,
designSystemContext: string,
width: number,
height: number,
): string {
const heightInstruction = height > 0
? `Height: ${height}px (fixed viewport).`
: `Height: auto (content determines height, estimate based on sections).`
return `Design request: ${userPrompt}
Viewport: Width ${width}px. ${heightInstruction}
${designSystemContext}
Generate the complete HTML file now.`
}