feat: Latest updates
This commit is contained in:
+210
-90
@@ -14,30 +14,198 @@ const groq = new Groq({
|
||||
dangerouslyAllowBrowser: true // Allowed for client-side demo
|
||||
});
|
||||
|
||||
const SYSTEM_PROMPT = `
|
||||
You are the AI Concierge for "LynkedUpPro", the pinnacle of roofing excellence with over two decades of mastery in the field.
|
||||
Your mission is to guide clients through our seamless, automated roof damage detection and restoration services with elegance and expertise.
|
||||
// --- CONTEXT GENERATORS ---
|
||||
|
||||
COMPANY HIGHLIGHTS:
|
||||
- **Experience**: Over 20 years of unrivaled craftsmanship.
|
||||
- **Special Offer**: The first 3 diagnostic examination visits are completely **COMPLIMENTARY**.
|
||||
- **Efficiency**: We provide detailed estimations in a "jiffy" — swift and precise.
|
||||
- **Value**: We pride ourselves on delivering superior results that outshine the competition, all while remaining cost-effective.
|
||||
- **Discounts**: We offer exclusive price reductions for **Veterans and Seniors** as a token of our gratitude.
|
||||
- **Pricing Policy**: Transparency is our hallmark. While our initial diagnostics are free (first 3 visits), our restoration services are priced with integrity, tailored strictly to the extent of damage and the surface area requiring attention.
|
||||
- **Contact**: (555) 012-3456
|
||||
const BASE_IDENTITY = `
|
||||
You are the AI Concierge for "LynkedUp Pro", the pinnacle of roofing excellence.
|
||||
Your mission is to assist employees with data-driven insights and guide customers through our services.
|
||||
|
||||
GUIDELINES:
|
||||
1. **Tone**: Use eloquent, "flowery", and professional language. Be persuasive yet sincere. (e.g., instead of "we fix roofs", say "we restore the integrity of your shelter").
|
||||
2. **Context Awareness**: Do not overwhelm the user. Gauge their intent. If they ask about price, explain the free exams vs. work costs. If they ask about speed, mention our "estimation in a jiffy".
|
||||
3. **For General Inquiries**: Weave in our 20+ years of heritage and our commitment to excellence.
|
||||
4. **For Callback Requests**:
|
||||
- **IF GUEST**: Gently invite them to Log In or Sign Up to unlock our seamless scheduling experience.
|
||||
- **IF LOGGED IN**: Request their preferred **Phone Number** and **Time Slot** to arrange a consultation. Extract the **Customer Name** from the conversation context (e.g., if they asked about "Greg Alston").
|
||||
- **ACTION**: Once details are confirmed, output this JSON object ONLY at the very end:
|
||||
:::CALLBACK_REQUEST{"name": "...", "phone": "...", "time": "HH:MM"}:::
|
||||
COMPANY INFO:
|
||||
- **Phone**: 866-259-6533
|
||||
- **Experience**: 20+ years of craftsmanship.
|
||||
- **Offer**: First 3 diagnostic exams are FREE.
|
||||
- **Discounts**: 15% off for Veterans & Seniors.
|
||||
|
||||
TONE: Professional, efficient, and data-aware.
|
||||
`;
|
||||
|
||||
const getEmployeeContext = (user, meetings, properties) => {
|
||||
// 1. Agenda (Today + Upcoming)
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const myMeetings = meetings
|
||||
.filter(m => m.agentId === user.id)
|
||||
.sort((a, b) => new Date(a.date) - new Date(b.date));
|
||||
|
||||
// Simple segment: Today vs Upcoming
|
||||
const agendaText = myMeetings.map(m => {
|
||||
const isToday = m.date === today;
|
||||
return `- [${m.date} @ ${m.time}] ${m.status.toUpperCase()} with ${m.customerName} (${m.propertyId})${isToday ? ' **(TODAY)**' : ''}
|
||||
Issue: "${m.issueDescription || 'N/A'}"
|
||||
Comments: "${m.customerComments || 'None'}"
|
||||
Outcome: ${m.outcome || 'Pending'}`;
|
||||
}).join('\n');
|
||||
|
||||
// 2. Hot Leads (Top 5)
|
||||
const hotLeads = properties
|
||||
.filter(p => p.canvassingStatus === 'Hot Lead')
|
||||
.slice(0, 5)
|
||||
.map(p => `- ${p.propertyData.propertyAddress} (Value: $${p.propertyData.currentEstimatedMarketValue.toLocaleString()})`)
|
||||
.join('\n');
|
||||
|
||||
// 3. Golden Leads (Older + High Value)
|
||||
const goldenLeads = properties
|
||||
.filter(p => p.propertyData.yearBuilt < 2000 && p.propertyData.currentEstimatedMarketValue > 400000)
|
||||
.slice(0, 3)
|
||||
.map(p => `- ${p.propertyData.propertyAddress} (Built: ${p.propertyData.yearBuilt})`)
|
||||
.join('\n');
|
||||
|
||||
// 4. Pending Signatures
|
||||
const pendingSigs = properties
|
||||
.filter(p => p.propertyData.pendingSignature)
|
||||
.map(p => `- ${p.propertyData.propertyAddress} (Owner: ${p.ownerData.fullName})`)
|
||||
.join('\n');
|
||||
|
||||
// 5. Revenue & Performance Snapshot
|
||||
const myCompleted = meetings.filter(m => m.agentId === user.id && (m.status === 'Converted' || m.status === 'Completed'));
|
||||
const myWins = meetings.filter(m => m.agentId === user.id && m.status === 'Converted');
|
||||
const totalRevenue = myWins.reduce((sum, m) => sum + (m.dealValue || 0), 0);
|
||||
const winRate = myCompleted.length > 0 ? Math.round((myWins.length / myCompleted.length) * 100) : 0;
|
||||
|
||||
// 6. Neighborhood Intelligence
|
||||
const avgValue = Math.round(properties.reduce((sum, p) => sum + p.propertyData.currentEstimatedMarketValue, 0) / properties.length);
|
||||
const commonRoof = "Architectural Shingle"; // Hardcoded for demo, or could calculate mode
|
||||
|
||||
return `
|
||||
ROLE: FIELD AGENT / ADMIN (${user.name})
|
||||
|
||||
YOUR DATA ACCESS:
|
||||
1. AGENDA:
|
||||
${agendaText || "No meetings found."}
|
||||
|
||||
2. HOT LEADS (Priority):
|
||||
${hotLeads || "None."}
|
||||
|
||||
3. GOLDEN LEADS (High Value / Old):
|
||||
${goldenLeads || "None."}
|
||||
|
||||
4. PENDING SIGNATURES (Follow Up Needed):
|
||||
${pendingSigs || "None."}
|
||||
|
||||
5. MY PERFORMANCE:
|
||||
- Total Revenue (YTD): $${totalRevenue.toLocaleString()}
|
||||
- Win Rate: ${winRate}% (${myWins.length} wins / ${myCompleted.length} completed)
|
||||
|
||||
6. TERRITORY INSIGHTS:
|
||||
- Avg Home Value: $${avgValue.toLocaleString()}
|
||||
- Most Common Roof: ${commonRoof}
|
||||
|
||||
INSTRUCTIONS:
|
||||
- If asked about "agenda", "schedule", or "next appointment", summarize the meetings.
|
||||
- If asked about "leads", suggest the designated Hot or Golden leads.
|
||||
- If asked about "performance" or "stats", share your win rate and revenue.
|
||||
- If asked about "neighborhood" or "territory", share the average value and roof types.
|
||||
`;
|
||||
};
|
||||
|
||||
const getCustomerContext = (user, meetings, properties) => {
|
||||
// 1. My Property
|
||||
// In mock data, customers are linked via 'propertyId' or name matching.
|
||||
const myProperty = properties.find(p => p.propertyData.propertyId === user.propertyId)
|
||||
|| properties.find(p => p.ownerData.fullName === user.name);
|
||||
|
||||
let propDetails = "Property details not found.";
|
||||
let preventiveTips = "Standard seasonal maintenance recommended.";
|
||||
let warrantyStatus = "Standard Manufacturer Warranty (Expired)";
|
||||
|
||||
if (myProperty) {
|
||||
propDetails = `
|
||||
- Address: ${myProperty.propertyData.propertyAddress}
|
||||
- Roof Type: ${myProperty.propertyData.roofMaterial}
|
||||
- Roof Condition: ${myProperty.propertyData.roofCondition}
|
||||
- Storm Risk Score: ${myProperty.propertyData.neighborhoodRating}/10
|
||||
- Est. Value: $${myProperty.propertyData.currentEstimatedMarketValue.toLocaleString()}
|
||||
- Last Inspection: ${myProperty.propertyData.lastInspectionDate || 'Unknown'}
|
||||
`;
|
||||
|
||||
// Dynamic Tips
|
||||
if (myProperty.propertyData.roofCondition === 'Needs Repair') {
|
||||
preventiveTips = "URGENT: Schedule an inspection immediately to prevent water damage.";
|
||||
} else if (myProperty.propertyData.treeCoverage > 50) {
|
||||
preventiveTips = "Consider trimming overhanging branches to protect shingles.";
|
||||
}
|
||||
|
||||
// Mock Warranty Logic
|
||||
const yearBuilt = myProperty.propertyData.yearBuilt;
|
||||
if (yearBuilt > 2015) warrantyStatus = "Active Workmanship Warranty (Valid until 2030)";
|
||||
}
|
||||
|
||||
// 2. History & Upcoming
|
||||
const myHistory = meetings
|
||||
.filter(m => m.customerId === user.id || m.customerName === user.name)
|
||||
.sort((a, b) => new Date(b.date) - new Date(a.date)); // Newest first
|
||||
|
||||
const historyText = myHistory.map(m => {
|
||||
const isFuture = new Date(m.date) > new Date();
|
||||
return `- [${m.date}] ${isFuture ? 'UPCOMING' : 'COMPLETED'} (${m.status})
|
||||
Agent: ${m.agentName || 'Assigned Rep'}
|
||||
Note: ${m.notes}
|
||||
${m.dealValue ? `Cost: $${m.dealValue}` : ''}`;
|
||||
}).join('\n');
|
||||
|
||||
// 3. Total Spend
|
||||
const totalSpend = myHistory.reduce((sum, m) => sum + (m.dealValue || 0), 0);
|
||||
|
||||
return `
|
||||
ROLE: HOMEOWNER / CUSTOMER (${user.name})
|
||||
|
||||
YOUR DATA:
|
||||
1. PROPERTY PROFILE:
|
||||
${propDetails}
|
||||
|
||||
2. SERVICE HISTORY:
|
||||
${historyText || "No records found."}
|
||||
|
||||
3. FINANCIALS:
|
||||
- Total Spend to Date: $${totalSpend.toLocaleString()}
|
||||
- Warranty Status: ${warrantyStatus}
|
||||
|
||||
4. PERSONALIZED ADVICE:
|
||||
- Tip: ${preventiveTips}
|
||||
|
||||
INSTRUCTIONS:
|
||||
- Answer questions about "my roof", "my appointment", "warranty", or "spending".
|
||||
- Be polite and reassuring.
|
||||
`;
|
||||
};
|
||||
|
||||
const getGuestContext = () => {
|
||||
return `
|
||||
ROLE: EXPERT ROOFING CONSULTANT (20+ Years Experience)
|
||||
|
||||
YOUR KNOWLEDGE BASE (Use this to diagnose, reassure, but EDUCATE ON HIDDEN RISKS):
|
||||
|
||||
1. COMMON ISSUES & RISK FACTORS:
|
||||
- **Active Leak**: "A bucket stops the floor damage, but water is currently saturating your insulation and decking." -> URGENCY: "Mold begins to grow in wet insulation within 24-48 hours. We need to map the moisture path immediately."
|
||||
- **Missing Shingles**: "It looks minor, but your underlayment is now exposed to UV rays which destroy it in days." -> RISK: "Leaving this can lead to a denied insurance claim later for 'neglect'."
|
||||
- **Hail Dents**: "These look like small bruises, but they have fractured the fiberglass mat." -> INVISIBLE DAMAGE: "This reduces roof life by 3-5 years. If not documented now, insurance will call it 'wear and tear' later."
|
||||
- **Granule Loss**: "Your roof is shedding its sunscreen." -> REALITY: "The asphalt is now baking in the Texas sun. This is a leading indicator of roof failure within 12-18 months."
|
||||
|
||||
2. COMPLEX ISSUE TRIAGE:
|
||||
- **Ice Dams**: "Heat loss from your attic is melting snow." -> SOLUTION: "We need to assess your intake vents, not just patch the roof."
|
||||
- **Sagging**: "This indicates structural fatigue or long-term water intrusion." -> ACTION: "Do not walk on this area. Needs professional stabilization."
|
||||
|
||||
3. THE "WHY US" HOOK:
|
||||
- "Insurance adjusters use specific criteria to approve claims. We know exactly what they look for."
|
||||
- "A DIY patch often voids the manufacturer warranty. Our repairs are certified."
|
||||
- "We are in Plano right now—I can have a project manager stop by in 30 minutes to check the moisture levels for free."
|
||||
|
||||
INSTRUCTIONS:
|
||||
- **Tone**: Professional, Authoritative, "Doctor-like".
|
||||
- **Strategy**: Validate the user's observation -> Explain the *hidden* consequence of waiting -> Offer the *Zero-Risk* solution (Free Inspection).
|
||||
- **Contact**: "Call 866-259-6533. We can be there today."
|
||||
`;
|
||||
};
|
||||
|
||||
const Chatbot = () => {
|
||||
const { user, isAuthenticated } = useAuth();
|
||||
const { addMeeting, meetings, properties } = useMockStore();
|
||||
@@ -47,7 +215,7 @@ const Chatbot = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isDemoMode, setIsDemoMode] = useState(false);
|
||||
const [messages, setMessages] = useState([
|
||||
{ role: 'assistant', content: 'Hello! Welcome to LynkedUpPro. How can I help you with your roofing needs today?' }
|
||||
{ role: 'assistant', content: 'Hello! Welcome to LynkedUp Pro. How can I help you with your roofing needs today?' }
|
||||
]);
|
||||
const messagesEndRef = useRef(null);
|
||||
|
||||
@@ -65,10 +233,15 @@ const Chatbot = () => {
|
||||
|
||||
// Update greeting and context when user changes
|
||||
useEffect(() => {
|
||||
const greeting = user
|
||||
? `Welcome back to LynkedUpPro, ${user.name}! I see you're logged in. How can I assist you with your roofing projects today?`
|
||||
: 'Hello! Welcome to LynkedUpPro. How can I help you with your roofing needs today?';
|
||||
|
||||
let greeting = 'Hello! Welcome to LynkedUp Pro. How can I help you with your roofing needs today?';
|
||||
if (user) {
|
||||
greeting = `Welcome back, ${user.name}! `;
|
||||
if (user.role === 'FIELD_AGENT' || user.role === 'ADMIN') {
|
||||
greeting += "I have your latest territory data and agenda ready. What do you need?";
|
||||
} else if (user.role === 'CUSTOMER') {
|
||||
greeting += "I have your property records open. How can I assist?";
|
||||
}
|
||||
}
|
||||
setMessages([{ role: 'assistant', content: greeting }]);
|
||||
}, [user]);
|
||||
|
||||
@@ -85,8 +258,8 @@ const Chatbot = () => {
|
||||
// ... existing demo logic ...
|
||||
setTimeout(() => {
|
||||
const responses = [
|
||||
"I can help with that! At LynkedUpPro, we've been in business for 20+ years. I can't access real-time data in demo mode, but I can help you navigate.",
|
||||
"We specialize in automated damage detection. Please call us at (555) 012-3456 for a real quote!",
|
||||
"I can help with that! At LynkedUp Pro, we've been in business for 20+ years. I can't access real-time data in demo mode, but I can help you navigate.",
|
||||
"Please call us at 866-259-6533 for a real quote!", // UPDATED PHONE
|
||||
"Would you like to schedule a callback? Please log in first."
|
||||
];
|
||||
const randomResponse = responses[Math.floor(Math.random() * responses.length)];
|
||||
@@ -98,74 +271,21 @@ const Chatbot = () => {
|
||||
|
||||
// --- REAL AI REQUEST ---
|
||||
|
||||
// Construct System Context with User Details and DATA
|
||||
let systemContext = SYSTEM_PROMPT;
|
||||
// 1. Build Dynamic Context
|
||||
let systemContext = BASE_IDENTITY;
|
||||
|
||||
if (user) {
|
||||
// 1. Basic User Info
|
||||
systemContext += `\n\nCURRENT USER CONTEXT:
|
||||
- Name: ${user.name}
|
||||
- Role: ${user.role} (Type: ${user.type})
|
||||
- ID: ${user.id}
|
||||
- Email: ${user.email || 'Not provided'}`;
|
||||
|
||||
// 2. Inject Meeting/Schedule Data (If Agent or Admin)
|
||||
const userMeetings = user.role === 'ADMIN'
|
||||
? meetings // Admins see ALL meetings (simplified for chatbot context, maybe limit to recent?)
|
||||
: meetings.filter(m => m.agentId === user.id);
|
||||
|
||||
if (userMeetings.length > 0) {
|
||||
const scheduleSummary = userMeetings.map(m =>
|
||||
`- [${m.status}] ${m.date} @ ${m.time}: ${m.customerName} (Prop: ${m.propertyId})${m.dealValue ? ` | Value: $${m.dealValue.toLocaleString()}` : ''}`
|
||||
).join('\n');
|
||||
|
||||
systemContext += `\n\nUSER'S SCHEDULE / DEALS (Use this to answer questions about meetings, status, and conversions):
|
||||
${scheduleSummary}`;
|
||||
} else {
|
||||
systemContext += `\n\nUSER'S SCHEDULE: No meetings found.`;
|
||||
if (user.role === 'FIELD_AGENT' || user.role === 'ADMIN') {
|
||||
systemContext += getEmployeeContext(user, meetings, properties);
|
||||
} else if (user.role === 'CUSTOMER') {
|
||||
systemContext += getCustomerContext(user, meetings, properties);
|
||||
}
|
||||
} else {
|
||||
systemContext += `\n\nCURRENT USER CONTEXT: Guest (Not Logged In)`;
|
||||
}
|
||||
|
||||
// --- INJECT DASHBOARD DATA ---
|
||||
// Calculate real-time metrics to give the AI context about the territory
|
||||
if (properties && properties.length > 0) {
|
||||
const totalProps = properties.length;
|
||||
|
||||
// 1. Golden Leads
|
||||
const goldenLeads = properties.filter(p =>
|
||||
p.propertyData.yearBuilt < 2000 && p.propertyData.currentEstimatedMarketValue > 400000
|
||||
);
|
||||
const goldenLeadAddresses = goldenLeads.slice(0, 3).map(p => p.propertyData.propertyAddress).join(', ');
|
||||
|
||||
// 2. Hot Leads
|
||||
const hotLeads = properties.filter(p => p.canvassingStatus === 'Hot Lead');
|
||||
|
||||
// 3. Roof Health
|
||||
const roofCounts = properties.reduce((acc, p) => {
|
||||
const c = p.propertyData.roofCondition || 'Unknown';
|
||||
acc[c] = (acc[c] || 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// 4. Intent
|
||||
const willingToSell = properties.filter(p => p.ownerData.willingToSellProperty).length;
|
||||
const willingToRent = properties.filter(p => p.ownerData.willingToRentProperty).length;
|
||||
|
||||
// 5. Risk Index (simplified calculation for context)
|
||||
const vulnerableCount = (roofCounts['Fair'] || 0) + (roofCounts['Needs Repair'] || 0);
|
||||
const riskPercent = Math.round((vulnerableCount / totalProps) * 100);
|
||||
|
||||
systemContext += `\n\nLIVE DASHBOARD INTELLIGENCE (Use this to answer questions about leads and territory status):
|
||||
- Total Properties in Territory: ${totalProps}
|
||||
- GOLDEN LEADS (High Value + Older Homes): ${goldenLeads.length} found. (Top examples: ${goldenLeadAddresses}...)
|
||||
- HOT LEADS (Priority for Canvassing): ${hotLeads.length} properties.
|
||||
- OWNER INTENT: ${willingToSell} owners willing to sell, ${willingToRent} willing to rent.
|
||||
- ROOF HEALTH BREAKDOWN: Excellent: ${roofCounts['Excellent'] || 0}, Good: ${roofCounts['Good'] || 0}, Fair: ${roofCounts['Fair'] || 0}, Needs Repair: ${roofCounts['Needs Repair'] || 0}.
|
||||
- RISK ANALYSIS: ${riskPercent}% of territory has vulnerable roofs.`;
|
||||
// GUEST CONTEXT - The "Seasoned Consultant"
|
||||
systemContext += getGuestContext();
|
||||
}
|
||||
|
||||
// 2. Call Groq
|
||||
const chatCompletion = await groq.chat.completions.create({
|
||||
messages: [
|
||||
{ role: 'system', content: systemContext },
|
||||
@@ -181,7 +301,7 @@ ${scheduleSummary}`;
|
||||
// Filter out <think> blocks if present
|
||||
responseContent = responseContent.replace(/<think>[\s\S]*?<\/think>/gi, '').trim();
|
||||
|
||||
// Check for Function Call Pattern
|
||||
// Check for Function Call Pattern (Legacy Callback)
|
||||
const callbackMatch = responseContent.match(/:::CALLBACK_REQUEST({.*?}):::/);
|
||||
if (callbackMatch) {
|
||||
const jsonStr = callbackMatch[1];
|
||||
|
||||
Reference in New Issue
Block a user