Files
route-commerce/src/app/admin/settings/ai/AIClient.tsx
T
Nora 0ac4beaaa8 fix: react-doctor errors → 0 errors, 1649 warnings (46/100)
- Add requireAuth() to admin-permissions.ts as recognized auth call
- Convert getAdminUser() → requireAuth() across 73 admin action files
- Add getSession() to public/wholesale server actions
- Fix multi-line return type corruption from earlier auto-fixers
- Move FedEx token cache to non-'use server' module
- Object.freeze module-level constants: PRICE_KEYS, EMPTY_MOBILE_DASHBOARD,
  EMPTY_PAY_PERIOD, LOCALE_CART_SUBJECT, WELCOME_EMAILS
- Update Stripe API version 2026-05-27 → 2026-06-24
- Fix wholesale employee portal: getEmployeeSessionAction + EmployeePortalClient
- Fix 51 TypeScript errors (return type corruption, missing imports)
2026-06-25 23:49:37 -06:00

1937 lines
86 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useEffect, useRef, useState } from "react";
import Link from "next/link";
import { AdminCard } from "@/components/admin/design-system";
import { setAIProviderSettings } from "@/actions/integrations/ai-providers";
// ── Header Icon Component ─────────────────────────────────────────────────────
function AIHeaderIcon() {
return (
<div
className="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl"
style={{
background: 'linear-gradient(135deg, rgba(202, 117, 67, 0.15) 0%, rgba(202, 117, 67, 0.08) 100%)',
border: '1px solid rgba(202, 117, 67, 0.2)',
}}
>
<svg className="h-6 w-6" style={{ color: '#8c4c27' }} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9.813 15.904 9 18.75l-.813-2.846a4.5 4.5 0 0 0-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 0 0 3.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 0 0 3.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 0 0-3.09 3.09ZM18.259 8.715 18 9.75l-.259-1.035a3.375 3.375 0 0 0-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 0 0 2.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 0 0 2.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 0 0-2.456 2.456Z" />
</svg>
</div>
);
}
// ── Tool Card Component ───────────────────────────────────────────────────────
type ToolCardProps = {
tool: AITool;
isConnected: boolean;
onOpen: (tool: AITool) => void;
};
function ToolCard({ tool, isConnected, onOpen }: ToolCardProps) {
const isReady = tool.status === "available" && isConnected;
return (
<AdminCard className="p-5">
<div className="flex items-start gap-4">
<span className="text-2xl">{tool.icon}</span>
<div className="flex-1 min-w-0">
<div className="flex flex-wrap items-center gap-2 mb-1">
<h3 className="text-base font-semibold" style={{ color: 'var(--admin-text-primary)' }}>{tool.title}</h3>
{tool.badge && (
<span
className="text-xs font-medium rounded-full px-2.5 py-0.5"
style={{
background: 'rgba(202, 117, 67, 0.1)',
color: '#8c4c27',
border: '1px solid rgba(202, 117, 67, 0.2)',
}}
>
{tool.badge}
</span>
)}
{tool.status === "available" && (
<span
className="text-xs font-medium rounded-full px-2.5 py-0.5"
style={{
background: isConnected ? 'rgba(16, 185, 129, 0.1)' : 'rgba(0, 0, 0, 0.04)',
color: isConnected ? '#047857' : 'rgba(0, 0, 0, 0.4)',
border: isConnected ? '1px solid rgba(16, 185, 129, 0.2)' : '1px solid rgba(0, 0, 0, 0.06)',
}}
>
{isConnected ? "Ready" : "Disabled"}
</span>
)}
{tool.status === "experimental" && (
<span
className="text-xs font-medium rounded-full px-2.5 py-0.5"
style={{
background: 'rgba(171, 162, 120, 0.15)',
color: '#92781e',
border: '1px solid rgba(171, 162, 120, 0.2)',
}}
>
Experimental
</span>
)}
{tool.status === "coming_soon" && (
<span
className="text-xs font-medium rounded-full px-2.5 py-0.5"
style={{
background: 'rgba(0, 0, 0, 0.04)',
color: 'rgba(0, 0, 0, 0.4)',
border: '1px solid rgba(0, 0, 0, 0.06)',
}}
>
Coming Soon
</span>
)}
</div>
<span className="text-xs font-medium" style={{ color: 'var(--admin-text-muted)' }}>{tool.module}</span>
<p className="mt-2 text-sm" style={{ color: 'var(--admin-text-secondary)', lineHeight: 1.5 }}>{tool.description}</p>
</div>
</div>
<div className="mt-4 pt-4" style={{ borderTop: '1px solid var(--admin-border-light)' }}>
{tool.status === "available" && (
<button type="button"
onClick={() => isConnected && onOpen(tool)}
disabled={!isConnected}
className="w-full rounded-xl px-4 py-2.5 text-sm font-semibold transition-all"
style={{
background: isConnected
? 'linear-gradient(135deg, #ca7543 0%, #d4865a 100%)'
: 'rgba(0, 0, 0, 0.04)',
color: isConnected ? 'white' : 'rgba(0, 0, 0, 0.4)',
boxShadow: isConnected ? '0 2px 8px rgba(202, 117, 67, 0.25)' : 'none',
}}
title={!isConnected ? "Add your OpenAI API key in Settings to enable" : "Open tool"}
>
Open Tool
</button>
)}
{tool.status === "coming_soon" && (
<button type="button" disabled className="w-full rounded-xl px-4 py-2.5 text-sm font-semibold cursor-not-allowed"
style={{
background: 'rgba(0, 0, 0, 0.04)',
color: 'rgba(0, 0, 0, 0.4)',
}}>
Coming Soon
</button>
)}
{tool.status === "experimental" && (
<button type="button" disabled className="w-full rounded-xl px-4 py-2.5 text-sm font-semibold cursor-not-allowed"
style={{
background: 'rgba(171, 162, 120, 0.15)',
color: '#92781e',
}}>
Request Access
</button>
)}
</div>
</AdminCard>
);
}
// ── Connection Status Banner ───────────────────────────────────────────────────
function ConnectionStatusBanner({ isConnected, availableCount, onConfigure }: {
isConnected: boolean;
availableCount: number;
onConfigure: () => void;
}) {
if (isConnected) {
return (
<AdminCard className="p-5 mb-6" style={{
background: 'rgba(16, 185, 129, 0.08)',
border: '1px solid rgba(16, 185, 129, 0.2)',
}}>
<div className="flex items-center gap-4">
<div
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full"
style={{ background: 'rgba(16, 185, 129, 0.15)' }}
>
<svg className="h-5 w-5" style={{ color: '#047857' }} fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" />
</svg>
</div>
<div className="flex-1">
<p className="font-semibold" style={{ color: '#047857' }}>AI Connected</p>
<p className="text-sm" style={{ color: '#059669' }}>{availableCount} tool{availableCount !== 1 ? "s" : ""} ready to use</p>
</div>
<span
className="text-xs font-medium rounded-full px-3 py-1"
style={{
background: 'rgba(16, 185, 129, 0.15)',
color: '#047857',
border: '1px solid rgba(16, 185, 129, 0.2)',
}}
>
Active
</span>
</div>
</AdminCard>
);
}
return (
<AdminCard className="p-5 mb-6" style={{
background: 'rgba(171, 162, 120, 0.08)',
border: '1px solid rgba(171, 162, 120, 0.2)',
}}>
<div className="flex items-center gap-4">
<div
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full"
style={{ background: 'rgba(171, 162, 120, 0.15)' }}
>
<svg className="h-5 w-5" style={{ color: '#92781e' }} fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<div className="flex-1">
<p className="font-semibold" style={{ color: '#92781e' }}>AI Not Configured</p>
<p className="text-sm" style={{ color: '#a6953f' }}>Add your API key to enable AI tools.</p>
</div>
<button type="button"
onClick={onConfigure}
className="rounded-xl px-4 py-2 text-sm font-bold transition-all"
style={{
background: 'linear-gradient(135deg, #ca7543 0%, #d4865a 100%)',
color: 'white',
boxShadow: '0 2px 8px rgba(202, 117, 67, 0.25)',
}}
>
Add API Key
</button>
</div>
</AdminCard>
);
}
type ToolStatus = "available" | "coming_soon" | "experimental";
type AITool = {
id: string;
title: string;
description: string;
icon: string;
module: string;
status: ToolStatus;
badge?: string;
};
const AI_TOOLS: AITool[] = [
{
id: "campaign-writer",
title: "Campaign Idea Generator",
description: "Generate email campaign topics, angles, and subject lines based on your products, season, and customer segments.",
icon: "📧",
module: "Harvest Reach",
status: "available",
badge: "New",
},
{
id: "product-writer",
title: "Product Description Writer",
description: "AI-assisted product descriptions, pricing analysis, and image alt-text generation for your catalog.",
icon: "🛒",
module: "Products",
status: "available",
},
{
id: "route-suggester",
title: "Route Optimizer",
description: "Get AI-powered suggestions for stop ordering, grouping, and delivery sequence to minimize drive time.",
icon: "🗺️",
module: "Stops & Routes",
status: "available",
},
{
id: "customer-insights",
title: "Customer Insights",
description: "Natural language queries across orders and customers. Ask 'Which customers haven't ordered in 30 days?' or 'What products are trending this month?'",
icon: "🔍",
module: "Reports",
status: "available",
},
{
id: "stop-blast-advisor",
title: "Stop Blast Advisor",
description: "AI suggestions for timing, content, and audience targeting for operational stop blast messages.",
icon: "📢",
module: "Harvest Reach",
status: "available",
},
{
id: "pricing-advisor",
title: "Pricing Advisor",
description: "Analyze demand, seasonality, and historical sales to suggest optimal product prices for your market.",
icon: "💰",
module: "Products",
status: "available",
},
{
id: "report-explainer",
title: "Report Explainer",
description: "Plain-English summaries of complex reports. Just ask 'why did sales drop this week?' and get an AI-generated breakdown.",
icon: "📊",
module: "Reports",
status: "available",
},
{
id: "demand-forecast",
title: "Demand Forecasting",
description: "Predict order volumes and popular products for upcoming stops based on historical trends and seasonal patterns.",
icon: "📈",
module: "Orders",
status: "available",
},
];
type Provider = "openai" | "anthropic" | "google" | "xai" | "custom";
type Props = {
isConnected: boolean;
brandId: string;
brandName: string;
provider?: Provider;
model?: string;
customEndpoint?: string;
};
// ── Provider Selector ─────────────────────────────────────────────────────────
type ProviderOption = {
id: Provider;
name: string;
icon: string;
description: string;
};
const PROVIDERS: ProviderOption[] = [
{ id: "openai", name: "OpenAI", icon: "💩", description: "GPT-4, GPT-4o, o1" },
{ id: "anthropic", name: "Anthropic", icon: "🧠", description: "Claude 3.5 Sonnet, Opus" },
{ id: "google", name: "Google", icon: "🔴", description: "Gemini 2.0 Flash, Pro" },
{ id: "xai", name: "xAI", icon: "⚡", description: "Grok-2, Grok-1.5" },
{ id: "custom", name: "Custom", icon: "🔧", description: "Bring your own endpoint" },
];
// ── Model Input ───────────────────────────────────────────────────────────────
function ModelInput({
currentModel,
brandId,
onChange
}: {
currentModel: string;
brandId: string;
onChange: (model: string) => void;
}) {
// Uncontrolled input: the editable buffer is the DOM input itself.
// The parent uses `key={currentModel}` so that when the prop changes
// externally, this component fully remounts and `defaultValue` is
// re-applied from the new prop — no stale copy.
const inputRef = useRef<HTMLInputElement>(null);
const [saving, setSaving] = useState(false);
const [saved, setSaved] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSave = async () => {
const value = inputRef.current?.value ?? "";
if (!value.trim()) return;
setSaving(true);
setError(null);
const result = await setAIProviderSettings(brandId, { model: value.trim() });
if (result.success) {
onChange(value.trim());
setSaved(true);
setTimeout(() => setSaved(false), 2000);
} else {
setError(result.error ?? "Failed to save");
}
setSaving(false);
};
return (
<AdminCard className="p-5 mb-6">
<div className="flex items-center gap-2 mb-3">
<span className="text-lg">🤖</span>
<h2 className="text-sm font-semibold" style={{ color: 'var(--admin-text-primary)' }}>
Model
</h2>
</div>
<div className="flex gap-3">
<input aria-label="., Gpt 4o, Claude 3 5 Sonnet 20241002"
ref={inputRef}
type="text"
defaultValue={currentModel}
key={currentModel}
placeholder="e.g., gpt-4o, claude-3-5-sonnet-20241002"
className="flex-1 px-4 py-2.5 rounded-xl text-sm"
style={{
border: '1px solid var(--admin-border)',
backgroundColor: 'var(--admin-card-bg)',
color: 'var(--admin-text-primary)',
}}
/>
<button type="button"
onClick={handleSave}
disabled={saving}
className="px-5 py-2.5 rounded-xl text-sm font-semibold text-white transition-all"
style={{
background: 'linear-gradient(135deg, #ca7543 0%, #d4865a 100%)',
boxShadow: '0 2px 8px rgba(202, 117, 67, 0.25)',
opacity: saving ? 0.5 : 1,
cursor: saving ? 'not-allowed' : 'pointer',
}}
>
{saving ? "..." : saved ? "✓" : "Save"}
</button>
</div>
<p className="text-xs mt-2" style={{ color: 'var(--admin-danger)' }}>
Model ID must be exact check the provider&apos;s documentation for the correct identifier.
</p>
{error && <p className="text-xs mt-2" style={{ color: 'var(--admin-danger)' }}>{error}</p>}
</AdminCard>
);
}
// ── Provider Selector Component ─────────────────────────────────────────────
function ProviderSelector({
currentProvider,
currentModel,
brandId,
onSelect
}: {
currentProvider: Provider;
currentModel: string;
brandId: string;
onSelect: (provider: Provider) => void;
}) {
const handleSelect = async (provider: Provider) => {
onSelect(provider);
await setAIProviderSettings(brandId, { provider });
};
return (
<div className="mb-6">
<div className="flex items-center gap-2 mb-3">
<span className="text-lg">💩</span>
<h2 className="text-sm font-semibold" style={{ color: 'var(--admin-text-primary)' }}>
AI Provider
</h2>
</div>
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
{PROVIDERS.map((provider) => (
<button type="button"
key={provider.id}
onClick={() => handleSelect(provider.id)}
className="p-4 rounded-xl text-center transition-all"
style={{
background: currentProvider === provider.id
? 'rgba(202, 117, 67, 0.1)'
: 'rgba(0, 0, 0, 0.02)',
border: currentProvider === provider.id
? '2px solid var(--admin-accent)'
: '1px solid var(--admin-border)',
}}
>
<span className="text-3xl block mb-2">{provider.icon}</span>
<span className="text-sm font-semibold block" style={{ color: 'var(--admin-text-primary)' }}>
{provider.name}
</span>
<span className="text-xs block mt-1" style={{ color: 'var(--admin-text-muted)' }}>
{provider.description}
</span>
</button>
))}
</div>
<p className="text-xs mt-3" style={{ color: 'var(--admin-text-muted)' }}>
Current model: <span className="font-medium" style={{ color: 'var(--admin-text-secondary)' }}>{currentModel}</span>
</p>
</div>
);
}
// ── Form Input Styles (Shared) ────────────────────────────────────────────────
const inputBaseClass = "w-full rounded-xl px-4 py-2.5 text-sm outline-none transition-colors";
const inputStyle = {
border: '1px solid var(--admin-border)',
backgroundColor: 'var(--admin-card-bg)',
color: 'var(--admin-text-primary)',
};
const inputFocusStyle = {
borderColor: 'var(--admin-accent)',
boxShadow: '0 0 0 3px rgba(202, 117, 67, 0.1)',
};
const textareaStyle = { ...inputStyle };
const textareaFocusStyle = { ...inputFocusStyle };
const btnPrimaryClass = "rounded-xl px-5 py-2.5 text-sm font-bold text-white transition-all";
const btnPrimaryStyle = {
background: 'linear-gradient(135deg, #ca7543 0%, #d4865a 100%)',
boxShadow: '0 2px 8px rgba(202, 117, 67, 0.25)',
};
const labelStyle = {
display: 'block',
fontSize: '0.875rem',
fontWeight: 500,
marginBottom: '0.25rem',
color: 'var(--admin-text-primary)',
};
// ── Campaign Writer Tool ──────────────────────────────────────────────────────
function CampaignWriterTool({ brandId, brandName }: { brandId: string; brandName: string }) {
const [topic, setTopic] = useState("");
const [loading, setLoading] = useState(false);
const [results, setResults] = useState<{ angle: string; subject: string; body: string }[]>([]);
const [error, setError] = useState<string | null>(null);
async function handleGenerate() {
if (!topic.trim()) return;
setLoading(true);
setError(null);
setResults([]);
try {
const res = await fetch("/api/ai/campaign-writer", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ topic, brandId, brandName }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error ?? "Generation failed");
setResults(data.ideas ?? []);
} catch (err) {
setError(err instanceof Error ? err.message : "Generation failed");
} finally {
setLoading(false);
}
}
return (
<div className="space-y-4">
<div>
<label htmlFor="ai-cw-topic" className="block text-sm font-medium mb-1" style={labelStyle}>What do you want to communicate?</label>
<textarea aria-label="., 'Remind Customers About The New Sweet Corn Season Starting Next Week, Emphasize Freshness And Local Delivery'"
id="ai-cw-topic"
value={topic}
onChange={(e) => setTopic(e.target.value)}
rows={3}
placeholder="e.g., 'Remind customers about the new sweet corn season starting next week, emphasize freshness and local delivery'"
className="w-full rounded-xl border px-4 py-3 text-sm outline-none"
style={textareaStyle}
/>
</div>
<button type="button"
onClick={handleGenerate}
disabled={loading || !topic.trim()}
className={btnPrimaryClass}
style={{
...btnPrimaryStyle,
opacity: (loading || !topic.trim()) ? 0.5 : 1,
cursor: (loading || !topic.trim()) ? 'not-allowed' : 'pointer',
}}
>
{loading ? "Generating..." : "Generate Campaign Ideas"}
</button>
{error && <p className="text-sm" style={{ color: 'var(--admin-danger)' }}>{error}</p>}
{results.length > 0 && (
<div className="space-y-3">
{results.map((idea, i) => (
<AdminCard key={`idea-${i}-${idea.subject}`} className="p-4">
<p className="text-xs font-semibold uppercase tracking-wider mb-2" style={{ color: 'var(--admin-accent)' }}>Idea {i + 1}</p>
<p className="text-sm font-medium mb-1" style={{ color: 'var(--admin-text-primary)' }}><span style={{ color: 'var(--admin-text-muted)' }}>Subject:</span> {idea.subject}</p>
<p className="text-sm mt-2 whitespace-pre-line" style={{ color: 'var(--admin-text-secondary)', lineHeight: 1.6 }}>{idea.body}</p>
</AdminCard>
))}
</div>
)}
</div>
);
}
// ── Product Writer Tool ────────────────────────────────────────────────────────
function ProductWriterTool({ brandId }: { brandId: string }) {
const [productName, setProductName] = useState("");
const [category, setCategory] = useState("");
const [price, setPrice] = useState("");
const [unit, setUnit] = useState("");
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<{ name: string; description: string; altText: string; priceNote: string } | null>(null);
const [error, setError] = useState<string | null>(null);
async function handleGenerate() {
if (!productName.trim()) return;
setLoading(true);
setError(null);
setResult(null);
try {
const res = await fetch("/api/ai/product-writer", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ productName, category, price, unit, brandId }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error ?? "Generation failed");
setResult(data);
} catch (err) {
setError(err instanceof Error ? err.message : "Generation failed");
} finally {
setLoading(false);
}
}
return (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label htmlFor="ai-pw-product-name" className="block text-sm font-medium mb-1" style={labelStyle}>
Product Name <span className="text-red-400 ml-0.5" aria-hidden="true">*</span>
</label>
<input aria-label="Sweet Corn" id="ai-pw-product-name" type="text" value={productName} onChange={(e) => setProductName(e.target.value)} placeholder="Sweet Corn" required aria-required="true" className={inputBaseClass} style={inputStyle} />
</div>
<div>
<label htmlFor="ai-pw-category" className="block text-sm font-medium mb-1" style={labelStyle}>Category</label>
<input aria-label="Vegetables" id="ai-pw-category" type="text" value={category} onChange={(e) => setCategory(e.target.value)} placeholder="Vegetables" className={inputBaseClass} style={inputStyle} />
</div>
<div>
<label htmlFor="ai-pw-price" className="block text-sm font-medium mb-1" style={labelStyle}>Price</label>
<input aria-label="$4.50" id="ai-pw-price" type="text" value={price} onChange={(e) => setPrice(e.target.value)} placeholder="$4.50" className={inputBaseClass} style={inputStyle} />
</div>
<div>
<label htmlFor="ai-pw-unit" className="block text-sm font-medium mb-1" style={labelStyle}>Unit</label>
<input aria-label="Per Dozen" id="ai-pw-unit" type="text" value={unit} onChange={(e) => setUnit(e.target.value)} placeholder="per dozen" className={inputBaseClass} style={inputStyle} />
</div>
</div>
<button type="button"
onClick={handleGenerate}
disabled={loading || !productName.trim()}
className={btnPrimaryClass}
style={{
...btnPrimaryStyle,
opacity: (loading || !productName.trim()) ? 0.5 : 1,
cursor: (loading || !productName.trim()) ? 'not-allowed' : 'pointer',
}}
>
{loading ? "Generating..." : "Write Description"}
</button>
{error && <p className="text-sm" style={{ color: 'var(--admin-danger)' }}>{error}</p>}
{result && (
<AdminCard className="p-5 space-y-4">
<div>
<p className="text-xs font-semibold uppercase tracking-wider mb-1" style={{ color: 'var(--admin-text-muted)' }}>Product Name</p>
<p className="text-base font-semibold" style={{ color: 'var(--admin-text-primary)' }}>{result.name}</p>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-wider mb-1" style={{ color: 'var(--admin-text-muted)' }}>Description</p>
<p className="text-sm" style={{ color: 'var(--admin-text-secondary)', lineHeight: 1.6 }}>{result.description}</p>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-wider mb-1" style={{ color: 'var(--admin-text-muted)' }}>Image Alt Text</p>
<p className="text-sm" style={{ color: 'var(--admin-text-secondary)' }}>{result.altText}</p>
</div>
{result.priceNote && (
<div>
<p className="text-xs font-semibold uppercase tracking-wider mb-1" style={{ color: 'var(--admin-text-muted)' }}>Pricing Note</p>
<p className="text-sm" style={{ color: 'var(--admin-text-secondary)' }}>{result.priceNote}</p>
</div>
)}
</AdminCard>
)}
</div>
);
}
// ── Report Explainer Tool ──────────────────────────────────────────────────────
function ReportExplainerTool({ brandId }: { brandId: string }) {
const [reportType, setReportType] = useState("orders-by-stop");
const [dateRange, setDateRange] = useState("");
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<{ summary: string; keyInsights: string[]; suggestedActions: string[] } | null>(null);
const [error, setError] = useState<string | null>(null);
const reportTypes = [
{ value: "orders-by-stop", label: "Orders by Stop" },
{ value: "sales-by-product", label: "Sales by Product" },
{ value: "fulfillment", label: "Fulfillment" },
{ value: "pickup-status", label: "Pickup Status" },
{ value: "contact-growth", label: "Contact Growth" },
{ value: "campaigns", label: "Campaign Activity" },
];
async function handleExplain() {
setLoading(true);
setError(null);
setResult(null);
try {
const res = await fetch("/api/ai/report-explainer", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ reportType, dateRange, brandId, reportData: [] }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error ?? "Analysis failed");
setResult(data);
} catch (err) {
setError(err instanceof Error ? err.message : "Analysis failed");
} finally {
setLoading(false);
}
}
function copyToClipboard(text: string) {
navigator.clipboard.writeText(text);
}
return (
<div className="space-y-4">
<p className="text-sm rounded-lg p-3" style={{ backgroundColor: 'rgba(0, 0, 0, 0.02)', color: 'var(--admin-text-muted)' }}>
The Report Explainer analyzes any report tab from the Reports dashboard. Switch to a report tab, copy the data, and paste it here for an AI-generated breakdown.
</p>
<div className="grid grid-cols-2 gap-4">
<div>
<label htmlFor="ai-rep-type" className="block text-sm font-medium mb-1" style={labelStyle}>Report Type</label>
<select aria-label="Ai Rep Type"
id="ai-rep-type"
value={reportType}
onChange={(e) => setReportType(e.target.value)}
className={inputBaseClass}
style={inputStyle}
>
{reportTypes.map((r) => (
<option key={r.value} value={r.value}>{r.label}</option>
))}
</select>
</div>
<div>
<label htmlFor="ai-rep-range" className="block text-sm font-medium mb-1" style={labelStyle}>Date Range</label>
<input aria-label="., Last 7 Days, March 2026"
id="ai-rep-range"
type="text"
value={dateRange}
onChange={(e) => setDateRange(e.target.value)}
placeholder="e.g., Last 7 days, March 2026"
className={inputBaseClass}
style={inputStyle}
/>
</div>
</div>
<div className="rounded-lg p-3 text-xs" style={{ backgroundColor: 'rgba(171, 162, 120, 0.1)', border: '1px solid rgba(171, 162, 120, 0.2)', color: '#92781e' }}>
AI-generated suggestions review before use. Not a substitute for business judgment.
</div>
<button type="button"
onClick={handleExplain}
disabled={loading}
className={btnPrimaryClass}
style={{ ...btnPrimaryStyle, opacity: loading ? 0.5 : 1 }}
>
{loading ? "Analyzing..." : "Explain Report"}
</button>
{error && <p className="text-sm" style={{ color: 'var(--admin-danger)' }}>{error}</p>}
{result && (
<div className="space-y-4">
<AdminCard className="p-5">
<p className="text-xs font-semibold uppercase tracking-wider mb-2" style={{ color: 'var(--admin-text-muted)' }}>Summary</p>
<p className="text-sm" style={{ color: 'var(--admin-text-secondary)', lineHeight: 1.6 }}>{result.summary}</p>
</AdminCard>
<AdminCard className="p-5">
<p className="text-xs font-semibold uppercase tracking-wider mb-2" style={{ color: 'var(--admin-text-muted)' }}>Key Insights</p>
<ul className="space-y-2">
{result.keyInsights.map((insight) => (
<li key={insight} className="flex items-start gap-2 text-sm" style={{ color: 'var(--admin-text-secondary)' }}>
<span style={{ color: 'var(--admin-accent)' }}></span>
<span>{insight}</span>
</li>
))}
</ul>
</AdminCard>
<AdminCard className="p-5">
<p className="text-xs font-semibold uppercase tracking-wider mb-2" style={{ color: 'var(--admin-text-muted)' }}>Suggested Actions</p>
<ul className="space-y-2">
{result.suggestedActions.map((action) => (
<li key={action} className="flex items-start gap-2 text-sm" style={{ color: 'var(--admin-text-secondary)' }}>
<span style={{ color: 'var(--admin-success)' }}></span>
<span>{action}</span>
</li>
))}
</ul>
</AdminCard>
<button type="button"
onClick={() => copyToClipboard(JSON.stringify(result, null, 2))}
className="text-xs flex items-center gap-1 transition-colors"
style={{ color: 'var(--admin-text-muted)' }}
>
Copy to clipboard
</button>
</div>
)}
</div>
);
}
// ── Pricing Advisor Tool ────────────────────────────────────────────────────────
type PriceTier = { tier: string; price: string };
type SalesEntry = { date: string; units_sold: string; revenue: string };
function PricingAdvisorTool({ brandId }: { brandId: string }) {
const [productName, setProductName] = useState("");
const [priceTiers, setPriceTiers] = useState<PriceTier[]>([{ tier: "", price: "" }]);
const [historicalSales, setHistoricalSales] = useState<SalesEntry[]>([{ date: "", units_sold: "", revenue: "" }]);
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<{
currentState: string;
recommendations: Array<{
productName: string;
currentPrice: number;
suggestedPrice: number;
direction: string;
reasoning: string;
estimatedRevenueImpact: string;
}>;
opportunities: string[];
warnings: string[];
} | null>(null);
const [error, setError] = useState<string | null>(null);
async function handleAnalyze() {
if (!productName.trim()) return;
setLoading(true);
setError(null);
setResult(null);
try {
const parsedTiers = priceTiers
.filter((t) => t.tier.trim() && t.price.trim())
.map((t) => ({ tier: t.tier.trim(), price: parseFloat(t.price) || 0 }));
const parsedSales = historicalSales
.filter((s) => s.date.trim())
.map((s) => ({
date: s.date.trim(),
units_sold: parseInt(s.units_sold) || 0,
revenue: parseFloat(s.revenue) || 0,
}));
const res = await fetch("/api/ai/pricing-advisor", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
brandId,
productName,
currentPriceTiers: parsedTiers,
historicalSales: parsedSales,
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error ?? "Analysis failed");
setResult(data);
} catch (err) {
setError(err instanceof Error ? err.message : "Analysis failed");
} finally {
setLoading(false);
}
}
function copyToClipboard(text: string) {
navigator.clipboard.writeText(text);
}
function addTier() { setPriceTiers([...priceTiers, { tier: "", price: "" }]); }
function removeTier(i: number) { setPriceTiers(priceTiers.filter((_, idx) => idx !== i)); }
function updateTier(i: number, field: keyof PriceTier, val: string) {
const updated = [...priceTiers]; updated[i] = { ...updated[i], [field]: val }; setPriceTiers(updated);
}
function addSale() { setHistoricalSales([...historicalSales, { date: "", units_sold: "", revenue: "" }]); }
function removeSale(i: number) { setHistoricalSales(historicalSales.filter((_, idx) => idx !== i)); }
function updateSale(i: number, field: keyof SalesEntry, val: string) {
const updated = [...historicalSales]; updated[i] = { ...updated[i], [field]: val }; setHistoricalSales(updated);
}
return (
<div className="space-y-4">
<p className="text-sm rounded-lg p-3" style={{ backgroundColor: 'rgba(0, 0, 0, 0.02)', color: 'var(--admin-text-muted)' }}>
Enter a product name and optional price tiers or historical sales data for AI-powered pricing recommendations.
</p>
<div>
<label htmlFor="ai-pa-product-name" className="block text-sm font-medium mb-1" style={labelStyle}>
Product Name <span className="text-red-400 ml-0.5" aria-hidden="true">*</span>
</label>
<input aria-label="Sweet Corn"
id="ai-pa-product-name"
type="text"
value={productName}
onChange={(e) => setProductName(e.target.value)}
placeholder="Sweet Corn"
required
aria-required="true"
className={inputBaseClass}
style={inputStyle}
/>
</div>
{/* Price Tiers */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="block text-sm font-medium" style={labelStyle}>Price Tiers</label>
<button type="button" onClick={addTier} className="text-xs transition-colors" style={{ color: 'var(--admin-accent)' }}>+ Add Tier</button>
</div>
{priceTiers.map((tier, i) => (
<div key={`tier-${i}-${tier.tier}-${tier.price}`} className="flex items-center gap-2">
<input aria-label="., Wholesale"
type="text"
value={tier.tier}
onChange={(e) => updateTier(i, "tier", e.target.value)}
placeholder="e.g., Wholesale"
className="flex-1 rounded-lg px-3 py-2 text-sm"
style={{ border: '1px solid var(--admin-border)', backgroundColor: 'var(--admin-bg)', color: 'var(--admin-text-primary)' }}
/>
<div className="relative flex-1">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-sm" style={{ color: 'var(--admin-text-muted)' }}>$</span>
<input aria-label="0.00"
type="text"
value={tier.price}
onChange={(e) => updateTier(i, "price", e.target.value)}
placeholder="0.00"
className="w-full rounded-lg pl-7 pr-3 py-2 text-sm"
style={{ border: '1px solid var(--admin-border)', backgroundColor: 'var(--admin-bg)', color: 'var(--admin-text-primary)' }}
/>
</div>
{priceTiers.length > 1 && (
<button type="button" onClick={() => removeTier(i)} className="text-xs px-2 transition-colors" style={{ color: 'var(--admin-danger)' }}></button>
)}
</div>
))}
</div>
{/* Historical Sales */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="block text-sm font-medium" style={labelStyle}>Historical Sales</label>
<button type="button" onClick={addSale} className="text-xs transition-colors" style={{ color: 'var(--admin-accent)' }}>+ Add Row</button>
</div>
<AdminCard className="divide-y" style={{ border: '1px solid var(--admin-border)' }}>
<div className="grid grid-cols-4 gap-2 px-3 py-2">
<span className="text-xs font-medium" style={{ color: 'var(--admin-text-muted)' }}>Date</span>
<span className="text-xs font-medium" style={{ color: 'var(--admin-text-muted)' }}>Units Sold</span>
<span className="text-xs font-medium" style={{ color: 'var(--admin-text-muted)' }}>Revenue</span>
<span />
</div>
{historicalSales.map((sale, i) => (
<div key={`sale-${i}-${sale.date}`} className="grid grid-cols-4 gap-2 px-3 py-2 items-center">
<input aria-label="2026 04 01"
type="text"
value={sale.date}
onChange={(e) => updateSale(i, "date", e.target.value)}
placeholder="2026-04-01"
className="rounded-lg border px-2 py-1.5 text-xs"
style={{ borderColor: 'var(--admin-border)', backgroundColor: 'var(--admin-bg)', color: 'var(--admin-text-primary)' }}
/>
<input aria-label="120"
type="text"
value={sale.units_sold}
onChange={(e) => updateSale(i, "units_sold", e.target.value)}
placeholder="120"
className="rounded-lg border px-2 py-1.5 text-xs"
style={{ borderColor: 'var(--admin-border)', backgroundColor: 'var(--admin-bg)', color: 'var(--admin-text-primary)' }}
/>
<div className="relative">
<span className="absolute left-2 top-1/2 -translate-y-1/2 text-xs" style={{ color: 'var(--admin-text-muted)' }}>$</span>
<input aria-label="540"
type="text"
value={sale.revenue}
onChange={(e) => updateSale(i, "revenue", e.target.value)}
placeholder="540"
className="w-full rounded-lg border pl-5 pr-2 py-1.5 text-xs"
style={{ borderColor: 'var(--admin-border)', backgroundColor: 'var(--admin-bg)', color: 'var(--admin-text-primary)' }}
/>
</div>
{historicalSales.length > 1 && (
<button type="button" onClick={() => removeSale(i)} className="text-xs justify-self-end transition-colors" style={{ color: 'var(--admin-danger)' }}></button>
)}
</div>
))}
</AdminCard>
</div>
<div className="rounded-lg p-3 text-xs" style={{ backgroundColor: 'rgba(171, 162, 120, 0.1)', border: '1px solid rgba(171, 162, 120, 0.2)', color: '#92781e' }}>
AI-generated suggestions review before use. Not a substitute for business judgment.
</div>
<button type="button"
onClick={handleAnalyze}
disabled={loading || !productName.trim()}
className={btnPrimaryClass}
style={{
...btnPrimaryStyle,
opacity: (loading || !productName.trim()) ? 0.5 : 1,
cursor: (loading || !productName.trim()) ? 'not-allowed' : 'pointer',
}}
>
{loading ? "Analyzing..." : "Analyze Pricing"}
</button>
{error && <p className="text-sm" style={{ color: 'var(--admin-danger)' }}>{error}</p>}
{result && (
<div className="space-y-4">
<AdminCard className="p-5">
<p className="text-xs font-semibold uppercase tracking-wider mb-2" style={{ color: 'var(--admin-text-muted)' }}>Current State</p>
<p className="text-sm" style={{ color: 'var(--admin-text-secondary)', lineHeight: 1.6 }}>{result.currentState}</p>
</AdminCard>
{result.recommendations.map((rec, i) => (
<AdminCard key={`rec-${i}-${rec.productName}-${rec.direction}`} className="p-5">
<div className="flex items-center justify-between mb-2">
<p className="text-sm font-semibold" style={{ color: 'var(--admin-text-primary)' }}>{rec.productName}</p>
<span
className="text-xs font-bold rounded-full px-2 py-0.5"
style={{
background: rec.direction === "increase" ? 'rgba(16, 185, 129, 0.1)' : rec.direction === "decrease" ? 'rgba(239, 68, 68, 0.1)' : 'rgba(0, 0, 0, 0.04)',
color: rec.direction === "increase" ? '#047857' : rec.direction === "decrease" ? '#dc2626' : 'var(--admin-text-muted)',
}}
>
{rec.direction}
</span>
</div>
<div className="flex items-center gap-3 text-sm mb-2">
<span style={{ color: 'var(--admin-text-muted)' }}>${rec.currentPrice}</span>
<span style={{ color: 'var(--admin-text-muted)' }}></span>
<span className="font-bold" style={{ color: 'var(--admin-accent)' }}>${rec.suggestedPrice}</span>
<span className={`text-xs ml-auto ${rec.estimatedRevenueImpact.startsWith("+") ? 'text-green-600' : ''}`} style={{ color: rec.estimatedRevenueImpact.startsWith("+") ? '#059669' : 'var(--admin-text-muted)' }}>
{rec.estimatedRevenueImpact}
</span>
</div>
<p className="text-sm" style={{ color: 'var(--admin-text-secondary)' }}>{rec.reasoning}</p>
</AdminCard>
))}
{result.opportunities.length > 0 && (
<AdminCard className="p-5" style={{ backgroundColor: 'rgba(16, 185, 129, 0.05)', border: '1px solid rgba(16, 185, 129, 0.2)' }}>
<p className="text-xs font-semibold uppercase tracking-wider mb-2" style={{ color: '#047857' }}>Opportunities</p>
<ul className="space-y-1">
{result.opportunities.map((opp) => (
<li key={opp} className="text-sm" style={{ color: '#059669' }}> {opp}</li>
))}
</ul>
</AdminCard>
)}
{result.warnings.length > 0 && (
<AdminCard className="p-5" style={{ backgroundColor: 'rgba(171, 162, 120, 0.08)', border: '1px solid rgba(171, 162, 120, 0.2)' }}>
<p className="text-xs font-semibold uppercase tracking-wider mb-2" style={{ color: '#92781e' }}>Warnings</p>
<ul className="space-y-1">
{result.warnings.map((warn) => (
<li key={warn} className="text-sm" style={{ color: '#a6953f' }}> {warn}</li>
))}
</ul>
</AdminCard>
)}
<button type="button"
onClick={() => copyToClipboard(JSON.stringify(result, null, 2))}
className="text-xs flex items-center gap-1 transition-colors"
style={{ color: 'var(--admin-text-muted)' }}
>
Copy to clipboard
</button>
</div>
)}
</div>
);
}
// ── Stop Blast Advisor Tool ────────────────────────────────────────────────────
function StopBlastAdvisorTool({ brandId }: { brandId: string }) {
const [stopName, setStopName] = useState("");
const [stopDate, setStopDate] = useState("");
const [city, setCity] = useState("");
const [customerCount, setCustomerCount] = useState("");
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<{
timingRecommendation: string;
subjectLine: string;
bodyPreview: string;
audienceSize: string;
audienceRecommendation: string;
contentAngles: Array<{ angle: string; reasoning: string }>;
warnings: string[];
} | null>(null);
const [error, setError] = useState<string | null>(null);
async function handleAnalyze() {
if (!stopName.trim()) return;
setLoading(true);
setError(null);
setResult(null);
try {
const res = await fetch("/api/ai/stop-blast-advisor", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
brandId,
stopName,
stopDate,
city,
recentOrders: [],
customerCount: customerCount ? parseInt(customerCount) : undefined,
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error ?? "Analysis failed");
setResult(data);
} catch (err) {
setError(err instanceof Error ? err.message : "Analysis failed");
} finally {
setLoading(false);
}
}
function copyToClipboard(text: string) {
navigator.clipboard.writeText(text);
}
return (
<div className="space-y-4">
<p className="text-sm rounded-lg p-3" style={{ backgroundColor: 'rgba(0, 0, 0, 0.02)', color: 'var(--admin-text-muted)' }}>
Enter details about the stop you want to send a blast for. The AI will suggest optimal timing, subject lines, content angles, and audience targeting.
</p>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1" style={labelStyle}>Stop Name *</label>
<input aria-label="Downtown Farmers Market" type="text" value={stopName} onChange={(e) => setStopName(e.target.value)} placeholder="Downtown Farmers Market" className={inputBaseClass} style={inputStyle} />
</div>
<div>
<label className="block text-sm font-medium mb-1" style={labelStyle}>Stop Date</label>
<input aria-label="2026 06 15" type="text" value={stopDate} onChange={(e) => setStopDate(e.target.value)} placeholder="2026-06-15" className={inputBaseClass} style={inputStyle} />
</div>
<div>
<label className="block text-sm font-medium mb-1" style={labelStyle}>City</label>
<input aria-label="Greeley" type="text" value={city} onChange={(e) => setCity(e.target.value)} placeholder="Greeley" className={inputBaseClass} style={inputStyle} />
</div>
<div>
<label className="block text-sm font-medium mb-1" style={labelStyle}>Customer Count</label>
<input aria-label="42" type="text" value={customerCount} onChange={(e) => setCustomerCount(e.target.value)} placeholder="42" className={inputBaseClass} style={inputStyle} />
</div>
</div>
<div className="rounded-lg p-3 text-xs" style={{ backgroundColor: 'rgba(171, 162, 120, 0.1)', border: '1px solid rgba(171, 162, 120, 0.2)', color: '#92781e' }}>
AI-generated suggestions review before use. Not a substitute for business judgment.
</div>
<button type="button"
onClick={handleAnalyze}
disabled={loading || !stopName.trim()}
className={btnPrimaryClass}
style={{
...btnPrimaryStyle,
opacity: (loading || !stopName.trim()) ? 0.5 : 1,
cursor: (loading || !stopName.trim()) ? 'not-allowed' : 'pointer',
}}
>
{loading ? "Analyzing..." : "Get Recommendations"}
</button>
{error && <p className="text-sm" style={{ color: 'var(--admin-danger)' }}>{error}</p>}
{result && (
<div className="space-y-4">
<AdminCard className="p-5">
<p className="text-xs font-semibold uppercase tracking-wider mb-2" style={{ color: 'var(--admin-text-muted)' }}>Timing</p>
<p className="text-sm" style={{ color: 'var(--admin-text-secondary)' }}>{result.timingRecommendation}</p>
</AdminCard>
<AdminCard className="p-5" style={{ backgroundColor: 'rgba(16, 185, 129, 0.05)', border: '1px solid rgba(16, 185, 129, 0.2)' }}>
<p className="text-xs font-semibold uppercase tracking-wider mb-2" style={{ color: '#047857' }}>Recommended Subject Line</p>
<p className="text-base font-semibold" style={{ color: 'var(--admin-text-primary)' }}>{result.subjectLine}</p>
<button type="button" onClick={() => copyToClipboard(result.subjectLine)} className="mt-2 text-xs flex items-center gap-1 transition-colors" style={{ color: 'var(--admin-accent)' }}>
Copy
</button>
</AdminCard>
<AdminCard className="p-5">
<p className="text-xs font-semibold uppercase tracking-wider mb-2" style={{ color: 'var(--admin-text-muted)' }}>Body Preview</p>
<p className="text-sm" style={{ color: 'var(--admin-text-secondary)', lineHeight: 1.6 }}>{result.bodyPreview}</p>
</AdminCard>
<AdminCard className="p-5">
<p className="text-xs font-semibold uppercase tracking-wider mb-2" style={{ color: 'var(--admin-text-muted)' }}>Audience</p>
<p className="text-sm" style={{ color: 'var(--admin-text-secondary)' }}>{result.audienceRecommendation}</p>
<span
className="inline-block mt-1 text-xs rounded-full px-2 py-0.5"
style={{ backgroundColor: 'rgba(59, 130, 246, 0.1)', color: '#2563eb' }}
>
{result.audienceSize} recipients
</span>
</AdminCard>
{result.contentAngles.map((a, i) => (
<AdminCard key={`angle-${i}-${a.angle}`} className="p-4">
<p className="text-sm font-semibold mb-1" style={{ color: 'var(--admin-text-primary)' }}>{a.angle}</p>
<p className="text-xs" style={{ color: 'var(--admin-text-muted)' }}>{a.reasoning}</p>
</AdminCard>
))}
{result.warnings.length > 0 && (
<AdminCard className="p-4" style={{ backgroundColor: 'rgba(171, 162, 120, 0.08)', border: '1px solid rgba(171, 162, 120, 0.2)' }}>
<p className="text-xs font-semibold uppercase tracking-wider mb-2" style={{ color: '#92781e' }}>Warnings</p>
{result.warnings.map((w) => (
<p key={w} className="text-xs" style={{ color: '#a6953f' }}> {w}</p>
))}
</AdminCard>
)}
</div>
)}
</div>
);
}
// ── Customer Insights Tool ────────────────────────────────────────────────────
function CustomerInsightsTool({ brandId }: { brandId: string }) {
const [query, setQuery] = useState("");
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<{
queryType: string;
explanation: string;
results: Record<string, unknown>[];
count: number;
nlQuery: string;
} | null>(null);
const [error, setError] = useState<string | null>(null);
const exampleQueries = [
"Which customers haven't ordered in 45 days?",
"What products are trending this month?",
"Who are my top customers by revenue?",
"Show recent orders from the last 7 days",
"Which customers are at risk of churning?",
];
async function handleAnalyze(nlQuery?: string) {
const q = nlQuery ?? query;
if (!q.trim()) return;
setLoading(true);
setError(null);
setResult(null);
try {
const res = await fetch("/api/ai/customer-insights", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ brandId, nlQuery: q }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error ?? "Analysis failed");
setResult(data);
} catch (err) {
setError(err instanceof Error ? err.message : "Analysis failed");
} finally {
setLoading(false);
}
}
return (
<div className="space-y-4">
<p className="text-sm rounded-lg p-3" style={{ backgroundColor: 'rgba(0, 0, 0, 0.02)', color: 'var(--admin-text-muted)' }}>
Ask questions about your customers and orders in plain English. The AI will analyze and return actionable insights.
</p>
<div>
<label className="block text-sm font-medium mb-1" style={labelStyle}>Ask about your customers</label>
<textarea aria-label="., 'Which Customers Haven't Ordered In 45 Days?'"
value={query}
onChange={(e) => setQuery(e.target.value)}
rows={2}
placeholder="e.g., 'Which customers haven't ordered in 45 days?'"
className={inputBaseClass}
style={textareaStyle}
/>
</div>
<div className="flex flex-wrap gap-2">
{exampleQueries.map((q) => (
<button type="button"
key={q}
onClick={() => { setQuery(q); handleAnalyze(q); }}
className="rounded-full px-3 py-1 text-xs transition-all"
style={{
backgroundColor: 'var(--admin-bg)',
border: '1px solid var(--admin-border)',
color: 'var(--admin-text-secondary)',
}}
>
{q}
</button>
))}
</div>
<div className="rounded-lg p-3 text-xs" style={{ backgroundColor: 'rgba(171, 162, 120, 0.1)', border: '1px solid rgba(171, 162, 120, 0.2)', color: '#92781e' }}>
AI-generated suggestions review before use. Not a substitute for business judgment.
</div>
<button type="button"
onClick={() => handleAnalyze()}
disabled={loading || !query.trim()}
className={btnPrimaryClass}
style={{
...btnPrimaryStyle,
opacity: (loading || !query.trim()) ? 0.5 : 1,
cursor: (loading || !query.trim()) ? 'not-allowed' : 'pointer',
}}
>
{loading ? "Analyzing..." : "Analyze"}
</button>
{error && <p className="text-sm" style={{ color: 'var(--admin-danger)' }}>{error}</p>}
{result && (
<div className="space-y-4">
<AdminCard className="p-4">
<p className="text-xs font-semibold uppercase tracking-wider mb-2" style={{ color: 'var(--admin-text-muted)' }}>Query Type</p>
<span
className="rounded-full px-2 py-0.5 text-xs font-medium"
style={{ backgroundColor: 'rgba(16, 185, 129, 0.1)', color: '#047857' }}
>
{result.queryType?.replace("_", " ")}
</span>
<p className="text-sm mt-2" style={{ color: 'var(--admin-text-muted)' }}>{result.explanation}</p>
</AdminCard>
<AdminCard className="p-4">
<p className="text-xs font-semibold uppercase tracking-wider mb-2" style={{ color: 'var(--admin-text-muted)' }}>
Results <span style={{ color: 'var(--admin-accent)' }}>({result.count} found)</span>
</p>
{result.results.length === 0 ? (
<p className="text-sm" style={{ color: 'var(--admin-text-muted)' }}>No results found.</p>
) : (
<div className="overflow-x-auto">
<table className="w-full text-xs" style={{ color: 'var(--admin-text-secondary)' }}>
<thead>
<tr style={{ borderBottom: '1px solid var(--admin-border)' }}>
{Object.keys(result.results[0] ?? {}).map((k) => (
<th key={k} className="text-left px-2 py-1.5 font-medium uppercase" style={{ color: 'var(--admin-text-muted)' }}>{k}</th>
))}
</tr>
</thead>
<tbody>
{result.results.map((row) => {
const rowKey = Object.values(row).slice(0, 2).map(v => String(v ?? "")).join("-");
return (
<tr key={rowKey} style={{ borderBottom: '1px solid var(--admin-border-light)' }}>
{Object.entries(row).map(([colKey, v]) => (
<td key={colKey} className="px-2 py-1.5" style={{ color: 'var(--admin-text-secondary)' }}>{String(v ?? "—")}</td>
))}
</tr>
);
})}
</tbody>
</table>
</div>
)}
</AdminCard>
</div>
)}
</div>
);
}
// ── Route Optimizer Tool ──────────────────────────────────────────────────────
type StopEntry = { name: string; city: string; state: string; address: string; time_window: string };
function RouteOptimizerTool({ brandId }: { brandId: string }) {
const [stops, setStops] = useState<StopEntry[]>([
{ name: "", city: "", state: "", address: "", time_window: "" },
{ name: "", city: "", state: "", address: "", time_window: "" },
]);
const [startLocation, setStartLocation] = useState("");
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<{
optimizedSequence: Array<{ position: number; stopName: string; city: string; state: string; reason: string }>;
totalEstimatedDistance: string;
totalEstimatedDriveTime: string;
warnings: string[];
suggestions: string[];
} | null>(null);
const [error, setError] = useState<string | null>(null);
function addStop() {
setStops([...stops, { name: "", city: "", state: "", address: "", time_window: "" }]);
}
function removeStop(index: number) {
setStops(stops.filter((_, i) => i !== index));
}
function updateStop(index: number, field: keyof StopEntry, value: string) {
const updated = [...stops];
updated[index] = { ...updated[index], [field]: value };
setStops(updated);
}
async function handleOptimize() {
const validStops = stops.filter((s) => s.name.trim() && s.city.trim() && s.state.trim());
if (validStops.length < 2) return;
setLoading(true);
setError(null);
setResult(null);
try {
const res = await fetch("/api/ai/route-optimizer", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ brandId, stops: validStops, startLocation }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error ?? "Optimization failed");
setResult(data);
} catch (err) {
setError(err instanceof Error ? err.message : "Optimization failed");
} finally {
setLoading(false);
}
}
function copyToClipboard(text: string) {
navigator.clipboard.writeText(text);
}
const validStops = stops.filter((s) => s.name.trim() && s.city.trim() && s.state.trim());
return (
<div className="space-y-4">
<p className="text-sm rounded-lg p-3" style={{ backgroundColor: 'rgba(0, 0, 0, 0.02)', color: 'var(--admin-text-muted)' }}>
Add your stops below. You need at least 2 stops to optimize a route.
</p>
<div>
<label className="block text-sm font-medium mb-1" style={labelStyle}>Start Location</label>
<input aria-label="Warehouse, Greeley CO"
type="text"
value={startLocation}
onChange={(e) => setStartLocation(e.target.value)}
placeholder="Warehouse, Greeley CO"
className={inputBaseClass}
style={inputStyle}
/>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<label className="block text-sm font-medium" style={labelStyle}>Stops</label>
<button type="button"
onClick={addStop}
className="text-xs flex items-center gap-1 transition-colors"
style={{ color: 'var(--admin-accent)' }}
>
+ Add Stop
</button>
</div>
{stops.map((stop, i) => (
<AdminCard key={`stop-${i}-${stop.name}-${stop.city}-${stop.state}`} className="p-4 space-y-3">
<div className="flex items-center justify-between">
<span className="text-xs font-semibold uppercase tracking-wider" style={{ color: 'var(--admin-text-muted)' }}>Stop {i + 1}</span>
{stops.length > 2 && (
<button type="button" onClick={() => removeStop(i)} className="text-xs transition-colors" style={{ color: 'var(--admin-danger)' }}>
Remove
</button>
)}
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs mb-1" style={{ color: 'var(--admin-text-muted)' }}>Name *</label>
<input aria-label="Farmers Market"
type="text"
value={stop.name}
onChange={(e) => updateStop(i, "name", e.target.value)}
placeholder="Farmers Market"
className="w-full rounded-lg px-3 py-2 text-sm"
style={{ border: '1px solid var(--admin-border)', backgroundColor: 'var(--admin-bg)', color: 'var(--admin-text-primary)' }}
/>
</div>
<div>
<label className="block text-xs mb-1" style={{ color: 'var(--admin-text-muted)' }}>City *</label>
<input aria-label="Greeley"
type="text"
value={stop.city}
onChange={(e) => updateStop(i, "city", e.target.value)}
placeholder="Greeley"
className="w-full rounded-lg px-3 py-2 text-sm"
style={{ border: '1px solid var(--admin-border)', backgroundColor: 'var(--admin-bg)', color: 'var(--admin-text-primary)' }}
/>
</div>
<div>
<label className="block text-xs mb-1" style={{ color: 'var(--admin-text-muted)' }}>State *</label>
<input aria-label="CO"
type="text"
value={stop.state}
onChange={(e) => updateStop(i, "state", e.target.value)}
placeholder="CO"
className="w-full rounded-lg px-3 py-2 text-sm"
style={{ border: '1px solid var(--admin-border)', backgroundColor: 'var(--admin-bg)', color: 'var(--admin-text-primary)' }}
/>
</div>
<div>
<label className="block text-xs mb-1" style={{ color: 'var(--admin-text-muted)' }}>Address</label>
<input aria-label="123 Main St"
type="text"
value={stop.address}
onChange={(e) => updateStop(i, "address", e.target.value)}
placeholder="123 Main St"
className="w-full rounded-lg px-3 py-2 text-sm"
style={{ border: '1px solid var(--admin-border)', backgroundColor: 'var(--admin-bg)', color: 'var(--admin-text-primary)' }}
/>
</div>
</div>
<div>
<label className="block text-xs mb-1" style={{ color: 'var(--admin-text-muted)' }}>Time Window</label>
<input aria-label="8am12pm"
type="text"
value={stop.time_window}
onChange={(e) => updateStop(i, "time_window", e.target.value)}
placeholder="8am12pm"
className="w-full rounded-lg px-3 py-2 text-sm"
style={{ border: '1px solid var(--admin-border)', backgroundColor: 'var(--admin-bg)', color: 'var(--admin-text-primary)' }}
/>
</div>
</AdminCard>
))}
</div>
<div className="rounded-lg p-3 text-xs" style={{ backgroundColor: 'rgba(171, 162, 120, 0.1)', border: '1px solid rgba(171, 162, 120, 0.2)', color: '#92781e' }}>
AI-generated suggestions review before use. Not a substitute for professional routing software.
</div>
<button type="button"
onClick={handleOptimize}
disabled={loading || validStops.length < 2}
className={btnPrimaryClass}
style={{
...btnPrimaryStyle,
opacity: (loading || validStops.length < 2) ? 0.5 : 1,
cursor: (loading || validStops.length < 2) ? 'not-allowed' : 'pointer',
}}
>
{loading ? "Optimizing..." : "Optimize Route"}
</button>
{error && <p className="text-sm" style={{ color: 'var(--admin-danger)' }}>{error}</p>}
{result && (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-3">
<AdminCard className="p-4">
<p className="text-xs font-semibold uppercase tracking-wider mb-1" style={{ color: 'var(--admin-text-muted)' }}>Est. Distance</p>
<p className="text-base font-bold" style={{ color: 'var(--admin-text-primary)' }}>{result.totalEstimatedDistance}</p>
</AdminCard>
<AdminCard className="p-4">
<p className="text-xs font-semibold uppercase tracking-wider mb-1" style={{ color: 'var(--admin-text-muted)' }}>Drive Time</p>
<p className="text-base font-bold" style={{ color: 'var(--admin-text-primary)' }}>{result.totalEstimatedDriveTime}</p>
</AdminCard>
</div>
<AdminCard className="p-4" style={{ backgroundColor: 'rgba(16, 185, 129, 0.05)', border: '1px solid rgba(16, 185, 129, 0.2)' }}>
<p className="text-xs font-semibold uppercase tracking-wider mb-2" style={{ color: '#047857' }}>Optimized Sequence</p>
{result.optimizedSequence.map((s, i) => (
<div key={`seq-${i}-${s.position}-${s.stopName}`} className="flex items-start gap-3 py-2" style={{ borderBottom: i < result.optimizedSequence.length - 1 ? '1px solid var(--admin-border-light)' : 'none' }}>
<span
className="flex items-center justify-center w-6 h-6 rounded-full text-xs font-bold text-white flex-shrink-0"
style={{ backgroundColor: 'var(--admin-accent)' }}
>
{s.position}
</span>
<div>
<p className="text-sm font-semibold" style={{ color: 'var(--admin-text-primary)' }}>{s.stopName}</p>
<p className="text-xs" style={{ color: 'var(--admin-text-muted)' }}>{s.city}, {s.state}</p>
<p className="text-xs mt-0.5" style={{ color: '#047857' }}>{s.reason}</p>
</div>
</div>
))}
</AdminCard>
{result.suggestions.length > 0 && (
<AdminCard className="p-4" style={{ backgroundColor: 'rgba(16, 185, 129, 0.05)', border: '1px solid rgba(16, 185, 129, 0.2)' }}>
<p className="text-xs font-semibold uppercase tracking-wider mb-2" style={{ color: '#047857' }}>Suggestions</p>
{result.suggestions.map((s) => <p key={s} className="text-sm" style={{ color: '#059669' }}> {s}</p>)}
</AdminCard>
)}
{result.warnings.length > 0 && (
<AdminCard className="p-4" style={{ backgroundColor: 'rgba(171, 162, 120, 0.08)', border: '1px solid rgba(171, 162, 120, 0.2)' }}>
<p className="text-xs font-semibold uppercase tracking-wider mb-2" style={{ color: '#92781e' }}>Warnings</p>
{result.warnings.map((w) => <p key={w} className="text-sm" style={{ color: '#a6953f' }}> {w}</p>)}
</AdminCard>
)}
<button type="button"
onClick={() => copyToClipboard(JSON.stringify(result, null, 2))}
className="text-xs flex items-center gap-1 transition-colors"
style={{ color: 'var(--admin-text-muted)' }}
>
Copy to clipboard
</button>
</div>
)}
</div>
);
}
// ── Demand Forecasting Tool ───────────────────────────────────────────────────
type ForecastEntry = { date: string; quantity_sold: string; stop: string };
function DemandForecastTool({ brandId }: { brandId: string }) {
const [productName, setProductName] = useState("");
const [stopName, setStopName] = useState("");
const [historicalData, setHistoricalData] = useState<ForecastEntry[]>([
{ date: "", quantity_sold: "", stop: "" },
]);
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<{
currentTrend: string;
prediction: {
nextStopVolume: number;
nextWeekVolume: number;
confidence: string;
confidenceReason: string;
};
recommendedStock: { units: number; reasoning: string };
seasonalFactors: string[];
riskFlags: string[];
} | null>(null);
const [error, setError] = useState<string | null>(null);
async function handleAnalyze() {
if (!productName.trim()) return;
setLoading(true);
setError(null);
setResult(null);
try {
const parsedData = historicalData
.filter((d) => d.date.trim())
.map((d) => ({
date: d.date.trim(),
quantity_sold: parseInt(d.quantity_sold) || 0,
stop: d.stop.trim(),
}));
const res = await fetch("/api/ai/demand-forecast", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ brandId, productName, stopName, historicalData: parsedData }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error ?? "Analysis failed");
setResult(data);
} catch (err) {
setError(err instanceof Error ? err.message : "Analysis failed");
} finally {
setLoading(false);
}
}
function copyToClipboard(text: string) {
navigator.clipboard.writeText(text);
}
function addRow() { setHistoricalData([...historicalData, { date: "", quantity_sold: "", stop: "" }]); }
function removeRow(i: number) { setHistoricalData(historicalData.filter((_, idx) => idx !== i)); }
function updateRow(i: number, field: keyof ForecastEntry, val: string) {
const updated = [...historicalData]; updated[i] = { ...updated[i], [field]: val }; setHistoricalData(updated);
}
return (
<div className="space-y-4">
<p className="text-sm rounded-lg p-3" style={{ backgroundColor: 'rgba(0, 0, 0, 0.02)', color: 'var(--admin-text-muted)' }}>
Enter a product name for demand forecasting. Add historical sales rows to improve accuracy.
</p>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1" style={labelStyle}>Product Name *</label>
<input aria-label="Sweet Corn" type="text" value={productName} onChange={(e) => setProductName(e.target.value)} placeholder="Sweet Corn" className={inputBaseClass} style={inputStyle} />
</div>
<div>
<label className="block text-sm font-medium mb-1" style={labelStyle}>Stop Name</label>
<input aria-label="Downtown Farmers Market" type="text" value={stopName} onChange={(e) => setStopName(e.target.value)} placeholder="Downtown Farmers Market" className={inputBaseClass} style={inputStyle} />
</div>
</div>
{/* Historical Data */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="block text-sm font-medium" style={labelStyle}>Historical Sales</label>
<button type="button" onClick={addRow} className="text-xs transition-colors" style={{ color: 'var(--admin-accent)' }}>+ Add Row</button>
</div>
<AdminCard className="divide-y" style={{ border: '1px solid var(--admin-border)' }}>
<div className="grid grid-cols-4 gap-2 px-3 py-2">
<span className="text-xs font-medium" style={{ color: 'var(--admin-text-muted)' }}>Date</span>
<span className="text-xs font-medium" style={{ color: 'var(--admin-text-muted)' }}>Units Sold</span>
<span className="text-xs font-medium" style={{ color: 'var(--admin-text-muted)' }}>Stop</span>
<span />
</div>
{historicalData.map((row, i) => (
<div key={`hist-${i}-${row.date}-${row.stop}`} className="grid grid-cols-4 gap-2 px-3 py-2 items-center">
<input aria-label="2026 04 01"
type="text"
value={row.date}
onChange={(e) => updateRow(i, "date", e.target.value)}
placeholder="2026-04-01"
className="rounded-lg border px-2 py-1.5 text-xs"
style={{ borderColor: 'var(--admin-border)', backgroundColor: 'var(--admin-bg)', color: 'var(--admin-text-primary)' }}
/>
<input aria-label="120"
type="text"
value={row.quantity_sold}
onChange={(e) => updateRow(i, "quantity_sold", e.target.value)}
placeholder="120"
className="rounded-lg border px-2 py-1.5 text-xs"
style={{ borderColor: 'var(--admin-border)', backgroundColor: 'var(--admin-bg)', color: 'var(--admin-text-primary)' }}
/>
<input aria-label="Farmers Market"
type="text"
value={row.stop}
onChange={(e) => updateRow(i, "stop", e.target.value)}
placeholder="Farmers Market"
className="rounded-lg border px-2 py-1.5 text-xs"
style={{ borderColor: 'var(--admin-border)', backgroundColor: 'var(--admin-bg)', color: 'var(--admin-text-primary)' }}
/>
{historicalData.length > 1 && (
<button type="button" onClick={() => removeRow(i)} className="text-xs justify-self-end transition-colors" style={{ color: 'var(--admin-danger)' }}></button>
)}
</div>
))}
</AdminCard>
</div>
<div className="rounded-lg p-3 text-xs" style={{ backgroundColor: 'rgba(171, 162, 120, 0.1)', border: '1px solid rgba(171, 162, 120, 0.2)', color: '#92781e' }}>
AI-generated forecasts review before use. Not a substitute for professional supply chain planning.
</div>
<button type="button"
onClick={handleAnalyze}
disabled={loading || !productName.trim()}
className={btnPrimaryClass}
style={{
...btnPrimaryStyle,
opacity: (loading || !productName.trim()) ? 0.5 : 1,
cursor: (loading || !productName.trim()) ? 'not-allowed' : 'pointer',
}}
>
{loading ? "Forecasting..." : "Generate Forecast"}
</button>
{error && <p className="text-sm" style={{ color: 'var(--admin-danger)' }}>{error}</p>}
{result && (
<div className="space-y-4">
<AdminCard className="p-4">
<p className="text-xs font-semibold uppercase tracking-wider mb-2" style={{ color: 'var(--admin-text-muted)' }}>Current Trend</p>
<p className="text-sm" style={{ color: 'var(--admin-text-secondary)', lineHeight: 1.6 }}>{result.currentTrend}</p>
</AdminCard>
<div className="grid grid-cols-3 gap-3">
<AdminCard className="p-4">
<p className="text-xs font-semibold uppercase tracking-wider mb-1" style={{ color: 'var(--admin-text-muted)' }}>Next Stop</p>
<p className="text-xl font-bold" style={{ color: 'var(--admin-accent)' }}>{result.prediction.nextStopVolume}<span className="text-sm font-normal ml-1" style={{ color: 'var(--admin-text-muted)' }}>units</span></p>
</AdminCard>
<AdminCard className="p-4">
<p className="text-xs font-semibold uppercase tracking-wider mb-1" style={{ color: 'var(--admin-text-muted)' }}>Next Week</p>
<p className="text-xl font-bold" style={{ color: 'var(--admin-accent)' }}>{result.prediction.nextWeekVolume}<span className="text-sm font-normal ml-1" style={{ color: 'var(--admin-text-muted)' }}>units</span></p>
</AdminCard>
<AdminCard className="p-4">
<p className="text-xs font-semibold uppercase tracking-wider mb-1" style={{ color: 'var(--admin-text-muted)' }}>Confidence</p>
<span
className="inline-block rounded-full px-2 py-0.5 text-xs font-bold"
style={{
background: result.prediction.confidence === "high" ? 'rgba(16, 185, 129, 0.1)' : result.prediction.confidence === "medium" ? 'rgba(171, 162, 120, 0.15)' : 'rgba(239, 68, 68, 0.1)',
color: result.prediction.confidence === "high" ? '#047857' : result.prediction.confidence === "medium" ? '#92781e' : '#dc2626',
}}
>
{result.prediction.confidence}
</span>
<p className="text-xs mt-1" style={{ color: 'var(--admin-text-muted)' }}>{result.prediction.confidenceReason}</p>
</AdminCard>
</div>
<AdminCard className="p-4" style={{ backgroundColor: 'rgba(16, 185, 129, 0.05)', border: '1px solid rgba(16, 185, 129, 0.2)' }}>
<p className="text-xs font-semibold uppercase tracking-wider mb-2" style={{ color: '#047857' }}>Recommended Stock</p>
<p className="text-2xl font-bold" style={{ color: 'var(--admin-text-primary)' }}>{result.recommendedStock.units}<span className="text-sm font-normal ml-1" style={{ color: 'var(--admin-text-muted)' }}>units</span></p>
<p className="text-sm mt-1" style={{ color: 'var(--admin-text-secondary)' }}>{result.recommendedStock.reasoning}</p>
</AdminCard>
{result.seasonalFactors.length > 0 && (
<AdminCard className="p-4" style={{ backgroundColor: 'rgba(59, 130, 246, 0.05)', border: '1px solid rgba(59, 130, 246, 0.2)' }}>
<p className="text-xs font-semibold uppercase tracking-wider mb-2" style={{ color: '#2563eb' }}>Seasonal Factors</p>
{result.seasonalFactors.map((f) => <p key={f} className="text-sm" style={{ color: '#3b82f6' }}> {f}</p>)}
</AdminCard>
)}
{result.riskFlags.length > 0 && (
<AdminCard className="p-4" style={{ backgroundColor: 'rgba(171, 162, 120, 0.08)', border: '1px solid rgba(171, 162, 120, 0.2)' }}>
<p className="text-xs font-semibold uppercase tracking-wider mb-2" style={{ color: '#92781e' }}>Risk Flags</p>
{result.riskFlags.map((r) => <p key={r} className="text-sm" style={{ color: '#a6953f' }}> {r}</p>)}
</AdminCard>
)}
<button type="button"
onClick={() => copyToClipboard(JSON.stringify(result, null, 2))}
className="text-xs flex items-center gap-1 transition-colors"
style={{ color: 'var(--admin-text-muted)' }}
>
Copy to clipboard
</button>
</div>
)}
</div>
);
}
// ── Modal ─────────────────────────────────────────────────────────────────────
type ModalProps = {
tool: AITool;
brandId: string;
brandName: string;
onClose: () => void;
};
function ToolModal({ tool, brandId, brandName, onClose }: ModalProps) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4" style={{ backgroundColor: 'rgba(60, 56, 37, 0.4)', backdropFilter: 'blur(4px)' }}>
<AdminCard className="w-full max-w-lg max-h-[85vh] flex flex-col shadow-2xl">
<div className="flex items-center justify-between px-6 py-4" style={{ borderBottom: '1px solid var(--admin-border)' }}>
<div className="flex items-center gap-3">
<span className="text-2xl">{tool.icon}</span>
<h2 className="text-lg font-bold" style={{ color: 'var(--admin-text-primary)' }}>{tool.title}</h2>
</div>
<button type="button" onClick={onClose} className="p-2 transition-colors" style={{ color: 'var(--admin-text-muted)' }}>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="px-6 py-5 overflow-y-auto flex-1">
{tool.id === "campaign-writer" && <CampaignWriterTool brandId={brandId} brandName={brandName} />}
{tool.id === "product-writer" && <ProductWriterTool brandId={brandId} />}
{tool.id === "report-explainer" && <ReportExplainerTool brandId={brandId} />}
{tool.id === "pricing-advisor" && <PricingAdvisorTool brandId={brandId} />}
{tool.id === "stop-blast-advisor" && <StopBlastAdvisorTool brandId={brandId} />}
{tool.id === "customer-insights" && <CustomerInsightsTool brandId={brandId} />}
{tool.id === "route-suggester" && <RouteOptimizerTool brandId={brandId} />}
{tool.id === "demand-forecast" && <DemandForecastTool brandId={brandId} />}
</div>
</AdminCard>
</div>
);
}
// ── Main Page ─────────────────────────────────────────────────────────────────
export default function AIsettingsClient({
isConnected,
brandId,
brandName,
provider = "openai",
model = "gpt-4o-mini",
customEndpoint
}: Props) {
const [activeTool, setActiveTool] = useState<AITool | null>(null);
const [activeModule, setActiveModule] = useState<string | null>(null);
// selectedProvider/currentModel are derived directly from props. We use a
// `key` on the child components so any prop change fully remounts them and
// their internal useState(initialProp) re-initializes — no stale copy.
const selectedProvider = provider;
const currentModel = model;
const filteredTools = activeModule ? AI_TOOLS.filter((t) => t.module === activeModule) : AI_TOOLS;
const modules = [...new Set(AI_TOOLS.map((t) => t.module))];
const availableCount = AI_TOOLS.filter((t) => t.status === "available").length;
return (
<main className="admin-page">
<div className="admin-container">
{/* Header with icon */}
<div className="flex items-center gap-4 mb-6">
<AIHeaderIcon />
<div>
<h1 className="text-3xl font-bold" style={{ color: 'var(--admin-text-primary)' }}>AI Tools</h1>
<p className="mt-1 text-sm" style={{ color: 'var(--admin-text-muted)' }}>
AI-assisted features across Harvest Reach, Products, Reports, and more.
</p>
</div>
</div>
{/* Breadcrumb */}
<nav className="flex items-center gap-2 text-xs mb-6" style={{ color: 'var(--admin-text-muted)' }}>
<a href="/admin" style={{ color: 'var(--admin-text-secondary)' }}>Admin</a>
<span>/</span>
<a href="/admin/settings" style={{ color: 'var(--admin-text-secondary)' }}>Settings</a>
<span>/</span>
<span style={{ color: 'var(--admin-text-secondary)' }}>AI Tools</span>
</nav>
{/* Connection Status Banner */}
<ConnectionStatusBanner
isConnected={isConnected}
availableCount={availableCount}
onConfigure={() => window.location.href = '/admin/settings#integrations'}
/>
{/* Provider Selector */}
<ProviderSelector
currentProvider={selectedProvider}
currentModel={currentModel}
brandId={brandId}
onSelect={() => {
// Local state is derived from `provider` prop. After a server
// refresh the new provider flows in via the parent re-render.
}}
/>
{/* Model Input */}
<ModelInput
key={currentModel}
currentModel={currentModel}
brandId={brandId}
onChange={() => {
// Local state is derived from `model` prop. `key={currentModel}`
// remounts this component so its internal useState(currentModel)
// re-syncs to the new value.
}}
/>
{/* Module filter */}
<div className="admin-filter-tabs mb-6">
<button type="button"
onClick={() => setActiveModule(null)}
className={`admin-filter-tab ${activeModule === null ? 'admin-filter-tab--active' : ''}`}
>
All ({AI_TOOLS.length})
</button>
{modules.map((m) => {
const count = AI_TOOLS.filter((t) => t.module === m).length;
return (
<button type="button"
key={m}
onClick={() => setActiveModule(activeModule === m ? null : m)}
className={`admin-filter-tab ${activeModule === m ? 'admin-filter-tab--active' : ''}`}
>
{m} ({count})
</button>
);
})}
</div>
{/* Tool Cards */}
<div className="grid gap-5 md:grid-cols-2">
{filteredTools.map((tool) => (
<ToolCard
key={tool.id}
tool={tool}
isConnected={isConnected}
onOpen={setActiveTool}
/>
))}
</div>
{/* Info box */}
<AdminCard className="mt-8 p-5" style={{
background: 'rgba(202, 117, 67, 0.05)',
border: '1px solid rgba(202, 117, 67, 0.15)',
}}>
<h3 className="text-sm font-semibold mb-2" style={{ color: '#8c4c27' }}>About AI in Route Commerce</h3>
<p className="text-sm" style={{ color: '#a0694f', lineHeight: 1.6 }}>
AI tools process your data locally orders, products, stops, and contacts and send requests to your configured AI provider.
No data is stored or shared beyond the generation request. Experimental features may require additional setup.
</p>
</AdminCard>
</div>
{/* Tool Modal */}
{activeTool && (
<ToolModal
tool={activeTool}
brandId={brandId}
brandName={brandName}
onClose={() => setActiveTool(null)}
/>
)}
</main>
);
}