Changes
This commit is contained in:
@@ -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<Lead[]>([]);
|
||||||
|
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 (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Sparkles className="h-5 w-5 text-primary" />
|
||||||
|
AI Kundprospektering
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Låt AI hitta potentiella B2B-kunder i Storstockholm
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<Button onClick={generateLeads} disabled={isLoading}>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
|
||||||
|
Genererar...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Sparkles className="h-4 w-4 mr-2" />
|
||||||
|
Generera leads
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{[...Array(5)].map((_, i) => (
|
||||||
|
<Skeleton key={i} className="h-16 w-full" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : leads.length > 0 ? (
|
||||||
|
<div className="rounded-md border overflow-hidden">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Building2 className="h-4 w-4" />
|
||||||
|
Företag
|
||||||
|
</div>
|
||||||
|
</TableHead>
|
||||||
|
<TableHead>Bransch</TableHead>
|
||||||
|
<TableHead>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Users className="h-4 w-4" />
|
||||||
|
Anställda
|
||||||
|
</div>
|
||||||
|
</TableHead>
|
||||||
|
<TableHead>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<MapPin className="h-4 w-4" />
|
||||||
|
Stadsdel
|
||||||
|
</div>
|
||||||
|
</TableHead>
|
||||||
|
<TableHead>Varför bra kund</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{leads.map((lead, index) => (
|
||||||
|
<TableRow key={index}>
|
||||||
|
<TableCell className="font-medium">{lead.company_name}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant="outline">{lead.industry}</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge className={getEmployeeCountBadge(lead.employee_count)}>
|
||||||
|
{lead.employee_count} st
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{lead.district}</TableCell>
|
||||||
|
<TableCell className="max-w-xs text-sm text-muted-foreground">
|
||||||
|
{lead.reason}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-12 text-muted-foreground">
|
||||||
|
<Sparkles className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
||||||
|
<p>Klicka på "Generera leads" för att hitta potentiella kunder</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
+10
-1
@@ -7,7 +7,8 @@ import { AdminStats } from "@/components/admin/AdminStats";
|
|||||||
import { OrdersTable } from "@/components/admin/OrdersTable";
|
import { OrdersTable } from "@/components/admin/OrdersTable";
|
||||||
import { CustomersTable } from "@/components/admin/CustomersTable";
|
import { CustomersTable } from "@/components/admin/CustomersTable";
|
||||||
import { PaymentsTable } from "@/components/admin/PaymentsTable";
|
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";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
export default function Admin() {
|
export default function Admin() {
|
||||||
@@ -99,6 +100,10 @@ export default function Admin() {
|
|||||||
<CreditCard className="h-4 w-4" />
|
<CreditCard className="h-4 w-4" />
|
||||||
Betalningar
|
Betalningar
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="leads" className="gap-2">
|
||||||
|
<Sparkles className="h-4 w-4" />
|
||||||
|
AI Leads
|
||||||
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="orders">
|
<TabsContent value="orders">
|
||||||
@@ -112,6 +117,10 @@ export default function Admin() {
|
|||||||
<TabsContent value="payments">
|
<TabsContent value="payments">
|
||||||
<PaymentsTable />
|
<PaymentsTable />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="leads">
|
||||||
|
<LeadsGenerator />
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -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" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user