diff --git a/src/components/admin/LeadsGenerator.tsx b/src/components/admin/LeadsGenerator.tsx index 2daa8de..e115ca1 100644 --- a/src/components/admin/LeadsGenerator.tsx +++ b/src/components/admin/LeadsGenerator.tsx @@ -3,7 +3,8 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Badge } from "@/components/ui/badge"; -import { Sparkles, RefreshCw, Building2, Users, MapPin } from "lucide-react"; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Sparkles, RefreshCw, Building2, Users, MapPin, Mail, Search, Copy, Check } from "lucide-react"; import { useToast } from "@/hooks/use-toast"; import { Skeleton } from "@/components/ui/skeleton"; @@ -15,9 +16,24 @@ interface Lead { reason: string; } +interface EmailContent { + subject: string; + body: string; + personalization_points: string[]; +} + +interface ResearchResult { + company_name: string; + research_summary: string; + email: EmailContent; +} + export function LeadsGenerator() { const [leads, setLeads] = useState([]); const [isLoading, setIsLoading] = useState(false); + const [researchingLead, setResearchingLead] = useState(null); + const [emailDialog, setEmailDialog] = useState(null); + const [copiedField, setCopiedField] = useState(null); const { toast } = useToast(); const generateLeads = async () => { @@ -57,6 +73,58 @@ export function LeadsGenerator() { } }; + const researchCompany = async (lead: Lead) => { + setResearchingLead(lead.company_name); + try { + const response = await fetch( + `${import.meta.env.VITE_SUPABASE_URL}/functions/v1/research-company`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY}`, + }, + body: JSON.stringify({ + company_name: lead.company_name, + district: lead.district, + industry: lead.industry, + }), + } + ); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || "Kunde inte researcha företaget"); + } + + const data: ResearchResult = await response.json(); + setEmailDialog(data); + toast({ + title: "Research klar!", + description: `Personligt mejl genererat för ${lead.company_name}`, + }); + } catch (error) { + console.error("Error researching company:", error); + toast({ + title: "Fel", + description: error instanceof Error ? error.message : "Kunde inte researcha företaget", + variant: "destructive", + }); + } finally { + setResearchingLead(null); + } + }; + + const copyToClipboard = async (text: string, field: string) => { + await navigator.clipboard.writeText(text); + setCopiedField(field); + setTimeout(() => setCopiedField(null), 2000); + toast({ + title: "Kopierat!", + description: `${field} kopierat till urklipp`, + }); + }; + const getEmployeeCountBadge = (count: number) => { if (count >= 100) return "bg-green-100 text-green-800"; if (count >= 50) return "bg-blue-100 text-blue-800"; @@ -65,95 +133,196 @@ export function LeadsGenerator() { }; return ( - - -
-
- - - AI Kundprospektering - - - Låt AI hitta potentiella B2B-kunder i Storstockholm - + <> + + +
+
+ + + AI Kundprospektering + + + Generera leads och skapa personliga mejl med AI-driven research + +
+
- -
- - - {isLoading ? ( -
- {[...Array(5)].map((_, i) => ( - - ))} -
- ) : leads.length > 0 ? ( -
- - - - -
- - Företag -
-
- Bransch - -
- - Anställda -
-
- -
- - Stadsdel -
-
- Varför bra kund -
-
- - {leads.map((lead, index) => ( - - {lead.company_name} - - {lead.industry} - - - - {lead.employee_count} st - - - {lead.district} - - {lead.reason} - + + + {isLoading ? ( +
+ {[...Array(5)].map((_, i) => ( + + ))} +
+ ) : leads.length > 0 ? ( +
+
+ + + +
+ + Företag +
+
+ Bransch + +
+ + Anställda +
+
+ +
+ + Stadsdel +
+
+ Varför bra kund + Åtgärd
- ))} - -
-
- ) : ( -
- -

Klicka på "Generera leads" för att hitta potentiella kunder

-
- )} -
- + + + {leads.map((lead, index) => ( + + {lead.company_name} + + {lead.industry} + + + + {lead.employee_count} st + + + {lead.district} + + {lead.reason} + + + + + + ))} + + +
+ ) : ( +
+ +

Klicka på "Generera leads" för att hitta potentiella kunder

+
+ )} + +
+ + setEmailDialog(null)}> + + + + + Personligt mejl för {emailDialog?.company_name} + + + AI-genererat mejl baserat på företagsresearch + + + + {emailDialog && ( +
+ {emailDialog.research_summary && ( +
+

Research-sammanfattning:

+

{emailDialog.research_summary.substring(0, 300)}...

+
+ )} + +
+
+
+ + +
+
+

{emailDialog.email.subject}

+
+
+ +
+
+ + +
+
+ {emailDialog.email.body} +
+
+ + {emailDialog.email.personalization_points?.length > 0 && ( +
+ +
    + {emailDialog.email.personalization_points.map((point, i) => ( +
  • {point}
  • + ))} +
+
+ )} +
+
+ )} +
+
+ ); } diff --git a/supabase/config.toml b/supabase/config.toml index 1b5881b..0715723 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -1 +1,10 @@ -project_id = "teeojvlqulhghditoakc" \ No newline at end of file +project_id = "teeojvlqulhghditoakc" + +[functions.generate-leads] +verify_jwt = false + +[functions.research-company] +verify_jwt = false + +[functions.send-contact-email] +verify_jwt = false diff --git a/supabase/functions/research-company/index.ts b/supabase/functions/research-company/index.ts new file mode 100644 index 0000000..44dd7be --- /dev/null +++ b/supabase/functions/research-company/index.ts @@ -0,0 +1,171 @@ +import { serve } from "https://deno.land/std@0.168.0/http/server.ts"; + +const corsHeaders = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type, x-supabase-client-platform, x-supabase-client-platform-version, x-supabase-client-runtime, x-supabase-client-runtime-version", +}; + +interface CompanyResearchRequest { + company_name: string; + district: string; + industry: string; +} + +serve(async (req) => { + if (req.method === "OPTIONS") { + return new Response(null, { headers: corsHeaders }); + } + + try { + const FIRECRAWL_API_KEY = Deno.env.get("FIRECRAWL_API_KEY"); + const LOVABLE_API_KEY = Deno.env.get("LOVABLE_API_KEY"); + + if (!FIRECRAWL_API_KEY) { + throw new Error("FIRECRAWL_API_KEY is not configured"); + } + if (!LOVABLE_API_KEY) { + throw new Error("LOVABLE_API_KEY is not configured"); + } + + const { company_name, district, industry }: CompanyResearchRequest = await req.json(); + + if (!company_name) { + throw new Error("company_name is required"); + } + + console.log(`Researching company: ${company_name}`); + + // Step 1: Search for company website using Firecrawl + const searchResponse = await fetch("https://api.firecrawl.dev/v1/search", { + method: "POST", + headers: { + "Authorization": `Bearer ${FIRECRAWL_API_KEY}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + query: `${company_name} ${district} Stockholm företag`, + limit: 3, + scrapeOptions: { + formats: ["markdown"], + }, + }), + }); + + if (!searchResponse.ok) { + const errorText = await searchResponse.text(); + console.error("Firecrawl search error:", errorText); + throw new Error(`Firecrawl search failed: ${searchResponse.status}`); + } + + const searchData = await searchResponse.json(); + const companyInfo = searchData.data?.slice(0, 2).map((result: any) => result.markdown || result.description).join("\n\n") || ""; + + console.log(`Found company info: ${companyInfo.substring(0, 200)}...`); + + // Step 2: Generate personalized email using Lovable AI + const emailResponse = await fetch("https://ai.gateway.lovable.dev/v1/chat/completions", { + method: "POST", + headers: { + Authorization: `Bearer ${LOVABLE_API_KEY}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model: "google/gemini-3-flash-preview", + messages: [ + { + role: "system", + content: `Du är en erfaren B2B-säljare för ett bageri i Stockholm som levererar färska kakor till företag. + +Din uppgift är att skriva ett personligt och engagerande kall-mejl till potentiella kunder. + +Regler: +- Håll mejlet kort (max 150 ord) +- Var personlig och referera till specifik info om företaget +- Nämn att vi levererar färska kakor varje vecka +- Föreslå ett möte eller provleverans +- Avsluta med en tydlig call-to-action +- Skriv på svenska +- Var professionell men varm i tonen` + }, + { + role: "user", + content: `Skriv ett personligt kall-mejl till: +Företag: ${company_name} +Bransch: ${industry} +Område: ${district} + +Information om företaget: +${companyInfo || "Ingen specifik information hittad, använd generella branschinsikter."}` + } + ], + tools: [ + { + type: "function", + function: { + name: "generate_email", + description: "Genererar ett personligt kall-mejl", + parameters: { + type: "object", + properties: { + subject: { type: "string", description: "Mejlets ämnesrad" }, + body: { type: "string", description: "Mejlets innehåll" }, + personalization_points: { + type: "array", + items: { type: "string" }, + description: "Punkter som gör mejlet personligt" + } + }, + required: ["subject", "body", "personalization_points"], + additionalProperties: false + } + } + } + ], + tool_choice: { type: "function", function: { name: "generate_email" } } + }), + }); + + if (!emailResponse.ok) { + if (emailResponse.status === 429) { + return new Response(JSON.stringify({ error: "Rate limit överskriden, försök igen senare." }), { + status: 429, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } + if (emailResponse.status === 402) { + return new Response(JSON.stringify({ error: "Krediter slut, vänligen fyll på." }), { + status: 402, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } + const errorText = await emailResponse.text(); + console.error("AI gateway error:", emailResponse.status, errorText); + throw new Error(`AI gateway error: ${emailResponse.status}`); + } + + const emailData = await emailResponse.json(); + const toolCall = emailData.choices?.[0]?.message?.tool_calls?.[0]; + + if (!toolCall || toolCall.function.name !== "generate_email") { + throw new Error("Unexpected AI response format"); + } + + const emailContent = JSON.parse(toolCall.function.arguments); + + return new Response(JSON.stringify({ + success: true, + company_name, + research_summary: companyInfo.substring(0, 500), + email: emailContent + }), { + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } catch (error) { + console.error("Error researching company:", error); + const errorMessage = error instanceof Error ? error.message : "Unknown error"; + return new Response(JSON.stringify({ error: errorMessage }), { + status: 500, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } +});