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)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
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";
|
||||
@@ -96,7 +96,7 @@ function ToolCard({ tool, isConnected, onOpen }: ToolCardProps) {
|
||||
</div>
|
||||
<div className="mt-4 pt-4" style={{ borderTop: '1px solid var(--admin-border-light)' }}>
|
||||
{tool.status === "available" && (
|
||||
<button
|
||||
<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"
|
||||
@@ -113,7 +113,7 @@ function ToolCard({ tool, isConnected, onOpen }: ToolCardProps) {
|
||||
</button>
|
||||
)}
|
||||
{tool.status === "coming_soon" && (
|
||||
<button disabled className="w-full rounded-xl px-4 py-2.5 text-sm font-semibold cursor-not-allowed"
|
||||
<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)',
|
||||
@@ -122,7 +122,7 @@ function ToolCard({ tool, isConnected, onOpen }: ToolCardProps) {
|
||||
</button>
|
||||
)}
|
||||
{tool.status === "experimental" && (
|
||||
<button disabled className="w-full rounded-xl px-4 py-2.5 text-sm font-semibold cursor-not-allowed"
|
||||
<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',
|
||||
@@ -194,7 +194,7 @@ function ConnectionStatusBanner({ isConnected, availableCount, onConfigure }: {
|
||||
<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
|
||||
<button type="button"
|
||||
onClick={onConfigure}
|
||||
className="rounded-xl px-4 py-2 text-sm font-bold transition-all"
|
||||
style={{
|
||||
@@ -320,28 +320,33 @@ const PROVIDERS: ProviderOption[] = [
|
||||
|
||||
// ── Model Input ───────────────────────────────────────────────────────────────
|
||||
|
||||
function ModelInput({
|
||||
currentModel,
|
||||
function ModelInput({
|
||||
currentModel,
|
||||
brandId,
|
||||
onChange
|
||||
}: {
|
||||
onChange
|
||||
}: {
|
||||
currentModel: string;
|
||||
brandId: string;
|
||||
onChange: (model: string) => void;
|
||||
}) {
|
||||
const [customModel, setCustomModel] = useState(currentModel);
|
||||
// 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 () => {
|
||||
if (!customModel.trim()) return;
|
||||
const value = inputRef.current?.value ?? "";
|
||||
if (!value.trim()) return;
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
|
||||
const result = await setAIProviderSettings(brandId, { model: customModel.trim() });
|
||||
|
||||
const result = await setAIProviderSettings(brandId, { model: value.trim() });
|
||||
if (result.success) {
|
||||
onChange(customModel.trim());
|
||||
onChange(value.trim());
|
||||
setSaved(true);
|
||||
setTimeout(() => setSaved(false), 2000);
|
||||
} else {
|
||||
@@ -359,10 +364,11 @@ function ModelInput({
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<input
|
||||
<input aria-label="., Gpt 4o, Claude 3 5 Sonnet 20241002"
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={customModel}
|
||||
onChange={(e) => setCustomModel(e.target.value)}
|
||||
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={{
|
||||
@@ -371,15 +377,15 @@ function ModelInput({
|
||||
color: 'var(--admin-text-primary)',
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={handleSave}
|
||||
disabled={saving || !customModel.trim()}
|
||||
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 || !customModel.trim()) ? 0.5 : 1,
|
||||
cursor: (saving || !customModel.trim()) ? 'not-allowed' : 'pointer',
|
||||
opacity: saving ? 0.5 : 1,
|
||||
cursor: saving ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
>
|
||||
{saving ? "..." : saved ? "✓" : "Save"}
|
||||
@@ -421,7 +427,7 @@ function ProviderSelector({
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
|
||||
{PROVIDERS.map((provider) => (
|
||||
<button
|
||||
<button type="button"
|
||||
key={provider.id}
|
||||
onClick={() => handleSelect(provider.id)}
|
||||
className="p-4 rounded-xl text-center transition-all"
|
||||
@@ -515,7 +521,7 @@ function CampaignWriterTool({ brandId, brandName }: { brandId: string; brandName
|
||||
<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
|
||||
<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)}
|
||||
@@ -525,7 +531,7 @@ function CampaignWriterTool({ brandId, brandName }: { brandId: string; brandName
|
||||
style={textareaStyle}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={handleGenerate}
|
||||
disabled={loading || !topic.trim()}
|
||||
className={btnPrimaryClass}
|
||||
@@ -541,7 +547,7 @@ function CampaignWriterTool({ brandId, brandName }: { brandId: string; brandName
|
||||
{results.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
{results.map((idea, i) => (
|
||||
<AdminCard key={i} className="p-4">
|
||||
<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>
|
||||
@@ -593,22 +599,22 @@ function ProductWriterTool({ brandId }: { brandId: string }) {
|
||||
<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 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} />
|
||||
<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 id="ai-pw-category" type="text" value={category} onChange={(e) => setCategory(e.target.value)} placeholder="Vegetables" className={inputBaseClass} style={inputStyle} />
|
||||
<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 id="ai-pw-price" type="text" value={price} onChange={(e) => setPrice(e.target.value)} placeholder="$4.50" className={inputBaseClass} style={inputStyle} />
|
||||
<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 id="ai-pw-unit" type="text" value={unit} onChange={(e) => setUnit(e.target.value)} placeholder="per dozen" className={inputBaseClass} style={inputStyle} />
|
||||
<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
|
||||
<button type="button"
|
||||
onClick={handleGenerate}
|
||||
disabled={loading || !productName.trim()}
|
||||
className={btnPrimaryClass}
|
||||
@@ -698,7 +704,7 @@ function ReportExplainerTool({ brandId }: { brandId: string }) {
|
||||
<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
|
||||
<select aria-label="Ai Rep Type"
|
||||
id="ai-rep-type"
|
||||
value={reportType}
|
||||
onChange={(e) => setReportType(e.target.value)}
|
||||
@@ -712,7 +718,7 @@ function ReportExplainerTool({ brandId }: { brandId: string }) {
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="ai-rep-range" className="block text-sm font-medium mb-1" style={labelStyle}>Date Range</label>
|
||||
<input
|
||||
<input aria-label="., Last 7 Days, March 2026"
|
||||
id="ai-rep-range"
|
||||
type="text"
|
||||
value={dateRange}
|
||||
@@ -726,7 +732,7 @@ function ReportExplainerTool({ brandId }: { brandId: string }) {
|
||||
<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
|
||||
<button type="button"
|
||||
onClick={handleExplain}
|
||||
disabled={loading}
|
||||
className={btnPrimaryClass}
|
||||
@@ -744,8 +750,8 @@ function ReportExplainerTool({ brandId }: { brandId: string }) {
|
||||
<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, i) => (
|
||||
<li key={i} className="flex items-start gap-2 text-sm" style={{ color: 'var(--admin-text-secondary)' }}>
|
||||
{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>
|
||||
@@ -755,15 +761,15 @@ function ReportExplainerTool({ brandId }: { brandId: string }) {
|
||||
<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, i) => (
|
||||
<li key={i} className="flex items-start gap-2 text-sm" style={{ color: 'var(--admin-text-secondary)' }}>
|
||||
{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
|
||||
<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)' }}
|
||||
@@ -864,7 +870,7 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) {
|
||||
<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
|
||||
<input aria-label="Sweet Corn"
|
||||
id="ai-pa-product-name"
|
||||
type="text"
|
||||
value={productName}
|
||||
@@ -881,11 +887,11 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) {
|
||||
<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 onClick={addTier} className="text-xs transition-colors" style={{ color: 'var(--admin-accent)' }}>+ Add Tier</button>
|
||||
<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={i} className="flex items-center gap-2">
|
||||
<input
|
||||
<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)}
|
||||
@@ -895,7 +901,7 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) {
|
||||
/>
|
||||
<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
|
||||
<input aria-label="0.00"
|
||||
type="text"
|
||||
value={tier.price}
|
||||
onChange={(e) => updateTier(i, "price", e.target.value)}
|
||||
@@ -905,7 +911,7 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) {
|
||||
/>
|
||||
</div>
|
||||
{priceTiers.length > 1 && (
|
||||
<button onClick={() => removeTier(i)} className="text-xs px-2 transition-colors" style={{ color: 'var(--admin-danger)' }}>✕</button>
|
||||
<button type="button" onClick={() => removeTier(i)} className="text-xs px-2 transition-colors" style={{ color: 'var(--admin-danger)' }}>✕</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
@@ -915,7 +921,7 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) {
|
||||
<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 onClick={addSale} className="text-xs transition-colors" style={{ color: 'var(--admin-accent)' }}>+ Add Row</button>
|
||||
<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">
|
||||
@@ -925,8 +931,8 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) {
|
||||
<span />
|
||||
</div>
|
||||
{historicalSales.map((sale, i) => (
|
||||
<div key={i} className="grid grid-cols-4 gap-2 px-3 py-2 items-center">
|
||||
<input
|
||||
<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)}
|
||||
@@ -934,7 +940,7 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) {
|
||||
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
|
||||
<input aria-label="120"
|
||||
type="text"
|
||||
value={sale.units_sold}
|
||||
onChange={(e) => updateSale(i, "units_sold", e.target.value)}
|
||||
@@ -944,7 +950,7 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) {
|
||||
/>
|
||||
<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
|
||||
<input aria-label="540"
|
||||
type="text"
|
||||
value={sale.revenue}
|
||||
onChange={(e) => updateSale(i, "revenue", e.target.value)}
|
||||
@@ -954,7 +960,7 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) {
|
||||
/>
|
||||
</div>
|
||||
{historicalSales.length > 1 && (
|
||||
<button onClick={() => removeSale(i)} className="text-xs justify-self-end transition-colors" style={{ color: 'var(--admin-danger)' }}>✕</button>
|
||||
<button type="button" onClick={() => removeSale(i)} className="text-xs justify-self-end transition-colors" style={{ color: 'var(--admin-danger)' }}>✕</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
@@ -963,7 +969,7 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) {
|
||||
<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
|
||||
<button type="button"
|
||||
onClick={handleAnalyze}
|
||||
disabled={loading || !productName.trim()}
|
||||
className={btnPrimaryClass}
|
||||
@@ -983,7 +989,7 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) {
|
||||
<p className="text-sm" style={{ color: 'var(--admin-text-secondary)', lineHeight: 1.6 }}>{result.currentState}</p>
|
||||
</AdminCard>
|
||||
{result.recommendations.map((rec, i) => (
|
||||
<AdminCard key={i} className="p-5">
|
||||
<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
|
||||
@@ -1011,8 +1017,8 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) {
|
||||
<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, i) => (
|
||||
<li key={i} className="text-sm" style={{ color: '#059669' }}>• {opp}</li>
|
||||
{result.opportunities.map((opp) => (
|
||||
<li key={opp} className="text-sm" style={{ color: '#059669' }}>• {opp}</li>
|
||||
))}
|
||||
</ul>
|
||||
</AdminCard>
|
||||
@@ -1021,13 +1027,13 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) {
|
||||
<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, i) => (
|
||||
<li key={i} className="text-sm" style={{ color: '#a6953f' }}>• {warn}</li>
|
||||
{result.warnings.map((warn) => (
|
||||
<li key={warn} className="text-sm" style={{ color: '#a6953f' }}>• {warn}</li>
|
||||
))}
|
||||
</ul>
|
||||
</AdminCard>
|
||||
)}
|
||||
<button
|
||||
<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)' }}
|
||||
@@ -1100,25 +1106,25 @@ function StopBlastAdvisorTool({ brandId }: { brandId: string }) {
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1" style={labelStyle}>Stop Name *</label>
|
||||
<input type="text" value={stopName} onChange={(e) => setStopName(e.target.value)} placeholder="Downtown Farmers Market" className={inputBaseClass} style={inputStyle} />
|
||||
<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 type="text" value={stopDate} onChange={(e) => setStopDate(e.target.value)} placeholder="2026-06-15" className={inputBaseClass} style={inputStyle} />
|
||||
<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 type="text" value={city} onChange={(e) => setCity(e.target.value)} placeholder="Greeley" className={inputBaseClass} style={inputStyle} />
|
||||
<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 type="text" value={customerCount} onChange={(e) => setCustomerCount(e.target.value)} placeholder="42" className={inputBaseClass} style={inputStyle} />
|
||||
<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
|
||||
<button type="button"
|
||||
onClick={handleAnalyze}
|
||||
disabled={loading || !stopName.trim()}
|
||||
className={btnPrimaryClass}
|
||||
@@ -1140,7 +1146,7 @@ function StopBlastAdvisorTool({ brandId }: { brandId: string }) {
|
||||
<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 onClick={() => copyToClipboard(result.subjectLine)} className="mt-2 text-xs flex items-center gap-1 transition-colors" style={{ color: 'var(--admin-accent)' }}>
|
||||
<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>
|
||||
@@ -1159,7 +1165,7 @@ function StopBlastAdvisorTool({ brandId }: { brandId: string }) {
|
||||
</span>
|
||||
</AdminCard>
|
||||
{result.contentAngles.map((a, i) => (
|
||||
<AdminCard key={i} className="p-4">
|
||||
<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>
|
||||
@@ -1167,8 +1173,8 @@ function StopBlastAdvisorTool({ brandId }: { brandId: string }) {
|
||||
{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, i) => (
|
||||
<p key={i} className="text-xs" style={{ color: '#a6953f' }}>• {w}</p>
|
||||
{result.warnings.map((w) => (
|
||||
<p key={w} className="text-xs" style={{ color: '#a6953f' }}>• {w}</p>
|
||||
))}
|
||||
</AdminCard>
|
||||
)}
|
||||
@@ -1230,7 +1236,7 @@ function CustomerInsightsTool({ brandId }: { brandId: string }) {
|
||||
</p>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1" style={labelStyle}>Ask about your customers</label>
|
||||
<textarea
|
||||
<textarea aria-label="., 'Which Customers Haven't Ordered In 45 Days?'"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
rows={2}
|
||||
@@ -1241,7 +1247,7 @@ function CustomerInsightsTool({ brandId }: { brandId: string }) {
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{exampleQueries.map((q) => (
|
||||
<button
|
||||
<button type="button"
|
||||
key={q}
|
||||
onClick={() => { setQuery(q); handleAnalyze(q); }}
|
||||
className="rounded-full px-3 py-1 text-xs transition-all"
|
||||
@@ -1258,7 +1264,7 @@ function CustomerInsightsTool({ brandId }: { brandId: string }) {
|
||||
<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
|
||||
<button type="button"
|
||||
onClick={() => handleAnalyze()}
|
||||
disabled={loading || !query.trim()}
|
||||
className={btnPrimaryClass}
|
||||
@@ -1300,13 +1306,16 @@ function CustomerInsightsTool({ brandId }: { brandId: string }) {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{result.results.map((row, i) => (
|
||||
<tr key={i} style={{ borderBottom: '1px solid var(--admin-border-light)' }}>
|
||||
{Object.values(row).map((v, j) => (
|
||||
<td key={j} className="px-2 py-1.5" style={{ color: 'var(--admin-text-secondary)' }}>{String(v ?? "—")}</td>
|
||||
{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>
|
||||
@@ -1388,7 +1397,7 @@ function RouteOptimizerTool({ brandId }: { brandId: string }) {
|
||||
</p>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1" style={labelStyle}>Start Location</label>
|
||||
<input
|
||||
<input aria-label="Warehouse, Greeley CO"
|
||||
type="text"
|
||||
value={startLocation}
|
||||
onChange={(e) => setStartLocation(e.target.value)}
|
||||
@@ -1401,7 +1410,7 @@ function RouteOptimizerTool({ brandId }: { brandId: string }) {
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="block text-sm font-medium" style={labelStyle}>Stops</label>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={addStop}
|
||||
className="text-xs flex items-center gap-1 transition-colors"
|
||||
style={{ color: 'var(--admin-accent)' }}
|
||||
@@ -1410,11 +1419,11 @@ function RouteOptimizerTool({ brandId }: { brandId: string }) {
|
||||
</button>
|
||||
</div>
|
||||
{stops.map((stop, i) => (
|
||||
<AdminCard key={i} className="p-4 space-y-3">
|
||||
<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 onClick={() => removeStop(i)} className="text-xs transition-colors" style={{ color: 'var(--admin-danger)' }}>
|
||||
<button type="button" onClick={() => removeStop(i)} className="text-xs transition-colors" style={{ color: 'var(--admin-danger)' }}>
|
||||
Remove
|
||||
</button>
|
||||
)}
|
||||
@@ -1422,7 +1431,7 @@ function RouteOptimizerTool({ brandId }: { brandId: string }) {
|
||||
<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
|
||||
<input aria-label="Farmers Market"
|
||||
type="text"
|
||||
value={stop.name}
|
||||
onChange={(e) => updateStop(i, "name", e.target.value)}
|
||||
@@ -1433,7 +1442,7 @@ function RouteOptimizerTool({ brandId }: { brandId: string }) {
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs mb-1" style={{ color: 'var(--admin-text-muted)' }}>City *</label>
|
||||
<input
|
||||
<input aria-label="Greeley"
|
||||
type="text"
|
||||
value={stop.city}
|
||||
onChange={(e) => updateStop(i, "city", e.target.value)}
|
||||
@@ -1444,7 +1453,7 @@ function RouteOptimizerTool({ brandId }: { brandId: string }) {
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs mb-1" style={{ color: 'var(--admin-text-muted)' }}>State *</label>
|
||||
<input
|
||||
<input aria-label="CO"
|
||||
type="text"
|
||||
value={stop.state}
|
||||
onChange={(e) => updateStop(i, "state", e.target.value)}
|
||||
@@ -1455,7 +1464,7 @@ function RouteOptimizerTool({ brandId }: { brandId: string }) {
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs mb-1" style={{ color: 'var(--admin-text-muted)' }}>Address</label>
|
||||
<input
|
||||
<input aria-label="123 Main St"
|
||||
type="text"
|
||||
value={stop.address}
|
||||
onChange={(e) => updateStop(i, "address", e.target.value)}
|
||||
@@ -1467,7 +1476,7 @@ function RouteOptimizerTool({ brandId }: { brandId: string }) {
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs mb-1" style={{ color: 'var(--admin-text-muted)' }}>Time Window</label>
|
||||
<input
|
||||
<input aria-label="8am–12pm"
|
||||
type="text"
|
||||
value={stop.time_window}
|
||||
onChange={(e) => updateStop(i, "time_window", e.target.value)}
|
||||
@@ -1482,7 +1491,7 @@ function RouteOptimizerTool({ brandId }: { brandId: string }) {
|
||||
<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
|
||||
<button type="button"
|
||||
onClick={handleOptimize}
|
||||
disabled={loading || validStops.length < 2}
|
||||
className={btnPrimaryClass}
|
||||
@@ -1510,7 +1519,7 @@ function RouteOptimizerTool({ brandId }: { brandId: string }) {
|
||||
<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={i} className="flex items-start gap-3 py-2" style={{ borderBottom: i < result.optimizedSequence.length - 1 ? '1px solid var(--admin-border-light)' : 'none' }}>
|
||||
<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)' }}
|
||||
@@ -1528,16 +1537,16 @@ function RouteOptimizerTool({ brandId }: { brandId: string }) {
|
||||
{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, i) => <p key={i} className="text-sm" style={{ color: '#059669' }}>✓ {s}</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, i) => <p key={i} className="text-sm" style={{ color: '#a6953f' }}>⚠ {w}</p>)}
|
||||
{result.warnings.map((w) => <p key={w} className="text-sm" style={{ color: '#a6953f' }}>⚠ {w}</p>)}
|
||||
</AdminCard>
|
||||
)}
|
||||
<button
|
||||
<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)' }}
|
||||
@@ -1623,11 +1632,11 @@ function DemandForecastTool({ brandId }: { brandId: string }) {
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1" style={labelStyle}>Product Name *</label>
|
||||
<input type="text" value={productName} onChange={(e) => setProductName(e.target.value)} placeholder="Sweet Corn" className={inputBaseClass} style={inputStyle} />
|
||||
<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 type="text" value={stopName} onChange={(e) => setStopName(e.target.value)} placeholder="Downtown Farmers Market" className={inputBaseClass} style={inputStyle} />
|
||||
<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>
|
||||
|
||||
@@ -1635,7 +1644,7 @@ function DemandForecastTool({ brandId }: { brandId: string }) {
|
||||
<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 onClick={addRow} className="text-xs transition-colors" style={{ color: 'var(--admin-accent)' }}>+ Add Row</button>
|
||||
<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">
|
||||
@@ -1645,8 +1654,8 @@ function DemandForecastTool({ brandId }: { brandId: string }) {
|
||||
<span />
|
||||
</div>
|
||||
{historicalData.map((row, i) => (
|
||||
<div key={i} className="grid grid-cols-4 gap-2 px-3 py-2 items-center">
|
||||
<input
|
||||
<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)}
|
||||
@@ -1654,7 +1663,7 @@ function DemandForecastTool({ brandId }: { brandId: string }) {
|
||||
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
|
||||
<input aria-label="120"
|
||||
type="text"
|
||||
value={row.quantity_sold}
|
||||
onChange={(e) => updateRow(i, "quantity_sold", e.target.value)}
|
||||
@@ -1662,7 +1671,7 @@ function DemandForecastTool({ brandId }: { brandId: string }) {
|
||||
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
|
||||
<input aria-label="Farmers Market"
|
||||
type="text"
|
||||
value={row.stop}
|
||||
onChange={(e) => updateRow(i, "stop", e.target.value)}
|
||||
@@ -1671,7 +1680,7 @@ function DemandForecastTool({ brandId }: { brandId: string }) {
|
||||
style={{ borderColor: 'var(--admin-border)', backgroundColor: 'var(--admin-bg)', color: 'var(--admin-text-primary)' }}
|
||||
/>
|
||||
{historicalData.length > 1 && (
|
||||
<button onClick={() => removeRow(i)} className="text-xs justify-self-end transition-colors" style={{ color: 'var(--admin-danger)' }}>✕</button>
|
||||
<button type="button" onClick={() => removeRow(i)} className="text-xs justify-self-end transition-colors" style={{ color: 'var(--admin-danger)' }}>✕</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
@@ -1680,7 +1689,7 @@ function DemandForecastTool({ brandId }: { brandId: string }) {
|
||||
<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
|
||||
<button type="button"
|
||||
onClick={handleAnalyze}
|
||||
disabled={loading || !productName.trim()}
|
||||
className={btnPrimaryClass}
|
||||
@@ -1730,16 +1739,16 @@ function DemandForecastTool({ brandId }: { brandId: string }) {
|
||||
{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, i) => <p key={i} className="text-sm" style={{ color: '#3b82f6' }}>☀ {f}</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, i) => <p key={i} className="text-sm" style={{ color: '#a6953f' }}>⚠ {r}</p>)}
|
||||
{result.riskFlags.map((r) => <p key={r} className="text-sm" style={{ color: '#a6953f' }}>⚠ {r}</p>)}
|
||||
</AdminCard>
|
||||
)}
|
||||
<button
|
||||
<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)' }}
|
||||
@@ -1770,7 +1779,7 @@ function ToolModal({ tool, brandId, brandName, onClose }: ModalProps) {
|
||||
<span className="text-2xl">{tool.icon}</span>
|
||||
<h2 className="text-lg font-bold" style={{ color: 'var(--admin-text-primary)' }}>{tool.title}</h2>
|
||||
</div>
|
||||
<button onClick={onClose} className="p-2 transition-colors" style={{ color: 'var(--admin-text-muted)' }}>
|
||||
<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>
|
||||
@@ -1803,8 +1812,11 @@ export default function AIsettingsClient({
|
||||
}: Props) {
|
||||
const [activeTool, setActiveTool] = useState<AITool | null>(null);
|
||||
const [activeModule, setActiveModule] = useState<string | null>(null);
|
||||
const [selectedProvider, setSelectedProvider] = useState<Provider>(provider);
|
||||
const [currentModel, setCurrentModel] = useState(model);
|
||||
// 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))];
|
||||
@@ -1846,19 +1858,27 @@ export default function AIsettingsClient({
|
||||
currentProvider={selectedProvider}
|
||||
currentModel={currentModel}
|
||||
brandId={brandId}
|
||||
onSelect={setSelectedProvider}
|
||||
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={setCurrentModel}
|
||||
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
|
||||
<button type="button"
|
||||
onClick={() => setActiveModule(null)}
|
||||
className={`admin-filter-tab ${activeModule === null ? 'admin-filter-tab--active' : ''}`}
|
||||
>
|
||||
@@ -1867,7 +1887,7 @@ export default function AIsettingsClient({
|
||||
{modules.map((m) => {
|
||||
const count = AI_TOOLS.filter((t) => t.module === m).length;
|
||||
return (
|
||||
<button
|
||||
<button type="button"
|
||||
key={m}
|
||||
onClick={() => setActiveModule(activeModule === m ? null : m)}
|
||||
className={`admin-filter-tab ${activeModule === m ? 'admin-filter-tab--active' : ''}`}
|
||||
|
||||
Reference in New Issue
Block a user