From 93748dded3f06496c7c44f04a29fafcffc95d29d Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 28 Jan 2026 14:25:18 +0000 Subject: [PATCH] Changes --- src/components/admin/LeadsGenerator.tsx | 159 +++++++++++++++++++++ src/pages/Admin.tsx | 11 +- supabase/functions/generate-leads/index.ts | 127 ++++++++++++++++ 3 files changed, 296 insertions(+), 1 deletion(-) create mode 100644 src/components/admin/LeadsGenerator.tsx create mode 100644 supabase/functions/generate-leads/index.ts diff --git a/src/components/admin/LeadsGenerator.tsx b/src/components/admin/LeadsGenerator.tsx new file mode 100644 index 0000000..2daa8de --- /dev/null +++ b/src/components/admin/LeadsGenerator.tsx @@ -0,0 +1,159 @@ +import { useState } from "react"; +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 { useToast } from "@/hooks/use-toast"; +import { Skeleton } from "@/components/ui/skeleton"; + +interface Lead { + company_name: string; + industry: string; + employee_count: number; + district: string; + reason: string; +} + +export function LeadsGenerator() { + const [leads, setLeads] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const { toast } = useToast(); + + const generateLeads = async () => { + setIsLoading(true); + try { + const response = await fetch( + `${import.meta.env.VITE_SUPABASE_URL}/functions/v1/generate-leads`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY}`, + }, + } + ); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || "Kunde inte generera leads"); + } + + const data = await response.json(); + setLeads(data.leads || []); + toast({ + title: "Leads genererade!", + description: `${data.leads?.length || 0} potentiella kunder hittades.`, + }); + } catch (error) { + console.error("Error generating leads:", error); + toast({ + title: "Fel", + description: error instanceof Error ? error.message : "Kunde inte generera leads", + variant: "destructive", + }); + } finally { + setIsLoading(false); + } + }; + + const getEmployeeCountBadge = (count: number) => { + if (count >= 100) return "bg-green-100 text-green-800"; + if (count >= 50) return "bg-blue-100 text-blue-800"; + if (count >= 25) return "bg-yellow-100 text-yellow-800"; + return "bg-gray-100 text-gray-800"; + }; + + return ( + + +
+
+ + + AI Kundprospektering + + + Låt AI hitta potentiella B2B-kunder i Storstockholm + +
+ +
+
+ + {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} + + + ))} + +
+
+ ) : ( +
+ +

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

+
+ )} +
+
+ ); +} diff --git a/src/pages/Admin.tsx b/src/pages/Admin.tsx index 83370f5..3cf9fb3 100644 --- a/src/pages/Admin.tsx +++ b/src/pages/Admin.tsx @@ -7,7 +7,8 @@ import { AdminStats } from "@/components/admin/AdminStats"; import { OrdersTable } from "@/components/admin/OrdersTable"; import { CustomersTable } from "@/components/admin/CustomersTable"; import { PaymentsTable } from "@/components/admin/PaymentsTable"; -import { Package, Users, CreditCard, LogOut, ArrowLeft } from "lucide-react"; +import { LeadsGenerator } from "@/components/admin/LeadsGenerator"; +import { Package, Users, CreditCard, LogOut, ArrowLeft, Sparkles } from "lucide-react"; import { Skeleton } from "@/components/ui/skeleton"; export default function Admin() { @@ -99,6 +100,10 @@ export default function Admin() { Betalningar + + + AI Leads + @@ -112,6 +117,10 @@ export default function Admin() { + + + + diff --git a/supabase/functions/generate-leads/index.ts b/supabase/functions/generate-leads/index.ts new file mode 100644 index 0000000..4818ebb --- /dev/null +++ b/supabase/functions/generate-leads/index.ts @@ -0,0 +1,127 @@ +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", +}; + +serve(async (req) => { + if (req.method === "OPTIONS") { + return new Response(null, { headers: corsHeaders }); + } + + try { + const LOVABLE_API_KEY = Deno.env.get("LOVABLE_API_KEY"); + if (!LOVABLE_API_KEY) { + throw new Error("LOVABLE_API_KEY is not configured"); + } + + const response = 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 B2B-säljexpert som identifierar potentiella kunder för ett bageri som levererar färska kakor till företag i Storstockholm. + +Generera en lista med 15 verkliga eller realistiska företag i Storstockholmsområdet som skulle vara bra kunder för veckovisa kakleveranser. +Fokusera på: +- Kontor med 10+ anställda +- Hotell och restauranger +- Caféer och fik +- Eventlokaler +- Coworking-spaces +- Större företag med personalrum/fika + +För varje företag, ange: +- Företagsnamn +- Bransch +- Uppskattat antal anställda +- Stadsdel i Stockholm +- Varför de är en bra kund (kort)` + }, + { + role: "user", + content: "Generera 15 potentiella B2B-kunder för kakleveranser i Storstockholm." + } + ], + tools: [ + { + type: "function", + function: { + name: "generate_leads", + description: "Returnerar en lista med potentiella B2B-kunder", + parameters: { + type: "object", + properties: { + leads: { + type: "array", + items: { + type: "object", + properties: { + company_name: { type: "string", description: "Företagets namn" }, + industry: { type: "string", description: "Bransch" }, + employee_count: { type: "number", description: "Antal anställda" }, + district: { type: "string", description: "Stadsdel i Stockholm" }, + reason: { type: "string", description: "Varför de är en bra kund" } + }, + required: ["company_name", "industry", "employee_count", "district", "reason"], + additionalProperties: false + } + } + }, + required: ["leads"], + additionalProperties: false + } + } + } + ], + tool_choice: { type: "function", function: { name: "generate_leads" } } + }), + }); + + if (!response.ok) { + if (response.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 (response.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 response.text(); + console.error("AI gateway error:", response.status, errorText); + throw new Error(`AI gateway error: ${response.status}`); + } + + const data = await response.json(); + + // Extract the tool call result + const toolCall = data.choices?.[0]?.message?.tool_calls?.[0]; + if (!toolCall || toolCall.function.name !== "generate_leads") { + throw new Error("Unexpected AI response format"); + } + + const leads = JSON.parse(toolCall.function.arguments); + + return new Response(JSON.stringify(leads), { + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } catch (error) { + console.error("Error generating leads:", 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" }, + }); + } +});