mirror of
https://github.com/ZSeven-W/openpencil.git
synced 2026-06-01 03:14:29 +07:00
* 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>
190 lines
5.8 KiB
TypeScript
190 lines
5.8 KiB
TypeScript
/**
|
||
* 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.`
|
||
}
|