Setting provider name auto
This commit is contained in:
parent
6448ad1e32
commit
232d734c98
|
|
@ -1,5 +1,6 @@
|
||||||
import { NavLink, useLocation, useNavigate } from 'react-router-dom';
|
import { NavLink, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { useBusiness } from '../context/BusinessContext';
|
import { useBusiness } from '../context/BusinessContext';
|
||||||
|
import { getBusinessImage } from '../utils/businessProfile';
|
||||||
|
|
||||||
const SVG_ICONS = {
|
const SVG_ICONS = {
|
||||||
analytics: (
|
analytics: (
|
||||||
|
|
@ -64,26 +65,6 @@ function StageMarker({ done, active, enabled }) {
|
||||||
return <span className="inline-block h-3 w-3 rounded-full bg-refresh-active" />;
|
return <span className="inline-block h-3 w-3 rounded-full bg-refresh-active" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeText(value) {
|
|
||||||
return typeof value === 'string' ? value.trim() : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSidebarBusinessImage(business) {
|
|
||||||
const brandingLogos = business?.scrapeArtifacts?.json?.branding?.logos;
|
|
||||||
const primaryLogo = Array.isArray(brandingLogos)
|
|
||||||
? brandingLogos.find((entry) => normalizeText(entry))
|
|
||||||
: '';
|
|
||||||
if (primaryLogo) return primaryLogo;
|
|
||||||
|
|
||||||
return (
|
|
||||||
normalizeText(business?.logoUrl)
|
|
||||||
|| normalizeText(business?.imageUrl)
|
|
||||||
|| (Array.isArray(business?.relevantImagePaths)
|
|
||||||
? business.relevantImagePaths.find((entry) => normalizeText(entry)) || ''
|
|
||||||
: '')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Sidebar({ onOpenReview, reviewLoading = false, reviewError = '' }) {
|
export default function Sidebar({ onOpenReview, reviewLoading = false, reviewError = '' }) {
|
||||||
const {
|
const {
|
||||||
activeBusiness,
|
activeBusiness,
|
||||||
|
|
@ -95,7 +76,7 @@ export default function Sidebar({ onOpenReview, reviewLoading = false, reviewErr
|
||||||
} = useBusiness();
|
} = useBusiness();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const businessImage = getSidebarBusinessImage(activeBusiness);
|
const businessImage = getBusinessImage(activeBusiness);
|
||||||
|
|
||||||
const analyticsPath = `/${activeBusinessId}/analytics`;
|
const analyticsPath = `/${activeBusinessId}/analytics`;
|
||||||
const globalSmsPath = `/${activeBusinessId}/global-sms`;
|
const globalSmsPath = `/${activeBusinessId}/global-sms`;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import apiClient from '../api/client';
|
||||||
import { useBusiness } from '../context/BusinessContext';
|
import { useBusiness } from '../context/BusinessContext';
|
||||||
|
|
||||||
const BASE_PROFILE_KEYS = new Set(['providerName', 'senderId', 'dltEntityId']);
|
const BASE_PROFILE_KEYS = new Set(['providerName', 'senderId', 'dltEntityId']);
|
||||||
|
const PENDING_SENDER_ID_PROFILE_NAME = 'Pending Sender ID';
|
||||||
|
|
||||||
function formatUpdatedAt(value) {
|
function formatUpdatedAt(value) {
|
||||||
if (!value) return 'Not updated yet';
|
if (!value) return 'Not updated yet';
|
||||||
|
|
@ -57,6 +58,12 @@ function getProfileSummary(profile) {
|
||||||
return parts.join(' • ') || 'Profile saved. Complete the remaining setup fields to continue.';
|
return parts.join(' • ') || 'Profile saved. Complete the remaining setup fields to continue.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isPendingSenderIdProfile(profile) {
|
||||||
|
const normalizedName = String(profile?.name || '').trim();
|
||||||
|
const senderId = String(profile?.provider?.senderId || '').trim();
|
||||||
|
return (profile?.isAutoNamed === true && !senderId) || normalizedName === PENDING_SENDER_ID_PROFILE_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
function DeleteProfileModal({ preview, deleting, onCancel, onConfirm }) {
|
function DeleteProfileModal({ preview, deleting, onCancel, onConfirm }) {
|
||||||
if (!preview) return null;
|
if (!preview) return null;
|
||||||
|
|
||||||
|
|
@ -133,7 +140,6 @@ export default function GlobalSms() {
|
||||||
const [savingInputs, setSavingInputs] = useState(false);
|
const [savingInputs, setSavingInputs] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [success, setSuccess] = useState('');
|
const [success, setSuccess] = useState('');
|
||||||
const [formName, setFormName] = useState('');
|
|
||||||
const [formCurl, setFormCurl] = useState('');
|
const [formCurl, setFormCurl] = useState('');
|
||||||
const [formSetActive, setFormSetActive] = useState(true);
|
const [formSetActive, setFormSetActive] = useState(true);
|
||||||
const [inputForm, setInputForm] = useState({});
|
const [inputForm, setInputForm] = useState({});
|
||||||
|
|
@ -146,10 +152,12 @@ export default function GlobalSms() {
|
||||||
() => profiles.find((profile) => profile.id === activeProfileId) || null,
|
() => profiles.find((profile) => profile.id === activeProfileId) || null,
|
||||||
[profiles, activeProfileId],
|
[profiles, activeProfileId],
|
||||||
);
|
);
|
||||||
const missingInputs = activeProfile?.executionReadiness?.missingProfileInputs || [];
|
const missingInputs = useMemo(
|
||||||
|
() => activeProfile?.executionReadiness?.missingProfileInputs || [],
|
||||||
|
[activeProfile?.executionReadiness?.missingProfileInputs],
|
||||||
|
);
|
||||||
const hasProfiles = profiles.length > 0;
|
const hasProfiles = profiles.length > 0;
|
||||||
const eventsPath = `/${businessId}/events`;
|
const eventsPath = `/${businessId}/events`;
|
||||||
const analyticsPath = `/${businessId}/analytics`;
|
|
||||||
|
|
||||||
const loadProfiles = useCallback(async () => {
|
const loadProfiles = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -198,7 +206,7 @@ export default function GlobalSms() {
|
||||||
|
|
||||||
async function handleSubmit(event) {
|
async function handleSubmit(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!formName.trim() || !formCurl.trim()) return;
|
if (!formCurl.trim()) return;
|
||||||
|
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
setError('');
|
setError('');
|
||||||
|
|
@ -207,12 +215,10 @@ export default function GlobalSms() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await apiClient.post(`/api/businesses/${businessId}/global-sms/profiles`, {
|
await apiClient.post(`/api/businesses/${businessId}/global-sms/profiles`, {
|
||||||
name: formName.trim(),
|
|
||||||
rawCurl: formCurl.trim(),
|
rawCurl: formCurl.trim(),
|
||||||
setActive: formSetActive,
|
setActive: formSetActive,
|
||||||
});
|
});
|
||||||
|
|
||||||
setFormName('');
|
|
||||||
setFormCurl('');
|
setFormCurl('');
|
||||||
setFormSetActive(true);
|
setFormSetActive(true);
|
||||||
setSuccess('Profile created successfully.');
|
setSuccess('Profile created successfully.');
|
||||||
|
|
@ -387,7 +393,12 @@ export default function GlobalSms() {
|
||||||
{activeProfile ? (
|
{activeProfile ? (
|
||||||
<div className={`rounded-lg border p-5 ${activeProfile.executionReadiness?.isSetupComplete ? 'border-primary-blue bg-white' : 'border-gray-200 bg-white'}`}>
|
<div className={`rounded-lg border p-5 ${activeProfile.executionReadiness?.isSetupComplete ? 'border-primary-blue bg-white' : 'border-gray-200 bg-white'}`}>
|
||||||
<div className="mb-4 flex items-center gap-3">
|
<div className="mb-4 flex items-center gap-3">
|
||||||
<h3 className="text-lg font-bold text-text-primary">Active Setup: {activeProfile.name}</h3>
|
<h3 className="text-lg font-bold text-text-primary">
|
||||||
|
Active Setup:{' '}
|
||||||
|
<span className={isPendingSenderIdProfile(activeProfile) ? 'text-error-text' : 'text-text-primary'}>
|
||||||
|
{activeProfile.name}
|
||||||
|
</span>
|
||||||
|
</h3>
|
||||||
<span className="rounded-full border border-gray-200 bg-white px-3 py-1 text-xs font-bold uppercase tracking-wide text-gray-700">
|
<span className="rounded-full border border-gray-200 bg-white px-3 py-1 text-xs font-bold uppercase tracking-wide text-gray-700">
|
||||||
{activeProfile.executionReadiness?.isSetupComplete ? 'Setup Complete' : 'Missing Information'}
|
{activeProfile.executionReadiness?.isSetupComplete ? 'Setup Complete' : 'Missing Information'}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -490,7 +501,7 @@ export default function GlobalSms() {
|
||||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="mb-3 flex flex-wrap items-center gap-3">
|
<div className="mb-3 flex flex-wrap items-center gap-3">
|
||||||
<h3 className="truncate text-base font-bold text-text-primary">{profile.name}</h3>
|
<h3 className={`truncate text-base font-bold ${isPendingSenderIdProfile(profile) ? 'text-error-text' : 'text-text-primary'}`}>{profile.name}</h3>
|
||||||
{isActive && (
|
{isActive && (
|
||||||
<span className="rounded-full border border-indigo-200 bg-indigo-50 px-2.5 py-1 text-[11px] font-bold uppercase tracking-wider text-primary-dark">
|
<span className="rounded-full border border-indigo-200 bg-indigo-50 px-2.5 py-1 text-[11px] font-bold uppercase tracking-wider text-primary-dark">
|
||||||
Active Profile
|
Active Profile
|
||||||
|
|
@ -572,18 +583,6 @@ export default function GlobalSms() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-5">
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
<div>
|
|
||||||
<label className="mb-1.5 block text-sm font-semibold text-text-primary">Profile Name</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={formName}
|
|
||||||
onChange={(event) => setFormName(event.target.value)}
|
|
||||||
placeholder="e.g. Production SMS"
|
|
||||||
className="w-full rounded-lg border border-border-main bg-white px-4 py-2 text-sm text-text-primary transition focus:outline-none focus:ring-2 focus:ring-primary-blue"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-semibold text-text-primary">Provider cURL Command</label>
|
<label className="mb-1.5 block text-sm font-semibold text-text-primary">Provider cURL Command</label>
|
||||||
<textarea
|
<textarea
|
||||||
|
|
@ -594,6 +593,9 @@ export default function GlobalSms() {
|
||||||
required
|
required
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
/>
|
/>
|
||||||
|
<p className="mt-2 text-xs font-medium text-text-muted">
|
||||||
|
Profile name is generated automatically from Sender ID. If Sender ID is not detected yet, it stays as <span className="text-error-text">{PENDING_SENDER_ID_PROFILE_NAME}</span> until completed.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label className="flex cursor-pointer items-center gap-2">
|
<label className="flex cursor-pointer items-center gap-2">
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,13 @@ import apiClient from '../api/client';
|
||||||
import { useBusiness } from '../context/BusinessContext';
|
import { useBusiness } from '../context/BusinessContext';
|
||||||
|
|
||||||
const BASE_PROFILE_KEYS = new Set(['providerName', 'senderId', 'dltEntityId']);
|
const BASE_PROFILE_KEYS = new Set(['providerName', 'senderId', 'dltEntityId']);
|
||||||
|
const PENDING_SENDER_ID_PROFILE_NAME = 'Pending Sender ID';
|
||||||
|
|
||||||
|
function isPendingSenderIdProfile(profile) {
|
||||||
|
const normalizedName = String(profile?.name || '').trim();
|
||||||
|
const senderId = String(profile?.provider?.senderId || '').trim();
|
||||||
|
return (profile?.isAutoNamed === true && !senderId) || normalizedName === PENDING_SENDER_ID_PROFILE_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeCurlForDisplay(value) {
|
function normalizeCurlForDisplay(value) {
|
||||||
if (!value) return '';
|
if (!value) return '';
|
||||||
|
|
@ -448,7 +455,7 @@ export default function Providers() {
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="truncate text-sm font-semibold text-gray-900">{profile.name}</p>
|
<p className={`truncate text-sm font-semibold ${isPendingSenderIdProfile(profile) ? 'text-error-text' : 'text-gray-900'}`}>{profile.name}</p>
|
||||||
<p className="mt-1 text-sm leading-relaxed text-gray-500">{getProfileSummary(profile)}</p>
|
<p className="mt-1 text-sm leading-relaxed text-gray-500">{getProfileSummary(profile)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex shrink-0 flex-wrap justify-end gap-2">
|
<div className="flex shrink-0 flex-wrap justify-end gap-2">
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,16 @@ function firstNonEmptyText(...values) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getScrapedLogoUrl(entity = {}) {
|
||||||
|
const branding = entity?.scrapeArtifacts?.json?.branding;
|
||||||
|
|
||||||
|
return firstNonEmptyText(
|
||||||
|
branding?.primaryLogoUrl,
|
||||||
|
Array.isArray(branding?.logoCandidates) ? branding.logoCandidates[0] : '',
|
||||||
|
Array.isArray(branding?.logos) ? branding.logos[0] : ''
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function extractDomainName(domain) {
|
function extractDomainName(domain) {
|
||||||
if (!domain) return '';
|
if (!domain) return '';
|
||||||
if (typeof domain === 'string') return normalizeText(domain);
|
if (typeof domain === 'string') return normalizeText(domain);
|
||||||
|
|
@ -170,11 +180,14 @@ export function getBusinessTagline(entity) {
|
||||||
|
|
||||||
export function getBusinessImage(entity) {
|
export function getBusinessImage(entity) {
|
||||||
const relevantImage = normalizeList(entity?.relevantImagePaths)[0];
|
const relevantImage = normalizeList(entity?.relevantImagePaths)[0];
|
||||||
if (relevantImage) return relevantImage;
|
const scrapedLogo = getScrapedLogoUrl(entity);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
entity?.imageUrl
|
entity?.logoUrl
|
||||||
|| entity?.logoUrl
|
|| scrapedLogo
|
||||||
|
|| entity?.imageUrl
|
||||||
|
|| entity?.previewImagePath
|
||||||
|
|| relevantImage
|
||||||
|| entity?.brandImageUrl
|
|| entity?.brandImageUrl
|
||||||
|| entity?.image
|
|| entity?.image
|
||||||
|| ''
|
|| ''
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,7 @@ const RUNTIME_TOKEN_LABELS = {
|
||||||
};
|
};
|
||||||
const CURL_DATA_FLAGS = new Set(['--data', '--data-raw', '--data-binary', '-d']);
|
const CURL_DATA_FLAGS = new Set(['--data', '--data-raw', '--data-binary', '-d']);
|
||||||
const DETERMINISTIC_SENDER_ID_KEYS = ['sender_id', 'senderId', 'sender', 'sender_code', 'senderCode'];
|
const DETERMINISTIC_SENDER_ID_KEYS = ['sender_id', 'senderId', 'sender', 'sender_code', 'senderCode'];
|
||||||
|
const PENDING_SENDER_ID_PROFILE_NAME = 'Pending Sender ID';
|
||||||
|
|
||||||
function createHttpError(status, message, extra = {}) {
|
function createHttpError(status, message, extra = {}) {
|
||||||
const err = new Error(message);
|
const err = new Error(message);
|
||||||
|
|
@ -218,6 +219,23 @@ function validateSenderId(senderId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildAutomaticProfileName(source = {}) {
|
||||||
|
const senderId = normalizeSenderId(
|
||||||
|
source?.provider?.senderId
|
||||||
|
|| source?.senderId
|
||||||
|
);
|
||||||
|
|
||||||
|
return senderId || PENDING_SENDER_ID_PROFILE_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncAutomaticProfileName(profile = {}) {
|
||||||
|
if (profile?.isAutoNamed !== true) return profile;
|
||||||
|
|
||||||
|
profile.name = buildAutomaticProfileName(profile);
|
||||||
|
profile.isAutoNamed = true;
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
|
||||||
function getSenderIdFromStructuredValue(value) {
|
function getSenderIdFromStructuredValue(value) {
|
||||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return '';
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return '';
|
||||||
|
|
||||||
|
|
@ -558,9 +576,10 @@ function getProfileDisplayValue(profile = {}, key) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function serializeProfile(profile = {}) {
|
function serializeProfile(profile = {}) {
|
||||||
const hydratedProfile = hydrateProfile(profile);
|
const hydratedProfile = syncAutomaticProfileName(hydrateProfile(profile));
|
||||||
return {
|
return {
|
||||||
...hydratedProfile,
|
...hydratedProfile,
|
||||||
|
isAutoNamed: hydratedProfile.isAutoNamed === true,
|
||||||
rawCurl: undefined,
|
rawCurl: undefined,
|
||||||
rawCurlTemplate: undefined,
|
rawCurlTemplate: undefined,
|
||||||
profileInputValues: undefined,
|
profileInputValues: undefined,
|
||||||
|
|
@ -607,7 +626,7 @@ function sanitizeStoredCurlAnalysis(profile = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function persistableProfile(profile = {}) {
|
function persistableProfile(profile = {}) {
|
||||||
const hydratedProfile = hydrateProfile(profile);
|
const hydratedProfile = syncAutomaticProfileName(hydrateProfile(profile));
|
||||||
const normalizedAuthKey = firstNonEmptyResolvedText(
|
const normalizedAuthKey = firstNonEmptyResolvedText(
|
||||||
hydratedProfile.profileInputValues?.authKey,
|
hydratedProfile.profileInputValues?.authKey,
|
||||||
hydratedProfile.provider?.authKey,
|
hydratedProfile.provider?.authKey,
|
||||||
|
|
@ -616,6 +635,7 @@ function persistableProfile(profile = {}) {
|
||||||
return {
|
return {
|
||||||
id: hydratedProfile.id,
|
id: hydratedProfile.id,
|
||||||
name: normalizeText(hydratedProfile.name),
|
name: normalizeText(hydratedProfile.name),
|
||||||
|
isAutoNamed: hydratedProfile.isAutoNamed === true,
|
||||||
rawCurl: getStoredCurlTemplate(hydratedProfile),
|
rawCurl: getStoredCurlTemplate(hydratedProfile),
|
||||||
isDefault: hydratedProfile.isDefault === true,
|
isDefault: hydratedProfile.isDefault === true,
|
||||||
provider: {
|
provider: {
|
||||||
|
|
@ -791,22 +811,124 @@ function normalizeUrlList(value) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LOGO_URL_POSITIVE_REGEX = /(?:^|[\/_.-])(logo|wordmark|brandmark|site-logo|header-logo)(?:[\/_.-]|$)/i;
|
||||||
|
const LOGO_URL_ICON_REGEX = /(?:^|[\/_.-])(favicon|apple-touch-icon|mask-icon|mstile|icon)(?:[\/_.-]|$)/i;
|
||||||
|
const LOGO_URL_NEGATIVE_REGEX = /(hero|banner|product|products|collection|collections|slide|carousel|thumbnail|thumb|promo|cover|background)/i;
|
||||||
|
const LOGO_URL_SOCIAL_REGEX = /(facebook|instagram|twitter|linkedin|youtube|pinterest|avatar|profile)/i;
|
||||||
|
|
||||||
|
function isAbsoluteHttpUrl(value) {
|
||||||
|
const normalized = normalizeText(value);
|
||||||
|
if (!normalized) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = new URL(normalized);
|
||||||
|
return url.protocol === 'http:' || url.protocol === 'https:';
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scoreLogoCandidateUrl(url, sources = new Set()) {
|
||||||
|
const normalized = normalizeText(url).toLowerCase();
|
||||||
|
if (!isAbsoluteHttpUrl(normalized)) return -100;
|
||||||
|
|
||||||
|
let score = 0;
|
||||||
|
|
||||||
|
if (sources.has('branding')) score += 70;
|
||||||
|
if (sources.has('model')) score += 45;
|
||||||
|
if (sources.has('relevant') && (LOGO_URL_POSITIVE_REGEX.test(normalized) || LOGO_URL_ICON_REGEX.test(normalized))) score += 12;
|
||||||
|
if (sources.has('crawl') && (LOGO_URL_POSITIVE_REGEX.test(normalized) || LOGO_URL_ICON_REGEX.test(normalized))) score += 6;
|
||||||
|
|
||||||
|
if (LOGO_URL_POSITIVE_REGEX.test(normalized)) score += 30;
|
||||||
|
if (LOGO_URL_ICON_REGEX.test(normalized)) score += 18;
|
||||||
|
if (normalized.endsWith('.svg') || normalized.includes('.svg?')) score += 12;
|
||||||
|
if (normalized.endsWith('.png') || normalized.includes('.png?')) score += 6;
|
||||||
|
if (normalized.endsWith('.webp') || normalized.includes('.webp?')) score += 3;
|
||||||
|
|
||||||
|
if (LOGO_URL_NEGATIVE_REGEX.test(normalized)) score -= 35;
|
||||||
|
if (LOGO_URL_SOCIAL_REGEX.test(normalized)) score -= 25;
|
||||||
|
if (/\/products?\//i.test(normalized) || /\/collections?\//i.test(normalized)) score -= 20;
|
||||||
|
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectLogoCandidates(crawlSummary = {}, brandContext = {}) {
|
||||||
|
const candidatesByUrl = new Map();
|
||||||
|
let order = 0;
|
||||||
|
|
||||||
|
function append(values, source) {
|
||||||
|
normalizeUrlList(values).forEach((url) => {
|
||||||
|
if (!isAbsoluteHttpUrl(url)) return;
|
||||||
|
|
||||||
|
if (!candidatesByUrl.has(url)) {
|
||||||
|
candidatesByUrl.set(url, {
|
||||||
|
url,
|
||||||
|
sources: new Set(),
|
||||||
|
order,
|
||||||
|
});
|
||||||
|
order += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
candidatesByUrl.get(url).sources.add(source);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
append([brandContext?.logoUrl], 'model');
|
||||||
|
append([crawlSummary?.branding?.primaryLogoUrl], 'branding');
|
||||||
|
append(crawlSummary?.branding?.logos, 'branding');
|
||||||
|
append(crawlSummary?.branding?.logoCandidates, 'branding');
|
||||||
|
append(brandContext?.relevantImageUrls, 'relevant');
|
||||||
|
append(crawlSummary?.topImages, 'crawl');
|
||||||
|
|
||||||
|
return Array.from(candidatesByUrl.values())
|
||||||
|
.map((candidate) => ({
|
||||||
|
...candidate,
|
||||||
|
score: scoreLogoCandidateUrl(candidate.url, candidate.sources),
|
||||||
|
}))
|
||||||
|
.sort((left, right) => right.score - left.score || left.order - right.order);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectCanonicalLogoSourceUrl(crawlSummary = {}, brandContext = {}) {
|
||||||
|
const bestCandidate = collectLogoCandidates(crawlSummary, brandContext)[0];
|
||||||
|
if (!bestCandidate || bestCandidate.score <= 0) return '';
|
||||||
|
|
||||||
|
if (
|
||||||
|
bestCandidate.sources.has('model')
|
||||||
|
|| bestCandidate.sources.has('branding')
|
||||||
|
|| LOGO_URL_POSITIVE_REGEX.test(bestCandidate.url)
|
||||||
|
|| LOGO_URL_ICON_REGEX.test(bestCandidate.url)
|
||||||
|
) {
|
||||||
|
return bestCandidate.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
function getBusinessPreviewSummary(source = {}) {
|
function getBusinessPreviewSummary(source = {}) {
|
||||||
const taglines = Array.isArray(source?.taglines)
|
const taglines = Array.isArray(source?.taglines)
|
||||||
? source.taglines.map((tagline) => normalizeText(tagline)).filter(Boolean)
|
? source.taglines.map((tagline) => normalizeText(tagline)).filter(Boolean)
|
||||||
: [];
|
: [];
|
||||||
|
const logoUrl = normalizeText(source?.logoUrl);
|
||||||
const relevantImagePaths = Array.isArray(source?.relevantImagePaths)
|
const relevantImagePaths = Array.isArray(source?.relevantImagePaths)
|
||||||
? source.relevantImagePaths.map((path) => normalizeText(path)).filter(Boolean)
|
? source.relevantImagePaths.map((path) => normalizeText(path)).filter(Boolean)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
previewTagline: taglines[0] || '',
|
previewTagline: taglines[0] || '',
|
||||||
previewImagePath: relevantImagePaths[0] || '',
|
previewImagePath: logoUrl || relevantImagePaths[0] || '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeBusinessSummary(baseBusiness = {}, context = null) {
|
function mergeBusinessSummary(baseBusiness = {}, context = null, crawlSummary = null) {
|
||||||
const previewSummary = getBusinessPreviewSummary(context || baseBusiness);
|
const logoUrl = firstNonEmptyText(
|
||||||
|
context?.logoUrl,
|
||||||
|
baseBusiness?.logoUrl,
|
||||||
|
selectCanonicalLogoSourceUrl(crawlSummary, context || baseBusiness)
|
||||||
|
);
|
||||||
|
const previewSummary = getBusinessPreviewSummary({
|
||||||
|
...(context || baseBusiness),
|
||||||
|
logoUrl,
|
||||||
|
});
|
||||||
const relevantImagePaths = normalizeUrlList(
|
const relevantImagePaths = normalizeUrlList(
|
||||||
Array.isArray(baseBusiness?.relevantImagePaths) && baseBusiness.relevantImagePaths.length
|
Array.isArray(baseBusiness?.relevantImagePaths) && baseBusiness.relevantImagePaths.length
|
||||||
? baseBusiness.relevantImagePaths
|
? baseBusiness.relevantImagePaths
|
||||||
|
|
@ -815,8 +937,9 @@ function mergeBusinessSummary(baseBusiness = {}, context = null) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...baseBusiness,
|
...baseBusiness,
|
||||||
|
logoUrl,
|
||||||
previewTagline: normalizeText(baseBusiness.previewTagline) || previewSummary.previewTagline,
|
previewTagline: normalizeText(baseBusiness.previewTagline) || previewSummary.previewTagline,
|
||||||
previewImagePath: normalizeText(baseBusiness.previewImagePath) || previewSummary.previewImagePath,
|
previewImagePath: logoUrl || normalizeText(baseBusiness.previewImagePath) || previewSummary.previewImagePath,
|
||||||
relevantImagePaths,
|
relevantImagePaths,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -897,7 +1020,12 @@ async function finalizeBusinessFromCrawlJob(job, crawlSummary, brandContext) {
|
||||||
const existingBusiness = await findBusinessByApplicationId(merchantId, applicationId);
|
const existingBusiness = await findBusinessByApplicationId(merchantId, applicationId);
|
||||||
if (existingBusiness) {
|
if (existingBusiness) {
|
||||||
const existingContext = await fetchJSON(businessRoot(merchantId, existingBusiness.businessId), 'context').catch(() => null);
|
const existingContext = await fetchJSON(businessRoot(merchantId, existingBusiness.businessId), 'context').catch(() => null);
|
||||||
const mergedBusiness = existingContext ? { ...existingContext } : mergeBusinessSummary(existingBusiness);
|
const mergedBusiness = existingContext
|
||||||
|
? {
|
||||||
|
...existingContext,
|
||||||
|
...mergeBusinessSummary(existingBusiness, existingContext, crawlSummary),
|
||||||
|
}
|
||||||
|
: mergeBusinessSummary(existingBusiness, null, crawlSummary);
|
||||||
return {
|
return {
|
||||||
business: {
|
business: {
|
||||||
...mergedBusiness,
|
...mergedBusiness,
|
||||||
|
|
@ -913,13 +1041,29 @@ async function finalizeBusinessFromCrawlJob(job, crawlSummary, brandContext) {
|
||||||
const bizRoot = businessRoot(merchantId, businessId);
|
const bizRoot = businessRoot(merchantId, businessId);
|
||||||
const imagesFolder = `${bizRoot}/images`;
|
const imagesFolder = `${bizRoot}/images`;
|
||||||
const imagePaths = [];
|
const imagePaths = [];
|
||||||
|
const uploadedImageBySourceUrl = new Map();
|
||||||
const imageCandidates = normalizeUrlList(brandContext?.relevantImageUrls);
|
const imageCandidates = normalizeUrlList(brandContext?.relevantImageUrls);
|
||||||
|
|
||||||
for (let i = 0; i < Math.min(imageCandidates.length, 6); i += 1) {
|
for (let i = 0; i < Math.min(imageCandidates.length, 6); i += 1) {
|
||||||
const uploaded = await uploadImageFromUrl(imageCandidates[i], imagesFolder, `image_${i + 1}`);
|
const uploaded = await uploadImageFromUrl(imageCandidates[i], imagesFolder, `image_${i + 1}`);
|
||||||
if (uploaded) imagePaths.push(uploaded);
|
if (uploaded) {
|
||||||
|
imagePaths.push(uploaded);
|
||||||
|
uploadedImageBySourceUrl.set(imageCandidates[i], uploaded);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const selectedLogoSourceUrl = selectCanonicalLogoSourceUrl(crawlSummary, brandContext);
|
||||||
|
const logoUploadUrl = selectedLogoSourceUrl
|
||||||
|
? (
|
||||||
|
uploadedImageBySourceUrl.get(selectedLogoSourceUrl)
|
||||||
|
|| await uploadImageFromUrl(selectedLogoSourceUrl, imagesFolder, 'logo')
|
||||||
|
)
|
||||||
|
: '';
|
||||||
|
const logoUrl = normalizeText(logoUploadUrl || selectedLogoSourceUrl);
|
||||||
|
const relevantImagePaths = normalizeUrlList(
|
||||||
|
logoUrl ? [logoUrl, ...imagePaths] : imagePaths
|
||||||
|
);
|
||||||
|
|
||||||
let domain = normalizeText(crawlSummary?.domain);
|
let domain = normalizeText(crawlSummary?.domain);
|
||||||
if (!domain) {
|
if (!domain) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -940,7 +1084,8 @@ async function finalizeBusinessFromCrawlJob(job, crawlSummary, brandContext) {
|
||||||
tone: brandContext.tone || 'professional',
|
tone: brandContext.tone || 'professional',
|
||||||
taglines: Array.isArray(brandContext.taglines) ? brandContext.taglines : [],
|
taglines: Array.isArray(brandContext.taglines) ? brandContext.taglines : [],
|
||||||
colors: Array.isArray(brandContext.colors) ? brandContext.colors : [],
|
colors: Array.isArray(brandContext.colors) ? brandContext.colors : [],
|
||||||
relevantImagePaths: imagePaths,
|
logoUrl,
|
||||||
|
relevantImagePaths,
|
||||||
aboutSummary: normalizeText(brandContext.aboutSummary) || extractAboutSummary(crawlSummary),
|
aboutSummary: normalizeText(brandContext.aboutSummary) || extractAboutSummary(crawlSummary),
|
||||||
websiteUrl,
|
websiteUrl,
|
||||||
crawlStats: crawlSummary?.siteStats || {},
|
crawlStats: crawlSummary?.siteStats || {},
|
||||||
|
|
@ -959,6 +1104,7 @@ async function finalizeBusinessFromCrawlJob(job, crawlSummary, brandContext) {
|
||||||
applicationId,
|
applicationId,
|
||||||
brandName: contextJson.brandName,
|
brandName: contextJson.brandName,
|
||||||
domain: contextJson.domain,
|
domain: contextJson.domain,
|
||||||
|
logoUrl: contextJson.logoUrl,
|
||||||
previewTagline: previewSummary.previewTagline,
|
previewTagline: previewSummary.previewTagline,
|
||||||
previewImagePath: previewSummary.previewImagePath,
|
previewImagePath: previewSummary.previewImagePath,
|
||||||
relevantImagePaths: normalizeUrlList(contextJson.relevantImagePaths),
|
relevantImagePaths: normalizeUrlList(contextJson.relevantImagePaths),
|
||||||
|
|
@ -970,7 +1116,7 @@ async function finalizeBusinessFromCrawlJob(job, crawlSummary, brandContext) {
|
||||||
return {
|
return {
|
||||||
business: {
|
business: {
|
||||||
...contextJson,
|
...contextJson,
|
||||||
scrapeArtifacts: buildScrapeArtifacts(crawlSummary, imagePaths),
|
scrapeArtifacts: buildScrapeArtifacts(crawlSummary, relevantImagePaths),
|
||||||
},
|
},
|
||||||
reusedExistingBusiness: false,
|
reusedExistingBusiness: false,
|
||||||
};
|
};
|
||||||
|
|
@ -2456,6 +2602,8 @@ function buildStoredProfileFromValidation(baseProfile = {}, validation = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
profile.updatedAt = now;
|
profile.updatedAt = now;
|
||||||
|
profile.isAutoNamed = baseProfile.isAutoNamed === true
|
||||||
|
|| (!Object.prototype.hasOwnProperty.call(baseProfile, 'isAutoNamed') && !normalizeText(baseProfile.name));
|
||||||
profile.provider = normalizeProvider({
|
profile.provider = normalizeProvider({
|
||||||
...profile.provider,
|
...profile.provider,
|
||||||
authKey: '',
|
authKey: '',
|
||||||
|
|
@ -2464,7 +2612,7 @@ function buildStoredProfileFromValidation(baseProfile = {}, validation = {}) {
|
||||||
profile.curlAnalysis = sanitizeStoredCurlAnalysis(profile);
|
profile.curlAnalysis = sanitizeStoredCurlAnalysis(profile);
|
||||||
profile.rawCurl = getStoredCurlTemplate(profile);
|
profile.rawCurl = getStoredCurlTemplate(profile);
|
||||||
profile.rawCurlTemplate = profile.rawCurl;
|
profile.rawCurlTemplate = profile.rawCurl;
|
||||||
return profile;
|
return syncAutomaticProfileName(profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectProfileInputPatch(payload = {}) {
|
function collectProfileInputPatch(payload = {}) {
|
||||||
|
|
@ -2518,7 +2666,7 @@ function applyProfileInputPatch(profile, patch = {}) {
|
||||||
}, profile.updatedAt);
|
}, profile.updatedAt);
|
||||||
profile.profileInputValues = normalizeProfileInputValues(profile.profileInputValues);
|
profile.profileInputValues = normalizeProfileInputValues(profile.profileInputValues);
|
||||||
profile.curlAnalysis = sanitizeStoredCurlAnalysis(profile);
|
profile.curlAnalysis = sanitizeStoredCurlAnalysis(profile);
|
||||||
return profile;
|
return syncAutomaticProfileName(profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateProfileInputPatch(patch = {}) {
|
function validateProfileInputPatch(patch = {}) {
|
||||||
|
|
@ -2554,9 +2702,14 @@ async function validateCurlAndExtractProvider(rawCurl) {
|
||||||
throw createHttpError(422, senderIdError);
|
throw createHttpError(422, senderIdError);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizedProvider = normalizeProvider({
|
||||||
|
...provider,
|
||||||
|
...(resolvedSenderId ? { senderId: resolvedSenderId } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...validation,
|
...validation,
|
||||||
provider,
|
provider: normalizedProvider,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.status) throw err;
|
if (err.status) throw err;
|
||||||
|
|
@ -2573,15 +2726,23 @@ router.get('/', async (req, res) => {
|
||||||
const businesses = await getIndex(merchantId);
|
const businesses = await getIndex(merchantId);
|
||||||
const hydratedBusinesses = await Promise.all(
|
const hydratedBusinesses = await Promise.all(
|
||||||
businesses.map(async (business) => {
|
businesses.map(async (business) => {
|
||||||
const hasPreviewSummary = normalizeText(business.previewTagline) || normalizeText(business.previewImagePath);
|
const hasPreviewSummary = normalizeText(business.previewTagline)
|
||||||
|
|| normalizeText(business.previewImagePath)
|
||||||
|
|| normalizeText(business.logoUrl);
|
||||||
const hasRelevantImagePaths = Array.isArray(business.relevantImagePaths) && business.relevantImagePaths.length > 0;
|
const hasRelevantImagePaths = Array.isArray(business.relevantImagePaths) && business.relevantImagePaths.length > 0;
|
||||||
|
const hasLogoUrl = normalizeText(business.logoUrl);
|
||||||
|
|
||||||
if (hasPreviewSummary && hasRelevantImagePaths) {
|
if (hasPreviewSummary && (hasRelevantImagePaths || hasLogoUrl)) {
|
||||||
return mergeBusinessSummary(business);
|
return mergeBusinessSummary(business);
|
||||||
}
|
}
|
||||||
|
|
||||||
const context = await fetchJSON(businessRoot(merchantId, business.businessId), 'context').catch(() => null);
|
const root = businessRoot(merchantId, business.businessId);
|
||||||
return mergeBusinessSummary(business, context);
|
const [context, crawlSummary] = await Promise.all([
|
||||||
|
fetchJSON(root, 'context').catch(() => null),
|
||||||
|
fetchJSON(root, 'crawl_summary').catch(() => null),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return mergeBusinessSummary(business, context, crawlSummary);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -2684,12 +2845,13 @@ router.get('/:businessId', async (req, res) => {
|
||||||
if (!context) return res.status(404).json({ error: 'Business not found' });
|
if (!context) return res.status(404).json({ error: 'Business not found' });
|
||||||
|
|
||||||
if (!crawlSummary) {
|
if (!crawlSummary) {
|
||||||
return res.json(context);
|
return res.json(mergeBusinessSummary(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const business = mergeBusinessSummary(context, null, crawlSummary);
|
||||||
res.json({
|
res.json({
|
||||||
...context,
|
...business,
|
||||||
scrapeArtifacts: buildScrapeArtifacts(crawlSummary, context.relevantImagePaths),
|
scrapeArtifacts: buildScrapeArtifacts(crawlSummary, business.relevantImagePaths),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(500).json({ error: err.message });
|
res.status(500).json({ error: err.message });
|
||||||
|
|
@ -2993,9 +3155,6 @@ router.get('/:businessId/global-sms/profiles/:profileId/delete-impact', async (r
|
||||||
router.post('/:businessId/global-sms/profiles', async (req, res) => {
|
router.post('/:businessId/global-sms/profiles', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { name, rawCurl, setActive } = req.body;
|
const { name, rawCurl, setActive } = req.body;
|
||||||
if (!normalizeText(name)) {
|
|
||||||
return res.status(400).json({ error: 'name is required' });
|
|
||||||
}
|
|
||||||
if (!normalizeText(rawCurl)) {
|
if (!normalizeText(rawCurl)) {
|
||||||
return res.status(400).json({ error: 'rawCurl is required' });
|
return res.status(400).json({ error: 'rawCurl is required' });
|
||||||
}
|
}
|
||||||
|
|
@ -3007,11 +3166,13 @@ router.post('/:businessId/global-sms/profiles', async (req, res) => {
|
||||||
const { profileData } = await getProfileState(bizRoot);
|
const { profileData } = await getProfileState(bizRoot);
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
const normalizedCurl = normalizeText(rawCurl);
|
const normalizedCurl = normalizeText(rawCurl);
|
||||||
|
const normalizedRequestedName = normalizeText(name);
|
||||||
const validation = await validateCurlAndExtractProvider(normalizedCurl);
|
const validation = await validateCurlAndExtractProvider(normalizedCurl);
|
||||||
|
|
||||||
const newProfile = buildStoredProfileFromValidation({
|
const newProfile = buildStoredProfileFromValidation({
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
name: normalizeText(name),
|
...(normalizedRequestedName ? { name: normalizedRequestedName } : {}),
|
||||||
|
isAutoNamed: !normalizedRequestedName,
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
|
|
@ -3053,7 +3214,10 @@ router.patch('/:businessId/global-sms/profiles/:profileId', async (req, res) =>
|
||||||
const profile = profileData.profiles.find(p => p.id === profileId);
|
const profile = profileData.profiles.find(p => p.id === profileId);
|
||||||
if (!profile) return res.status(404).json({ error: 'Profile not found' });
|
if (!profile) return res.status(404).json({ error: 'Profile not found' });
|
||||||
|
|
||||||
if (name !== undefined) profile.name = normalizeText(name);
|
if (name !== undefined) {
|
||||||
|
profile.name = normalizeText(name);
|
||||||
|
profile.isAutoNamed = false;
|
||||||
|
}
|
||||||
applyProfileInputPatch(profile, profilePatch);
|
applyProfileInputPatch(profile, profilePatch);
|
||||||
|
|
||||||
await saveGlobalSmsProfiles(bizRoot, profileData);
|
await saveGlobalSmsProfiles(bizRoot, profileData);
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,38 @@ function normalizeList(value) {
|
||||||
return Array.isArray(value) ? value : [];
|
return Array.isArray(value) ? value : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isAbsoluteHttpUrl(value) {
|
||||||
|
const normalized = normalizeText(value);
|
||||||
|
if (!normalized) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = new URL(normalized);
|
||||||
|
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectImageUrls(value, bucket = []) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach((entry) => collectImageUrls(entry, bucket));
|
||||||
|
return bucket;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
if (isAbsoluteHttpUrl(value)) bucket.push(value);
|
||||||
|
return bucket;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value || typeof value !== 'object') {
|
||||||
|
return bucket;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = normalizeText(value.url || value.href || value.src || value.secure_url);
|
||||||
|
if (isAbsoluteHttpUrl(url)) bucket.push(url);
|
||||||
|
return bucket;
|
||||||
|
}
|
||||||
|
|
||||||
function uniqueStrings(values) {
|
function uniqueStrings(values) {
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
return normalizeList(values)
|
return normalizeList(values)
|
||||||
|
|
@ -76,6 +108,46 @@ function dedupeLinks(links) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scoreLogoCandidate(url) {
|
||||||
|
const normalized = normalizeText(url).toLowerCase();
|
||||||
|
if (!normalized) return -100;
|
||||||
|
|
||||||
|
let score = 0;
|
||||||
|
|
||||||
|
if (/logo|brandmark|wordmark|logomark/.test(normalized)) score += 40;
|
||||||
|
if (/favicon|apple-touch-icon|android-chrome|mstile|site-icon|siteicon|icon/.test(normalized)) score += 25;
|
||||||
|
if (/header|navbar|nav/.test(normalized)) score += 8;
|
||||||
|
|
||||||
|
if (/hero|banner|carousel|slider|product|collection|catalog|lookbook/.test(normalized)) score -= 35;
|
||||||
|
if (/social|facebook|instagram|twitter|linkedin|youtube|pinterest|avatar|profile/.test(normalized)) score -= 25;
|
||||||
|
if (/sprite|tracking|pixel|placeholder/.test(normalized)) score -= 40;
|
||||||
|
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rankLogoCandidates(values) {
|
||||||
|
return uniqueStrings(values)
|
||||||
|
.map((url, index) => ({
|
||||||
|
index,
|
||||||
|
score: scoreLogoCandidate(url),
|
||||||
|
url,
|
||||||
|
}))
|
||||||
|
.sort((left, right) => right.score - left.score || left.index - right.index)
|
||||||
|
.map((entry) => entry.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractMetadataImageCandidates(metadata = {}) {
|
||||||
|
const candidates = [];
|
||||||
|
|
||||||
|
Object.entries(metadata || {}).forEach(([key, value]) => {
|
||||||
|
const normalizedKey = normalizeText(key).toLowerCase();
|
||||||
|
if (!/(image|logo|icon|favicon|thumbnail|apple)/.test(normalizedKey)) return;
|
||||||
|
collectImageUrls(value, candidates);
|
||||||
|
});
|
||||||
|
|
||||||
|
return rankLogoCandidates(candidates).filter((url) => scoreLogoCandidate(url) > 0);
|
||||||
|
}
|
||||||
|
|
||||||
function summarizePage(page, pageType) {
|
function summarizePage(page, pageType) {
|
||||||
const metadata = page?.metadata && typeof page.metadata === 'object' ? page.metadata : {};
|
const metadata = page?.metadata && typeof page.metadata === 'object' ? page.metadata : {};
|
||||||
|
|
||||||
|
|
@ -101,7 +173,7 @@ function buildRepresentativeTextBlocks(homepage, aboutPage, productPages) {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function flattenBranding(homepage) {
|
function flattenBranding(homepage, topImages = []) {
|
||||||
const branding = homepage?.branding && typeof homepage.branding === 'object' ? homepage.branding : {};
|
const branding = homepage?.branding && typeof homepage.branding === 'object' ? homepage.branding : {};
|
||||||
const colorEntries = [];
|
const colorEntries = [];
|
||||||
const logos = [];
|
const logos = [];
|
||||||
|
|
@ -142,12 +214,22 @@ function flattenBranding(homepage) {
|
||||||
const brandName = normalizeText(branding.brandName || branding.brand_name || branding.name);
|
const brandName = normalizeText(branding.brandName || branding.brand_name || branding.name);
|
||||||
if (brandName) brandNames.push(brandName);
|
if (brandName) brandNames.push(brandName);
|
||||||
|
|
||||||
|
const metadataImageCandidates = extractMetadataImageCandidates(homepage?.metadata || {});
|
||||||
|
const topLogoCandidates = rankLogoCandidates(topImages).filter((url) => scoreLogoCandidate(url) > 0);
|
||||||
|
const logoCandidates = uniqueStrings([
|
||||||
|
...logos,
|
||||||
|
...metadataImageCandidates,
|
||||||
|
...topLogoCandidates,
|
||||||
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
colors: uniqueStrings(colorEntries.map((entry) => entry.hex)),
|
colors: uniqueStrings(colorEntries.map((entry) => entry.hex)),
|
||||||
labeledColors: colorEntries.filter((entry, index, values) => (
|
labeledColors: colorEntries.filter((entry, index, values) => (
|
||||||
values.findIndex((candidate) => candidate.name === entry.name && candidate.hex === entry.hex) === index
|
values.findIndex((candidate) => candidate.name === entry.name && candidate.hex === entry.hex) === index
|
||||||
)),
|
)),
|
||||||
logos: uniqueStrings(logos),
|
logos: uniqueStrings(logos),
|
||||||
|
logoCandidates,
|
||||||
|
primaryLogoUrl: logoCandidates[0] || '',
|
||||||
brandNames: uniqueStrings(brandNames),
|
brandNames: uniqueStrings(brandNames),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -188,7 +270,7 @@ function buildCrawlSummary(data = {}, startUrlOverride = '') {
|
||||||
...normalizeList(aboutRaw?.images),
|
...normalizeList(aboutRaw?.images),
|
||||||
...productRawPages.flatMap((page) => normalizeList(page?.images)),
|
...productRawPages.flatMap((page) => normalizeList(page?.images)),
|
||||||
]).slice(0, 60);
|
]).slice(0, 60);
|
||||||
const branding = flattenBranding(homepageRaw);
|
const branding = flattenBranding(homepageRaw, topImages);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
startUrl,
|
startUrl,
|
||||||
|
|
|
||||||
|
|
@ -1006,6 +1006,7 @@ async function parseBrandContext(scrapedData = {}) {
|
||||||
` "tone": "one of ${BRAND_CONTEXT_TONE_OPTIONS.join(', ')}",`,
|
` "tone": "one of ${BRAND_CONTEXT_TONE_OPTIONS.join(', ')}",`,
|
||||||
' "taglines": ["up to 3 strings"],',
|
' "taglines": ["up to 3 strings"],',
|
||||||
' "colors": ["hex colors only"],',
|
' "colors": ["hex colors only"],',
|
||||||
|
' "logoUrl": "absolute http(s) URL for the best company logo or empty string",',
|
||||||
' "relevantImageUrls": ["3-5 absolute http(s) image URLs only"],',
|
' "relevantImageUrls": ["3-5 absolute http(s) image URLs only"],',
|
||||||
' "aboutSummary": "2-4 concise customer-facing sentences"',
|
' "aboutSummary": "2-4 concise customer-facing sentences"',
|
||||||
'}',
|
'}',
|
||||||
|
|
@ -1014,6 +1015,8 @@ async function parseBrandContext(scrapedData = {}) {
|
||||||
'- No markdown.',
|
'- No markdown.',
|
||||||
'- No explanatory prose.',
|
'- No explanatory prose.',
|
||||||
'- Do not copy the About page verbatim.',
|
'- Do not copy the About page verbatim.',
|
||||||
|
'- `logoUrl` must be a real brand logo or wordmark when one is available. Prefer storefront/header logos from branding_json.logos.',
|
||||||
|
'- If no credible logo is present, return an empty string for `logoUrl`.',
|
||||||
'- Exclude icons, tracking pixels, and data URLs from images.',
|
'- Exclude icons, tracking pixels, and data URLs from images.',
|
||||||
'',
|
'',
|
||||||
`start_url: ${String(scrapedData.startUrl || '')}`,
|
`start_url: ${String(scrapedData.startUrl || '')}`,
|
||||||
|
|
@ -1037,12 +1040,14 @@ async function parseBrandContext(scrapedData = {}) {
|
||||||
});
|
});
|
||||||
|
|
||||||
const normalizedTone = normalizeText(String(result.tone || '')).toLowerCase();
|
const normalizedTone = normalizeText(String(result.tone || '')).toLowerCase();
|
||||||
|
const normalizedLogoUrl = normalizeText(String(result.logoUrl || ''));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
brandName: normalizeText(String(result.brandName || '')) || 'Unknown Brand',
|
brandName: normalizeText(String(result.brandName || '')) || 'Unknown Brand',
|
||||||
tone: BRAND_CONTEXT_TONE_OPTIONS.includes(normalizedTone) ? normalizedTone : 'professional',
|
tone: BRAND_CONTEXT_TONE_OPTIONS.includes(normalizedTone) ? normalizedTone : 'professional',
|
||||||
taglines: sanitizeStringArray(result.taglines, { maxItems: 3 }),
|
taglines: sanitizeStringArray(result.taglines, { maxItems: 3 }),
|
||||||
colors: sanitizeStringArray(result.colors),
|
colors: sanitizeStringArray(result.colors),
|
||||||
|
logoUrl: isAbsoluteHttpUrl(normalizedLogoUrl) ? normalizedLogoUrl : '',
|
||||||
relevantImageUrls: sanitizeStringArray(result.relevantImageUrls, { maxItems: 5, allowUrlsOnly: true }),
|
relevantImageUrls: sanitizeStringArray(result.relevantImageUrls, { maxItems: 5, allowUrlsOnly: true }),
|
||||||
aboutSummary: normalizeText(String(result.aboutSummary || '')),
|
aboutSummary: normalizeText(String(result.aboutSummary || '')),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user