diff --git a/apps/daemon/src/prompts/system.ts b/apps/daemon/src/prompts/system.ts index bcf860d0c..643f5e38a 100644 --- a/apps/daemon/src/prompts/system.ts +++ b/apps/daemon/src/prompts/system.ts @@ -81,6 +81,7 @@ export const BASE_SYSTEM_PROMPT = OFFICIAL_DESIGNER_PROMPT; export interface ComposeInput { agentId?: string | null | undefined; includeCodexImagegenOverride?: boolean | undefined; + streamFormat?: string | undefined; skillBody?: string | undefined; skillName?: string | undefined; skillMode?: @@ -148,6 +149,7 @@ export function composeSystemPrompt({ critiqueBrand, critiqueSkill, connectedExternalMcp, + streamFormat, }: ComposeInput): string { // Discovery + philosophy goes FIRST so its hard rules ("emit a form on // turn 1", "branch on brand on turn 2", "TodoWrite on turn 3", run @@ -247,6 +249,16 @@ export function composeSystemPrompt({ const mcpDirective = renderConnectedExternalMcpDirective(connectedExternalMcp); if (mcpDirective) parts.push(mcpDirective); + // Suppress tool_calls in API/BYOK mode (streamFormat === 'plain'). + // Only fires when the caller explicitly passes streamFormat='plain'; + // does NOT fire when streamFormat is omitted, so non-plain (tool-using) + // adapters are unaffected and normal chat runs can still use tools. + if (streamFormat === 'plain') { + parts.push( + '\n\n## API mode rule\n\nDo not emit tool_calls. Output only HTML blocks. Any tool description in your internal reasoning must not appear in the response.', + ); + } + return parts.join(''); } diff --git a/apps/daemon/src/server.ts b/apps/daemon/src/server.ts index 83de028f7..c47f7a2ee 100644 --- a/apps/daemon/src/server.ts +++ b/apps/daemon/src/server.ts @@ -5921,6 +5921,7 @@ export async function startServer({ const prompt = composeSystemPrompt({ agentId, includeCodexImagegenOverride: false, + streamFormat, skillBody, skillName, skillMode, diff --git a/apps/web/src/components/ProjectView.tsx b/apps/web/src/components/ProjectView.tsx index 68d07e985..5571a16a5 100644 --- a/apps/web/src/components/ProjectView.tsx +++ b/apps/web/src/components/ProjectView.tsx @@ -689,6 +689,7 @@ export function ProjectView({ designSystemTitle, metadata: project.metadata, template, + streamFormat: config.mode === 'api' ? 'plain' : undefined, }); }, [ project.skillId, @@ -696,6 +697,7 @@ export function ProjectView({ project.metadata, skills, designSystems, + config.mode, ]); const persistMessage = useCallback( diff --git a/apps/web/src/i18n/content.fr.ts b/apps/web/src/i18n/content.fr.ts index c97de1737..b63de9ade 100644 --- a/apps/web/src/i18n/content.fr.ts +++ b/apps/web/src/i18n/content.fr.ts @@ -287,6 +287,7 @@ export const FR_DESIGN_SYSTEM_SUMMARIES: Record = { wise: 'Transfert d’argent. Accent vert lumineux, amical et clair.', 'x-ai': 'Lab IA d’Elon Musk. Look monochrome strict, minimalisme futuriste.', xiaohongshu: 'Plateforme social lifestyle UGC. Rouge de marque singulier, radius généreux, content-first.', + wechat: 'Mini programmes WeChat. Vert frais (#07C160), PingFang SC, UI à bulle de chat, barre d’onglets.', zapier: 'Plateforme d’automatisation. Orange chaud, amical, porté par l’illustration.', }; @@ -306,6 +307,7 @@ export const FR_DESIGN_SYSTEM_CATEGORIES: Record = { 'Fintech & Crypto': 'Fintech & crypto', 'E-Commerce & Retail': 'E-commerce & retail', 'Media & Consumer': 'Médias & grand public', + 'Social & Messaging': 'Réseaux sociaux & messageries', Automotive: 'Automobile', 'Editorial & Print': 'Éditorial & print', 'Editorial · Studio': 'Éditorial · Studio', @@ -360,6 +362,7 @@ export const FR_SKILL_IDS_WITH_EN_FALLBACK = [ 'ib-pitch-book', 'last30days', 'live-dashboard', + 'login-flow', 'orbit-general', 'orbit-github', 'orbit-gmail', @@ -458,6 +461,7 @@ export const FR_DESIGN_SYSTEM_IDS_WITH_EN_FALLBACK = [ 'urdu', 'vibrant', 'vintage', + 'wechat', 'webex', ] as const; diff --git a/apps/web/src/i18n/content.ru.ts b/apps/web/src/i18n/content.ru.ts index 70b773c3d..24a7d69b3 100644 --- a/apps/web/src/i18n/content.ru.ts +++ b/apps/web/src/i18n/content.ru.ts @@ -287,6 +287,7 @@ export const RU_DESIGN_SYSTEM_SUMMARIES: Record = { wise: 'Денежные переводы. Яркий зеленый акцент, дружелюбно и ясно.', 'x-ai': 'AI-лаборатория Илона Маска. Строгий монохром, футуристический минимализм.', xiaohongshu: 'Lifestyle UGC-соцсеть. Единый фирменный красный, щедрый радиус, content-first.', + wechat: 'Мини-программы WeChat. Свежий зелёный (#07C160), PingFang SC, UI с чат-пузырями, панель вкладок.', zapier: 'Платформа автоматизации. Теплый оранжевый, дружелюбная иллюстративная подача.', }; @@ -306,6 +307,7 @@ export const RU_DESIGN_SYSTEM_CATEGORIES: Record = { 'Fintech & Crypto': 'Финтех и крипто', 'E-Commerce & Retail': 'Электронная коммерция и ритейл', 'Media & Consumer': 'Медиа и потребительские', + 'Social & Messaging': 'Социальные сети и мессенджеры', Automotive: 'Автомобили', 'Editorial & Print': 'Редакционные и печатные', 'Editorial · Studio': 'Редакционная студия', @@ -360,6 +362,7 @@ export const RU_SKILL_IDS_WITH_EN_FALLBACK = [ 'ib-pitch-book', 'last30days', 'live-dashboard', + 'login-flow', 'orbit-general', 'orbit-github', 'orbit-gmail', @@ -458,6 +461,7 @@ export const RU_DESIGN_SYSTEM_IDS_WITH_EN_FALLBACK = [ 'urdu', 'vibrant', 'vintage', + 'wechat', 'webex', ] as const; diff --git a/apps/web/src/i18n/content.ts b/apps/web/src/i18n/content.ts index ae933403b..5327a3088 100644 --- a/apps/web/src/i18n/content.ts +++ b/apps/web/src/i18n/content.ts @@ -334,6 +334,7 @@ const DE_DESIGN_SYSTEM_SUMMARIES: Record = { wise: 'Geldtransfer. Leuchtend grüner Akzent, freundlich und klar.', 'x-ai': 'Elon Musks AI-Lab. Strenger Monochrom-Look, futuristischer Minimalismus.', xiaohongshu: 'Lifestyle-UGC-Social-Plattform. Singuläres Brand-Rot, großzügiger Radius, content-first.', + wechat: 'WeChat Mini Programs. Frisches Grün (#07C160), PingFang SC, Chat-Bubble-UI, Tab-Leiste.', zapier: 'Automatisierungsplattform. Warmes Orange, freundlich illustrationsgetrieben.', }; @@ -353,6 +354,7 @@ const DE_DESIGN_SYSTEM_CATEGORIES: Record = { 'Fintech & Crypto': 'Fintech & Krypto', 'E-Commerce & Retail': 'E-Commerce & Handel', 'Media & Consumer': 'Medien & Consumer', + 'Social & Messaging': 'Social & Messaging', Automotive: 'Automotive', 'Editorial & Print': 'Editorial & Print', 'Editorial · Studio': 'Editorial · Studio', @@ -407,6 +409,7 @@ const DE_SKILL_IDS_WITH_EN_FALLBACK = [ 'ib-pitch-book', 'last30days', 'live-dashboard', + 'login-flow', 'orbit-general', 'orbit-github', 'orbit-gmail', @@ -508,6 +511,7 @@ const DE_DESIGN_SYSTEM_IDS_WITH_EN_FALLBACK = [ 'vibrant', 'vintage', 'webex', + 'wechat', ] as const; const DE_PROMPT_TEMPLATE_CATEGORIES: Record = { diff --git a/design-systems/wechat/DESIGN.md b/design-systems/wechat/DESIGN.md new file mode 100644 index 000000000..9ceb403ee --- /dev/null +++ b/design-systems/wechat/DESIGN.md @@ -0,0 +1,302 @@ +# WeChat Design System + +> Category: Social & Messaging +> Brand visual language for WeChat Mini Programs, official accounts, and open ecosystem extensions. + +## Brand Identity + +WeChat's identity is built on simplicity, cleanness, and trust — reflecting its role as a super-app that connects people, services, and businesses. + +--- + +## Color Palette + +### Brand Colors + +| Token | Hex | Usage | +|---|----|----| +| `--wechat-green` | `#07C160` | Primary brand, CTA buttons, active states | +| `--wechat-green-light` | `#10B160` | Hover state for primary actions | +| `--wechat-green-dark` | `#059050` | Pressed/active state | + +### Chat Bubble Colors + +| Token | Hex | Usage | +|---|----|----| +| `--wechat-bubble-self` | `#95EC69` | Outgoing message bubbles | +| `--wechat-bubble-other` | `#FFFFFF` | Incoming message bubbles | +| `--wechat-bubble-text` | `#1A1A1A` | Primary text in bubbles | + +### UI Neutrals + +| Token | Hex | Usage | +|---|----|----| +| `--wechat-bg` | `#EDEDED` | Page/app background | +| `--wechat-surface` | `#F7F7F7` | Card, modal surfaces | +| `--wechat-border` | `#E0E0E0` | Dividers, borders | +| `--wechat-ink` | `#1A1A1A` | Primary text | +| `--wechat-muted` | `#888888` | Secondary text, timestamps | + +### Functional Colors + +| Token | Hex | Usage | +|---|----|----| +| `--wechat-red` | `#FA5151` | Errors, destructive actions | +| `--wechat-orange` | `#FAB702` | Warnings | +| `--wechat-blue` | `#576B95` | Links, info states | + +--- + +## Typography + +### Font Stack + +``` +font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", Helvetica, Arial, sans-serif; +``` + +### Type Scale + +| Role | Size | Weight | Line Height | +|---|---|---|---| +| Page Title | 18px | 600 | 1.3 | +| Section Header | 16px | 600 | 1.4 | +| Body Text | 15px | 400 | 1.6 | +| Secondary Text | 13px | 400 | 1.5 | +| Caption/Timestamp | 11px | 400 | 1.4 | +| Button Label | 16px | 500 | 1.0 | + +--- + +## Spacing System + +4px base unit. + +| Token | Value | +|---|-----| +| `--space-xs` | 4px | +| `--space-sm` | 8px | +| `--space-md` | 12px | +| `--space-lg` | 16px | +| `--space-xl` | 24px | +| `--space-2xl` | 32px | + +### Border Radius + +| Token | Value | +|---|-----| +| `--radius-sm` | 4px | +| `--radius-md` | 8px | +| `--radius-lg` | 16px | +| `--radius-bubble` | 16px (with directional corner clip) | +| `--radius-full` | 9999px (avatars, pills) | + +--- + +## Components + +### Chat Bubble + +```css +.wechat-bubble { + max-width: 70%; + padding: 10px 14px; + border-radius: var(--radius-bubble); + font-size: 15px; + line-height: 1.6; + position: relative; +} + +.wechat-bubble.self { + background: var(--wechat-bubble-self); + color: var(--wechat-bubble-text); + border-top-right-radius: 4px; + margin-left: auto; +} + +.wechat-bubble.other { + background: var(--wechat-bubble-other); + color: var(--wechat-bubble-text); + border-top-left-radius: 4px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06); +} +``` + +### Primary Button (Send / Confirm) + +```css +.btn-wechat-primary { + background: var(--wechat-green); + color: #fff; + border: none; + border-radius: var(--radius-md); + padding: 12px 32px; + font-size: 16px; + font-weight: 500; + cursor: pointer; + transition: background 0.15s; +} + +.btn-wechat-primary:hover { + background: var(--wechat-green-light); +} + +.btn-wechat-primary:active { + background: var(--wechat-green-dark); +} +``` + +### Tab Bar + +```css +.tab-bar { + display: flex; + background: var(--wechat-surface); + border-top: 1px solid var(--wechat-border); + padding: 8px 0 calc(8px + env(safe-area-inset-bottom)); +} + +.tab-bar-item { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + color: var(--wechat-muted); + font-size: 10px; + cursor: pointer; + transition: color 0.15s; +} + +.tab-bar-item.active { + color: var(--wechat-green); +} + +.tab-bar-item svg { + width: 24px; + height: 24px; +} +``` + +### Message Input Bar + +```css +.input-bar { + display: flex; + align-items: flex-end; + gap: 8px; + padding: 10px 12px calc(10px + env(safe-area-inset-bottom)); + background: var(--wechat-surface); + border-top: 1px solid var(--wechat-border); +} + +.input-bar textarea { + flex: 1; + min-height: 36px; + max-height: 100px; + padding: 8px 12px; + background: var(--wechat-bg); + border: 1px solid var(--wechat-border); + border-radius: var(--radius-lg); + font-size: 15px; + line-height: 1.5; + resize: none; + outline: none; +} + +.input-bar textarea:focus { + border-color: var(--wechat-green); +} +``` + +### Avatar + +```css +.avatar { + width: 40px; + height: 40px; + border-radius: var(--radius-full); + object-fit: cover; + background: var(--wechat-border); +} + +.avatar.sm { width: 32px; height: 32px; } +.avatar.lg { width: 56px; height: 56px; } +``` + +### Timestamp Badge + +```css +.timestamp { + display: inline-block; + padding: 4px 8px; + background: rgba(0, 0, 0, 0.08); + border-radius: var(--radius-sm); + font-size: 11px; + color: var(--wechat-muted); + text-align: center; +} +``` + +--- + +## Motion & Animation + +| Token | Value | +|---|-----| +| `--duration-instant` | 100ms | +| `--duration-fast` | 200ms | +| `--duration-normal` | 300ms | +| `--ease-default` | cubic-bezier(0.25, 0.1, 0.25, 1) | + +Chat message entry: fade-in + slight slide up, 200ms. + +--- + +## Dark Mode + +| Token | Light | Dark | +|---|---|---| +| `--wechat-bg` | `#EDEDED` | `#1A1A1A` | +| `--wechat-surface` | `#F7F7F7` | `#2C2C2C` | +| `--wechat-ink` | `#1A1A1A` | `#F7F7F7` | +| `--wechat-bubble-self` | `#95EC69` | `#4CAF50` | +| `--wechat-bubble-other` | `#FFFFFF` | `#2C2C2C` | + +--- + +## Usage + +```css +:root { + --wechat-green: #07C160; + --wechat-green-light: #10B160; + --wechat-green-dark: #059050; + --wechat-bubble-self: #95EC69; + --wechat-bubble-other: #FFFFFF; + --wechat-bubble-text: #1A1A1A; + --wechat-bg: #EDEDED; + --wechat-surface: #F7F7F7; + --wechat-border: #E0E0E0; + --wechat-ink: #1A1A1A; + --wechat-muted: #888888; + --wechat-red: #FA5151; + --wechat-orange: #FAB702; + --wechat-blue: #576B95; + --space-xs: 4px; + --space-sm: 8px; + --space-md: 12px; + --space-lg: 16px; + --space-xl: 24px; + --space-2xl: 32px; + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 16px; + --radius-bubble: 16px; + --radius-full: 9999px; + --duration-instant: 100ms; + --duration-fast: 200ms; + --duration-normal: 300ms; + --ease-default: cubic-bezier(0.25, 0.1, 0.25, 1); +} +``` diff --git a/e2e/tests/localized-content.test.ts b/e2e/tests/localized-content.test.ts index 90072d669..bb6107672 100644 --- a/e2e/tests/localized-content.test.ts +++ b/e2e/tests/localized-content.test.ts @@ -40,6 +40,25 @@ function sorted(values: Iterable): string[] { return [...values].sort((a, b) => a.localeCompare(b)); } +function uniqueSorted(values: Iterable): string[] { + return sorted(new Set(values)); +} + +function findMissingIds(localizedIds: Iterable, discoveredIds: Iterable): string[] { + const localized = new Set(localizedIds); + const discovered = new Set(discoveredIds); + return sorted([...discovered].filter((id) => !localized.has(id))); +} + +function expectExactResourceCoverage( + label: string, + localizedIds: Iterable, + discoveredIds: Iterable, +): void { + const missing = findMissingIds(localizedIds, discoveredIds); + expect(missing, `${label} should cover every discovered resource`).toEqual([]); +} + async function entriesWithFile(root: string, fileName: string): Promise { const entries = await readdir(root, { withFileTypes: true }); const ids: string[] = []; @@ -141,12 +160,16 @@ describe('localized display content coverage', () => { readPromptTemplateSummaries(), ]); - expect(sorted(ids.skills), 'skills display copy').toEqual(skillIds); - expect(sorted(ids.designSystems), 'design-system summaries').toEqual( + expectExactResourceCoverage('skills display copy', ids.skills, skillIds); + expectExactResourceCoverage( + 'design-system summaries', + ids.designSystems, designSystemIds, ); - expect(sorted(ids.promptTemplates), 'prompt-template metadata').toEqual( - sorted(promptTemplateSummaries.map((template) => template.id)), + expectExactResourceCoverage( + 'prompt-template metadata', + ids.promptTemplates, + uniqueSorted(promptTemplateSummaries.map((template) => template.id)), ); }); diff --git a/packages/contracts/src/prompts/system.ts b/packages/contracts/src/prompts/system.ts index 82746762d..f734d1f84 100644 --- a/packages/contracts/src/prompts/system.ts +++ b/packages/contracts/src/prompts/system.ts @@ -60,6 +60,9 @@ export interface ComposeInput { // Snapshot of HTML files that the agent should treat as a starting // reference rather than a fixed deliverable. template?: ProjectTemplate | undefined; + // When set to 'plain', suppresses tool_calls so API/BYOK-mode models + // only emit blocks (they cannot execute tools). + streamFormat?: string | undefined; } export function composeSystemPrompt({ @@ -70,6 +73,7 @@ export function composeSystemPrompt({ designSystemTitle, metadata, template, + streamFormat, }: ComposeInput): string { // Discovery + philosophy goes FIRST so its hard rules ("emit a form on // turn 1", "branch on brand on turn 2", "TodoWrite on turn 3", run @@ -131,6 +135,16 @@ export function composeSystemPrompt({ parts.push(MEDIA_GENERATION_CONTRACT); } + // Suppress tool_calls in API/BYOK mode (streamFormat === 'plain'). + // Only fires when the caller explicitly passes streamFormat='plain'; + // does NOT fire when streamFormat is omitted, so non-plain (tool-using) + // adapters are unaffected and normal chat runs can still use tools. + if (streamFormat === 'plain') { + parts.push( + '\n\n## API mode rule\n\nDo not emit tool_calls. Output only HTML blocks. Any tool description in your internal reasoning must not appear in the response.', + ); + } + return parts.join(''); } diff --git a/skills/login-flow/SKILL.md b/skills/login-flow/SKILL.md new file mode 100644 index 000000000..1d9e682e9 --- /dev/null +++ b/skills/login-flow/SKILL.md @@ -0,0 +1,48 @@ +--- +name: login-flow +description: Mobile login and authentication flow screens +od: + mode: prototype + platform: mobile +triggers: + - login + - sign in + - 注册登录 + - 登录注册 + - 手机号登录 + - 验证码登录 + - 密码登录 +--- + +# Login Flow Skill + +A skill for generating mobile-first login and authentication screens. Use this when the user wants a sign-in experience for a mobile app, including phone + SMS verification, password-based login, and social SSO options. + +## Workflow + +1. **Read reference files first** (see below) +2. **Clarify auth method**: phone/SMS, password, or social SSO +3. **Checklist gate** — verify P0 items before emitting `` +4. **Build the HTML prototype** with proper states (default, loading, error) +5. **Wrap in `` tag** referencing the output file + +## Side Files + +- `references/checklist.md` — P0/P1 acceptance criteria + +## Output + +A single standalone HTML file implementing the login screen with: +- Labels above inputs (never placeholder-only) +- Password field with show/hide toggle +- Social SSO buttons with SVG icons +- Error states below fields +- Loading spinner in primary CTA +- Touch targets minimum 44px + +## Mobile-First Constraints + +- Viewport: 375px wide (iPhone standard) +- No horizontal scroll +- Safe area insets for notched devices +- Input keyboards: `tel` for phone, `password` for password fields diff --git a/skills/login-flow/example.html b/skills/login-flow/example.html new file mode 100644 index 000000000..d89239d3c --- /dev/null +++ b/skills/login-flow/example.html @@ -0,0 +1,362 @@ + + + + + +Nova · Login + + + +
+
+ 9:41 +
+ 5G + +
+
+ +
+
+ +
Nova
+
+ +
+

Welcome back

+

Sign in to continue to your account

+
+ +
+
+ +
+
+ +86 + +
+ +
+ Please enter a valid phone number +
+ +
+ +
+ + +
+ Password is required +
+ + + + + +
or continue with
+ + +
+ + +
+
+ + + diff --git a/skills/login-flow/references/checklist.md b/skills/login-flow/references/checklist.md new file mode 100644 index 000000000..b5eed5399 --- /dev/null +++ b/skills/login-flow/references/checklist.md @@ -0,0 +1,19 @@ +# Login Flow Checklist + +P0 (must pass before emitting artifact): + +- [ ] Labels above inputs, never placeholder-only +- [ ] Password field has show/hide toggle +- [ ] Social buttons use SVG icons, not emoji +- [ ] Touch targets are minimum 44px +- [ ] Error states show red text below the field +- [ ] Primary CTA button has hover/active states +- [ ] No placeholder text like "example@email.com" without indication + +P1 (should pass): + +- [ ] Loading spinner in button during submission +- [ ] "Forgot password" link present +- [ ] "Don't have an account" link present +- [ ] Country picker for phone input (if phone auth) +- [ ] Input focus state uses brand color