bolt-templates-sms-extensio.../client/src/pages/Providers.jsx
2026-04-09 15:30:14 +05:30

714 lines
30 KiB
JavaScript

import { useCallback, useEffect, useMemo, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import apiClient from '../api/client';
import { useBusiness } from '../context/BusinessContext';
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) {
if (!value) return '';
return String(value)
.trim()
.replace(/\r\n/g, '\n')
.replace(/\\r\\n/g, '\n')
.replace(/\\n/g, '\n')
.replace(/\\t/g, ' ')
.replace(/\\'/g, '\'')
.replace(/\\"/g, '"');
}
function stripWrappingQuotes(value) {
if (!value || value.length < 2) return value;
if (
(value.startsWith('\'') && value.endsWith('\''))
|| (value.startsWith('"') && value.endsWith('"'))
) {
return value.slice(1, -1);
}
return value;
}
function formatCurlCommand(normalizedCurl) {
if (!normalizedCurl) return '';
let output = normalizedCurl;
if (!output.includes('\n')) {
output = output
.replace(/^curl\s+/, 'curl\n ')
.replace(/\s+(--request|-X|--url|--header|-H|--data-raw|--data|-d|--compressed|--location|--insecure|--fail)\b/g, '\n $1');
}
return output.replace(/\n{3,}/g, '\n\n').trim();
}
function extractCurlBody(normalizedCurl) {
if (!normalizedCurl) return '';
const quotedMatch = normalizedCurl.match(
/(?:--data-raw|--data|-d)\s+(["'])([\s\S]*?)\1(?=\s+(?:--[a-z-]+|-\w)\b|$)/i,
);
if (quotedMatch?.[2]) return stripWrappingQuotes(quotedMatch[2].trim());
const braceMatch = normalizedCurl.match(
/(?:--data-raw|--data|-d)\s+({[\s\S]*})(?=\s+(?:--[a-z-]+|-\w)\b|$)/i,
);
return braceMatch?.[1]?.trim() || '';
}
function buildCurlViewModel(value) {
const normalizedCurl = normalizeCurlForDisplay(value);
const headers = [
...normalizedCurl.matchAll(/(?:--header|-H)\s+(?:"([^"]+)"|'([^']+)')/g),
]
.map((match) => (match[1] || match[2] || '').trim())
.filter(Boolean);
const methodMatch = normalizedCurl.match(/(?:--request|-X)\s+([A-Z]+)/i);
const method = (methodMatch?.[1] || (/(?:--data-raw|--data|-d)\b/i.test(normalizedCurl) ? 'POST' : 'GET')).toUpperCase();
const url = normalizedCurl.match(/https?:\/\/[^\s'"]+/i)?.[0] || '';
const rawBody = extractCurlBody(normalizedCurl);
let payload = stripWrappingQuotes(rawBody || '').trim();
let prettyPayload = '';
let payloadFormat = '';
if (payload) {
try {
const parsed = JSON.parse(payload);
prettyPayload = JSON.stringify(parsed, null, 2);
payloadFormat = 'json';
} catch {
prettyPayload = payload;
payloadFormat = 'text';
}
}
let host = '';
try {
host = url ? new URL(url).host : '';
} catch {
host = '';
}
const shellLines = [];
if (url) {
shellLines.push('curl \\');
shellLines.push(` --request ${method} \\`);
shellLines.push(` --url '${url}'${headers.length || rawBody ? ' \\' : ''}`);
headers.forEach((header, index) => {
const hasTrailingSection = index < headers.length - 1 || Boolean(rawBody);
shellLines.push(` --header '${header}'${hasTrailingSection ? ' \\' : ''}`);
});
if (rawBody) {
shellLines.push(` --data-raw '${payloadFormat === 'json' ? '<payload shown below>' : rawBody}'`);
}
}
return {
command: shellLines.length > 0 ? shellLines.join('\n') : formatCurlCommand(normalizedCurl),
headers,
host,
method,
payload: prettyPayload,
payloadFormat,
url,
};
}
function formatUpdatedAt(value) {
if (!value) return 'Not updated yet';
try {
return new Date(value).toLocaleString();
} catch {
return 'Not updated yet';
}
}
function buildProfilePatchPayload(inputs = [], values = {}) {
const provider = {};
const profileInputValues = {};
inputs.forEach((input) => {
const rawValue = String(values[input.key] ?? '').trim();
if (!rawValue) return;
if (BASE_PROFILE_KEYS.has(input.key)) {
provider[input.key] = input.key === 'senderId' ? rawValue.toUpperCase() : rawValue;
return;
}
profileInputValues[input.key] = rawValue;
});
return {
...(Object.keys(provider).length > 0 ? { provider } : {}),
...(Object.keys(profileInputValues).length > 0 ? { profileInputValues } : {}),
};
}
function getInitialFormValues(inputs = []) {
return inputs.reduce((accumulator, input) => {
accumulator[input.key] = input.secret ? '' : (input.value || '');
return accumulator;
}, {});
}
function getProfileSummary(profile) {
const parts = [];
const provider = profile?.provider || {};
const missingCount = profile?.executionReadiness?.missingProfileInputs?.length || 0;
if (provider.providerName) parts.push(provider.providerName);
if (provider.senderId) parts.push(`Sender ${provider.senderId}`);
if (provider.dltEntityId) parts.push('DLT ready');
if (missingCount > 0) parts.push(`${missingCount} pending`);
return parts.join(' • ') || 'Profile saved. Complete the required fields to use it everywhere.';
}
function ProfileStatusPill({ complete }) {
return (
<span
className={`rounded-full border px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.14em] ${
complete
? 'border-emerald-200 bg-emerald-50 text-emerald-700'
: 'border-amber-200 bg-amber-50 text-amber-700'
}`}
>
{complete ? 'Ready' : 'Needs Fields'}
</span>
);
}
function InspectorRow({ label, value, valueClassName = '' }) {
return (
<div className="rounded-2xl border border-gray-200 bg-white px-4 py-3">
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-gray-400">{label}</p>
<p className={`mt-2 text-sm font-medium text-gray-900 ${valueClassName}`}>{value}</p>
</div>
);
}
export default function Providers() {
const { businessId } = useParams();
const navigate = useNavigate();
const { refreshOnboardingState } = useBusiness();
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [profiles, setProfiles] = useState([]);
const [activeProfileId, setActiveProfileId] = useState('');
const [selectedProfileId, setSelectedProfileId] = useState('');
const [formValues, setFormValues] = useState({});
const [revealedProfiles, setRevealedProfiles] = useState({});
const [showSecretsByProfileId, setShowSecretsByProfileId] = useState({});
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const globalSmsPath = `/${businessId}/global-sms`;
const loadProfiles = useCallback(async () => {
try {
setLoading(true);
const res = await apiClient.get(`/api/businesses/${businessId}/global-sms/profiles`);
const fetchedProfiles = res.data?.profiles || [];
const nextActiveProfileId = String(res.data?.activeProfileId || '');
setProfiles(fetchedProfiles);
setActiveProfileId(nextActiveProfileId);
setSelectedProfileId((currentSelectedProfileId) => (
fetchedProfiles.some((profile) => profile.id === currentSelectedProfileId)
? currentSelectedProfileId
: ''
));
} catch (err) {
setError(err.response?.data?.error || 'Failed to load provider profiles');
} finally {
setLoading(false);
}
}, [businessId]);
useEffect(() => {
loadProfiles();
}, [loadProfiles]);
const selectedProfile = useMemo(
() => profiles.find((profile) => profile.id === selectedProfileId) || null,
[profiles, selectedProfileId],
);
const selectedProfileInputs = selectedProfile?.profileInputs || [];
const isSelectedProfileRevealed = selectedProfile ? showSecretsByProfileId[selectedProfile.id] === true : false;
const selectedRevealData = selectedProfile ? revealedProfiles[selectedProfile.id] : null;
const selectedDisplayCurl = selectedProfile
? (isSelectedProfileRevealed
? (selectedRevealData?.rawCurl || selectedProfile.maskedCurl)
: selectedProfile.maskedCurl)
: '';
const selectedCurlView = useMemo(
() => buildCurlViewModel(selectedDisplayCurl),
[selectedDisplayCurl],
);
const missingInputCount = selectedProfile?.executionReadiness?.missingProfileInputs?.length || 0;
const curlWarnings = selectedProfile?.curlAnalysis?.warnings || [];
useEffect(() => {
if (!selectedProfile) {
setFormValues({});
return;
}
setFormValues(getInitialFormValues(selectedProfile.profileInputs));
}, [selectedProfile]);
const ensureRevealData = useCallback(async (profileId) => {
if (revealedProfiles[profileId]) return revealedProfiles[profileId];
const res = await apiClient.get(`/api/businesses/${businessId}/global-sms/profiles/${profileId}/reveal`);
setRevealedProfiles((current) => ({ ...current, [profileId]: res.data }));
return res.data;
}, [businessId, revealedProfiles]);
function handleSelectProfile(profileId) {
setSelectedProfileId(profileId);
setError('');
setSuccess('');
}
function handleReturnToList() {
setSelectedProfileId('');
setError('');
setSuccess('');
}
async function handleActivate(profile) {
if (!profile?.id) return;
try {
setError('');
setSuccess('');
await apiClient.post(`/api/businesses/${businessId}/global-sms/profiles/${profile.id}/activate`);
setSelectedProfileId(profile.id);
await loadProfiles();
await refreshOnboardingState(businessId).catch(() => null);
setSuccess(`${profile.name} is now the active profile.`);
} catch (err) {
setError(err.response?.data?.error || 'Failed to activate profile');
}
}
async function handleToggleReveal(profile) {
if (!profile?.id) return;
const shouldReveal = !showSecretsByProfileId[profile.id];
if (shouldReveal) {
try {
const revealData = await ensureRevealData(profile.id);
const revealedValues = (revealData?.profileInputs || []).reduce((accumulator, input) => {
accumulator[input.key] = input.value || '';
return accumulator;
}, {});
setFormValues((current) => ({ ...current, ...revealedValues }));
} catch (err) {
setError(err.response?.data?.error || 'Failed to reveal saved values');
return;
}
}
setShowSecretsByProfileId((current) => ({
...current,
[profile.id]: shouldReveal,
}));
}
async function handleCopyCurl(profile) {
if (!profile?.id) return;
try {
const revealData = await ensureRevealData(profile.id);
if (!revealData?.rawCurl) return;
await navigator.clipboard.writeText(revealData.rawCurl);
setSuccess(`Copied ${profile.name} cURL.`);
} catch (err) {
setError(err.response?.data?.error || 'Failed to copy the cURL command.');
}
}
async function handleSave(event) {
event.preventDefault();
if (!selectedProfile?.id) return;
setSaving(true);
setError('');
setSuccess('');
try {
const payload = buildProfilePatchPayload(selectedProfileInputs, formValues);
await apiClient.patch(`/api/businesses/${businessId}/global-sms/profiles/${selectedProfile.id}`, payload);
await loadProfiles();
await refreshOnboardingState(businessId).catch(() => null);
setSuccess(`Provider configuration saved for ${selectedProfile.name}.`);
} catch (err) {
setError(err.response?.data?.error || 'Failed to save configuration');
} finally {
setSaving(false);
}
}
if (loading) {
return (
<div className="flex h-64 items-center justify-center">
<div className="h-8 w-8 animate-spin rounded-full border-2 border-gray-200 border-t-indigo-600" />
</div>
);
}
return (
<div className="mx-auto max-w-6xl space-y-6 pb-12">
<div className="flex flex-col gap-4 border-b border-gray-200 pb-5 lg:flex-row lg:items-end lg:justify-between">
<div>
<h1 className="text-2xl font-bold tracking-tight text-gray-800">Provider Configuration</h1>
<p className="mt-1 text-sm font-medium text-gray-500">
Review each saved cURL profile and update the required provider or credential fields without changing the stored cURL.
</p>
</div>
<button
type="button"
onClick={() => navigate(globalSmsPath)}
className="inline-flex items-center justify-center rounded-lg border border-gray-200 px-4 py-2 text-sm font-semibold text-gray-700 transition hover:border-primary-blue hover:bg-white hover:text-primary-blue"
>
Manage cURLs
</button>
</div>
{error && (
<div className="flex items-center justify-between rounded-md border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700">
{error}
<button type="button" onClick={() => setError('')} className="font-bold text-gray-600 hover:text-gray-700">
&times;
</button>
</div>
)}
{success && (
<div className="flex items-center justify-between rounded-md border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700">
{success}
<button type="button" onClick={() => setSuccess('')} className="font-bold text-gray-600 hover:text-gray-700">
&times;
</button>
</div>
)}
{!selectedProfile ? (
<section className="overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm">
<div className="border-b border-gray-200 px-5 py-4">
<div className="flex items-start justify-between gap-4">
<div>
<p className="text-sm font-semibold text-gray-900">Saved Profiles</p>
</div>
<span className="rounded-full border border-gray-200 bg-gray-50 px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.14em] text-gray-500">
{profiles.length} total
</span>
</div>
</div>
{profiles.length === 0 ? (
<div className="px-5 py-8 text-center">
<p className="text-sm font-semibold text-gray-900">No saved profiles yet</p>
<p className="mt-2 text-sm text-gray-500">
Add and validate a cURL profile from Omni-channel SMS before configuring provider details here.
</p>
<button
type="button"
onClick={() => navigate(globalSmsPath)}
className="mt-4 inline-flex items-center justify-center rounded-lg bg-primary-blue px-4 py-2 text-sm font-semibold text-white transition hover:bg-primary-dark"
>
Go to Omni-channel SMS
</button>
</div>
) : (
<div className="p-3">
<div className="space-y-3">
{profiles.map((profile) => {
const isActive = profile.id === activeProfileId;
const complete = profile.executionReadiness?.isSetupComplete === true;
return (
<button
key={profile.id}
type="button"
onClick={() => handleSelectProfile(profile.id)}
className="w-full rounded-xl border border-gray-200 bg-white p-4 text-left transition hover:border-primary-blue hover:bg-gray-50"
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<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>
</div>
<div className="flex shrink-0 flex-wrap justify-end gap-2">
{isActive && (
<span className="rounded-full border border-indigo-200 bg-indigo-50 px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.14em] text-primary-dark">
Active
</span>
)}
<ProfileStatusPill complete={complete} />
</div>
</div>
<div className="mt-3 flex flex-wrap items-center gap-x-4 gap-y-2 text-xs font-medium text-gray-500">
<span>Updated {formatUpdatedAt(profile.updatedAt)}</span>
{profile.provider?.senderId && <span>Sender {profile.provider.senderId}</span>}
{profile.provider?.dltEntityId && <span>DLT ready</span>}
</div>
</button>
);
})}
</div>
</div>
)}
</section>
) : (
<section className="overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm">
<div className="border-b border-gray-200 px-6 py-5">
<div className="flex flex-col gap-4">
<button
type="button"
onClick={handleReturnToList}
className="inline-flex w-fit items-center gap-2 text-sm font-semibold text-gray-500 transition hover:text-primary-blue"
>
<span>Saved Profiles</span>
<span className="text-gray-300">/</span>
<span className="text-gray-900">{selectedProfile.name}</span>
</button>
<div className="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between">
<div>
<div className="flex flex-wrap items-center gap-2">
<h2 className="text-xl font-semibold tracking-tight text-gray-900">{selectedProfile.name}</h2>
{selectedProfile.id === activeProfileId && (
<span className="rounded-full border border-indigo-200 bg-indigo-50 px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.14em] text-primary-dark">
Active profile
</span>
)}
<ProfileStatusPill complete={selectedProfile.executionReadiness?.isSetupComplete === true} />
</div>
<p className="mt-2 text-sm text-gray-500">
The stored cURL is immutable after validation. You can review it, reveal it, and update the profile fields it depends on.
</p>
</div>
<div className="flex flex-wrap items-center gap-2">
{selectedProfile.id !== activeProfileId && (
<button
type="button"
onClick={() => handleActivate(selectedProfile)}
className="rounded-lg bg-primary-blue px-4 py-2 text-sm font-semibold text-white transition hover:bg-primary-dark"
>
Set Active
</button>
)}
<button
type="button"
onClick={() => handleToggleReveal(selectedProfile)}
className="rounded-lg border border-gray-200 px-4 py-2 text-sm font-semibold text-gray-700 transition hover:border-primary-blue hover:bg-gray-50 hover:text-primary-blue"
>
{showSecretsByProfileId[selectedProfile.id] ? 'Hide Values' : 'Reveal Values'}
</button>
<button
type="button"
onClick={() => handleCopyCurl(selectedProfile)}
className="rounded-lg border border-gray-200 px-4 py-2 text-sm font-semibold text-gray-700 transition hover:border-primary-blue hover:bg-gray-50 hover:text-primary-blue"
>
Copy cURL
</button>
</div>
</div>
</div>
</div>
<div className="space-y-6 px-6 py-6">
<div className="grid gap-5 xl:grid-cols-[minmax(0,1.45fr)_360px] xl:items-start">
<div className="overflow-hidden rounded-[28px] border border-slate-200 bg-slate-950 shadow-[0_28px_60px_-42px_rgba(15,23,42,0.75)]">
<div className="border-b border-white/10 bg-[radial-gradient(circle_at_top_left,_rgba(96,165,250,0.2),_transparent_42%),linear-gradient(135deg,_rgba(15,23,42,0.98),_rgba(2,6,23,0.96))] px-5 py-5">
<div className="flex flex-wrap items-start gap-3">
<span className="rounded-full border border-sky-400/30 bg-sky-400/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-100">
{selectedCurlView.method}
</span>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-semibold text-white">
{selectedCurlView.url || 'Endpoint not detected from stored cURL'}
</p>
<p className="mt-1 text-xs font-medium text-slate-400">
{isSelectedProfileRevealed
? 'Saved values are currently rendered inside this request preview.'
: 'Sensitive values stay masked until you explicitly reveal them.'}
</p>
</div>
<span className="rounded-full border border-white/10 bg-white/5 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-300">
Updated {formatUpdatedAt(selectedProfile.updatedAt)}
</span>
</div>
</div>
<div className="space-y-5 px-5 py-5">
<div>
<p className="mb-3 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Shell View</p>
<pre className="max-h-[26rem] overflow-y-auto overscroll-contain rounded-2xl border border-white/8 bg-black/20 px-4 py-4 text-[13px] leading-7 text-slate-100 shadow-inner">
<code>{selectedCurlView.command || 'No cURL stored.'}</code>
</pre>
</div>
{selectedCurlView.payload && (
<div>
<div className="mb-3 flex items-center justify-between gap-3">
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Request Payload</p>
<span className="rounded-full border border-white/10 bg-white/5 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-300">
{selectedCurlView.payloadFormat === 'json' ? 'JSON' : 'Text'}
</span>
</div>
<pre className="max-h-[22rem] overflow-y-auto overscroll-contain rounded-2xl border border-emerald-400/10 bg-emerald-400/5 px-4 py-4 text-[13px] leading-7 text-emerald-50 shadow-inner">
<code>{selectedCurlView.payload}</code>
</pre>
</div>
)}
</div>
</div>
<aside className="space-y-4">
<div className="rounded-[28px] border border-gray-200 bg-gray-50 p-5">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-sm font-semibold text-gray-900">Profile Inspector</p>
<p className="mt-1 text-sm text-gray-500">
Keep the request front and center, then reveal stored values only when you need to inspect or edit them.
</p>
</div>
<span className={`rounded-full border px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.14em] ${isSelectedProfileRevealed ? 'border-indigo-200 bg-indigo-50 text-primary-dark' : 'border-gray-200 bg-white text-gray-500'}`}>
{isSelectedProfileRevealed ? 'Values visible' : 'Values hidden'}
</span>
</div>
<div className="mt-4 grid gap-3">
<InspectorRow
label="Profile State"
value={selectedProfile.id === activeProfileId ? 'Currently active for generation' : 'Inactive profile'}
/>
<InspectorRow
label="Provider"
value={selectedProfile.provider?.providerName || selectedProfile.curlAnalysis?.providerName || 'Awaiting provider name'}
/>
<InspectorRow
label="Endpoint Host"
value={selectedCurlView.host || 'Not detected'}
/>
<InspectorRow
label="Auth Mode"
value={selectedProfile.curlAnalysis?.authMode || 'Not detected'}
/>
<InspectorRow
label="Profile Fields"
value={`${selectedProfileInputs.length} stored value${selectedProfileInputs.length === 1 ? '' : 's'}`}
/>
<InspectorRow
label="Setup"
value={selectedProfile.executionReadiness?.isSetupComplete
? 'All required profile inputs are complete.'
: `${missingInputCount} required field${missingInputCount === 1 ? '' : 's'} still missing`}
valueClassName={selectedProfile.executionReadiness?.isSetupComplete ? '' : 'text-amber-700'}
/>
</div>
{curlWarnings.length > 0 && (
<div className="mt-4 rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3">
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-amber-700">Warnings</p>
<ul className="mt-2 space-y-2 text-sm text-amber-900">
{curlWarnings.map((warning) => (
<li key={warning}>{warning}</li>
))}
</ul>
</div>
)}
</div>
{isSelectedProfileRevealed ? (
<form onSubmit={handleSave} className="overflow-hidden rounded-[28px] border border-gray-200 bg-white shadow-sm">
<div className="border-b border-gray-200 px-5 py-4">
<p className="text-sm font-semibold text-gray-900">Stored Profile Values</p>
<p className="mt-1 text-sm text-gray-500">
These fields appear only in reveal mode and stay tied to this immutable cURL profile.
</p>
</div>
<div className="space-y-4 px-5 py-5">
{selectedProfileInputs.length > 0 ? selectedProfileInputs.map((input) => (
<div key={input.key} className="rounded-2xl border border-gray-200 bg-gray-50 px-4 py-4">
<label className={`mb-1.5 block text-sm font-semibold ${input.required && !input.hasValue && !String(formValues[input.key] || '').trim() ? 'text-error-text' : 'text-gray-900'}`}>
{input.label} {input.required ? <span className="text-error-text">*</span> : null}
</label>
<input
type="text"
value={formValues[input.key] || ''}
onChange={(event) => setFormValues((current) => ({
...current,
[input.key]: input.key === 'senderId'
? event.target.value.toUpperCase()
: event.target.value,
}))}
className={`w-full rounded-xl border bg-white px-4 py-2.5 text-sm font-medium text-gray-900 transition focus:border-transparent focus:outline-none focus:ring-2 ${input.required && !input.hasValue && !String(formValues[input.key] || '').trim() ? 'border-error-text focus:ring-error-text' : 'border-gray-200 focus:ring-primary-blue'}`}
placeholder={input.label}
/>
<p className="mt-2 text-xs font-medium text-gray-500">
{input.secret
? 'Sensitive value revealed for this inspection session.'
: input.source === 'embedded'
? 'Extracted from the accepted cURL and stored against this profile.'
: 'Stored on the profile before publish and runtime sends continue.'}
</p>
</div>
)) : (
<div className="rounded-2xl border border-gray-200 bg-gray-50 px-4 py-4 text-sm text-gray-600">
No profile-level stored values were extracted from this cURL.
</div>
)}
</div>
<div className="flex justify-end border-t border-gray-200 bg-white px-5 py-4">
<button
type="submit"
disabled={saving}
className="flex items-center justify-center gap-2 rounded-lg bg-primary-blue px-6 py-2 text-sm font-semibold text-white transition hover:bg-primary-dark disabled:opacity-50"
>
{saving ? 'Saving…' : 'Save Changes'}
</button>
</div>
</form>
) : (
<div className="rounded-[28px] border border-dashed border-gray-300 bg-white px-5 py-5">
<p className="text-sm font-semibold text-gray-900">Values stay hidden by default</p>
<p className="mt-2 text-sm leading-relaxed text-gray-500">
Reveal mode will render stored values inside the cURL on the left and open the editable field inspector here.
</p>
</div>
)}
</aside>
</div>
</div>
</section>
)}
</div>
);
}