stitch-forge 0.3.1
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.
- package/.claude/skills/forge-build/SKILL.md +79 -0
- package/.claude/skills/forge-design/SKILL.md +64 -0
- package/.claude/skills/forge-discover/SKILL.md +139 -0
- package/.claude/skills/forge-generate/SKILL.md +77 -0
- package/.claude/skills/forge-preview/SKILL.md +26 -0
- package/.claude/skills/forge-research/SKILL.md +42 -0
- package/.claude/skills/forge-sync/SKILL.md +45 -0
- package/DESIGN.md +113 -0
- package/LICENSE +21 -0
- package/README.es.md +242 -0
- package/README.md +242 -0
- package/dist/adapters/astro.d.ts +8 -0
- package/dist/adapters/astro.js +24 -0
- package/dist/adapters/astro.js.map +1 -0
- package/dist/adapters/index.d.ts +3 -0
- package/dist/adapters/index.js +9 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/nextjs.d.ts +7 -0
- package/dist/adapters/nextjs.js +136 -0
- package/dist/adapters/nextjs.js.map +1 -0
- package/dist/adapters/static.d.ts +7 -0
- package/dist/adapters/static.js +43 -0
- package/dist/adapters/static.js.map +1 -0
- package/dist/adapters/types.d.ts +22 -0
- package/dist/adapters/types.js +6 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/commands/build.d.ts +7 -0
- package/dist/commands/build.js +98 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/design.d.ts +3 -0
- package/dist/commands/design.js +39 -0
- package/dist/commands/design.js.map +1 -0
- package/dist/commands/discover.d.ts +9 -0
- package/dist/commands/discover.js +91 -0
- package/dist/commands/discover.js.map +1 -0
- package/dist/commands/generate.d.ts +7 -0
- package/dist/commands/generate.js +105 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +99 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/preview.d.ts +5 -0
- package/dist/commands/preview.js +41 -0
- package/dist/commands/preview.js.map +1 -0
- package/dist/commands/research.d.ts +1 -0
- package/dist/commands/research.js +38 -0
- package/dist/commands/research.js.map +1 -0
- package/dist/commands/sync.d.ts +1 -0
- package/dist/commands/sync.js +53 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/commands/workflow.d.ts +1 -0
- package/dist/commands/workflow.js +38 -0
- package/dist/commands/workflow.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +113 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/auth.d.ts +15 -0
- package/dist/mcp/auth.js +56 -0
- package/dist/mcp/auth.js.map +1 -0
- package/dist/mcp/client.d.ts +65 -0
- package/dist/mcp/client.js +302 -0
- package/dist/mcp/client.js.map +1 -0
- package/dist/mcp/tools.d.ts +26 -0
- package/dist/mcp/tools.js +46 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/research/business-researcher.d.ts +41 -0
- package/dist/research/business-researcher.js +888 -0
- package/dist/research/business-researcher.js.map +1 -0
- package/dist/research/crawler.d.ts +11 -0
- package/dist/research/crawler.js +56 -0
- package/dist/research/crawler.js.map +1 -0
- package/dist/research/design-synthesizer.d.ts +46 -0
- package/dist/research/design-synthesizer.js +628 -0
- package/dist/research/design-synthesizer.js.map +1 -0
- package/dist/research/differ.d.ts +19 -0
- package/dist/research/differ.js +58 -0
- package/dist/research/differ.js.map +1 -0
- package/dist/research/known-state.json +68 -0
- package/dist/research/research-cache.d.ts +6 -0
- package/dist/research/research-cache.js +62 -0
- package/dist/research/research-cache.js.map +1 -0
- package/dist/research/types.d.ts +98 -0
- package/dist/research/types.js +6 -0
- package/dist/research/types.js.map +1 -0
- package/dist/research/updater.d.ts +5 -0
- package/dist/research/updater.js +43 -0
- package/dist/research/updater.js.map +1 -0
- package/dist/templates/design-md.d.ts +52 -0
- package/dist/templates/design-md.js +315 -0
- package/dist/templates/design-md.js.map +1 -0
- package/dist/templates/prompts.d.ts +31 -0
- package/dist/templates/prompts.js +39 -0
- package/dist/templates/prompts.js.map +1 -0
- package/dist/templates/workflows.d.ts +9 -0
- package/dist/templates/workflows.js +21 -0
- package/dist/templates/workflows.js.map +1 -0
- package/dist/tui/App.d.ts +1 -0
- package/dist/tui/App.js +87 -0
- package/dist/tui/App.js.map +1 -0
- package/dist/tui/Dashboard.d.ts +5 -0
- package/dist/tui/Dashboard.js +23 -0
- package/dist/tui/Dashboard.js.map +1 -0
- package/dist/tui/DesignEditor.d.ts +6 -0
- package/dist/tui/DesignEditor.js +76 -0
- package/dist/tui/DesignEditor.js.map +1 -0
- package/dist/tui/PromptBuilder.d.ts +5 -0
- package/dist/tui/PromptBuilder.js +102 -0
- package/dist/tui/PromptBuilder.js.map +1 -0
- package/dist/tui/components/QuotaMeter.d.ts +8 -0
- package/dist/tui/components/QuotaMeter.js +10 -0
- package/dist/tui/components/QuotaMeter.js.map +1 -0
- package/dist/tui/components/ScreenCard.d.ts +7 -0
- package/dist/tui/components/ScreenCard.js +6 -0
- package/dist/tui/components/ScreenCard.js.map +1 -0
- package/dist/tui/components/Spinner.d.ts +5 -0
- package/dist/tui/components/Spinner.js +7 -0
- package/dist/tui/components/Spinner.js.map +1 -0
- package/dist/tui/components/StatusBar.d.ts +7 -0
- package/dist/tui/components/StatusBar.js +6 -0
- package/dist/tui/components/StatusBar.js.map +1 -0
- package/dist/utils/config.d.ts +26 -0
- package/dist/utils/config.js +66 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/design-validator.d.ts +44 -0
- package/dist/utils/design-validator.js +396 -0
- package/dist/utils/design-validator.js.map +1 -0
- package/dist/utils/logger.d.ts +8 -0
- package/dist/utils/logger.js +10 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/output-validator.d.ts +18 -0
- package/dist/utils/output-validator.js +194 -0
- package/dist/utils/output-validator.js.map +1 -0
- package/dist/utils/preview.d.ts +4 -0
- package/dist/utils/preview.js +49 -0
- package/dist/utils/preview.js.map +1 -0
- package/dist/utils/prompt-enhancer.d.ts +21 -0
- package/dist/utils/prompt-enhancer.js +104 -0
- package/dist/utils/prompt-enhancer.js.map +1 -0
- package/dist/utils/quota.d.ts +18 -0
- package/dist/utils/quota.js +49 -0
- package/dist/utils/quota.js.map +1 -0
- package/dist/utils/validators.d.ts +125 -0
- package/dist/utils/validators.js +110 -0
- package/dist/utils/validators.js.map +1 -0
- package/package.json +77 -0
|
@@ -0,0 +1,888 @@
|
|
|
1
|
+
import { load } from 'cheerio';
|
|
2
|
+
// ─── Color Extraction ──────────────────────────────────────────────
|
|
3
|
+
const HEX_PATTERN = /#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b/g;
|
|
4
|
+
function normalizeHex(hex) {
|
|
5
|
+
const h = hex.toUpperCase();
|
|
6
|
+
if (h.length === 4) {
|
|
7
|
+
return `#${h[1]}${h[1]}${h[2]}${h[2]}${h[3]}${h[3]}`;
|
|
8
|
+
}
|
|
9
|
+
return h;
|
|
10
|
+
}
|
|
11
|
+
function classifyContext(declaration) {
|
|
12
|
+
const lower = declaration.toLowerCase();
|
|
13
|
+
if (/\bborder-color\b|\bborder\b/.test(lower))
|
|
14
|
+
return 'border';
|
|
15
|
+
if (/\bbackground-color\b|\bbackground\b|\bbg\b/.test(lower))
|
|
16
|
+
return 'background';
|
|
17
|
+
if (/\bcolor\b/.test(lower))
|
|
18
|
+
return 'text';
|
|
19
|
+
return 'unknown';
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Extract color palette from HTML by parsing style blocks and inline styles.
|
|
23
|
+
*/
|
|
24
|
+
export function extractPalette(html) {
|
|
25
|
+
const $ = load(html);
|
|
26
|
+
const colorMap = new Map();
|
|
27
|
+
function recordColor(hex, context) {
|
|
28
|
+
const normalized = normalizeHex(hex);
|
|
29
|
+
const existing = colorMap.get(normalized);
|
|
30
|
+
if (existing) {
|
|
31
|
+
existing.frequency += 1;
|
|
32
|
+
// Prefer more specific context over 'unknown'
|
|
33
|
+
if (existing.context === 'unknown' && context !== 'unknown') {
|
|
34
|
+
existing.context = context;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
colorMap.set(normalized, { frequency: 1, context });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function extractFromCSS(css) {
|
|
42
|
+
// Match CSS custom properties: --var: #hex
|
|
43
|
+
const customPropRegex = /--[\w-]+\s*:\s*(#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b)/g;
|
|
44
|
+
let match;
|
|
45
|
+
while ((match = customPropRegex.exec(css)) !== null) {
|
|
46
|
+
recordColor(match[1], 'unknown');
|
|
47
|
+
}
|
|
48
|
+
// Match property: value pairs containing hex colors
|
|
49
|
+
const declRegex = /([\w-]+)\s*:\s*([^;}{]+)/g;
|
|
50
|
+
while ((match = declRegex.exec(css)) !== null) {
|
|
51
|
+
const property = match[1];
|
|
52
|
+
const value = match[2];
|
|
53
|
+
const hexMatches = value.match(HEX_PATTERN);
|
|
54
|
+
if (hexMatches) {
|
|
55
|
+
const ctx = classifyContext(property);
|
|
56
|
+
for (const hex of hexMatches) {
|
|
57
|
+
recordColor(hex, ctx);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Extract from <style> blocks
|
|
63
|
+
$('style').each((_i, el) => {
|
|
64
|
+
const css = $(el).text();
|
|
65
|
+
extractFromCSS(css);
|
|
66
|
+
});
|
|
67
|
+
// Extract from inline style attributes
|
|
68
|
+
$('[style]').each((_i, el) => {
|
|
69
|
+
const style = $(el).attr('style') ?? '';
|
|
70
|
+
extractFromCSS(style);
|
|
71
|
+
});
|
|
72
|
+
// Build sorted color list
|
|
73
|
+
const colors = [];
|
|
74
|
+
for (const [hex, data] of colorMap) {
|
|
75
|
+
colors.push({ hex, frequency: data.frequency, context: data.context });
|
|
76
|
+
}
|
|
77
|
+
colors.sort((a, b) => b.frequency - a.frequency);
|
|
78
|
+
// Filter out structural colors — grays, blacks, whites used for layout, not brand identity
|
|
79
|
+
const isStructuralColor = (hex) => {
|
|
80
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
81
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
82
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
83
|
+
const maxDiff = Math.max(Math.abs(r - g), Math.abs(g - b), Math.abs(r - b));
|
|
84
|
+
// Any near-gray (R≈G≈B) is structural: text colors, borders, backgrounds
|
|
85
|
+
if (maxDiff < 20)
|
|
86
|
+
return true;
|
|
87
|
+
// Pure white or near-white
|
|
88
|
+
if (r > 240 && g > 240 && b > 240)
|
|
89
|
+
return true;
|
|
90
|
+
return false;
|
|
91
|
+
};
|
|
92
|
+
const brandColors = colors.filter(c => !isStructuralColor(c.hex));
|
|
93
|
+
const brandBgColors = brandColors.filter(c => c.context === 'background');
|
|
94
|
+
const brandAccentColors = brandColors.filter(c => c.context !== 'background');
|
|
95
|
+
// Dominant = most frequent brand-colored background, or most frequent brand color overall
|
|
96
|
+
const dominantHex = brandBgColors.length > 0
|
|
97
|
+
? brandBgColors[0].hex
|
|
98
|
+
: brandColors.length > 0
|
|
99
|
+
? brandColors[0].hex
|
|
100
|
+
: colors.length > 0 ? colors[0].hex : '';
|
|
101
|
+
const accentHex = brandAccentColors.length > 0
|
|
102
|
+
? brandAccentColors.find(c => c.hex !== dominantHex)?.hex
|
|
103
|
+
: brandColors.find(c => c.hex !== dominantHex)?.hex;
|
|
104
|
+
return { colors, dominantHex, accentHex };
|
|
105
|
+
}
|
|
106
|
+
// ─── Typography Extraction ─────────────────────────────────────────
|
|
107
|
+
const GENERIC_FONTS = new Set([
|
|
108
|
+
'sans-serif',
|
|
109
|
+
'serif',
|
|
110
|
+
'monospace',
|
|
111
|
+
'cursive',
|
|
112
|
+
'fantasy',
|
|
113
|
+
'system-ui',
|
|
114
|
+
'ui-sans-serif',
|
|
115
|
+
'ui-serif',
|
|
116
|
+
'ui-monospace',
|
|
117
|
+
'ui-rounded',
|
|
118
|
+
'inherit',
|
|
119
|
+
'initial',
|
|
120
|
+
'unset',
|
|
121
|
+
]);
|
|
122
|
+
// Icon fonts that should never be used as heading/body fonts
|
|
123
|
+
const ICON_FONTS = new Set([
|
|
124
|
+
'dashicons',
|
|
125
|
+
'material icons',
|
|
126
|
+
'material symbols outlined',
|
|
127
|
+
'material symbols rounded',
|
|
128
|
+
'fontawesome',
|
|
129
|
+
'font awesome',
|
|
130
|
+
'fa',
|
|
131
|
+
'glyphicons',
|
|
132
|
+
'icomoon',
|
|
133
|
+
'ionicons',
|
|
134
|
+
'feather',
|
|
135
|
+
'lucide',
|
|
136
|
+
'phosphor',
|
|
137
|
+
'heroicons',
|
|
138
|
+
'bootstrap-icons',
|
|
139
|
+
]);
|
|
140
|
+
function cleanFontName(name) {
|
|
141
|
+
return name
|
|
142
|
+
.replace(/['"]/g, '')
|
|
143
|
+
.replace(/\+/g, ' ')
|
|
144
|
+
.trim();
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Extract typography information from HTML.
|
|
148
|
+
*/
|
|
149
|
+
export function extractTypography(html) {
|
|
150
|
+
const $ = load(html);
|
|
151
|
+
const fontsSet = new Set();
|
|
152
|
+
let headingFont;
|
|
153
|
+
let bodyFont;
|
|
154
|
+
// Check for Google Fonts <link> tags
|
|
155
|
+
$('link[href*="fonts.googleapis.com"]').each((_i, el) => {
|
|
156
|
+
const href = $(el).attr('href') ?? '';
|
|
157
|
+
// Match family=Font+Name patterns (CSS2 and legacy formats)
|
|
158
|
+
const familyMatches = href.matchAll(/family=([^&:;]+)/g);
|
|
159
|
+
for (const m of familyMatches) {
|
|
160
|
+
// CSS2 format may have weight specs after colon
|
|
161
|
+
const familyStr = m[1].split(':')[0];
|
|
162
|
+
const cleaned = cleanFontName(familyStr);
|
|
163
|
+
if (cleaned)
|
|
164
|
+
fontsSet.add(cleaned);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
// Parse font-family from CSS
|
|
168
|
+
const cssBlocks = [];
|
|
169
|
+
function parseCSS(css) {
|
|
170
|
+
// Simple CSS rule parser: selector { body }
|
|
171
|
+
const ruleRegex = /([^{}]+)\{([^}]+)\}/g;
|
|
172
|
+
let match;
|
|
173
|
+
while ((match = ruleRegex.exec(css)) !== null) {
|
|
174
|
+
cssBlocks.push({ selector: match[1].trim(), body: match[2] });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
$('style').each((_i, el) => {
|
|
178
|
+
parseCSS($(el).text());
|
|
179
|
+
});
|
|
180
|
+
for (const block of cssBlocks) {
|
|
181
|
+
const fontFamilyMatch = block.body.match(/font-family\s*:\s*([^;]+)/);
|
|
182
|
+
if (!fontFamilyMatch)
|
|
183
|
+
continue;
|
|
184
|
+
const families = fontFamilyMatch[1].split(',');
|
|
185
|
+
for (const raw of families) {
|
|
186
|
+
const cleaned = cleanFontName(raw);
|
|
187
|
+
if (cleaned && !GENERIC_FONTS.has(cleaned.toLowerCase()) && !ICON_FONTS.has(cleaned.toLowerCase())) {
|
|
188
|
+
fontsSet.add(cleaned);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Heuristic: heading vs body font
|
|
192
|
+
const selector = block.selector.toLowerCase();
|
|
193
|
+
const firstFont = families
|
|
194
|
+
.map((f) => cleanFontName(f))
|
|
195
|
+
.find((f) => f && !GENERIC_FONTS.has(f.toLowerCase()) && !ICON_FONTS.has(f.toLowerCase()));
|
|
196
|
+
if (firstFont) {
|
|
197
|
+
if (/\bh[1-3]\b/.test(selector) && !headingFont) {
|
|
198
|
+
headingFont = firstFont;
|
|
199
|
+
}
|
|
200
|
+
if (/\bbody\b|\bp\b/.test(selector) && !bodyFont) {
|
|
201
|
+
bodyFont = firstFont;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
fonts: Array.from(fontsSet),
|
|
207
|
+
headingFont,
|
|
208
|
+
bodyFont,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
// ─── Layout Detection ──────────────────────────────────────────────
|
|
212
|
+
/**
|
|
213
|
+
* Detect common layout patterns from HTML DOM structure.
|
|
214
|
+
*/
|
|
215
|
+
export function detectLayoutPatterns(html) {
|
|
216
|
+
const $ = load(html);
|
|
217
|
+
const patterns = [];
|
|
218
|
+
// Hero banner: first <section> or large div with background image/gradient + heading
|
|
219
|
+
const firstSection = $('section').first();
|
|
220
|
+
if (firstSection.length) {
|
|
221
|
+
const style = (firstSection.attr('style') ?? '').toLowerCase();
|
|
222
|
+
const cls = (firstSection.attr('class') ?? '').toLowerCase();
|
|
223
|
+
const hasH1 = firstSection.find('h1').length > 0;
|
|
224
|
+
const hasBg = style.includes('background-image') ||
|
|
225
|
+
style.includes('background:') ||
|
|
226
|
+
cls.includes('hero');
|
|
227
|
+
if (hasH1 || hasBg) {
|
|
228
|
+
patterns.push('hero-banner');
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// Sticky/fixed nav
|
|
232
|
+
$('nav, header').each((_i, el) => {
|
|
233
|
+
const style = ($(el).attr('style') ?? '').toLowerCase();
|
|
234
|
+
const cls = ($(el).attr('class') ?? '').toLowerCase();
|
|
235
|
+
if (style.includes('position: sticky') ||
|
|
236
|
+
style.includes('position: fixed') ||
|
|
237
|
+
style.includes('position:sticky') ||
|
|
238
|
+
style.includes('position:fixed') ||
|
|
239
|
+
cls.includes('sticky') ||
|
|
240
|
+
cls.includes('fixed')) {
|
|
241
|
+
if (!patterns.includes('sticky-nav')) {
|
|
242
|
+
patterns.push('sticky-nav');
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
// Card patterns: repeating child elements with image + text
|
|
247
|
+
$('div, section, ul').each((_i, el) => {
|
|
248
|
+
const children = $(el).children();
|
|
249
|
+
if (children.length >= 3) {
|
|
250
|
+
let cardLikeCount = 0;
|
|
251
|
+
children.each((_j, child) => {
|
|
252
|
+
const hasImage = $(child).find('img').length > 0;
|
|
253
|
+
const hasText = $(child).find('p, span, h2, h3, h4').length > 0 ||
|
|
254
|
+
$(child).text().trim().length > 0;
|
|
255
|
+
if (hasImage && hasText)
|
|
256
|
+
cardLikeCount++;
|
|
257
|
+
});
|
|
258
|
+
if (cardLikeCount >= 3 && !patterns.includes('card-grid')) {
|
|
259
|
+
patterns.push('card-grid');
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
// Multi-column grid: 3+ sibling elements
|
|
264
|
+
$('div, section').each((_i, el) => {
|
|
265
|
+
const children = $(el).children();
|
|
266
|
+
const style = ($(el).attr('style') ?? '').toLowerCase();
|
|
267
|
+
const cls = ($(el).attr('class') ?? '').toLowerCase();
|
|
268
|
+
const isGrid = style.includes('grid') ||
|
|
269
|
+
style.includes('flex') ||
|
|
270
|
+
cls.includes('grid') ||
|
|
271
|
+
cls.includes('columns');
|
|
272
|
+
if (children.length >= 3 && isGrid) {
|
|
273
|
+
if (children.length === 3 && !patterns.includes('three-column-grid')) {
|
|
274
|
+
patterns.push('three-column-grid');
|
|
275
|
+
}
|
|
276
|
+
else if (children.length > 3 && !patterns.includes('multi-column-grid')) {
|
|
277
|
+
patterns.push('multi-column-grid');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
// Full-width sections: checking for vw units or full-width classes
|
|
282
|
+
$('section, div').each((_i, el) => {
|
|
283
|
+
const style = ($(el).attr('style') ?? '').toLowerCase();
|
|
284
|
+
const cls = ($(el).attr('class') ?? '').toLowerCase();
|
|
285
|
+
if (style.includes('100vw') ||
|
|
286
|
+
style.includes('width: 100%') ||
|
|
287
|
+
style.includes('width:100%') ||
|
|
288
|
+
cls.includes('full-width') ||
|
|
289
|
+
cls.includes('container-fluid')) {
|
|
290
|
+
if (!patterns.includes('full-width-sections')) {
|
|
291
|
+
patterns.push('full-width-sections');
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
return patterns;
|
|
296
|
+
}
|
|
297
|
+
const INDUSTRY_PROFILES = [
|
|
298
|
+
{
|
|
299
|
+
keywords: /\b(retail|grocery|discount|store|tienda|supermercado|mart)\b/i,
|
|
300
|
+
trustSignals: [
|
|
301
|
+
'Visible pricing on every product',
|
|
302
|
+
'Location finder',
|
|
303
|
+
'Weekly deals prominently displayed',
|
|
304
|
+
'Clean organized layout',
|
|
305
|
+
],
|
|
306
|
+
accessibilityNeeds: [
|
|
307
|
+
'High contrast text',
|
|
308
|
+
'Large tap targets for mobile',
|
|
309
|
+
'Simple navigation',
|
|
310
|
+
],
|
|
311
|
+
expectations: [
|
|
312
|
+
'Find nearest store',
|
|
313
|
+
'See current deals',
|
|
314
|
+
'Browse product categories',
|
|
315
|
+
'Check prices quickly',
|
|
316
|
+
],
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
keywords: /\b(fintech|banking|payments?|financial|finance|bank)\b/i,
|
|
320
|
+
trustSignals: [
|
|
321
|
+
'Security certifications visible',
|
|
322
|
+
'Regulatory compliance badges',
|
|
323
|
+
'Data encryption indicators',
|
|
324
|
+
],
|
|
325
|
+
accessibilityNeeds: [
|
|
326
|
+
'Screen reader support for financial data',
|
|
327
|
+
'Color-blind safe charts',
|
|
328
|
+
],
|
|
329
|
+
expectations: [
|
|
330
|
+
'Clear pricing/fee structure',
|
|
331
|
+
'API documentation',
|
|
332
|
+
'Dashboard preview',
|
|
333
|
+
'Customer support access',
|
|
334
|
+
],
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
keywords: /\b(saas|software|technology|tech|platform|app|cloud)\b/i,
|
|
338
|
+
trustSignals: [
|
|
339
|
+
'Customer logos',
|
|
340
|
+
'Usage metrics',
|
|
341
|
+
'SOC2/ISO badges',
|
|
342
|
+
],
|
|
343
|
+
accessibilityNeeds: [
|
|
344
|
+
'Keyboard navigation',
|
|
345
|
+
'Responsive design',
|
|
346
|
+
],
|
|
347
|
+
expectations: [
|
|
348
|
+
'Product demo/screenshots',
|
|
349
|
+
'Pricing tiers',
|
|
350
|
+
'Integration list',
|
|
351
|
+
'Documentation link',
|
|
352
|
+
],
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
keywords: /\b(healthcare|health|wellness|medical|clinic|hospital)\b/i,
|
|
356
|
+
trustSignals: [
|
|
357
|
+
'Provider credentials',
|
|
358
|
+
'Patient testimonials',
|
|
359
|
+
'HIPAA compliance',
|
|
360
|
+
],
|
|
361
|
+
accessibilityNeeds: [
|
|
362
|
+
'WCAG AA compliance',
|
|
363
|
+
'Large readable text',
|
|
364
|
+
'Clear form labels',
|
|
365
|
+
],
|
|
366
|
+
expectations: [
|
|
367
|
+
'Appointment booking',
|
|
368
|
+
'Service listings',
|
|
369
|
+
'Insurance information',
|
|
370
|
+
],
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
keywords: /\b(education|school|university|learning|academy|course)\b/i,
|
|
374
|
+
trustSignals: [
|
|
375
|
+
'Accreditation badges',
|
|
376
|
+
'Student outcomes',
|
|
377
|
+
'Faculty credentials',
|
|
378
|
+
],
|
|
379
|
+
accessibilityNeeds: [
|
|
380
|
+
'Screen reader compatible',
|
|
381
|
+
'Captioned media',
|
|
382
|
+
'Dyslexia-friendly fonts',
|
|
383
|
+
],
|
|
384
|
+
expectations: [
|
|
385
|
+
'Course catalog',
|
|
386
|
+
'Enrollment process',
|
|
387
|
+
'Student portal access',
|
|
388
|
+
],
|
|
389
|
+
},
|
|
390
|
+
];
|
|
391
|
+
const LOCALE_PROFILES = [
|
|
392
|
+
{
|
|
393
|
+
keywords: /\b(es-mx|mexican|mexico|m[eé]xico)\b/i,
|
|
394
|
+
considerations: [
|
|
395
|
+
'Use Spanish copy naturally, not translated',
|
|
396
|
+
'Reflect Mexican family values',
|
|
397
|
+
'Avoid US-centric imagery',
|
|
398
|
+
'Consider regional color meanings',
|
|
399
|
+
],
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
keywords: /\b(es-|latin|latino|latina|latinx|hispano|hispanic)\b/i,
|
|
403
|
+
considerations: [
|
|
404
|
+
'Warm, family-oriented imagery',
|
|
405
|
+
'Community-focused messaging',
|
|
406
|
+
],
|
|
407
|
+
},
|
|
408
|
+
];
|
|
409
|
+
/**
|
|
410
|
+
* Infer audience insights from industry and audience descriptions.
|
|
411
|
+
* Knowledge-base lookup, not web search.
|
|
412
|
+
*/
|
|
413
|
+
export function inferAudienceInsights(industry, audience, locale) {
|
|
414
|
+
const combined = `${industry} ${audience}`;
|
|
415
|
+
const trustSignals = [];
|
|
416
|
+
const accessibilityNeeds = [];
|
|
417
|
+
const expectations = [];
|
|
418
|
+
const culturalConsiderations = [];
|
|
419
|
+
// Match industry profiles
|
|
420
|
+
for (const profile of INDUSTRY_PROFILES) {
|
|
421
|
+
if (profile.keywords.test(combined)) {
|
|
422
|
+
for (const s of profile.trustSignals) {
|
|
423
|
+
if (!trustSignals.includes(s))
|
|
424
|
+
trustSignals.push(s);
|
|
425
|
+
}
|
|
426
|
+
for (const a of profile.accessibilityNeeds) {
|
|
427
|
+
if (!accessibilityNeeds.includes(a))
|
|
428
|
+
accessibilityNeeds.push(a);
|
|
429
|
+
}
|
|
430
|
+
for (const e of profile.expectations) {
|
|
431
|
+
if (!expectations.includes(e))
|
|
432
|
+
expectations.push(e);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
// Fallback if no industry matched
|
|
437
|
+
if (trustSignals.length === 0) {
|
|
438
|
+
trustSignals.push('Clear contact information', 'Professional design');
|
|
439
|
+
accessibilityNeeds.push('Responsive design', 'Readable font sizes');
|
|
440
|
+
expectations.push('Easy navigation', 'Clear value proposition');
|
|
441
|
+
}
|
|
442
|
+
// Locale/cultural considerations
|
|
443
|
+
const localeStr = `${locale ?? ''} ${audience}`;
|
|
444
|
+
for (const lp of LOCALE_PROFILES) {
|
|
445
|
+
if (lp.keywords.test(localeStr)) {
|
|
446
|
+
for (const c of lp.considerations) {
|
|
447
|
+
if (!culturalConsiderations.includes(c))
|
|
448
|
+
culturalConsiderations.push(c);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return {
|
|
453
|
+
trustSignals,
|
|
454
|
+
accessibilityNeeds,
|
|
455
|
+
culturalConsiderations,
|
|
456
|
+
expectations,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
// ─── Market Position ───────────────────────────────────────────────
|
|
460
|
+
/**
|
|
461
|
+
* Infer market position from the business brief text.
|
|
462
|
+
*/
|
|
463
|
+
export function inferMarketPosition(brief, _siteAnalysis) {
|
|
464
|
+
const text = `${brief.industry} ${brief.targetAudience} ${brief.aesthetic} ${brief.companyName}`.toLowerCase();
|
|
465
|
+
// Price point
|
|
466
|
+
let pricePoint = 'mid-range';
|
|
467
|
+
if (/\b(luxury|haute|bespoke)\b/.test(text)) {
|
|
468
|
+
pricePoint = 'luxury';
|
|
469
|
+
}
|
|
470
|
+
else if (/\b(premium|enterprise|exclusive)\b/.test(text)) {
|
|
471
|
+
pricePoint = 'premium';
|
|
472
|
+
}
|
|
473
|
+
else if (/\b(discount|cheap|ahorro|barato|value|affordable|budget)\b/.test(text)) {
|
|
474
|
+
pricePoint = 'budget';
|
|
475
|
+
}
|
|
476
|
+
// Reach
|
|
477
|
+
let reach = 'national';
|
|
478
|
+
if (/\b(neighborhood|barrio|local|community)\b/.test(text)) {
|
|
479
|
+
reach = 'local';
|
|
480
|
+
}
|
|
481
|
+
else if (/\b(state|regional)\b/.test(text)) {
|
|
482
|
+
reach = 'regional';
|
|
483
|
+
}
|
|
484
|
+
else if (/\b(global|international|worldwide)\b/.test(text)) {
|
|
485
|
+
reach = 'global';
|
|
486
|
+
}
|
|
487
|
+
else if (/\b(national|nationwide|country-wide|pa[ií]s)\b/.test(text)) {
|
|
488
|
+
reach = 'national';
|
|
489
|
+
}
|
|
490
|
+
// Personality
|
|
491
|
+
let personality = 'modern';
|
|
492
|
+
if (/\b(disruptive|revolutionary)\b/.test(text)) {
|
|
493
|
+
personality = 'disruptive';
|
|
494
|
+
}
|
|
495
|
+
else if (/\b(innovative|cutting-edge|ai|tech-forward)\b/.test(text)) {
|
|
496
|
+
personality = 'innovative';
|
|
497
|
+
}
|
|
498
|
+
else if (/\b(traditional|heritage|classic|established)\b/.test(text)) {
|
|
499
|
+
personality = 'traditional';
|
|
500
|
+
}
|
|
501
|
+
else if (/\b(modern|contemporary)\b/.test(text)) {
|
|
502
|
+
personality = 'modern';
|
|
503
|
+
}
|
|
504
|
+
return { pricePoint, reach, personality };
|
|
505
|
+
}
|
|
506
|
+
// ─── Business Model Inference ─────────────────────────────────────
|
|
507
|
+
// Signal patterns for detecting business model from site content
|
|
508
|
+
const PHYSICAL_RETAIL_SIGNALS = /\b(sucursal|tienda|ubicaci|locali|horario|store|location|find.?us|visit|nearest)\b/i;
|
|
509
|
+
const ECOMMERCE_SIGNALS = /\b(carrito|cart|checkout|comprar|buy|shop|add.?to.?cart|wishlist|order)\b/i;
|
|
510
|
+
const SAAS_SIGNALS = /\b(login|sign.?up|dashboard|api|pricing|plan|trial|demo|docs|documentation)\b/i;
|
|
511
|
+
const SERVICE_SIGNALS = /\b(contact|agendar|book|cita|appointment|schedule|consult|quote)\b/i;
|
|
512
|
+
/**
|
|
513
|
+
* Infer what kind of business this is and what the website should (and should NOT) do.
|
|
514
|
+
*
|
|
515
|
+
* Uses a combination of site navigation signals, CTA text, layout patterns, and
|
|
516
|
+
* industry keywords from the brief to determine the business model. This prevents
|
|
517
|
+
* generating e-commerce pages for physical-only retailers, or store locators for SaaS products.
|
|
518
|
+
*/
|
|
519
|
+
export function inferBusinessModel(brief, siteAnalysis) {
|
|
520
|
+
const hasSite = siteAnalysis != null;
|
|
521
|
+
// Combine all text signals from the site
|
|
522
|
+
const navText = hasSite ? siteAnalysis.navItems.join(' ') : '';
|
|
523
|
+
const ctaText = hasSite ? siteAnalysis.ctaTexts.join(' ') : '';
|
|
524
|
+
const allSiteText = `${navText} ${ctaText}`;
|
|
525
|
+
// Step 1: Detect signals from site content
|
|
526
|
+
const hasPhysicalRetailSignals = PHYSICAL_RETAIL_SIGNALS.test(allSiteText);
|
|
527
|
+
const hasEcommerceSignals = ECOMMERCE_SIGNALS.test(allSiteText);
|
|
528
|
+
const hasSaasSignals = SAAS_SIGNALS.test(allSiteText);
|
|
529
|
+
const hasServiceSignals = SERVICE_SIGNALS.test(allSiteText);
|
|
530
|
+
// Check layout patterns for additional hints
|
|
531
|
+
const hasCardGridWithoutEcommerce = hasSite &&
|
|
532
|
+
siteAnalysis.layoutPatterns.includes('card-grid') &&
|
|
533
|
+
!hasEcommerceSignals;
|
|
534
|
+
// Step 2: Detect from industry keywords in brief
|
|
535
|
+
const industry = brief.industry.toLowerCase();
|
|
536
|
+
const audience = brief.targetAudience.toLowerCase();
|
|
537
|
+
const briefText = `${industry} ${audience}`;
|
|
538
|
+
let type = 'other';
|
|
539
|
+
// Site signals take priority when available
|
|
540
|
+
if (hasSite) {
|
|
541
|
+
if (hasEcommerceSignals && !hasPhysicalRetailSignals) {
|
|
542
|
+
type = 'e-commerce';
|
|
543
|
+
}
|
|
544
|
+
else if (hasPhysicalRetailSignals && !hasEcommerceSignals) {
|
|
545
|
+
type = 'physical-retail';
|
|
546
|
+
}
|
|
547
|
+
else if (hasSaasSignals && !hasPhysicalRetailSignals && !hasEcommerceSignals) {
|
|
548
|
+
type = 'saas';
|
|
549
|
+
}
|
|
550
|
+
else if (hasServiceSignals && !hasEcommerceSignals && !hasSaasSignals) {
|
|
551
|
+
type = 'service';
|
|
552
|
+
}
|
|
553
|
+
else if (hasCardGridWithoutEcommerce && /\b(retail|grocery|tienda|store|supermercado|despensa|mercado)\b/.test(briefText)) {
|
|
554
|
+
// Card grid without e-commerce signals + retail industry = likely physical retail catalog
|
|
555
|
+
type = 'physical-retail';
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
// Fall back to industry keywords if site didn't determine the type
|
|
559
|
+
if (type === 'other') {
|
|
560
|
+
if (/\b(tienda|store|retail|grocery|supermercado|despensa|discount|mercado)\b/.test(briefText)) {
|
|
561
|
+
// Check if e-commerce signals contradict physical retail
|
|
562
|
+
if (hasEcommerceSignals) {
|
|
563
|
+
type = 'e-commerce';
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
type = 'physical-retail';
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
else if (/\b(saas|software|platform|app\b|cloud)\b/.test(briefText)) {
|
|
570
|
+
type = 'saas';
|
|
571
|
+
}
|
|
572
|
+
else if (/\b(marketplace|market.?place)\b/.test(briefText)) {
|
|
573
|
+
type = 'marketplace';
|
|
574
|
+
}
|
|
575
|
+
else if (/\b(agency|consulting|lawyer|doctor|dentist|clinic|salon|service|freelance)\b/.test(briefText)) {
|
|
576
|
+
type = 'service';
|
|
577
|
+
}
|
|
578
|
+
else if (/\b(blog|news|media|magazine|publication|journal)\b/.test(briefText)) {
|
|
579
|
+
type = 'media';
|
|
580
|
+
}
|
|
581
|
+
else if (/\b(nonprofit|non-profit|ngo|charity|foundation)\b/.test(briefText)) {
|
|
582
|
+
type = 'nonprofit';
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
// Step 3: Build the full context based on detected type
|
|
586
|
+
const result = buildModelContext(type, hasSite);
|
|
587
|
+
// Step 4: Extract differentiators from site content
|
|
588
|
+
if (hasSite) {
|
|
589
|
+
const allItems = [...siteAnalysis.navItems, ...siteAnalysis.ctaTexts];
|
|
590
|
+
for (const item of allItems) {
|
|
591
|
+
// Look for text containing numbers (e.g., "3,300+ tiendas", "50 estados")
|
|
592
|
+
if (/\d[\d,]*\+?\s*\w+/.test(item) && item.length < 80) {
|
|
593
|
+
result.differentiators.push(item.trim());
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
return result;
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Build a complete BusinessModelContext for a given business type.
|
|
601
|
+
*/
|
|
602
|
+
function buildModelContext(type, hasSiteData) {
|
|
603
|
+
switch (type) {
|
|
604
|
+
case 'physical-retail':
|
|
605
|
+
return {
|
|
606
|
+
type: 'physical-retail',
|
|
607
|
+
primaryRevenue: 'Physical store sales',
|
|
608
|
+
websitePurpose: 'Drive foot traffic to nearest store location. Inform about deals and product availability.',
|
|
609
|
+
primaryUserGoals: [
|
|
610
|
+
'Find the nearest store location',
|
|
611
|
+
'Check current deals and prices',
|
|
612
|
+
'Browse available product categories',
|
|
613
|
+
'Get store hours and contact information',
|
|
614
|
+
],
|
|
615
|
+
keyFeatures: ['Store locator', 'Weekly deals/flyer', 'Product category browse', 'Store hours', 'Brand information'],
|
|
616
|
+
notFeatures: ['Shopping cart', 'Online checkout', 'Add to cart buttons', 'User accounts', 'Wishlist', 'Online payment'],
|
|
617
|
+
differentiators: [],
|
|
618
|
+
confidence: hasSiteData ? 80 : 50,
|
|
619
|
+
};
|
|
620
|
+
case 'e-commerce':
|
|
621
|
+
return {
|
|
622
|
+
type: 'e-commerce',
|
|
623
|
+
primaryRevenue: 'Online product sales',
|
|
624
|
+
websitePurpose: 'Sell products directly to customers online.',
|
|
625
|
+
primaryUserGoals: [
|
|
626
|
+
'Browse and search products',
|
|
627
|
+
'Compare prices and options',
|
|
628
|
+
'Add items to cart and checkout',
|
|
629
|
+
'Track orders and manage account',
|
|
630
|
+
],
|
|
631
|
+
keyFeatures: ['Product catalog', 'Shopping cart', 'Checkout', 'User accounts', 'Order tracking', 'Search'],
|
|
632
|
+
notFeatures: [],
|
|
633
|
+
differentiators: [],
|
|
634
|
+
confidence: hasSiteData ? 80 : 50,
|
|
635
|
+
};
|
|
636
|
+
case 'saas':
|
|
637
|
+
return {
|
|
638
|
+
type: 'saas',
|
|
639
|
+
primaryRevenue: 'Software subscriptions',
|
|
640
|
+
websitePurpose: 'Convert visitors into trial/paid users.',
|
|
641
|
+
primaryUserGoals: [
|
|
642
|
+
'Understand what the product does',
|
|
643
|
+
'See pricing and compare plans',
|
|
644
|
+
'Start a free trial or demo',
|
|
645
|
+
'Access documentation',
|
|
646
|
+
],
|
|
647
|
+
keyFeatures: ['Product demo/screenshots', 'Pricing tiers', 'Free trial CTA', 'Documentation', 'Customer testimonials'],
|
|
648
|
+
notFeatures: ['Physical store locator', 'Inventory browsing'],
|
|
649
|
+
differentiators: [],
|
|
650
|
+
confidence: hasSiteData ? 80 : 50,
|
|
651
|
+
};
|
|
652
|
+
case 'marketplace':
|
|
653
|
+
return {
|
|
654
|
+
type: 'marketplace',
|
|
655
|
+
primaryRevenue: 'Transaction fees or commissions',
|
|
656
|
+
websitePurpose: 'Connect buyers and sellers on a shared platform.',
|
|
657
|
+
primaryUserGoals: [
|
|
658
|
+
'Browse listings from multiple sellers',
|
|
659
|
+
'Compare options and prices',
|
|
660
|
+
'Complete secure transactions',
|
|
661
|
+
'Manage buyer/seller profile',
|
|
662
|
+
],
|
|
663
|
+
keyFeatures: ['Search and filtering', 'Seller profiles', 'Reviews/ratings', 'Secure checkout', 'Messaging'],
|
|
664
|
+
notFeatures: ['Single-brand store locator'],
|
|
665
|
+
differentiators: [],
|
|
666
|
+
confidence: hasSiteData ? 75 : 45,
|
|
667
|
+
};
|
|
668
|
+
case 'service':
|
|
669
|
+
return {
|
|
670
|
+
type: 'service',
|
|
671
|
+
primaryRevenue: 'Service fees',
|
|
672
|
+
websitePurpose: 'Generate leads and bookings.',
|
|
673
|
+
primaryUserGoals: [
|
|
674
|
+
'Understand available services',
|
|
675
|
+
'Book an appointment or consultation',
|
|
676
|
+
'Contact the business',
|
|
677
|
+
'Read reviews and credentials',
|
|
678
|
+
],
|
|
679
|
+
keyFeatures: ['Service catalog', 'Booking form', 'Contact information', 'Testimonials', 'Team/credentials'],
|
|
680
|
+
notFeatures: ['Shopping cart', 'Product inventory'],
|
|
681
|
+
differentiators: [],
|
|
682
|
+
confidence: hasSiteData ? 70 : 40,
|
|
683
|
+
};
|
|
684
|
+
case 'media':
|
|
685
|
+
return {
|
|
686
|
+
type: 'media',
|
|
687
|
+
primaryRevenue: 'Advertising or subscriptions',
|
|
688
|
+
websitePurpose: 'Publish and distribute content to readers.',
|
|
689
|
+
primaryUserGoals: [
|
|
690
|
+
'Read articles and news',
|
|
691
|
+
'Browse by category or topic',
|
|
692
|
+
'Subscribe for updates',
|
|
693
|
+
'Share content',
|
|
694
|
+
],
|
|
695
|
+
keyFeatures: ['Article listings', 'Category navigation', 'Search', 'Newsletter signup', 'Social sharing'],
|
|
696
|
+
notFeatures: ['Shopping cart', 'Store locator'],
|
|
697
|
+
differentiators: [],
|
|
698
|
+
confidence: hasSiteData ? 70 : 40,
|
|
699
|
+
};
|
|
700
|
+
case 'nonprofit':
|
|
701
|
+
return {
|
|
702
|
+
type: 'nonprofit',
|
|
703
|
+
primaryRevenue: 'Donations and grants',
|
|
704
|
+
websitePurpose: 'Inspire action and facilitate donations.',
|
|
705
|
+
primaryUserGoals: [
|
|
706
|
+
'Understand the mission',
|
|
707
|
+
'Make a donation',
|
|
708
|
+
'Find volunteer opportunities',
|
|
709
|
+
'Learn about impact',
|
|
710
|
+
],
|
|
711
|
+
keyFeatures: ['Mission statement', 'Donate button', 'Impact metrics', 'Volunteer signup', 'Event calendar'],
|
|
712
|
+
notFeatures: ['Shopping cart', 'Product catalog'],
|
|
713
|
+
differentiators: [],
|
|
714
|
+
confidence: hasSiteData ? 70 : 40,
|
|
715
|
+
};
|
|
716
|
+
case 'other':
|
|
717
|
+
default:
|
|
718
|
+
return {
|
|
719
|
+
type: 'other',
|
|
720
|
+
primaryRevenue: 'Unknown',
|
|
721
|
+
websitePurpose: 'Inform visitors about the organization.',
|
|
722
|
+
primaryUserGoals: ['Learn about the organization', 'Find contact information'],
|
|
723
|
+
keyFeatures: ['About page', 'Contact form'],
|
|
724
|
+
notFeatures: [],
|
|
725
|
+
differentiators: [],
|
|
726
|
+
confidence: 20,
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
// ─── Site Analysis ─────────────────────────────────────────────────
|
|
731
|
+
/**
|
|
732
|
+
* Detect the content tone of a page.
|
|
733
|
+
*/
|
|
734
|
+
function detectContentTone(text) {
|
|
735
|
+
const lower = text.toLowerCase();
|
|
736
|
+
const formalWords = ['enterprise', 'solutions', 'leverage', 'optimize', 'facilitate', 'utilize'];
|
|
737
|
+
const casualWords = ['hey', 'awesome', 'check out', 'cool', 'love', "let's"];
|
|
738
|
+
const technicalWords = ['api', 'sdk', 'integration', 'deploy', 'infrastructure', 'latency'];
|
|
739
|
+
const warmWords = ['family', 'together', 'community', 'home', 'welcome', 'care', 'heart'];
|
|
740
|
+
const scores = {
|
|
741
|
+
formal: formalWords.filter((w) => lower.includes(w)).length,
|
|
742
|
+
casual: casualWords.filter((w) => lower.includes(w)).length,
|
|
743
|
+
technical: technicalWords.filter((w) => lower.includes(w)).length,
|
|
744
|
+
warm: warmWords.filter((w) => lower.includes(w)).length,
|
|
745
|
+
};
|
|
746
|
+
const max = Math.max(scores.formal, scores.casual, scores.technical, scores.warm);
|
|
747
|
+
if (max === 0)
|
|
748
|
+
return 'neutral';
|
|
749
|
+
if (scores.formal === max)
|
|
750
|
+
return 'formal';
|
|
751
|
+
if (scores.casual === max)
|
|
752
|
+
return 'casual';
|
|
753
|
+
if (scores.technical === max)
|
|
754
|
+
return 'technical';
|
|
755
|
+
if (scores.warm === max)
|
|
756
|
+
return 'warm';
|
|
757
|
+
return 'neutral';
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Analyze a website by fetching it and extracting design signals.
|
|
761
|
+
* Returns null on fetch failure (graceful degradation).
|
|
762
|
+
*/
|
|
763
|
+
export async function analyzeSite(url) {
|
|
764
|
+
try {
|
|
765
|
+
const response = await fetch(url, {
|
|
766
|
+
headers: { 'User-Agent': 'StitchForge/0.3.0' },
|
|
767
|
+
signal: AbortSignal.timeout(15000),
|
|
768
|
+
});
|
|
769
|
+
if (!response.ok)
|
|
770
|
+
return null;
|
|
771
|
+
const html = await response.text();
|
|
772
|
+
const $ = load(html);
|
|
773
|
+
const palette = extractPalette(html);
|
|
774
|
+
const typography = extractTypography(html);
|
|
775
|
+
const layoutPatterns = detectLayoutPatterns(html);
|
|
776
|
+
// Content tone from body text
|
|
777
|
+
const bodyText = $('body').text().replace(/\s+/g, ' ').trim();
|
|
778
|
+
const contentTone = detectContentTone(bodyText);
|
|
779
|
+
// Extract nav items
|
|
780
|
+
const navItems = [];
|
|
781
|
+
$('nav a, header a').each((_i, el) => {
|
|
782
|
+
const text = $(el).text().trim();
|
|
783
|
+
if (text && text.length < 50 && !navItems.includes(text)) {
|
|
784
|
+
navItems.push(text);
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
// Extract CTA texts from buttons and prominent links
|
|
788
|
+
const ctaTexts = [];
|
|
789
|
+
$('button, a.btn, [role="button"], .cta, [class*="cta"], [class*="button"]').each((_i, el) => {
|
|
790
|
+
const text = $(el).text().trim();
|
|
791
|
+
if (text && text.length < 50 && !ctaTexts.includes(text)) {
|
|
792
|
+
ctaTexts.push(text);
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
return {
|
|
796
|
+
url,
|
|
797
|
+
palette,
|
|
798
|
+
typography,
|
|
799
|
+
layoutPatterns,
|
|
800
|
+
contentTone,
|
|
801
|
+
navItems,
|
|
802
|
+
ctaTexts,
|
|
803
|
+
fetchedAt: new Date().toISOString(),
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
catch {
|
|
807
|
+
return null;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
// ─── Orchestrator ──────────────────────────────────────────────────
|
|
811
|
+
/**
|
|
812
|
+
* Perform complete business research for a brief.
|
|
813
|
+
* Orchestrates site analysis, competitor analysis, audience insights,
|
|
814
|
+
* market positioning, and business model inference.
|
|
815
|
+
*/
|
|
816
|
+
export async function researchBusiness(brief) {
|
|
817
|
+
const fallbacksUsed = [];
|
|
818
|
+
let confidence = 10; // baseline
|
|
819
|
+
// Analyze current site
|
|
820
|
+
let currentSite;
|
|
821
|
+
if (brief.websiteUrl) {
|
|
822
|
+
const analysis = await analyzeSite(brief.websiteUrl);
|
|
823
|
+
if (analysis) {
|
|
824
|
+
currentSite = analysis;
|
|
825
|
+
confidence += 30;
|
|
826
|
+
}
|
|
827
|
+
else {
|
|
828
|
+
fallbacksUsed.push('current_site_unavailable');
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
else {
|
|
832
|
+
fallbacksUsed.push('current_site_unavailable');
|
|
833
|
+
}
|
|
834
|
+
// Analyze competitors
|
|
835
|
+
const competitors = [];
|
|
836
|
+
if (brief.competitorUrls && brief.competitorUrls.length > 0) {
|
|
837
|
+
for (const compUrl of brief.competitorUrls) {
|
|
838
|
+
const analysis = await analyzeSite(compUrl);
|
|
839
|
+
if (analysis) {
|
|
840
|
+
// Derive a name from the URL
|
|
841
|
+
let name;
|
|
842
|
+
try {
|
|
843
|
+
name = new URL(compUrl).hostname.replace('www.', '').split('.')[0];
|
|
844
|
+
}
|
|
845
|
+
catch {
|
|
846
|
+
name = compUrl;
|
|
847
|
+
}
|
|
848
|
+
competitors.push({
|
|
849
|
+
url: compUrl,
|
|
850
|
+
name,
|
|
851
|
+
palette: analysis.palette,
|
|
852
|
+
typography: analysis.typography,
|
|
853
|
+
strengths: [],
|
|
854
|
+
commonPatterns: analysis.layoutPatterns,
|
|
855
|
+
fetchedAt: analysis.fetchedAt,
|
|
856
|
+
});
|
|
857
|
+
confidence += 20;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
if (competitors.length === 0) {
|
|
861
|
+
fallbacksUsed.push('competitors_unavailable');
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
else {
|
|
865
|
+
fallbacksUsed.push('competitors_unavailable');
|
|
866
|
+
}
|
|
867
|
+
// Audience insights (always available)
|
|
868
|
+
const audienceInsights = inferAudienceInsights(brief.industry, brief.targetAudience, brief.locale);
|
|
869
|
+
confidence += 20;
|
|
870
|
+
// Market position
|
|
871
|
+
const marketPosition = inferMarketPosition(brief, currentSite);
|
|
872
|
+
// Business model inference
|
|
873
|
+
const businessModel = inferBusinessModel(brief, currentSite);
|
|
874
|
+
// Cap confidence at 100
|
|
875
|
+
confidence = Math.min(confidence, 100);
|
|
876
|
+
return {
|
|
877
|
+
brief,
|
|
878
|
+
businessModel,
|
|
879
|
+
currentSite,
|
|
880
|
+
competitors,
|
|
881
|
+
audienceInsights,
|
|
882
|
+
marketPosition,
|
|
883
|
+
researchedAt: new Date().toISOString(),
|
|
884
|
+
confidence,
|
|
885
|
+
fallbacksUsed,
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
//# sourceMappingURL=business-researcher.js.map
|