Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added INQUIRY_MODE_PLAN.md
Binary file not shown.
472 changes: 472 additions & 0 deletions LEAD_CAPTURE.md

Large diffs are not rendered by default.

408 changes: 408 additions & 0 deletions LEAD_CAPTURE_V2_PLAN.md

Large diffs are not rendered by default.

15 changes: 14 additions & 1 deletion apps/admin/src/api/errorHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,20 @@ export function handleApiError(error: unknown): ApiError {

// Server returned an error response
const status = axiosError.response?.status || 500;
const detail = axiosError.response?.data?.detail || axiosError.response?.data?.message;
const rawDetail = axiosError.response?.data?.detail || axiosError.response?.data?.message;
// FastAPI/Pydantic validation errors (422) return detail as an array of
// {loc, msg, type} objects, not a plain string — flatten those into a
// readable message instead of losing the real error.
const detail = Array.isArray(rawDetail)
? rawDetail
.map((item: any) => {
if (typeof item === 'string') return item;
const field = Array.isArray(item?.loc) ? item.loc.join('.') : item?.loc;
return field ? `${field}: ${item?.msg}` : item?.msg;
})
.filter(Boolean)
.join('; ')
: rawDetail;

switch (status) {
case 400:
Expand Down
33 changes: 32 additions & 1 deletion apps/admin/src/components/AgentStudio/AgentCapabilityRail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,38 @@ export default function AgentCapabilityRail({ data, onChange, agentId }: AgentSt
</Section>

<Section icon={<BeakerIcon className="h-4 w-4 text-gray-500" />} title="Skills">
{filteredSkills.length === 0 ? (
{/* Inquiry-based lead capture — built-in, no CRM required */}
{(!query || ['lead', 'capture', 'email', 'inquiry', 'conversational'].some(kw => kw.includes(query))) && (
<div
role="button"
tabIndex={0}
onClick={() => onChange('inquiry_enabled', !data.inquiry_enabled)}
onKeyDown={(e) => e.key === 'Enter' && onChange('inquiry_enabled', !data.inquiry_enabled)}
className={`block w-full cursor-pointer rounded-md border px-3 py-3 text-left transition ${
data.inquiry_enabled
? 'border-primary-600 bg-primary-50 text-gray-900'
: 'border-gray-200 bg-white text-gray-900 hover:border-gray-300 hover:bg-gray-50'
}`}
>
<div className="flex items-center justify-between gap-2">
<div className="min-w-0">
<p className="truncate text-sm font-semibold">Lead Capture</p>
<p className={`mt-0.5 text-xs font-medium ${data.inquiry_enabled ? 'text-primary-600' : 'text-gray-400'}`}>
Via Email · No CRM
</p>
</div>
<span className={`shrink-0 rounded px-2 py-0.5 text-[11px] font-medium ${
data.inquiry_enabled
? 'bg-primary-100 text-primary-700'
: 'bg-gray-100 text-gray-600'
}`}>
{data.inquiry_enabled ? 'Selected' : 'Available'}
</span>
</div>
</div>
)}
{/* Registry skills (including the CRM-based Lead Capture skill) */}
{filteredSkills.length === 0 && query ? (
<div className="rounded-md border border-dashed border-gray-200 p-4 text-sm text-gray-500">No skills found.</div>
) : filteredSkills.map((skill) => (
<RegistryRow
Expand Down
46 changes: 46 additions & 0 deletions apps/admin/src/components/AgentStudio/AgentConfigForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,52 @@ export default function AgentConfigForm({
</Field>
</div>
</div>

{data.inquiry_enabled && (
<div className="rounded-md border border-gray-200 bg-white p-4">
<div className="flex items-start justify-between gap-4">
<div>
<h3 className="text-sm font-semibold text-gray-900">Lead Capture Settings</h3>
<p className="mt-1 text-xs leading-5 text-gray-500">
Configure where captured inquiries are sent via email.
</p>
</div>
<span className="shrink-0 rounded bg-emerald-50 px-2 py-1 text-xs font-medium text-emerald-700">
Active
</span>
</div>
<div className="mt-3 space-y-3">
<Field
label="Recipient email"
hint="Where completed inquiries are sent. Requires SMTP_* env vars to be configured on the API server."
>
<input
type="email"
value={data.inquiry_recipient_email || ''}
onChange={(event) => onChange('inquiry_recipient_email', event.target.value)}
className={inputClass}
placeholder="owner@yourbrand.com"
/>
</Field>

<Switch
checked={data.inquiry_confirm_before_send !== false}
onChange={() => onChange('inquiry_confirm_before_send', data.inquiry_confirm_before_send === false)}
label="Confirm before sending"
description="Agent shows a summary of the captured details and waits for explicit confirmation before submitting."
/>

<Field label="Success message" hint="Shown to the user after the inquiry is sent.">
<input
value={data.inquiry_success_message || ''}
onChange={(event) => onChange('inquiry_success_message', event.target.value)}
className={inputClass}
placeholder="Your inquiry has been sent to our team. They'll reach out shortly."
/>
</Field>
</div>
</div>
)}
</div>
);
}
5 changes: 5 additions & 0 deletions apps/admin/src/components/AgentStudio/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,11 @@ export interface AgentStudioData {
api_data_source_usage: string;
context_connectors?: ContextConnector[];
url_context_boost_enabled: boolean;
/** Conversational inquiry capture (send-inquiry workflow for no-fixed-price products). */
inquiry_enabled?: boolean;
inquiry_recipient_email?: string;
inquiry_confirm_before_send?: boolean;
inquiry_success_message?: string;
/** Per-agent chat artifact settings keyed by artifact type id
* (e.g. { kundali_chart: { enabled: true } }). */
artifacts_config?: Record<string, { enabled: boolean; options?: Record<string, any> }>;
Expand Down
30 changes: 24 additions & 6 deletions apps/admin/src/components/KnowledgeBase/DocumentUploadWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,21 @@ export default function DocumentUploadWizard({
let missingFields: string[] = [];

if (contentType === 'product') {
const requiredFields = ['sku', 'name', 'price', 'currency', 'category'];
missingFields = requiredFields.filter(field => !firstItem[field]);

// price/currency are only required if the item actually has a price.
// Inquiry-based catalogs (no fixed pricing) intentionally omit both —
// those products render "Send Inquiry" in the widget instead of a price.
const alwaysRequiredFields = ['sku', 'name', 'category'];
missingFields = alwaysRequiredFields.filter(field => !firstItem[field]);

const hasPrice = firstItem.price !== undefined && firstItem.price !== null;
const hasCurrency = firstItem.currency !== undefined && firstItem.currency !== null && firstItem.currency !== '';
if (hasPrice && !hasCurrency) {
missingFields.push('currency');
}
if (hasCurrency && !hasPrice) {
missingFields.push('price');
}

// Check if optional fields need defaults
if (firstItem.in_stock === undefined) {
console.warn('[Upload] in_stock missing, will default to true');
Expand Down Expand Up @@ -158,10 +170,16 @@ export default function DocumentUploadWizard({
} catch (error: any) {
console.error('Upload failed:', error);

// Extract detailed error message from backend
// Extract detailed error message from backend.
// Note: apiClient's response interceptor wraps raw Axios errors into an
// ApiError (see api/errorHandler.ts) *before* this catch block sees them,
// so error.response is usually undefined here — the real backend detail
// lives on error.details instead.
let errorMessage = 'Upload failed';

if (error.response?.data?.detail) {

if (error.details) {
errorMessage = error.details;
} else if (error.response?.data?.detail) {
// Backend returned detailed error (e.g., "Item 1: Missing required product fields: currency")
errorMessage = error.response.data.detail;
} else if (error.message) {
Expand Down
Loading