refactor: remove unrelated files and restructure

This commit is contained in:
Sosokker 2025-04-08 01:21:07 +07:00
parent ae15103e1f
commit 30d135ba0e
55 changed files with 6102 additions and 3046 deletions

View File

@ -0,0 +1,407 @@
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { ArrowLeft, Globe, FileUp, DatabaseIcon, Plus, Trash2, BrainCircuit } from "lucide-react"
import Link from "next/link"
import PageHeader from "@/components/page-header"
import { Badge } from "@/components/ui/badge"
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"
import { Switch } from "@/components/ui/switch"
export default function CreatePipelinePage() {
return (
<div className="container mx-auto p-6">
<PageHeader
title="Create Data Pipeline"
description="Set up a new automated data collection pipeline"
breadcrumb={[
{ title: "Home", href: "/" },
{ title: "Data Pipeline", href: "/data-pipeline" },
{ title: "Create", href: "/data-pipeline/create" },
]}
/>
<div className="mt-6">
<Link href="/data-pipeline">
<Button variant="outline" className="mb-6">
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Pipelines
</Button>
</Link>
<div className="grid gap-6 md:grid-cols-2">
<div>
<Card className="border-2 hover:border-highlight-border transition-all duration-200">
<CardHeader>
<CardTitle>Pipeline Details</CardTitle>
<CardDescription>Basic information about your data pipeline</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Pipeline Name</Label>
<Input id="name" placeholder="e.g., Property Listings Pipeline" />
</div>
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
placeholder="Describe what this pipeline collects and how it will be used"
rows={4}
/>
</div>
<div className="space-y-2">
<Label htmlFor="tags">Tags (optional)</Label>
<Input id="tags" placeholder="e.g., real-estate, properties, listings" />
<p className="text-xs text-muted-foreground mt-1">Separate tags with commas</p>
</div>
</div>
</CardContent>
</Card>
<Card className="mt-6 border-2 hover:border-highlight-border transition-all duration-200">
<CardHeader>
<div>
<CardTitle className="text-lg flex items-center">
<BrainCircuit className="mr-2 h-5 w-5 text-primary" />
AI Assistant
</CardTitle>
<CardDescription>Customize how AI processes your data</CardDescription>
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="ai-prompt">Additional Instructions for AI</Label>
<Textarea
id="ai-prompt"
placeholder="E.g., Focus on extracting pricing trends, ignore promotional content, prioritize property features..."
rows={4}
className="border-primary/20"
/>
<p className="text-xs text-muted-foreground mt-1">
Provide specific instructions to guide the AI in processing your data sources
</p>
</div>
<div className="space-y-3 pt-2">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="detect-fields">Auto-detect common fields</Label>
<p className="text-xs text-muted-foreground">Automatically identify price, location, etc.</p>
</div>
<Switch id="detect-fields" defaultChecked />
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="suggest-mappings">Suggest field mappings</Label>
<p className="text-xs text-muted-foreground">
Get AI suggestions for matching fields across sources
</p>
</div>
<Switch id="suggest-mappings" defaultChecked />
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="deduplicate">Deduplicate records</Label>
<p className="text-xs text-muted-foreground">Remove duplicate entries automatically</p>
</div>
<Switch id="deduplicate" defaultChecked />
</div>
</div>
</div>
</CardContent>
</Card>
</div>
<div>
<Card className="border-2 hover:border-highlight-border transition-all duration-200">
<CardHeader>
<CardTitle>Data Sources</CardTitle>
<CardDescription>Add one or more data sources to your pipeline</CardDescription>
</CardHeader>
<CardContent>
<Accordion type="single" collapsible className="w-full" defaultValue="source-1">
<AccordionItem value="source-1" className="border rounded-md mb-4 data-source-card active">
<AccordionTrigger className="px-4">
<div className="flex items-center">
<Globe className="mr-2 h-5 w-5 text-primary" />
<span>Website Source #1</span>
</div>
</AccordionTrigger>
<AccordionContent className="px-4 pb-4">
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="url-1">Website URL</Label>
<Input id="url-1" placeholder="https://example.com/listings" />
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor="additional-urls-1">Additional URLs (optional)</Label>
<Badge variant="outline" className="text-xs">
Pattern Detection
</Badge>
</div>
<Textarea
id="additional-urls-1"
placeholder="https://example.com/listings/page2&#10;https://example.com/listings/page3"
rows={3}
/>
<p className="text-xs text-muted-foreground mt-1">
Add multiple URLs from the same website (one per line)
</p>
</div>
<div className="flex justify-end">
<Button variant="outline" size="sm" className="text-destructive">
<Trash2 className="mr-2 h-4 w-4" />
Remove Source
</Button>
</div>
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value="source-2" className="border rounded-md mb-4 data-source-card">
<AccordionTrigger className="px-4">
<div className="flex items-center">
<FileUp className="mr-2 h-5 w-5 text-primary" />
<span>File Upload Source #1</span>
</div>
</AccordionTrigger>
<AccordionContent className="px-4 pb-4">
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="file-upload-1">Upload File</Label>
<div className="flex items-center justify-center p-6 border-2 border-dashed rounded-lg">
<div className="text-center">
<p className="text-sm text-muted-foreground">
Drag and drop your file here, or click to browse
</p>
<Button variant="outline" className="mt-2">
Select File
</Button>
</div>
</div>
<p className="text-xs text-muted-foreground mt-1">
Supported formats: CSV, JSON, Excel, XML (up to 50MB)
</p>
</div>
<div className="flex justify-end">
<Button variant="outline" size="sm" className="text-destructive">
<Trash2 className="mr-2 h-4 w-4" />
Remove Source
</Button>
</div>
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value="source-3" className="border rounded-md mb-4 data-source-card">
<AccordionTrigger className="px-4">
<div className="flex items-center">
<DatabaseIcon className="mr-2 h-5 w-5 text-primary" />
<span>API Source #1</span>
</div>
</AccordionTrigger>
<AccordionContent className="px-4 pb-4">
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="api-url-1">API Endpoint URL</Label>
<Input id="api-url-1" placeholder="https://api.example.com/data" />
</div>
<div className="space-y-2">
<Label htmlFor="auth-type-1">Authentication Type</Label>
<select
id="auth-type-1"
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
>
<option value="none">None</option>
<option value="basic">Basic Auth</option>
<option value="bearer">Bearer Token</option>
<option value="api-key">API Key</option>
</select>
</div>
<div className="flex justify-end">
<Button variant="outline" size="sm" className="text-destructive">
<Trash2 className="mr-2 h-4 w-4" />
Remove Source
</Button>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
<div className="flex flex-col gap-2 mt-4">
<Button variant="outline" className="w-full justify-start gap-2">
<Plus className="h-4 w-4" />
Add Website Source
</Button>
<Button variant="outline" className="w-full justify-start gap-2">
<Plus className="h-4 w-4" />
Add File Upload Source
</Button>
<Button variant="outline" className="w-full justify-start gap-2">
<Plus className="h-4 w-4" />
Add API Source
</Button>
</div>
</CardContent>
</Card>
<Card className="mt-6 border-2 hover:border-highlight-border transition-all duration-200">
<CardHeader>
<CardTitle>Schedule & Automation</CardTitle>
<CardDescription>Configure when and how your pipeline should run</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-6 md:grid-cols-2">
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="frequency">Run Frequency</Label>
<select
id="frequency"
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
>
<option value="manual">Manual (Run on demand)</option>
<option value="hourly">Hourly</option>
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
<option value="monthly">Monthly</option>
<option value="custom">Custom Schedule</option>
</select>
</div>
<div className="space-y-2">
<Label htmlFor="timezone">Timezone</Label>
<select
id="timezone"
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
>
<option value="utc">UTC</option>
<option value="est">Eastern Time (ET)</option>
<option value="cst">Central Time (CT)</option>
<option value="mst">Mountain Time (MT)</option>
<option value="pst">Pacific Time (PT)</option>
</select>
</div>
<div className="space-y-2">
<Label htmlFor="max-records">Collection Limits</Label>
<div className="space-y-3">
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="limit-records"
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
/>
<div className="flex-1">
<Label htmlFor="limit-records" className="text-sm font-normal">
Limit total records
</Label>
<Input id="max-records" type="number" placeholder="e.g., 1000" className="mt-1" />
</div>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="stop-no-new"
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
/>
<Label htmlFor="stop-no-new" className="text-sm font-normal">
Stop when no new records found
</Label>
</div>
</div>
</div>
</div>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="notifications">Notifications</Label>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="notify-complete"
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
defaultChecked
/>
<Label htmlFor="notify-complete" className="text-sm font-normal">
Notify when pipeline completes
</Label>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="notify-error"
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
defaultChecked
/>
<Label htmlFor="notify-error" className="text-sm font-normal">
Notify on errors
</Label>
</div>
<div className="mt-2">
<Input id="email" type="email" placeholder="Email for notifications" />
</div>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="retry-settings">Retry Settings</Label>
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label htmlFor="retry-attempts" className="text-sm font-normal">
Retry Attempts
</Label>
<Input
id="retry-attempts"
type="number"
placeholder="e.g., 3"
defaultValue="3"
className="w-24"
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="retry-delay" className="text-sm font-normal">
Delay Between Retries (minutes)
</Label>
<Input
id="retry-delay"
type="number"
placeholder="e.g., 5"
defaultValue="5"
className="w-24"
/>
</div>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
<div className="mt-6 flex justify-end space-x-4">
<Button variant="outline">Save as Draft</Button>
<Button>Create Pipeline</Button>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,242 @@
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Badge } from "@/components/ui/badge"
import { Clock, Database, Play, Plus, RefreshCw, Pause, AlertTriangle, Copy } from "lucide-react"
import Link from "next/link"
import PageHeader from "@/components/page-header"
export default function DataPipelinePage() {
return (
<div className="container mx-auto p-6">
<PageHeader
title="Data Pipelines"
description="Manage your automated data collection pipelines"
breadcrumb={[
{ title: "Home", href: "/" },
{ title: "Data Pipeline", href: "/data-pipeline" },
]}
/>
<div className="flex justify-between items-center mt-6">
<Tabs defaultValue="active" className="w-full">
<TabsList>
<TabsTrigger value="active">Active Pipelines</TabsTrigger>
<TabsTrigger value="paused">Paused</TabsTrigger>
<TabsTrigger value="all">All Pipelines</TabsTrigger>
</TabsList>
<div className="flex justify-end mt-4">
<Link href="/data-pipeline/create">
<Button className="gap-2">
<Plus className="h-4 w-4" />
Create Pipeline
</Button>
</Link>
</div>
<TabsContent value="active" className="mt-4">
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
<PipelineCard
title="Property Listings"
description="Scrapes real estate listings from multiple websites"
status="active"
lastRun="2 hours ago"
nextRun="Tomorrow at 9:00 AM"
sources={3}
records={1240}
aiPowered={true}
/>
<PipelineCard
title="Rental Market Data"
description="Collects rental prices and availability"
status="active"
lastRun="Yesterday"
nextRun="In 3 days"
sources={2}
records={830}
aiPowered={true}
/>
<PipelineCard
title="Price Comparison"
description="Tracks property price changes over time"
status="error"
lastRun="2 days ago"
nextRun="Scheduled retry in 12 hours"
sources={4}
records={1560}
error="Connection timeout on 1 source"
/>
</div>
</TabsContent>
<TabsContent value="paused" className="mt-4">
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
<PipelineCard
title="Commercial Properties"
description="Collects data on commercial real estate"
status="paused"
lastRun="1 week ago"
nextRun="Paused"
sources={2}
records={450}
/>
</div>
</TabsContent>
<TabsContent value="all" className="mt-4">
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
<PipelineCard
title="Property Listings"
description="Scrapes real estate listings from multiple websites"
status="active"
lastRun="2 hours ago"
nextRun="Tomorrow at 9:00 AM"
sources={3}
records={1240}
aiPowered={true}
/>
<PipelineCard
title="Rental Market Data"
description="Collects rental prices and availability"
status="active"
lastRun="Yesterday"
nextRun="In 3 days"
sources={2}
records={830}
aiPowered={true}
/>
<PipelineCard
title="Price Comparison"
description="Tracks property price changes over time"
status="error"
lastRun="2 days ago"
nextRun="Scheduled retry in 12 hours"
sources={4}
records={1560}
error="Connection timeout on 1 source"
/>
<PipelineCard
title="Commercial Properties"
description="Collects data on commercial real estate"
status="paused"
lastRun="1 week ago"
nextRun="Paused"
sources={2}
records={450}
/>
</div>
</TabsContent>
</Tabs>
</div>
</div>
)
}
interface PipelineCardProps {
title: string
description: string
status: "active" | "paused" | "error"
lastRun: string
nextRun: string
sources: number
records: number
error?: string
aiPowered?: boolean
}
function PipelineCard({
title,
description,
status,
lastRun,
nextRun,
sources,
records,
error,
aiPowered,
}: PipelineCardProps) {
return (
<Card className={`pipeline-card ${status === "active" ? "border-2 border-green-500 dark:border-green-600" : ""}`}>
<CardHeader className="pb-2">
<div className="flex justify-between items-start">
<CardTitle className="text-lg">{title}</CardTitle>
<StatusBadge status={status} />
</div>
<CardDescription>{description}</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="flex items-center text-sm">
<Clock className="h-4 w-4 mr-2 text-muted-foreground" />
<span className="text-muted-foreground">Last run:</span>
<span className="ml-1 font-medium">{lastRun}</span>
</div>
<div className="flex items-center text-sm">
<RefreshCw className="h-4 w-4 mr-2 text-muted-foreground" />
<span className="text-muted-foreground">Next run:</span>
<span className="ml-1 font-medium">{nextRun}</span>
</div>
<div className="flex items-center text-sm">
<Database className="h-4 w-4 mr-2 text-muted-foreground" />
<span className="text-muted-foreground">Sources:</span>
<span className="ml-1 font-medium">{sources}</span>
<span className="mx-2"></span>
<span className="text-muted-foreground">Records:</span>
<span className="ml-1 font-medium">{records}</span>
</div>
{error && (
<div className="flex items-center text-sm text-destructive mt-2">
<AlertTriangle className="h-4 w-4 mr-2" />
{error}
</div>
)}
</div>
</CardContent>
<CardFooter className="flex justify-between">
<Link href={`/data-pipeline/${title.toLowerCase().replace(/\s+/g, "-")}`}>
<Button variant="outline" size="sm">
View Details
</Button>
</Link>
<div className="flex gap-2">
<Button variant="outline" size="icon" className="h-8 w-8 text-primary border-primary/20 hover:border-primary">
<Copy className="h-4 w-4" />
</Button>
{status === "active" ? (
<Button variant="outline" size="icon" className="h-8 w-8 border-primary/20 hover:border-primary">
<Pause className="h-4 w-4" />
</Button>
) : (
<Button variant="outline" size="icon" className="h-8 w-8 border-primary/20 hover:border-primary">
<Play className="h-4 w-4" />
</Button>
)}
<Button variant="outline" size="icon" className="h-8 w-8 border-primary/20 hover:border-primary">
<RefreshCw className="h-4 w-4" />
</Button>
</div>
</CardFooter>
</Card>
)
}
function StatusBadge({ status }: { status: "active" | "paused" | "error" }) {
if (status === "active") {
return (
<Badge variant="default" className="bg-green-500 hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700">
Active
</Badge>
)
} else if (status === "paused") {
return <Badge variant="secondary">Paused</Badge>
} else {
return <Badge variant="destructive">Error</Badge>
}
}

View File

@ -0,0 +1,687 @@
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Badge } from "@/components/ui/badge"
import { ArrowLeft, Download, Edit, Play, Trash, Copy, Check, Plus } from "lucide-react"
import Link from "next/link"
import PageHeader from "@/components/page-header"
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
export default function PipelineDetailsPage() {
return (
<div className="container mx-auto p-6">
<PageHeader
title="Property Listings Pipeline"
breadcrumb={[
{ title: "Home", href: "/" },
{ title: "Data Pipeline", href: "/data-pipeline" },
{ title: "Property Listings", href: "/data-pipeline/property-listings" },
]}
/>
<div className="flex justify-between items-center mt-6">
<Link href="/data-pipeline">
<Button variant="outline">
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Pipelines
</Button>
</Link>
<div className="flex space-x-2">
<Button variant="outline" className="gap-2 border-primary/20 hover:border-primary">
<Copy className="h-4 w-4" />
Clone
</Button>
<Button variant="outline" className="gap-2 border-primary/20 hover:border-primary">
<Edit className="h-4 w-4" />
Edit
</Button>
<Button variant="outline" className="gap-2 border-primary/20 hover:border-primary">
<Play className="h-4 w-4" />
Run Now
</Button>
<Button variant="destructive" size="icon">
<Trash className="h-4 w-4" />
</Button>
</div>
</div>
<div className="grid gap-6 md:grid-cols-3 mt-6">
<Card className="border-2 border-green-500 dark:border-green-600">
<CardHeader>
<CardTitle className="text-lg">Pipeline Status</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex justify-between items-center">
<span className="text-muted-foreground">Status:</span>
<Badge
variant="default"
className="bg-green-500 hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700"
>
Active
</Badge>
</div>
<div className="flex justify-between items-center">
<span className="text-muted-foreground">Last Run:</span>
<span>2 hours ago</span>
</div>
<div className="flex justify-between items-center">
<span className="text-muted-foreground">Next Run:</span>
<span>Tomorrow at 9:00 AM</span>
</div>
<div className="flex justify-between items-center">
<span className="text-muted-foreground">Run Frequency:</span>
<span>Daily</span>
</div>
<div className="flex justify-between items-center">
<span className="text-muted-foreground">Total Records:</span>
<span>1,240</span>
</div>
</div>
</CardContent>
</Card>
<Card className="border-2 hover:border-highlight-border transition-all duration-200">
<CardHeader>
<CardTitle className="text-lg">Data Sources</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="p-3 border-2 rounded-md hover:border-highlight-border transition-all duration-200">
<div className="flex justify-between items-center">
<span className="font-medium">example-realty.com</span>
<Badge>Website</Badge>
</div>
<p className="text-sm text-muted-foreground mt-1">Last updated: 2 hours ago</p>
<p className="text-sm mt-1">540 records</p>
</div>
<div className="p-3 border-2 rounded-md hover:border-highlight-border transition-all duration-200">
<div className="flex justify-between items-center">
<span className="font-medium">property-listings.com</span>
<Badge>Website</Badge>
</div>
<p className="text-sm text-muted-foreground mt-1">Last updated: 2 hours ago</p>
<p className="text-sm mt-1">420 records</p>
</div>
<div className="p-3 border-2 rounded-md hover:border-highlight-border transition-all duration-200">
<div className="flex justify-between items-center">
<span className="font-medium">real-estate-api.com</span>
<Badge>API</Badge>
</div>
<p className="text-sm text-muted-foreground mt-1">Last updated: 2 hours ago</p>
<p className="text-sm mt-1">280 records</p>
</div>
</div>
</CardContent>
</Card>
<Card className="border-2 hover:border-highlight-border transition-all duration-200">
<CardHeader>
<CardTitle className="text-lg">Export Options</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<Accordion type="single" collapsible className="w-full" defaultValue="format-1">
<AccordionItem value="format-1" className="border-0">
<div className="border rounded-md p-2 hover:border-highlight-border transition-all duration-200">
<AccordionTrigger className="py-1 px-2">
<div className="flex items-center">
<Download className="mr-2 h-4 w-4 text-primary" />
<span>Export as JSON</span>
</div>
</AccordionTrigger>
<AccordionContent className="pt-2 pb-1 px-2">
<div className="space-y-2">
<div className="flex items-center space-x-2">
<input type="checkbox" id="pretty-json" className="h-4 w-4" defaultChecked />
<label htmlFor="pretty-json" className="text-sm">
Pretty print
</label>
</div>
<Button size="sm" className="w-full">
Download JSON
</Button>
</div>
</AccordionContent>
</div>
</AccordionItem>
<AccordionItem value="format-2" className="border-0">
<div className="border rounded-md p-2 hover:border-highlight-border transition-all duration-200">
<AccordionTrigger className="py-1 px-2">
<div className="flex items-center">
<Download className="mr-2 h-4 w-4 text-primary" />
<span>Export as CSV</span>
</div>
</AccordionTrigger>
<AccordionContent className="pt-2 pb-1 px-2">
<div className="space-y-2">
<div className="flex items-center space-x-2">
<input type="checkbox" id="include-headers" className="h-4 w-4" defaultChecked />
<label htmlFor="include-headers" className="text-sm">
Include headers
</label>
</div>
<Button size="sm" className="w-full">
Download CSV
</Button>
</div>
</AccordionContent>
</div>
</AccordionItem>
<AccordionItem value="format-3" className="border-0">
<div className="border rounded-md p-2 hover:border-highlight-border transition-all duration-200">
<AccordionTrigger className="py-1 px-2">
<div className="flex items-center">
<Download className="mr-2 h-4 w-4 text-primary" />
<span>Export as SQLite</span>
</div>
</AccordionTrigger>
<AccordionContent className="pt-2 pb-1 px-2">
<Button size="sm" className="w-full">
Download SQLite
</Button>
</AccordionContent>
</div>
</AccordionItem>
<AccordionItem value="format-4" className="border-0">
<div className="border rounded-md p-2 hover:border-highlight-border transition-all duration-200">
<AccordionTrigger className="py-1 px-2">
<div className="flex items-center">
<Download className="mr-2 h-4 w-4 text-primary" />
<span>Export as YAML</span>
</div>
</AccordionTrigger>
<AccordionContent className="pt-2 pb-1 px-2">
<Button size="sm" className="w-full">
Download YAML
</Button>
</AccordionContent>
</div>
</AccordionItem>
</Accordion>
</div>
</CardContent>
</Card>
</div>
<div className="mt-6">
<Tabs defaultValue="schema">
<TabsList>
<TabsTrigger value="schema">Data Schema</TabsTrigger>
<TabsTrigger value="preview">Data Preview</TabsTrigger>
<TabsTrigger value="output">Output Configuration</TabsTrigger>
<TabsTrigger value="history">Run History</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
</TabsList>
<TabsContent value="schema" className="mt-4">
<Card className="border-2 hover:border-highlight-border transition-all duration-200">
<CardHeader>
<div className="flex justify-between items-center">
<div>
<CardTitle>Data Schema & Field Management</CardTitle>
<CardDescription>Customize fields detected from your data sources</CardDescription>
</div>
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium">Detected Fields</h3>
<Button variant="outline" size="sm">
Refresh Detection
</Button>
</div>
<div className="border rounded-md p-4">
<div className="space-y-3">
<div className="field-mapping-item flex items-center">
<input type="checkbox" id="field-title" className="h-4 w-4 mr-3" defaultChecked />
<div className="flex-1">
<div className="flex items-center">
<Label htmlFor="field-title" className="font-medium">
Title
</Label>
<Badge className="ml-2 bg-green-500 text-white">Auto-detected</Badge>
</div>
<p className="text-xs text-muted-foreground">Property title or name</p>
</div>
<select className="h-8 rounded-md border border-input bg-background px-2 py-1 text-xs">
<option>String</option>
<option>Number</option>
<option>Boolean</option>
<option>Date</option>
</select>
</div>
<div className="field-mapping-item flex items-center">
<input type="checkbox" id="field-price" className="h-4 w-4 mr-3" defaultChecked />
<div className="flex-1">
<div className="flex items-center">
<Label htmlFor="field-price" className="font-medium">
Price
</Label>
<Badge className="ml-2 bg-green-500 text-white">Auto-detected</Badge>
</div>
<p className="text-xs text-muted-foreground">Property price</p>
</div>
<select className="h-8 rounded-md border border-input bg-background px-2 py-1 text-xs">
<option>Number</option>
<option>String</option>
<option>Boolean</option>
<option>Date</option>
</select>
</div>
<div className="field-mapping-item flex items-center">
<input type="checkbox" id="field-location" className="h-4 w-4 mr-3" defaultChecked />
<div className="flex-1">
<div className="flex items-center">
<Label htmlFor="field-location" className="font-medium">
Location
</Label>
<Badge className="ml-2 bg-green-500 text-white">Auto-detected</Badge>
</div>
<p className="text-xs text-muted-foreground">Property location</p>
</div>
<select className="h-8 rounded-md border border-input bg-background px-2 py-1 text-xs">
<option>String</option>
<option>Number</option>
<option>Boolean</option>
<option>Date</option>
</select>
</div>
<div className="field-mapping-item flex items-center">
<input type="checkbox" id="field-bedrooms" className="h-4 w-4 mr-3" defaultChecked />
<div className="flex-1">
<div className="flex items-center">
<Label htmlFor="field-bedrooms" className="font-medium">
Bedrooms
</Label>
<Badge className="ml-2 bg-green-500 text-white">Auto-detected</Badge>
</div>
<p className="text-xs text-muted-foreground">Number of bedrooms</p>
</div>
<select className="h-8 rounded-md border border-input bg-background px-2 py-1 text-xs">
<option>Number</option>
<option>String</option>
<option>Boolean</option>
<option>Date</option>
</select>
</div>
<div className="field-mapping-item flex items-center">
<input type="checkbox" id="field-bathrooms" className="h-4 w-4 mr-3" defaultChecked />
<div className="flex-1">
<div className="flex items-center">
<Label htmlFor="field-bathrooms" className="font-medium">
Bathrooms
</Label>
<Badge className="ml-2 bg-green-500 text-white">Auto-detected</Badge>
</div>
<p className="text-xs text-muted-foreground">Number of bathrooms</p>
</div>
<select className="h-8 rounded-md border border-input bg-background px-2 py-1 text-xs">
<option>Number</option>
<option>String</option>
<option>Boolean</option>
<option>Date</option>
</select>
</div>
<div className="field-mapping-item flex items-center border-dashed">
<input type="checkbox" id="field-custom" className="h-4 w-4 mr-3" />
<div className="flex-1">
<Input placeholder="Add custom field" className="border-none text-sm p-0 h-6" />
</div>
<Button variant="ghost" size="sm" className="h-8 px-2">
<Plus className="h-4 w-4" />
</Button>
</div>
</div>
</div>
<div className="space-y-2 mt-4">
<Label htmlFor="derived-fields">Derived Fields</Label>
<Card className="border border-dashed">
<CardHeader className="py-3">
<CardTitle className="text-sm">Create calculated fields</CardTitle>
<CardDescription>Use formulas to generate new fields from existing data</CardDescription>
</CardHeader>
<CardContent className="py-0">
<div className="space-y-3">
<div className="field-mapping-item">
<div className="flex items-center justify-between">
<Label className="font-medium">Price Per Square Foot</Label>
<Badge variant="outline">Derived</Badge>
</div>
<div className="flex items-center mt-2">
<span className="text-xs text-muted-foreground mr-2">Formula:</span>
<code className="text-xs bg-muted/50 p-1 rounded">price / squareFeet</code>
</div>
</div>
<Button variant="outline" size="sm" className="w-full">
<Plus className="h-4 w-4 mr-2" />
Add Derived Field
</Button>
</div>
</CardContent>
</Card>
</div>
<div className="flex justify-end">
<Button variant="outline" className="gap-2 mr-2">
Reset to Default
</Button>
<Button>Save Field Configuration</Button>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="preview" className="mt-4">
<Card className="border-2 hover:border-highlight-border transition-all duration-200">
<CardHeader>
<CardTitle>Data Preview</CardTitle>
<CardDescription>Sample of the collected data</CardDescription>
</CardHeader>
<CardContent>
<div className="border rounded-md overflow-x-auto">
<table className="w-full">
<thead className="bg-muted">
<tr>
<th className="px-4 py-2 text-left text-sm font-medium">ID</th>
<th className="px-4 py-2 text-left text-sm font-medium">Title</th>
<th className="px-4 py-2 text-left text-sm font-medium">Price</th>
<th className="px-4 py-2 text-left text-sm font-medium">Bedrooms</th>
<th className="px-4 py-2 text-left text-sm font-medium">Bathrooms</th>
<th className="px-4 py-2 text-left text-sm font-medium">Location</th>
<th className="px-4 py-2 text-left text-sm font-medium">Sq. Ft.</th>
</tr>
</thead>
<tbody className="divide-y">
<tr>
<td className="px-4 py-2 text-sm">P001</td>
<td className="px-4 py-2 text-sm">Modern Apartment</td>
<td className="px-4 py-2 text-sm">$350,000</td>
<td className="px-4 py-2 text-sm">2</td>
<td className="px-4 py-2 text-sm">2</td>
<td className="px-4 py-2 text-sm">Downtown</td>
<td className="px-4 py-2 text-sm">1,200</td>
</tr>
<tr>
<td className="px-4 py-2 text-sm">P002</td>
<td className="px-4 py-2 text-sm">Luxury Villa</td>
<td className="px-4 py-2 text-sm">$1,250,000</td>
<td className="px-4 py-2 text-sm">5</td>
<td className="px-4 py-2 text-sm">4</td>
<td className="px-4 py-2 text-sm">Suburbs</td>
<td className="px-4 py-2 text-sm">3,500</td>
</tr>
<tr>
<td className="px-4 py-2 text-sm">P003</td>
<td className="px-4 py-2 text-sm">Cozy Studio</td>
<td className="px-4 py-2 text-sm">$180,000</td>
<td className="px-4 py-2 text-sm">1</td>
<td className="px-4 py-2 text-sm">1</td>
<td className="px-4 py-2 text-sm">City Center</td>
<td className="px-4 py-2 text-sm">650</td>
</tr>
</tbody>
</table>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="output" className="mt-4">
<Card className="border-2 hover:border-highlight-border transition-all duration-200">
<CardHeader>
<CardTitle>Output Configuration</CardTitle>
<CardDescription>Configure how your data will be structured and exported</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="space-y-2">
<Label>Output Format</Label>
<div className="grid grid-cols-2 gap-2">
<div className="border rounded-md p-3 data-source-card active">
<div className="flex items-center justify-between">
<span className="font-medium">JSON</span>
<Check className="h-4 w-4 text-primary" />
</div>
<p className="text-xs text-muted-foreground mt-1">Structured data format</p>
</div>
<div className="border rounded-md p-3 data-source-card">
<span className="font-medium">CSV</span>
<p className="text-xs text-muted-foreground mt-1">Spreadsheet compatible</p>
</div>
<div className="border rounded-md p-3 data-source-card">
<span className="font-medium">SQLite</span>
<p className="text-xs text-muted-foreground mt-1">Portable database</p>
</div>
<div className="border rounded-md p-3 data-source-card">
<span className="font-medium">YAML</span>
<p className="text-xs text-muted-foreground mt-1">Human-readable format</p>
</div>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label>Format Preview</Label>
<Badge variant="outline">Sample Data</Badge>
</div>
<div className="bg-muted/50 p-3 rounded-md overflow-x-auto">
<pre className="text-xs">
{`{
"properties": [
{
"id": "P001",
"title": "Modern Apartment",
"price": 350000,
"bedrooms": 2,
"location": "Downtown"
},
{
"id": "P002",
"title": "Luxury Villa",
"price": 1250000,
"bedrooms": 5,
"location": "Suburbs"
}
]
}`}
</pre>
</div>
</div>
<div className="flex justify-end">
<Button className="gap-2">
<Download className="h-4 w-4" />
Export Data
</Button>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="history" className="mt-4">
<Card className="border-2 hover:border-highlight-border transition-all duration-200">
<CardHeader>
<CardTitle>Run History</CardTitle>
<CardDescription>History of pipeline executions</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="border rounded-md overflow-hidden">
<table className="w-full">
<thead className="bg-muted">
<tr>
<th className="px-4 py-2 text-left text-sm font-medium">Run ID</th>
<th className="px-4 py-2 text-left text-sm font-medium">Date</th>
<th className="px-4 py-2 text-left text-sm font-medium">Status</th>
<th className="px-4 py-2 text-left text-sm font-medium">Duration</th>
<th className="px-4 py-2 text-left text-sm font-medium">Records</th>
<th className="px-4 py-2 text-left text-sm font-medium">Actions</th>
</tr>
</thead>
<tbody className="divide-y">
<tr>
<td className="px-4 py-2 text-sm">RUN-123</td>
<td className="px-4 py-2 text-sm">Today, 10:30 AM</td>
<td className="px-4 py-2 text-sm">
<Badge variant="default" className="bg-success hover:bg-success">
Success
</Badge>
</td>
<td className="px-4 py-2 text-sm">2m 15s</td>
<td className="px-4 py-2 text-sm">1,240</td>
<td className="px-4 py-2 text-sm">
<Button variant="outline" size="sm">
View Log
</Button>
</td>
</tr>
<tr>
<td className="px-4 py-2 text-sm">RUN-122</td>
<td className="px-4 py-2 text-sm">Yesterday, 10:30 AM</td>
<td className="px-4 py-2 text-sm">
<Badge variant="default" className="bg-success hover:bg-success">
Success
</Badge>
</td>
<td className="px-4 py-2 text-sm">2m 10s</td>
<td className="px-4 py-2 text-sm">1,235</td>
<td className="px-4 py-2 text-sm">
<Button variant="outline" size="sm">
View Log
</Button>
</td>
</tr>
<tr>
<td className="px-4 py-2 text-sm">RUN-121</td>
<td className="px-4 py-2 text-sm">2 days ago, 10:30 AM</td>
<td className="px-4 py-2 text-sm">
<Badge variant="default" className="bg-success hover:bg-success">
Success
</Badge>
</td>
<td className="px-4 py-2 text-sm">2m 05s</td>
<td className="px-4 py-2 text-sm">1,228</td>
<td className="px-4 py-2 text-sm">
<Button variant="outline" size="sm">
View Log
</Button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="settings" className="mt-4">
<Card className="border-2 hover:border-highlight-border transition-all duration-200">
<CardHeader>
<CardTitle>Pipeline Settings</CardTitle>
<CardDescription>Configure pipeline behavior</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
<div className="space-y-2">
<h3 className="text-lg font-medium">Scheduling</h3>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="frequency">Run Frequency</Label>
<select
id="frequency"
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
defaultValue="daily"
>
<option value="manual">Manual (Run on demand)</option>
<option value="hourly">Hourly</option>
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
<option value="monthly">Monthly</option>
<option value="custom">Custom Schedule</option>
</select>
</div>
<div className="space-y-2">
<Label htmlFor="time">Run Time</Label>
<Input id="time" type="time" defaultValue="09:00" />
</div>
</div>
</div>
<div className="space-y-2">
<h3 className="text-lg font-medium">Data Collection</h3>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="max-records">Maximum Records</Label>
<Input id="max-records" type="number" defaultValue="2000" />
</div>
<div className="space-y-2">
<Label htmlFor="retry-attempts">Retry Attempts</Label>
<Input id="retry-attempts" type="number" defaultValue="3" />
</div>
</div>
</div>
<div className="space-y-2">
<h3 className="text-lg font-medium">Notifications</h3>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="notify-complete"
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
defaultChecked
/>
<Label htmlFor="notify-complete" className="text-sm font-normal">
Notify when pipeline completes
</Label>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="notify-error"
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
defaultChecked
/>
<Label htmlFor="notify-error" className="text-sm font-normal">
Notify on errors
</Label>
</div>
</div>
</div>
<div className="flex justify-end">
<Button>Save Settings</Button>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
</div>
)
}

View File

@ -0,0 +1,4 @@
export default function Loading() {
return null
}

View File

@ -0,0 +1,408 @@
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import {
ArrowLeft,
BrainCircuit,
Database,
Play,
Sliders,
ArrowRight,
Info,
AlertTriangle,
HelpCircle,
} from "lucide-react"
import Link from "next/link"
import PageHeader from "@/components/page-header"
export default function ModelsDocumentationPage() {
return (
<div className="container mx-auto p-6">
<PageHeader
title="Models Documentation"
description="Learn how to use and train AI models for property analysis"
breadcrumb={[
{ title: "Home", href: "/" },
{ title: "Documentation", href: "/documentation" },
{ title: "Models", href: "/documentation/models" },
]}
/>
<div className="flex mb-6">
<Link href="/documentation">
<Button variant="outline" size="sm" className="gap-2">
<ArrowLeft className="h-4 w-4" />
Back to Documentation
</Button>
</Link>
</div>
<div className="grid gap-6 md:grid-cols-3">
<div className="md:col-span-2 space-y-6">
<Card>
<CardHeader>
<CardTitle>Understanding Models</CardTitle>
<CardDescription>Learn about the different types of models available</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<p>
BorBann uses machine learning models to analyze property data and make predictions. These models are
trained on historical property data and can be used to predict property prices, identify trends, and
provide insights.
</p>
<h3 className="text-lg font-medium mt-4">Types of Models</h3>
<div className="space-y-3">
<div className="p-3 border rounded-md">
<h4 className="font-medium flex items-center gap-2">
Regression Models
<Badge variant="outline">Standard ML Model v2.4</Badge>
</h4>
<p className="text-sm text-muted-foreground mt-1">
Used for predicting continuous values like property prices. These models analyze various features to
estimate a property's value.
</p>
</div>
<div className="p-3 border rounded-md">
<h4 className="font-medium flex items-center gap-2">
Neural Networks
<Badge variant="outline">Enhanced Neural Network v1.8</Badge>
</h4>
<p className="text-sm text-muted-foreground mt-1">
Deep learning models that can capture complex patterns in property data. Ideal for analyzing
multiple factors simultaneously.
</p>
</div>
<div className="p-3 border rounded-md">
<h4 className="font-medium flex items-center gap-2">
Geospatial Models
<Badge variant="outline">Geospatial Regression v3.1</Badge>
</h4>
<p className="text-sm text-muted-foreground mt-1">
Specialized models that incorporate location data and spatial relationships between properties.
</p>
</div>
<div className="p-3 border rounded-md">
<h4 className="font-medium flex items-center gap-2">
Time Series Models
<Badge variant="outline">Time Series Forecast v2.0</Badge>
</h4>
<p className="text-sm text-muted-foreground mt-1">
Models designed to analyze property price trends over time and make future predictions.
</p>
</div>
</div>
<div className="p-4 bg-muted/50 rounded-lg border mt-4">
<div className="flex items-start gap-2">
<Info className="h-5 w-5 text-primary mt-0.5" />
<div>
<h4 className="font-medium">System vs. Custom Models</h4>
<p className="text-sm text-muted-foreground mt-1">
<strong>System Models</strong> are pre-trained models provided by BorBann. They are regularly
updated and maintained by our team.
</p>
<p className="text-sm text-muted-foreground mt-1">
<strong>Custom Models</strong> are models that you train using your own data pipelines. These can
be tailored to your specific needs.
</p>
</div>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Using Models</CardTitle>
<CardDescription>How to select and use models for property analysis</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<h3 className="text-lg font-medium">Selecting a Model</h3>
<p className="text-sm text-muted-foreground">
You can select different models when using the Maps or Price Prediction features. Look for the model
selector dropdown in the interface.
</p>
<div className="border rounded-md p-4 mt-2">
<h4 className="font-medium mb-2">Step-by-Step Guide</h4>
<ol className="space-y-2 text-sm">
<li className="flex items-start gap-2">
<div className="h-5 w-5 rounded-full bg-primary/10 text-primary flex items-center justify-center flex-shrink-0 mt-0.5">
1
</div>
<span>Navigate to the Maps or Price Prediction page</span>
</li>
<li className="flex items-start gap-2">
<div className="h-5 w-5 rounded-full bg-primary/10 text-primary flex items-center justify-center flex-shrink-0 mt-0.5">
2
</div>
<span>Look for the model selector dropdown in the top navigation bar</span>
</li>
<li className="flex items-start gap-2">
<div className="h-5 w-5 rounded-full bg-primary/10 text-primary flex items-center justify-center flex-shrink-0 mt-0.5">
3
</div>
<span>Click on the dropdown to see available models</span>
</li>
<li className="flex items-start gap-2">
<div className="h-5 w-5 rounded-full bg-primary/10 text-primary flex items-center justify-center flex-shrink-0 mt-0.5">
4
</div>
<span>Select the model that best suits your analysis needs</span>
</li>
<li className="flex items-start gap-2">
<div className="h-5 w-5 rounded-full bg-primary/10 text-primary flex items-center justify-center flex-shrink-0 mt-0.5">
5
</div>
<span>The page will update to use the selected model for analysis</span>
</li>
</ol>
</div>
<h3 className="text-lg font-medium mt-4">Understanding Model Results</h3>
<p className="text-sm text-muted-foreground">
Different models may produce slightly different results. Here's how to interpret them:
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-2">
<div className="p-3 border rounded-md">
<h4 className="font-medium">Price Predictions</h4>
<p className="text-sm text-muted-foreground mt-1">
Models provide a predicted price along with a confidence level. The higher the confidence, the more
reliable the prediction.
</p>
</div>
<div className="p-3 border rounded-md">
<h4 className="font-medium">Feature Importance</h4>
<p className="text-sm text-muted-foreground mt-1">
Models show which features (location, size, etc.) have the most impact on the property price.
</p>
</div>
<div className="p-3 border rounded-md">
<h4 className="font-medium">Price Range</h4>
<p className="text-sm text-muted-foreground mt-1">
Models provide a range of possible prices based on the confidence level.
</p>
</div>
<div className="p-3 border rounded-md">
<h4 className="font-medium">Environmental Impact</h4>
<p className="text-sm text-muted-foreground mt-1">
Models analyze how environmental factors affect property values in the area.
</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Training Custom Models</CardTitle>
<CardDescription>Learn how to create and train your own models</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<p>
You can train custom models using your own data pipelines. This allows you to create models tailored to
your specific needs.
</p>
<div className="border rounded-md p-4 mt-2">
<h4 className="font-medium mb-2">Step-by-Step Guide</h4>
<ol className="space-y-2 text-sm">
<li className="flex items-start gap-2">
<div className="h-5 w-5 rounded-full bg-primary/10 text-primary flex items-center justify-center flex-shrink-0 mt-0.5">
1
</div>
<div>
<span className="font-medium">Navigate to the Models page</span>
<p className="text-xs text-muted-foreground mt-0.5">
Go to the Models section from the sidebar navigation
</p>
</div>
</li>
<li className="flex items-start gap-2">
<div className="h-5 w-5 rounded-full bg-primary/10 text-primary flex items-center justify-center flex-shrink-0 mt-0.5">
2
</div>
<div>
<span className="font-medium">Click "Train New Model"</span>
<p className="text-xs text-muted-foreground mt-0.5">
This will take you to the model training interface
</p>
</div>
</li>
<li className="flex items-start gap-2">
<div className="h-5 w-5 rounded-full bg-primary/10 text-primary flex items-center justify-center flex-shrink-0 mt-0.5">
3
</div>
<div>
<span className="font-medium">Configure your model</span>
<p className="text-xs text-muted-foreground mt-0.5">
Enter a name, description, and select the model type
</p>
</div>
</li>
<li className="flex items-start gap-2">
<div className="h-5 w-5 rounded-full bg-primary/10 text-primary flex items-center justify-center flex-shrink-0 mt-0.5">
4
</div>
<div>
<span className="font-medium">Select a data pipeline</span>
<p className="text-xs text-muted-foreground mt-0.5">
Choose which data pipeline to use for training the model
</p>
</div>
</li>
<li className="flex items-start gap-2">
<div className="h-5 w-5 rounded-full bg-primary/10 text-primary flex items-center justify-center flex-shrink-0 mt-0.5">
5
</div>
<div>
<span className="font-medium">Start training</span>
<p className="text-xs text-muted-foreground mt-0.5">
Click the "Start Training" button to begin the training process
</p>
</div>
</li>
<li className="flex items-start gap-2">
<div className="h-5 w-5 rounded-full bg-primary/10 text-primary flex items-center justify-center flex-shrink-0 mt-0.5">
6
</div>
<div>
<span className="font-medium">Monitor training progress</span>
<p className="text-xs text-muted-foreground mt-0.5">
The system will show you the training progress and notify you when it's complete
</p>
</div>
</li>
</ol>
</div>
<div className="p-4 bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-400 rounded-md flex items-start gap-2 mt-4">
<AlertTriangle className="h-5 w-5 mt-0.5" />
<div>
<h4 className="font-medium">Important Notes</h4>
<ul className="text-sm mt-1 space-y-1 list-disc list-inside">
<li>Training a model requires a data pipeline with sufficient data</li>
<li>The training process may take several minutes depending on the data size</li>
<li>You can cancel training at any time if needed</li>
<li>Models with more data generally produce more accurate results</li>
</ul>
</div>
</div>
</CardContent>
</Card>
</div>
<div className="md:col-span-1 space-y-6">
<Card>
<CardHeader>
<CardTitle className="text-base">Quick Links</CardTitle>
</CardHeader>
<CardContent>
<nav className="space-y-2">
<Link
href="#understanding-models"
className="flex items-center gap-2 text-sm p-2 rounded-md hover:bg-muted"
>
<BrainCircuit className="h-4 w-4 text-primary" />
<span>Understanding Models</span>
</Link>
<Link href="#using-models" className="flex items-center gap-2 text-sm p-2 rounded-md hover:bg-muted">
<Play className="h-4 w-4 text-primary" />
<span>Using Models</span>
</Link>
<Link
href="#training-custom-models"
className="flex items-center gap-2 text-sm p-2 rounded-md hover:bg-muted"
>
<Database className="h-4 w-4 text-primary" />
<span>Training Custom Models</span>
</Link>
<Link
href="#model-parameters"
className="flex items-center gap-2 text-sm p-2 rounded-md hover:bg-muted"
>
<Sliders className="h-4 w-4 text-primary" />
<span>Model Parameters</span>
</Link>
</nav>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">Related Guides</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
<Link
href="/documentation/price-prediction"
className="block p-3 border rounded-md hover:border-primary/50 transition-colors"
>
<h4 className="font-medium">Price Prediction</h4>
<p className="text-xs text-muted-foreground mt-1">
Learn how to use models for property price prediction
</p>
</Link>
<Link
href="/documentation/data-pipeline"
className="block p-3 border rounded-md hover:border-primary/50 transition-colors"
>
<h4 className="font-medium">Data Pipelines</h4>
<p className="text-xs text-muted-foreground mt-1">Set up data pipelines for model training</p>
</Link>
<Link
href="/documentation/maps"
className="block p-3 border rounded-md hover:border-primary/50 transition-colors"
>
<h4 className="font-medium">Maps & Geospatial</h4>
<p className="text-xs text-muted-foreground mt-1">Use models with the interactive map</p>
</Link>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<HelpCircle className="h-4 w-4 text-primary" />
Need Help?
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground mb-4">
Can't find what you're looking for? Our support team is here to help.
</p>
<Button className="w-full" asChild>
<Link href="/support">Contact Support</Link>
</Button>
</CardContent>
</Card>
</div>
</div>
<div className="mt-8 flex justify-between">
<Button variant="outline" asChild>
<Link href="/documentation">
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Documentation
</Link>
</Button>
<Button variant="outline" asChild>
<Link href="/documentation/price-prediction">
Next: Price Prediction
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</div>
</div>
)
}

View File

@ -0,0 +1,454 @@
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import {
ArrowRight,
Map,
Database,
BrainCircuit,
BarChart2,
Building,
Search,
BookOpen,
Lightbulb,
HelpCircle,
Zap,
Video,
FileQuestion,
Play,
} from "lucide-react"
import Link from "next/link"
import PageHeader from "@/components/page-header"
export default function DocumentationPage() {
return (
<div className="container mx-auto p-6">
<PageHeader
title="Documentation"
description="Learn how to use BorBann's property analytics platform"
breadcrumb={[
{ title: "Home", href: "/" },
{ title: "Documentation", href: "/documentation" },
]}
/>
<div className="mt-6 mb-10">
<div className="bg-muted/50 p-6 rounded-lg border">
<div className="flex flex-col md:flex-row gap-6 items-center">
<div className="md:w-2/3">
<h2 className="text-2xl font-bold mb-2">Welcome to BorBann Documentation</h2>
<p className="text-muted-foreground mb-4">
This documentation will help you get the most out of our property analytics platform. No coding
experience is required to use our tools.
</p>
<div className="flex gap-3">
<Button asChild>
<Link href="#getting-started">
Get Started <ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
<Button variant="outline" asChild>
<Link href="#tutorials">View Tutorials</Link>
</Button>
</div>
</div>
<div className="md:w-1/3 flex justify-center">
<BookOpen className="h-24 w-24 text-primary/20" />
</div>
</div>
</div>
</div>
<section id="getting-started" className="mb-12">
<h2 className="text-2xl font-bold mb-6 flex items-center gap-2">
<Lightbulb className="h-6 w-6 text-primary" />
Getting Started
</h2>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Map className="h-5 w-5 text-primary" />
Maps & Geospatial
</CardTitle>
<CardDescription>Learn how to use the interactive map</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground mb-4">
Explore properties on our interactive map, apply filters, and analyze environmental factors.
</p>
<ul className="space-y-2 text-sm">
<li className="flex items-center gap-2">
<div className="h-1.5 w-1.5 rounded-full bg-primary"></div>
<span>Navigating the map interface</span>
</li>
<li className="flex items-center gap-2">
<div className="h-1.5 w-1.5 rounded-full bg-primary"></div>
<span>Applying filters and radius search</span>
</li>
<li className="flex items-center gap-2">
<div className="h-1.5 w-1.5 rounded-full bg-primary"></div>
<span>Understanding property markers</span>
</li>
</ul>
</CardContent>
<CardFooter>
<Button variant="ghost" className="w-full" asChild>
<Link href="/documentation/maps">
Read Guide <ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</CardFooter>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<BarChart2 className="h-5 w-5 text-primary" />
Price Prediction
</CardTitle>
<CardDescription>Understand property price predictions</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground mb-4">
Learn how our AI models predict property prices and how to interpret the results.
</p>
<ul className="space-y-2 text-sm">
<li className="flex items-center gap-2">
<div className="h-1.5 w-1.5 rounded-full bg-primary"></div>
<span>Understanding prediction factors</span>
</li>
<li className="flex items-center gap-2">
<div className="h-1.5 w-1.5 rounded-full bg-primary"></div>
<span>Adjusting parameters</span>
</li>
<li className="flex items-center gap-2">
<div className="h-1.5 w-1.5 rounded-full bg-primary"></div>
<span>Generating and using reports</span>
</li>
</ul>
</CardContent>
<CardFooter>
<Button variant="ghost" className="w-full" asChild>
<Link href="/documentation/price-prediction">
Read Guide <ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</CardFooter>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Database className="h-5 w-5 text-primary" />
Data Pipelines
</CardTitle>
<CardDescription>Set up automated data collection</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground mb-4">
Learn how to create and manage data pipelines for property data collection.
</p>
<ul className="space-y-2 text-sm">
<li className="flex items-center gap-2">
<div className="h-1.5 w-1.5 rounded-full bg-primary"></div>
<span>Creating your first pipeline</span>
</li>
<li className="flex items-center gap-2">
<div className="h-1.5 w-1.5 rounded-full bg-primary"></div>
<span>Configuring data sources</span>
</li>
<li className="flex items-center gap-2">
<div className="h-1.5 w-1.5 rounded-full bg-primary"></div>
<span>Scheduling and automation</span>
</li>
</ul>
</CardContent>
<CardFooter>
<Button variant="ghost" className="w-full" asChild>
<Link href="/documentation/data-pipeline">
Read Guide <ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</CardFooter>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<BrainCircuit className="h-5 w-5 text-primary" />
Models
</CardTitle>
<CardDescription>Work with AI models</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground mb-4">
Understand how to use and train custom AI models for property analysis.
</p>
<ul className="space-y-2 text-sm">
<li className="flex items-center gap-2">
<div className="h-1.5 w-1.5 rounded-full bg-primary"></div>
<span>Selecting the right model</span>
</li>
<li className="flex items-center gap-2">
<div className="h-1.5 w-1.5 rounded-full bg-primary"></div>
<span>Training custom models</span>
</li>
<li className="flex items-center gap-2">
<div className="h-1.5 w-1.5 rounded-full bg-primary"></div>
<span>Understanding model parameters</span>
</li>
</ul>
</CardContent>
<CardFooter>
<Button variant="ghost" className="w-full" asChild>
<Link href="/documentation/models">
Read Guide <ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</CardFooter>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Building className="h-5 w-5 text-primary" />
Property Listings
</CardTitle>
<CardDescription>Browse and analyze properties</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground mb-4">
Learn how to browse property listings and analyze property details.
</p>
<ul className="space-y-2 text-sm">
<li className="flex items-center gap-2">
<div className="h-1.5 w-1.5 rounded-full bg-primary"></div>
<span>Filtering and sorting properties</span>
</li>
<li className="flex items-center gap-2">
<div className="h-1.5 w-1.5 rounded-full bg-primary"></div>
<span>Understanding property analytics</span>
</li>
<li className="flex items-center gap-2">
<div className="h-1.5 w-1.5 rounded-full bg-primary"></div>
<span>Exporting property data</span>
</li>
</ul>
</CardContent>
<CardFooter>
<Button variant="ghost" className="w-full" asChild>
<Link href="/documentation/properties">
Read Guide <ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</CardFooter>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Search className="h-5 w-5 text-primary" />
Search & Filters
</CardTitle>
<CardDescription>Find exactly what you need</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground mb-4">
Master the search and filtering capabilities across the platform.
</p>
<ul className="space-y-2 text-sm">
<li className="flex items-center gap-2">
<div className="h-1.5 w-1.5 rounded-full bg-primary"></div>
<span>Advanced search techniques</span>
</li>
<li className="flex items-center gap-2">
<div className="h-1.5 w-1.5 rounded-full bg-primary"></div>
<span>Creating and saving filters</span>
</li>
<li className="flex items-center gap-2">
<div className="h-1.5 w-1.5 rounded-full bg-primary"></div>
<span>Combining multiple filters</span>
</li>
</ul>
</CardContent>
<CardFooter>
<Button variant="ghost" className="w-full" asChild>
<Link href="/documentation/search">
Read Guide <ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</CardFooter>
</Card>
</div>
</section>
<section id="tutorials" className="mb-12">
<h2 className="text-2xl font-bold mb-6 flex items-center gap-2">
<Video className="h-6 w-6 text-primary" />
Video Tutorials
</h2>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
<Card>
<div className="aspect-video bg-muted/50 rounded-t-lg flex items-center justify-center">
<Play className="h-12 w-12 text-primary/30" />
</div>
<CardHeader className="pb-2">
<CardTitle className="text-base">Getting Started with BorBann</CardTitle>
<CardDescription>5:32 Beginner</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
A complete overview of the platform and its main features.
</p>
</CardContent>
<CardFooter>
<Button variant="ghost" className="w-full">
Watch Tutorial
</Button>
</CardFooter>
</Card>
<Card>
<div className="aspect-video bg-muted/50 rounded-t-lg flex items-center justify-center">
<Play className="h-12 w-12 text-primary/30" />
</div>
<CardHeader className="pb-2">
<CardTitle className="text-base">Creating Your First Data Pipeline</CardTitle>
<CardDescription>8:45 Beginner</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Step-by-step guide to setting up automated data collection.
</p>
</CardContent>
<CardFooter>
<Button variant="ghost" className="w-full">
Watch Tutorial
</Button>
</CardFooter>
</Card>
<Card>
<div className="aspect-video bg-muted/50 rounded-t-lg flex items-center justify-center">
<Play className="h-12 w-12 text-primary/30" />
</div>
<CardHeader className="pb-2">
<CardTitle className="text-base">Advanced Map Analysis</CardTitle>
<CardDescription>12:20 Intermediate</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Learn how to use advanced map features for property analysis.
</p>
</CardContent>
<CardFooter>
<Button variant="ghost" className="w-full">
Watch Tutorial
</Button>
</CardFooter>
</Card>
</div>
<div className="mt-4 text-center">
<Button variant="outline" asChild>
<Link href="/documentation/tutorials">
View All Tutorials <ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</div>
</section>
<section id="faq" className="mb-12">
<h2 className="text-2xl font-bold mb-6 flex items-center gap-2">
<HelpCircle className="h-6 w-6 text-primary" />
Frequently Asked Questions
</h2>
<div className="grid md:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle className="text-base">How accurate are the price predictions?</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Our price predictions typically have an accuracy of 85-95% depending on the model used and the data
available. System models are regularly updated to maintain accuracy.
</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">Can I export data for use in other tools?</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Yes, you can export data in various formats including CSV, JSON, and PDF reports. Look for the export
options in the property details and analytics pages.
</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">Do I need coding experience to use this platform?</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
No, our platform is designed to be user-friendly for non-technical users. All features can be accessed
through the intuitive user interface without any coding.
</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">How often is the property data updated?</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Data update frequency depends on your data pipeline configuration. System data is typically updated
daily, while custom pipelines can be scheduled according to your needs.
</p>
</CardContent>
</Card>
</div>
<div className="mt-4 text-center">
<Button variant="outline" asChild>
<Link href="/documentation/faq">
View All FAQs <ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</div>
</section>
<section className="mb-6">
<Card>
<CardContent className="p-6">
<div className="flex flex-col md:flex-row gap-6 items-center">
<div className="md:w-2/3">
<h2 className="text-xl font-bold mb-2 flex items-center gap-2">
<FileQuestion className="h-5 w-5 text-primary" />
Need More Help?
</h2>
<p className="text-muted-foreground mb-4">
Can't find what you're looking for? Our support team is here to help.
</p>
<Button asChild>
<Link href="/support">Contact Support</Link>
</Button>
</div>
<div className="md:w-1/3 flex justify-center">
<Zap className="h-20 w-20 text-primary/20" />
</div>
</div>
</CardContent>
</Card>
</section>
</div>
)
}

View File

@ -1,703 +0,0 @@
/*
========================================
File: frontend/app/(routes)/model-explanation/page.tsx
========================================
*/
"use client";
import React, { useState, useEffect } from "react";
import Link from "next/link";
import {
ChevronRight,
Info,
ArrowRight,
Home,
Building,
Ruler,
Calendar,
Coins,
Droplets,
Wind,
Sun,
Car,
School,
ShoppingBag,
} from "lucide-react";
// Common UI components
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { Slider } from "@/components/ui/slider";
import { Skeleton } from "@/components/ui/skeleton";
// Removed SidebarProvider & ThemeProvider - should be in root layout
// Removed MapSidebar - assuming it's not needed here or use a common one
// Feature-specific components
import { FeatureImportanceChart } from "@/features/model-explanation/components/feature-importance-chart";
import { PriceComparisonChart } from "@/features/model-explanation/components/price-comparison-chart";
// Feature-specific API and types
import { fetchModelExplanation } from "@/features/model-explanation/api/explanationApi";
import type { ModelExplanationData, PropertyBaseDetails } from "@/features/model-explanation/types";
export default function ModelExplanationPage() {
const [activeStep, setActiveStep] = useState(1);
const [isLoading, setIsLoading] = useState(true);
const [explanationData, setExplanationData] = useState<ModelExplanationData | null>(null);
// State for interactive elements based on fetched data
const [propertySize, setPropertySize] = useState<number>(0);
const [propertyAge, setPropertyAge] = useState<number>(0);
// Fetch data on mount
useEffect(() => {
async function loadExplanation() {
setIsLoading(true);
// TODO: Get actual property ID from route params or state
const propertyId = "dummy-prop-123";
const response = await fetchModelExplanation(propertyId);
if (response.success && response.data) {
setExplanationData(response.data);
// Initialize sliders with fetched data
setPropertySize(response.data.propertyDetails.size);
setPropertyAge(response.data.propertyDetails.age);
} else {
console.error("Failed to load model explanation:", response.error);
// Handle error state in UI
}
setIsLoading(false);
}
loadExplanation();
}, []);
// Stepper configuration
const steps = [
{ id: 1, title: "Property Details", icon: Home },
{ id: 2, title: "Feature Analysis", icon: Ruler },
{ id: 3, title: "Market Comparison", icon: Building },
{ id: 4, title: "Environmental Factors", icon: Droplets },
{ id: 5, title: "Final Prediction", icon: Coins },
];
// Calculate adjusted price based on slider interaction
const calculateAdjustedPrice = () => {
if (!explanationData) return 0;
// Simple formula for demonstration - refine with actual logic if possible
const basePrice = explanationData.propertyDetails.predictedPrice;
const baseSize = explanationData.propertyDetails.size;
const baseAge = explanationData.propertyDetails.age;
const sizeImpact = (propertySize - baseSize) * 50000; // 50,000 THB per sqm diff
const ageImpact = (baseAge - propertyAge) * 200000; // 200,000 THB per year newer
return basePrice + sizeImpact + ageImpact;
};
const adjustedPrice = explanationData ? calculateAdjustedPrice() : 0;
// Loading State UI
if (isLoading) {
return (
<div className="flex flex-1 flex-col overflow-hidden p-6">
<div className="mx-auto w-full max-w-7xl">
<Skeleton className="h-10 w-1/3 mb-2" />
<Skeleton className="h-4 w-2/3 mb-8" />
<Skeleton className="h-10 w-full mb-4" />
<Skeleton className="h-2 w-full mb-8" />
<div className="grid gap-6 md:grid-cols-3">
<div className="space-y-6">
<Skeleton className="h-48 w-full" />
<Skeleton className="h-64 w-full" />
</div>
<div className="md:col-span-2 space-y-6">
<Skeleton className="h-96 w-full" />
</div>
</div>
</div>
</div>
);
}
// Error State UI
if (!explanationData) {
return (
<div className="flex flex-1 flex-col items-center justify-center p-6">
<p className="text-destructive">Failed to load model explanation.</p>
<Link href="/map">
<Button variant="link" className="mt-4">
Back to Map
</Button>
</Link>
</div>
);
}
// Main Content UI
const { propertyDetails, features, similarProperties, environmentalFactors, confidence, priceRange } =
explanationData;
return (
// Assuming ThemeProvider is in the root layout
// Assuming SidebarProvider and a common sidebar are in root layout or parent layout
<div className="flex flex-1 flex-col overflow-hidden">
{" "}
{/* Adjusted for page content */}
{/* Header */}
<header className="flex h-14 items-center justify-between border-b px-4 bg-background shrink-0">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Link href="/map" className="hover:text-foreground">
Map
</Link>
<ChevronRight className="h-4 w-4" />
<span className="font-medium text-foreground">Price Prediction Model</span>
</div>
{/* Add any specific header actions if needed */}
</header>
{/* Main content */}
<div className="flex-1 overflow-auto p-6">
{" "}
{/* Make content area scrollable */}
<div className="mx-auto w-full max-w-7xl">
<div className="mb-8">
<h1 className="text-3xl font-bold tracking-tight">Explainable Price Prediction Model</h1>
<p className="mt-2 text-muted-foreground">
Understand how our AI model predicts property prices and what factors influence the valuation.
</p>
</div>
{/* Steps navigation */}
<div className="mb-8">
<div className="flex flex-wrap gap-4">
{steps.map((step) => (
<Button
key={step.id}
variant={activeStep === step.id ? "default" : "outline"}
className="flex items-center gap-2"
onClick={() => setActiveStep(step.id)}>
<step.icon className="h-4 w-4" />
<span>{step.title}</span>
</Button>
))}
</div>
<div className="mt-4">
<Progress value={(activeStep / steps.length) * 100} className="h-2" />
</div>
</div>
{/* Step content */}
<div className="grid gap-6 md:grid-cols-3">
{/* --- Left Column: Property Details & Interaction --- */}
<div className="space-y-6 md:sticky md:top-6">
{" "}
{/* Make left column sticky */}
<Card>
<CardHeader>
<CardTitle>Property Details</CardTitle>
<CardDescription>{propertyDetails.address}</CardDescription>
</CardHeader>
<CardContent className="space-y-2 text-sm">
{/* Dynamically display details */}
<DetailRow label="Type" value={propertyDetails.type} />
<DetailRow label="Size" value={`${propertySize} sqm`} />
{propertyDetails.bedrooms && <DetailRow label="Bedrooms" value={propertyDetails.bedrooms} />}
{propertyDetails.bathrooms && <DetailRow label="Bathrooms" value={propertyDetails.bathrooms} />}
<DetailRow label="Age" value={`${propertyAge} years`} />
{propertyDetails.floor && <DetailRow label="Floor" value={propertyDetails.floor} />}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Adjust Parameters</CardTitle>
<CardDescription>See how changes affect the prediction</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Size Slider */}
<div className="space-y-2">
<div className="flex justify-between">
<Label htmlFor="prop-size-slider" className="text-sm font-medium">
Property Size
</Label>
<span className="text-sm text-muted-foreground">{propertySize} sqm</span>
</div>
<Slider
id="prop-size-slider"
value={[propertySize]}
min={50} // Example range
max={300} // Example range
step={5}
onValueChange={(value) => setPropertySize(value[0])}
/>
</div>
{/* Age Slider */}
<div className="space-y-2">
<div className="flex justify-between">
<Label htmlFor="prop-age-slider" className="text-sm font-medium">
Property Age
</Label>
<span className="text-sm text-muted-foreground">{propertyAge} years</span>
</div>
<Slider
id="prop-age-slider"
value={[propertyAge]}
min={0} // Example range
max={50} // Example range
step={1}
onValueChange={(value) => setPropertyAge(value[0])}
/>
</div>
</CardContent>
<CardFooter>
<div className="w-full">
<div className="flex justify-between items-baseline">
<span className="text-sm text-muted-foreground">Adjusted Price</span>
<span className="text-xl font-bold">{formatCurrency(adjustedPrice)}</span>
</div>
{/* Show difference */}
{propertyDetails.predictedPrice !== adjustedPrice && (
<div className="mt-2 text-xs text-muted-foreground">
{adjustedPrice > propertyDetails.predictedPrice ? "↑" : "↓"}
{Math.abs(adjustedPrice - propertyDetails.predictedPrice).toLocaleString()} THB from original
</div>
)}
</div>
</CardFooter>
</Card>
</div>
{/* --- Right Column: Step Content --- */}
<div className="md:col-span-2 space-y-6">
{activeStep === 1 && <Step1Content propertyDetails={propertyDetails} setActiveStep={setActiveStep} />}
{activeStep === 2 && <Step2Content features={features} setActiveStep={setActiveStep} />}
{activeStep === 3 && (
<Step3Content
property={propertyDetails}
comparisons={similarProperties}
setActiveStep={setActiveStep}
/>
)}
{activeStep === 4 && <Step4Content factors={environmentalFactors} setActiveStep={setActiveStep} />}
{activeStep === 5 && (
<Step5Content
predictedPrice={adjustedPrice}
confidence={confidence}
priceRange={priceRange}
setActiveStep={setActiveStep}
/>
)}
</div>
</div>
</div>
</div>
</div>
);
}
// --- Helper Components for Steps ---
function DetailRow({ label, value }: { label: string; value: string | number }) {
return (
<div className="flex justify-between">
<span className="text-muted-foreground">{label}</span>
<span className="font-medium">{value}</span>
</div>
);
}
function formatCurrency(value: number): string {
return new Intl.NumberFormat("th-TH", {
style: "currency",
currency: "THB",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(value);
}
// Step 1 Component
function Step1Content({
propertyDetails,
setActiveStep,
}: {
propertyDetails: PropertyBaseDetails;
setActiveStep: (step: number) => void;
}) {
return (
<Card>
<CardHeader>
<CardTitle>Property Overview</CardTitle>
<CardDescription>Basic information used in our prediction model</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<p>
Our AI model begins by analyzing the core attributes of your property. These fundamental characteristics
form the baseline for our prediction.
</p>
<div className="grid gap-4 md:grid-cols-2">
<InfoCard
icon={Home}
title="Property Type"
description={`${propertyDetails.type} properties in this area have specific market dynamics`}
/>
<InfoCard
icon={Ruler}
title="Size & Layout"
description={`${propertyDetails.size} sqm${
propertyDetails.bedrooms ? ` with ${propertyDetails.bedrooms} beds` : ""
}${propertyDetails.bathrooms ? ` and ${propertyDetails.bathrooms} baths` : ""}`}
/>
<InfoCard
icon={Calendar}
title="Property Age"
description={`Built ${propertyDetails.age} years ago, affecting depreciation calculations`}
/>
{propertyDetails.floor && (
<InfoCard
icon={Building}
title="Floor & View"
description={`Located on floor ${propertyDetails.floor}, impacting value`}
/>
)}
</div>
</div>
</CardContent>
<StepFooter onNext={() => setActiveStep(2)} />
</Card>
);
}
// Step 2 Component
function Step2Content({
features,
setActiveStep,
}: {
features: ModelExplanationData["features"];
setActiveStep: (step: number) => void;
}) {
return (
<Card>
<CardHeader>
<CardTitle>Feature Analysis</CardTitle>
<CardDescription>How different features impact the predicted price</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
<p>
Our model analyzes various features and determines how each contributes to the price prediction. Below is a
breakdown of the most important factors.
</p>
<div className="h-[300px]">
<FeatureImportanceChart features={features} />
</div>
<div className="space-y-4">
{features.map((feature) => (
<div key={feature.name} className="space-y-1">
<div className="flex justify-between">
<span className="text-sm font-medium">{feature.name}</span>
<span
className={`text-sm font-semibold ${
feature.impact === "positive"
? "text-green-600 dark:text-green-400"
: feature.impact === "negative"
? "text-red-600 dark:text-red-400"
: "text-yellow-600 dark:text-yellow-400"
}`}>
{feature.impact === "positive"
? "↑ Positive"
: feature.impact === "negative"
? "↓ Negative"
: "→ Neutral"}
</span>
</div>
<div className="flex items-center gap-2">
<Progress
value={feature.importance}
className="h-2"
aria-label={`${feature.name} importance ${feature.importance}%`}
/>
<span className="text-sm text-muted-foreground">{feature.importance}%</span>
</div>
<p className="text-xs text-muted-foreground">{feature.value}</p>
</div>
))}
</div>
</div>
</CardContent>
<StepFooter onPrev={() => setActiveStep(1)} onNext={() => setActiveStep(3)} />
</Card>
);
}
// Step 3 Component
function Step3Content({
property,
comparisons,
setActiveStep,
}: {
property: PropertyBaseDetails;
comparisons: ModelExplanationData["similarProperties"];
setActiveStep: (step: number) => void;
}) {
// Prepare data for the chart, ensuring the main property is clearly labeled
const chartProperty = {
name: "Your Property",
price: property.predictedPrice,
size: property.size,
age: property.age,
};
const chartComparisons = comparisons.map((p, i) => ({
name: `Comp ${i + 1}`,
address: p.address,
price: p.price,
size: p.size,
age: p.age,
}));
return (
<Card>
<CardHeader>
<CardTitle>Market Comparison</CardTitle>
<CardDescription>
How your property compares to similar properties recently analyzed or sold in the area
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
<p>
We analyze recent data from similar properties to establish a baseline. This ensures our prediction aligns
with current market conditions.
</p>
<div className="h-[300px]">
<PriceComparisonChart property={chartProperty} comparisons={chartComparisons} />
</div>
<div className="space-y-4">
<h4 className="font-medium">Similar Properties Details</h4>
<div className="grid gap-4 md:grid-cols-3">
{comparisons.map((p, index) => (
<div key={index} className="rounded-lg border p-3 text-xs">
<div className="font-medium truncate" title={p.address}>
{p.address}
</div>
<div className="mt-1 text-muted-foreground">
{p.size} sqm, {p.age} years old
</div>
<div className="mt-2 font-bold">{formatCurrency(p.price)}</div>
</div>
))}
</div>
</div>
</div>
</CardContent>
<StepFooter onPrev={() => setActiveStep(2)} onNext={() => setActiveStep(4)} />
</Card>
);
}
// Step 4 Component
function Step4Content({
factors,
setActiveStep,
}: {
factors: ModelExplanationData["environmentalFactors"];
setActiveStep: (step: number) => void;
}) {
const factorDetails = {
floodRisk: {
icon: Droplets,
color: factors.floodRisk === "low" ? "green" : factors.floodRisk === "moderate" ? "yellow" : "red",
text: "Historical data suggests this level of risk.",
},
airQuality: {
icon: Wind,
color: factors.airQuality === "good" ? "green" : factors.airQuality === "moderate" ? "yellow" : "red",
text: "Compared to city average.",
},
noiseLevel: {
icon: Sun,
color: factors.noiseLevel === "low" ? "green" : factors.noiseLevel === "moderate" ? "yellow" : "red",
text: "Based on proximity to major roads/sources.",
},
};
return (
<Card>
<CardHeader>
<CardTitle>Environmental & Location Factors</CardTitle>
<CardDescription>How surrounding conditions and amenities affect value</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
<p>
Environmental conditions and nearby amenities significantly impact desirability and value. Our model
considers these external factors.
</p>
<div className="grid gap-4 md:grid-cols-3">
{/* Environmental Factors */}
<FactorCard title="Flood Risk" factor={factors.floodRisk} details={factorDetails.floodRisk} />
<FactorCard title="Air Quality" factor={factors.airQuality} details={factorDetails.airQuality} />
<FactorCard title="Noise Level" factor={factors.noiseLevel} details={factorDetails.noiseLevel} />
</div>
{/* Proximity Example */}
<div className="space-y-2">
<h4 className="font-medium">Proximity to Amenities</h4>
<div className="grid gap-3 md:grid-cols-2">
<ProximityItem icon={Car} text="Public Transport: 300m" />
<ProximityItem icon={School} text="Schools: 1.2km" />
<ProximityItem icon={ShoppingBag} text="Shopping: 500m" />
<ProximityItem icon={Building} text="Hospitals: 2.5km" />
</div>
</div>
</div>
</CardContent>
<StepFooter onPrev={() => setActiveStep(3)} onNext={() => setActiveStep(5)} />
</Card>
);
}
// Step 5 Component
function Step5Content({
predictedPrice,
confidence,
priceRange,
setActiveStep,
}: {
predictedPrice: number;
confidence: number;
priceRange: { lower: number; upper: number };
setActiveStep: (step: number) => void;
}) {
return (
<Card>
<CardHeader>
<CardTitle>Final Prediction</CardTitle>
<CardDescription>The AI-predicted price based on all analyzed factors</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
{/* Price Box */}
<div className="rounded-lg bg-muted p-6 text-center">
<h3 className="text-lg font-medium text-muted-foreground">Predicted Price</h3>
<div className="mt-2 text-4xl font-bold">{formatCurrency(predictedPrice)}</div>
<div className="mt-2 text-sm text-muted-foreground">Confidence Level: {(confidence * 100).toFixed(0)}%</div>
</div>
{/* Price Range Box */}
<div className="rounded-lg border p-4">
<h4 className="font-medium flex items-center gap-2">
<Info className="h-4 w-4 text-primary" /> Price Range
</h4>
<p className="mt-2 text-sm text-muted-foreground">
Based on our model's confidence, the likely market range is:
</p>
<div className="mt-3 flex justify-between text-sm">
<div>
<div className="font-medium">Lower Bound</div>
<div className="text-muted-foreground">{formatCurrency(priceRange.lower)}</div>
</div>
<div className="text-center">
<div className="font-medium">Prediction</div>
<div className="text-primary font-bold">{formatCurrency(predictedPrice)}</div>
</div>
<div className="text-right">
<div className="font-medium">Upper Bound</div>
<div className="text-muted-foreground">{formatCurrency(priceRange.upper)}</div>
</div>
</div>
</div>
{/* Summary */}
<div className="space-y-2">
<h4 className="font-medium">Summary of Factors</h4>
<p className="text-sm text-muted-foreground">This prediction considers:</p>
<ul className="mt-2 space-y-1 text-sm list-disc list-inside">
<li>Property characteristics (size, age, layout)</li>
<li>Location and neighborhood profile</li>
<li>Recent market trends and comparable sales</li>
<li>Environmental factors and amenity access</li>
</ul>
</div>
</div>
</CardContent>
<CardFooter className="flex justify-between">
<Button variant="outline" onClick={() => setActiveStep(4)}>
Previous
</Button>
<Link href="/map">
<Button variant="default">Back to Map</Button>
</Link>
</CardFooter>
</Card>
);
}
// --- Sub-components for Steps ---
function InfoCard({ icon: Icon, title, description }: { icon: React.ElementType; title: string; description: string }) {
return (
<div className="flex items-start gap-3 rounded-lg border p-3">
<Icon className="mt-0.5 h-5 w-5 text-primary shrink-0" />
<div>
<h4 className="font-medium text-sm">{title}</h4>
<p className="text-xs text-muted-foreground">{description}</p>
</div>
</div>
);
}
function FactorCard({
title,
factor,
details,
}: {
title: string;
factor: string;
details: { icon: React.ElementType; color: string; text: string };
}) {
const Icon = details.icon;
const colorClass = `bg-${details.color}-500`; // Requires Tailwind JIT or safelisting
const textColorClass = `text-${details.color}-500`;
return (
<div className="flex flex-col items-center rounded-lg border p-4 text-center">
<Icon className={`h-8 w-8 mb-2 ${textColorClass}`} />
<h4 className="font-medium text-sm">{title}</h4>
<div className={`mt-2 flex items-center gap-2`}>
{/* Explicit colors might be safer than dynamic Tailwind classes */}
<div
className={`h-3 w-3 rounded-full`}
style={{
backgroundColor: details.color === "green" ? "#22c55e" : details.color === "yellow" ? "#eab308" : "#ef4444",
}}></div>
<span className="text-sm capitalize">{factor}</span>
</div>
<p className="mt-2 text-xs text-muted-foreground">{details.text}</p>
</div>
);
}
function ProximityItem({ icon: Icon, text }: { icon: React.ElementType; text: string }) {
return (
<div className="flex items-center gap-2 rounded-lg border p-2 text-xs">
<Icon className="h-4 w-4 text-primary shrink-0" />
<div>{text}</div>
</div>
);
}
function StepFooter({ onPrev, onNext }: { onPrev?: () => void; onNext?: () => void }) {
return (
<CardFooter className="flex justify-between">
{onPrev ? (
<Button variant="outline" onClick={onPrev}>
Previous
</Button>
) : (
<div></div>
)}
{onNext ? (
<Button onClick={onNext}>
Next Step <ArrowRight className="ml-2 h-4 w-4" />
</Button>
) : (
<div></div>
)}
</CardFooter>
);
}

View File

@ -1,21 +0,0 @@
/*
========================================
File: frontend/app/(routes)/map/layout.tsx
========================================
*/
import type React from "react";
// import { PageLayout } from "@/components/common/PageLayout"; // Example using a common layout
// This layout is specific to the map feature's route group
export default function MapFeatureLayout({ children }: { children: React.ReactNode }) {
return (
// <PageLayout className="flex flex-row"> {/* Example using common layout */}
// The MapSidebar might be rendered here if it's part of the layout
<div className="relative flex-1 h-full w-full">
{" "}
{/* Ensure content takes up space */}
{children}
</div>
// </PageLayout>
);
}

View File

@ -1,73 +0,0 @@
/*
========================================
File: frontend/app/(routes)/map/page.tsx
========================================
*/
"use client";
import React, { useState } from "react";
import Link from "next/link";
import { ArrowRight } from "lucide-react";
// Import common components
import { Button } from "@/components/ui/button";
// NOTE: ThemeProvider and ThemeController are in the root layout or a higher common layout now
// Import feature-specific components/contexts/types
import { MapContainer } from "@/features/map/components/map-container";
// MapSidebar might be part of the layout now, if shared, otherwise import here
// import { MapSidebar } from "@/features/map/components/map-sidebar";
import { MapHeader } from "@/features/map/components/map-header"; // Map specific header
import { OverlayProvider } from "@/features/map/components/overlay-system/overlay-context";
import { OverlayDock } from "@/features/map/components/overlay-system/overlay-dock";
import { AnalyticsOverlay } from "@/features/map/components/analytics-overlay";
import { FiltersOverlay } from "@/features/map/components/filters-overlay";
import { ChatOverlay } from "@/features/map/components/chat-overlay";
import type { MapLocation } from "@/features/map/types";
export default function MapPage() {
const [selectedLocation, setSelectedLocation] = useState<MapLocation>({
lat: 13.7563,
lng: 100.5018,
name: "Bangkok",
});
// Main page structure remains similar, but imports are updated
return (
// ThemeProvider/Controller likely moved to root layout
// SidebarProvider might be moved too, depending on its scope
// Assuming OverlayProvider is specific to this map page context
<OverlayProvider>
{/* The outer div with flex, h-screen etc. should be handled by the layout file or a common PageLayout */}
<div className="flex h-full w-full flex-col overflow-hidden">
{" "}
{/* Simplified for page content */}
<MapHeader />
<div className="relative flex-1 overflow-hidden">
<MapContainer selectedLocation={selectedLocation} />
{/* Prediction model banner */}
<div className="absolute left-1/2 top-4 -translate-x-1/2 z-10">
<div className="flex items-center gap-2 rounded-lg bg-card/95 backdrop-blur-xs border border-border/50 px-4 py-2 shadow-lg">
<div>
<h3 className="font-medium">Price Prediction: 15,000,000 ฿</h3>
<p className="text-xs text-muted-foreground">Based on our AI model analysis</p>
</div>
<Link href="/model-explanation">
<Button size="sm" variant="outline" className="gap-1">
Explain <ArrowRight className="h-3 w-3" />
</Button>
</Link>
</div>
</div>
{/* Overlay System */}
<AnalyticsOverlay />
<FiltersOverlay />
<ChatOverlay />
<OverlayDock position="bottom" />
</div>
</div>
</OverlayProvider>
);
}

View File

@ -0,0 +1,720 @@
"use client"
import { useState, useRef } from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Badge } from "@/components/ui/badge"
import { Slider } from "@/components/ui/slider"
import { Switch } from "@/components/ui/switch"
import {
MapPin,
Home,
BarChart2,
Filter,
MessageCircle,
X,
Plus,
Minus,
Droplets,
Wind,
Sun,
LineChart,
Send,
Newspaper,
Building,
BedDouble,
Bath,
Star,
Clock,
ChevronDown,
Check,
RefreshCw,
} from "lucide-react"
import Link from "next/link"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
export default function MapsPage() {
const [showFilters, setShowFilters] = useState(false)
const [showAnalytics, setShowAnalytics] = useState(false)
const [showChat, setShowChat] = useState(false)
const [showPropertyInfo, setShowPropertyInfo] = useState(false)
const [activeTab, setActiveTab] = useState("basic")
const [priceRange, setPriceRange] = useState([5000000, 20000000])
const [radius, setRadius] = useState(30)
const [message, setMessage] = useState("")
const [messages, setMessages] = useState([{ role: "assistant", content: "Hi! How can I help you today?" }])
const [mapZoom, setMapZoom] = useState(14)
const [selectedModel, setSelectedModel] = useState("Standard ML Model v2.4")
const mapRef = useRef(null)
const models = [
"Standard ML Model v2.4",
"Enhanced Neural Network v1.8",
"Geospatial Regression v3.1",
"Time Series Forecast v2.0",
"Custom Model (User #1242)",
]
const handleSendMessage = () => {
if (message.trim()) {
setMessages([...messages, { role: "user", content: message }])
// Simulate AI response
setTimeout(() => {
setMessages((prev) => [
...prev,
{
role: "assistant",
content:
"I can provide information about properties in this area. Would you like to know about flood risks, air quality, or nearby amenities?",
},
])
}, 1000)
setMessage("")
}
}
const handleZoomIn = () => {
setMapZoom((prev) => Math.min(prev + 1, 20))
}
const handleZoomOut = () => {
setMapZoom((prev) => Math.max(prev - 1, 10))
}
const handlePropertyClick = () => {
setShowPropertyInfo(true)
setShowFilters(false)
setShowChat(false)
}
return (
<div className="relative h-screen w-full overflow-hidden bg-gray-100 dark:bg-gray-900">
{/* Map Container */}
<div className="absolute inset-0 bg-[url('/map.png')] bg-cover bg-center">
{/* Map Placeholder - In a real implementation, this would be a map component */}
<div className="absolute inset-0 flex items-center justify-center">
<div className="text-2xl text-muted-foreground opacity-0">Map View</div>
</div>
{/* Sample Property Markers */}
<div className="absolute left-1/4 top-1/3 text-primary cursor-pointer group" onClick={handlePropertyClick}>
<div className="relative transition-transform transform group-hover:scale-125">
<div className="absolute inset-0 w-10 h-10 bg-green-500 opacity-30 blur-lg rounded-full"></div>
<MapPin className="h-10 w-10 text-green-500 drop-shadow-xl" />
<div className="absolute -top-2 -right-2 h-5 w-5 bg-green-500 rounded-full border-2 border-white animate-pulse"></div>
<span className="absolute top-12 left-1/2 -translate-x-1/2 hidden group-hover:flex bg-black text-white text-xs font-bold px-3 py-1 rounded-lg shadow-lg">
Available
</span>
</div>
</div>
<div className="absolute left-1/2 top-1/2 text-primary cursor-pointer group" onClick={handlePropertyClick}>
<div className="relative transition-transform transform group-hover:scale-125">
<div className="absolute inset-0 w-10 h-10 bg-yellow-500 opacity-30 blur-lg rounded-full"></div>
<MapPin className="h-10 w-10 text-yellow-500 drop-shadow-xl" />
<div className="absolute -top-2 -right-2 h-5 w-5 bg-amber-500 rounded-full border-2 border-white animate-pulse"></div>
<span className="absolute top-12 left-1/2 -translate-x-1/2 hidden group-hover:flex bg-black text-white text-xs font-bold px-3 py-1 rounded-lg shadow-lg">
Pending
</span>
</div>
</div>
<div className="absolute right-1/4 top-2/3 text-primary cursor-pointer group" onClick={handlePropertyClick}>
<div className="relative transition-transform transform group-hover:scale-125">
<div className="absolute inset-0 w-10 h-10 bg-red-500 opacity-30 blur-lg rounded-full"></div>
<MapPin className="h-10 w-10 text-red-500 drop-shadow-xl" />
<div className="absolute -top-2 -right-2 h-5 w-5 bg-red-500 rounded-full border-2 border-white animate-pulse"></div>
<span className="absolute top-12 left-1/2 -translate-x-1/2 hidden group-hover:flex bg-black text-white text-xs font-bold px-3 py-1 rounded-lg shadow-lg">
Sold
</span>
</div>
</div>
</div>
{/* Top Navigation Bar */}
<div className="absolute top-0 left-0 right-0 bg-background/95 backdrop-blur-sm p-4 flex items-center justify-between z-10 border-b">
<Link href="/" className="flex items-center gap-2">
<Home className="h-5 w-5" />
<span className="font-semibold">BorBann</span>
</Link>
<div className="flex-1 max-w-md mx-4">
<div className="relative">
<input
type="text"
placeholder="Search locations..."
className="w-full h-10 px-4 rounded-md border border-input bg-background"
/>
<div className="absolute right-3 top-1/2 -translate-y-1/2">
<MapPin className="h-4 w-4 text-muted-foreground" />
</div>
</div>
</div>
<div className="flex items-center gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="gap-1">
<BarChart2 className="h-4 w-4" />
<span className="hidden sm:inline">{selectedModel}</span>
<ChevronDown className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[240px]">
{models.map((model) => (
<DropdownMenuItem
key={model}
onClick={() => setSelectedModel(model)}
className="flex items-center gap-2"
>
{model === selectedModel && <Check className="h-4 w-4 text-primary" />}
<span className={model === selectedModel ? "font-medium" : ""}>{model}</span>
</DropdownMenuItem>
))}
<DropdownMenuItem>
<Link href="/models" className="flex items-center w-full">
<span className="text-primary">Manage Models...</span>
</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Link href="/properties">
<Button variant="outline" size="sm" className="gap-1">
<Building className="h-4 w-4" />
<span className="hidden sm:inline">Properties</span>
</Button>
</Link>
<Link href="/price-prediction">
<Button variant="outline" size="sm" className="gap-1">
<BarChart2 className="h-4 w-4" />
<span className="hidden sm:inline">Price Prediction</span>
</Button>
</Link>
</div>
</div>
{/* Map Controls */}
<div className="absolute top-20 right-4 flex flex-col gap-2 z-10">
<Button
variant="secondary"
size="icon"
className="h-10 w-10 rounded-full bg-background/95 backdrop-blur-sm shadow-md"
onClick={handleZoomIn}
>
<Plus className="h-5 w-5" />
</Button>
<Button
variant="secondary"
size="icon"
className="h-10 w-10 rounded-full bg-background/95 backdrop-blur-sm shadow-md"
onClick={handleZoomOut}
>
<Minus className="h-5 w-5" />
</Button>
</div>
{/* Map Overlay Controls */}
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 flex gap-2 z-10">
<Button
variant="secondary"
size="icon"
className={`h-12 w-12 rounded-full bg-background/95 backdrop-blur-sm shadow-md ${showAnalytics ? "bg-primary text-primary-foreground" : ""}`}
onClick={() => {
setShowAnalytics(!showAnalytics)
if (showAnalytics) {
setShowFilters(false)
setShowPropertyInfo(false)
}
}}
>
<BarChart2 className="h-5 w-5" />
</Button>
<Button
variant="secondary"
size="icon"
className={`h-12 w-12 rounded-full bg-background/95 backdrop-blur-sm shadow-md ${showFilters ? "bg-primary text-primary-foreground" : ""}`}
onClick={() => {
setShowFilters(!showFilters)
if (showFilters) {
setShowAnalytics(false)
setShowPropertyInfo(false)
}
}}
>
<Filter className="h-5 w-5" />
</Button>
<Button
variant="secondary"
size="icon"
className={`h-12 w-12 rounded-full bg-background/95 backdrop-blur-sm shadow-md ${showChat ? "bg-primary text-primary-foreground" : ""}`}
onClick={() => {
setShowChat(!showChat)
if (showChat) {
setShowPropertyInfo(false)
}
}}
>
<MessageCircle className="h-5 w-5" />
</Button>
</div>
{/* Property Info Panel */}
{showPropertyInfo && (
<div className="absolute top-20 right-4 w-96 map-overlay z-20">
<div className="map-overlay-header">
<div className="flex items-center gap-2">
<Building className="h-5 w-5 text-primary" />
<span className="font-medium">Property Details</span>
</div>
<div className="flex items-center gap-1">
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => setShowPropertyInfo(false)}>
<X className="h-4 w-4" />
</Button>
</div>
</div>
<div className="map-overlay-content">
<div className="relative mb-4">
<img
src="/placeholder.svg?height=200&width=400"
alt="Property"
className="w-full h-40 object-cover rounded-md"
/>
<div className="absolute top-2 left-2 flex gap-1">
<Badge className="bg-primary">Condominium</Badge>
<Badge className="bg-amber-500">
<Star className="h-3 w-3 mr-1" /> Premium
</Badge>
</div>
</div>
<h3 className="font-medium text-lg mb-1">Modern Condominium</h3>
<div className="flex items-center text-muted-foreground text-sm mb-2">
<MapPin className="h-3.5 w-3.5 mr-1 flex-shrink-0" />
<span className="truncate">Sukhumvit, Bangkok</span>
</div>
<div className="flex items-center gap-4 text-sm mb-3">
<div className="flex items-center">
<BedDouble className="h-4 w-4 mr-1 text-primary" />
<span>3 Beds</span>
</div>
<div className="flex items-center">
<Bath className="h-4 w-4 mr-1 text-primary" />
<span>2 Baths</span>
</div>
<div className="flex items-center">
<Home className="h-4 w-4 mr-1 text-primary" />
<span>150 m²</span>
</div>
</div>
<div className="font-semibold text-lg mb-4">฿15,000,000</div>
<div className="space-y-4">
<div>
<h4 className="font-medium text-sm mb-2">Environmental Factors</h4>
<div className="grid grid-cols-3 gap-2">
<div className="flex flex-col items-center p-2 border rounded-md">
<Droplets className="h-5 w-5 text-blue-500 mb-1" />
<span className="text-xs font-medium">Flood Risk</span>
<Badge className="mt-1 text-xs bg-amber-500">Moderate</Badge>
</div>
<div className="flex flex-col items-center p-2 border rounded-md">
<Wind className="h-5 w-5 text-purple-500 mb-1" />
<span className="text-xs font-medium">Air Quality</span>
<Badge className="mt-1 text-xs bg-destructive">Poor</Badge>
</div>
<div className="flex flex-col items-center p-2 border rounded-md">
<Sun className="h-5 w-5 text-amber-500 mb-1" />
<span className="text-xs font-medium">Noise</span>
<Badge className="mt-1 text-xs bg-green-500">Low</Badge>
</div>
</div>
</div>
<div>
<h4 className="font-medium text-sm mb-2">Nearby Facilities</h4>
<div className="space-y-1 text-sm">
<div className="flex justify-between">
<span>BTS Phrom Phong</span>
<span className="text-muted-foreground">300m</span>
</div>
<div className="flex justify-between">
<span>EmQuartier Mall</span>
<span className="text-muted-foreground">500m</span>
</div>
<div className="flex justify-between">
<span>Benchasiri Park</span>
<span className="text-muted-foreground">700m</span>
</div>
</div>
</div>
<div className="flex gap-2">
<Link href="/properties/prop1" className="flex-1">
<Button className="w-full">View Details</Button>
</Link>
<Link href="/price-prediction" className="flex-1">
<Button variant="outline" className="w-full">
Price Analysis
</Button>
</Link>
</div>
</div>
</div>
</div>
)}
{/* Analytics Panel */}
{showAnalytics && (
<div className="absolute top-20 right-4 w-96 max-h-[800px] overflow-y-auto z-20 map-overlay">
<div className="map-overlay-header">
<div className="flex items-center gap-2">
<BarChart2 className="h-5 w-5 text-primary" />
<span className="font-medium">Analytics</span>
</div>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="sm"
className="h-8 w-8 flex items-center justify-center"
onClick={() => setSelectedModel(selectedModel)}
>
<RefreshCw className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => setShowAnalytics(false)}>
<X className="h-4 w-4" />
</Button>
</div>
</div>
<div className="map-overlay-content">
<div className="flex items-center justify-between mb-4">
<p className="text-sm text-muted-foreground">Information in radius will be analyzed</p>
<Badge variant="outline" className="text-xs">
Using: {selectedModel}
</Badge>
</div>
<Card className="mb-4">
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-2">
<LineChart className="h-4 w-4 text-primary" />
<span className="font-medium">Area Price History</span>
</div>
<h3 className="text-2xl font-bold mb-1">10,000,000 Baht</h3>
<p className="text-xs text-muted-foreground mb-3">Overall Price History of this area</p>
<div className="h-20 w-full relative">
{/* Simple line chart simulation */}
<div className="absolute bottom-0 left-0 w-full h-px bg-border"></div>
<div className="absolute bottom-0 left-0 h-full flex items-end">
<div className="w-1/6 h-8 border-b-2 border-r-2 border-primary rounded-br"></div>
<div className="w-1/6 h-6 border-b-2 border-r-2 border-primary rounded-br"></div>
<div className="w-1/6 h-7 border-b-2 border-r-2 border-primary rounded-br"></div>
<div className="w-1/6 h-10 border-b-2 border-r-2 border-primary rounded-br"></div>
<div className="w-1/6 h-12 border-b-2 border-r-2 border-primary rounded-br"></div>
<div className="w-1/6 h-16 border-b-2 border-r-2 border-primary rounded-br"></div>
</div>
</div>
</CardContent>
</Card>
<Card className="mb-4">
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-2">
<LineChart className="h-4 w-4 text-primary" />
<span className="font-medium">Price Prediction</span>
</div>
<h3 className="text-2xl font-bold mb-1">15,000,000 Baht</h3>
<p className="text-xs text-muted-foreground mb-3">The estimated price based on various factors.</p>
<div className="h-20 w-full relative">
{/* Simple line chart simulation */}
<div className="absolute bottom-0 left-0 w-full h-px bg-border"></div>
<div className="absolute bottom-0 left-0 h-full flex items-end">
<div className="w-1/6 h-4 border-b-2 border-r-2 border-green-500 rounded-br"></div>
<div className="w-1/6 h-6 border-b-2 border-r-2 border-green-500 rounded-br"></div>
<div className="w-1/6 h-8 border-b-2 border-r-2 border-green-500 rounded-br"></div>
<div className="w-1/6 h-10 border-b-2 border-r-2 border-green-500 rounded-br"></div>
<div className="w-1/6 h-14 border-b-2 border-r-2 border-green-500 rounded-br"></div>
<div className="w-1/6 h-18 border-b-2 border-r-2 border-green-500 rounded-br"></div>
</div>
</div>
</CardContent>
</Card>
<div className="grid grid-cols-2 gap-2 mb-4">
<Card>
<CardContent className="p-3">
<div className="flex flex-col items-center">
<Droplets className="h-5 w-5 text-blue-500 mb-1" />
<span className="text-sm font-medium">Flood Factor</span>
<Badge className="mt-1 bg-amber-500">Moderate</Badge>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-3">
<div className="flex flex-col items-center">
<Wind className="h-5 w-5 text-purple-500 mb-1" />
<span className="text-sm font-medium">Air Factor</span>
<Badge className="mt-1 bg-destructive">Bad</Badge>
</div>
</CardContent>
</Card>
</div>
{/* Local News Section */}
<div className="mb-4">
<h4 className="font-medium text-sm mb-2 flex items-center">
<Newspaper className="h-4 w-4 mr-1 text-primary" />
Local News
</h4>
<div className="space-y-2">
<Card>
<CardContent className="p-3">
<h5 className="text-sm font-medium">New BTS Extension Planned</h5>
<p className="text-xs text-muted-foreground">
The BTS Skytrain will be extended to cover more areas in Sukhumvit by 2025.
</p>
<div className="flex items-center text-xs text-muted-foreground mt-1">
<Clock className="h-3 w-3 mr-1" />
<span>2 days ago</span>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-3">
<h5 className="text-sm font-medium">Property Tax Changes</h5>
<p className="text-xs text-muted-foreground">
New property tax regulations will take effect next month affecting luxury condominiums.
</p>
<div className="flex items-center text-xs text-muted-foreground mt-1">
<Clock className="h-3 w-3 mr-1" />
<span>1 week ago</span>
</div>
</CardContent>
</Card>
</div>
</div>
<div className="flex gap-2">
<Button variant="outline" className="w-full gap-2">
<MessageCircle className="h-4 w-4" />
Chat With AI
</Button>
<Link href="/price-prediction" className="flex-1">
<Button className="w-full">Full Analysis</Button>
</Link>
</div>
</div>
</div>
)}
{/* Filters Panel */}
{showFilters && (
<div className="absolute top-20 right-4 w-96 map-overlay z-20">
<div className="map-overlay-header">
<div className="flex items-center gap-2">
<Filter className="h-5 w-5 text-primary" />
<span className="font-medium">Property Filters</span>
</div>
<div className="flex items-center gap-1">
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => setShowFilters(false)}>
<X className="h-4 w-4" />
</Button>
</div>
</div>
<Tabs defaultValue="basic" value={activeTab} onValueChange={setActiveTab}>
<TabsList className="w-full grid grid-cols-2">
<TabsTrigger value="basic">Basic</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList>
<TabsContent value="basic" className="p-4">
<div className="space-y-4">
<div>
<label className="text-sm font-medium mb-1.5 block">Area Radius</label>
<div className="flex items-center gap-2">
<Slider
defaultValue={[30]}
max={50}
min={1}
step={1}
onValueChange={(value) => setRadius(value[0])}
className="flex-1"
/>
<span className="text-sm w-16 text-right">{radius} km</span>
</div>
</div>
<div>
<label className="text-sm font-medium mb-1.5 block">Time Period</label>
<select className="w-full h-10 rounded-md border border-input bg-background px-3 py-2 text-sm">
<option value="all">All Time</option>
<option value="1m">Last Month</option>
<option value="3m">Last 3 Months</option>
<option value="6m">Last 6 Months</option>
<option value="1y">Last Year</option>
</select>
</div>
<div>
<label className="text-sm font-medium mb-1.5 block">Property Type</label>
<select className="w-full h-10 rounded-md border border-input bg-background px-3 py-2 text-sm">
<option value="any">Any Type</option>
<option value="house">House</option>
<option value="condo">Condominium</option>
<option value="townhouse">Townhouse</option>
<option value="land">Land</option>
</select>
</div>
<Button className="w-full">Apply Filters</Button>
</div>
</TabsContent>
<TabsContent value="advanced" className="p-4">
<div className="space-y-4">
<div>
<label className="text-sm font-medium mb-1.5 block">Price Range</label>
<div className="flex items-center justify-between text-xs text-muted-foreground mb-2">
<span>฿{priceRange[0].toLocaleString()}</span>
<span>฿{priceRange[1].toLocaleString()}</span>
</div>
<Slider
defaultValue={[5000000, 20000000]}
max={50000000}
min={1000000}
step={1000000}
onValueChange={(value) => setPriceRange(value)}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium mb-1.5 block">Environmental Factors</label>
<div className="flex items-center justify-between">
<span className="text-sm">Low Flood Risk</span>
<Switch />
</div>
<div className="flex items-center justify-between">
<span className="text-sm">Good Air Quality</span>
<Switch />
</div>
<div className="flex items-center justify-between">
<span className="text-sm">Low Noise Pollution</span>
<Switch />
</div>
</div>
<div>
<label className="text-sm font-medium mb-1.5 block">Facilities Nearby</label>
<div className="grid grid-cols-2 gap-2">
<div className="flex items-center space-x-2">
<input type="checkbox" id="bts" className="h-4 w-4" />
<label htmlFor="bts" className="text-sm">
BTS/MRT Station
</label>
</div>
<div className="flex items-center space-x-2">
<input type="checkbox" id="school" className="h-4 w-4" />
<label htmlFor="school" className="text-sm">
Schools
</label>
</div>
<div className="flex items-center space-x-2">
<input type="checkbox" id="hospital" className="h-4 w-4" />
<label htmlFor="hospital" className="text-sm">
Hospitals
</label>
</div>
<div className="flex items-center space-x-2">
<input type="checkbox" id="mall" className="h-4 w-4" />
<label htmlFor="mall" className="text-sm">
Shopping Malls
</label>
</div>
</div>
</div>
<Button className="w-full">Apply Filters</Button>
</div>
</TabsContent>
</Tabs>
</div>
)}
{/* Chat Panel */}
{showChat && (
<div className="absolute top-20 right-4 w-96 h-[500px] map-overlay z-20 flex flex-col">
<div className="map-overlay-header">
<div className="flex items-center gap-2">
<MessageCircle className="h-5 w-5 text-primary" />
<span className="font-medium">Chat Assistant</span>
</div>
<div className="flex items-center gap-1">
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => setShowChat(false)}>
<X className="h-4 w-4" />
</Button>
</div>
</div>
<div className="flex-1 overflow-y-auto p-3 space-y-3">
{messages.map((msg, index) => (
<div key={index} className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}>
<div
className={`max-w-[80%] rounded-lg px-3 py-2 ${
msg.role === "user" ? "bg-primary text-primary-foreground" : "bg-muted"
}`}
>
{msg.content}
</div>
</div>
))}
</div>
<div className="p-3 border-t">
<div className="flex gap-2">
<input
type="text"
placeholder="Type your message..."
className="flex-1 h-10 px-3 rounded-md border border-input bg-background"
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") handleSendMessage()
}}
/>
<Button variant="default" size="icon" className="h-10 w-10" onClick={handleSendMessage}>
<Send className="h-4 w-4" />
</Button>
</div>
</div>
</div>
)}
{/* Map Legend */}
<div className="absolute bottom-8 left-4 bg-background/95 backdrop-blur-sm p-2 rounded-lg shadow-md z-10">
<div className="text-xs font-medium mb-1">Property Status</div>
<div className="space-y-1">
<div className="flex items-center gap-1.5">
<div className="h-3 w-3 bg-green-500 rounded-full"></div>
<span className="text-xs">Available</span>
</div>
<div className="flex items-center gap-1.5">
<div className="h-3 w-3 bg-amber-500 rounded-full"></div>
<span className="text-xs">Pending</span>
</div>
<div className="flex items-center gap-1.5">
<div className="h-3 w-3 bg-red-500 rounded-full"></div>
<span className="text-xs">Sold</span>
</div>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,500 @@
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Switch } from "@/components/ui/switch";
import { Progress } from "@/components/ui/progress";
import {
BrainCircuit,
Clock,
Database,
Play,
Plus,
Settings,
Sliders,
Trash2,
AlertTriangle,
Check,
ArrowRight,
Info,
} from "lucide-react";
import Link from "next/link";
import PageHeader from "@/components/page-header";
export default function ModelsPage() {
const [activeTab, setActiveTab] = useState("my-models");
const [selectedPipeline, setSelectedPipeline] = useState<string | null>(null);
const [trainingProgress, setTrainingProgress] = useState(0);
const [isTraining, setIsTraining] = useState(false);
const [modelName, setModelName] = useState("");
const [modelDescription, setModelDescription] = useState("");
const dataPipelines = [
{ id: "pipeline-1", name: "Property Listings", records: 1240, lastUpdated: "2 hours ago" },
{ id: "pipeline-2", name: "Rental Market Data", records: 830, lastUpdated: "Yesterday" },
{ id: "pipeline-3", name: "Price Comparison", records: 1560, lastUpdated: "2 days ago" },
{ id: "pipeline-4", name: "Commercial Properties", records: 450, lastUpdated: "1 week ago" },
];
const models = [
{
id: "model-1",
name: "Standard ML Model v2.4",
type: "Regression",
hyperparameters: {
learningRate: "0.01",
maxDepth: "6",
numEstimators: "100",
},
dataSource: "System Base Model",
status: "active",
lastUpdated: "3 days ago",
isSystem: true,
},
{
id: "model-2",
name: "Enhanced Neural Network v1.8",
type: "Neural Network",
hyperparameters: {
layers: "4",
neurons: "128,64,32,16",
dropout: "0.2",
},
dataSource: "System Base Model",
status: "active",
lastUpdated: "1 week ago",
isSystem: true,
},
{
id: "model-3",
name: "Geospatial Regression v3.1",
type: "Geospatial",
hyperparameters: {
spatialWeight: "0.7",
kernelType: "gaussian",
bandwidth: "adaptive",
},
dataSource: "System Base Model",
status: "active",
lastUpdated: "2 weeks ago",
isSystem: true,
},
{
id: "model-4",
name: "Time Series Forecast v2.0",
type: "Time Series",
hyperparameters: {
p: "2",
d: "1",
q: "2",
seasonal: "true",
},
dataSource: "System Base Model",
status: "active",
lastUpdated: "1 month ago",
isSystem: true,
},
{
id: "model-5",
name: "Custom Model (User #1242)",
type: "Ensemble",
hyperparameters: {
baseEstimators: "3",
votingMethod: "weighted",
weights: "0.4,0.4,0.2",
},
dataSource: "Property Listings Pipeline",
status: "active",
lastUpdated: "5 days ago",
isSystem: false,
},
];
const handleStartTraining = () => {
if (!selectedPipeline || !modelName) return;
setIsTraining(true);
setTrainingProgress(0);
// Simulate training progress
const interval = setInterval(() => {
setTrainingProgress((prev) => {
if (prev >= 100) {
clearInterval(interval);
setIsTraining(false);
return 100;
}
return prev + 5;
});
}, 500);
};
return (
<div className="container mx-auto p-6">
<PageHeader
title="Model Management"
description="Train, manage, and deploy machine learning models for property analysis"
breadcrumb={[
{ title: "Home", href: "/" },
{ title: "Models", href: "/models" },
]}
/>
<Tabs defaultValue="my-models" className="mt-6" onValueChange={setActiveTab}>
<div className="flex justify-between items-center mb-6">
<TabsList>
<TabsTrigger value="my-models">My Models</TabsTrigger>
<TabsTrigger value="system-models">System Models</TabsTrigger>
<TabsTrigger value="train-model">Train New Model</TabsTrigger>
</TabsList>
{activeTab !== "train-model" && (
<Button onClick={() => setActiveTab("train-model")} className="gap-2">
<Plus className="h-4 w-4" />
Train New Model
</Button>
)}
</div>
<TabsContent value="my-models">
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{models
.filter((model) => !model.isSystem)
.map((model) => (
<ModelCard key={model.id} model={model} />
))}
</div>
{models.filter((model) => !model.isSystem).length === 0 && (
<Card className="border-dashed">
<CardContent className="pt-6 text-center">
<BrainCircuit className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
<h3 className="text-lg font-medium mb-2">No Custom Models Yet</h3>
<p className="text-sm text-muted-foreground mb-4">
Train your first custom model to get started with personalized property predictions.
</p>
<Button onClick={() => setActiveTab("train-model")}>Train Your First Model</Button>
</CardContent>
</Card>
)}
</TabsContent>
<TabsContent value="system-models">
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{models
.filter((model) => model.isSystem)
.map((model) => (
<ModelCard key={model.id} model={model} />
))}
</div>
</TabsContent>
<TabsContent value="train-model">
<div className="grid gap-6 md:grid-cols-3">
<div className="md:col-span-1">
<Card>
<CardHeader>
<CardTitle>Training Configuration</CardTitle>
<CardDescription>Configure your new model</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="model-name">Model Name</Label>
<Input
id="model-name"
placeholder="e.g., My Custom Model v1.0"
value={modelName}
onChange={(e) => setModelName(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="model-description">Description (Optional)</Label>
<Textarea
id="model-description"
placeholder="Describe the purpose of this model..."
rows={3}
value={modelDescription}
onChange={(e) => setModelDescription(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label>Model Type</Label>
<select className="w-full h-10 rounded-md border border-input bg-background px-3 py-2 text-sm">
<option value="regression">Regression (Default)</option>
<option value="neural-network">Neural Network</option>
<option value="ensemble">Ensemble</option>
<option value="geospatial">Geospatial</option>
<option value="time-series">Time Series</option>
</select>
</div>
<div className="pt-2">
<h3 className="text-sm font-medium mb-2">Advanced Settings</h3>
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="feature-selection">Automatic Feature Selection</Label>
<p className="text-xs text-muted-foreground">Let AI select the most relevant features</p>
</div>
<Switch id="feature-selection" defaultChecked />
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="hyperparameter-tuning">Hyperparameter Tuning</Label>
<p className="text-xs text-muted-foreground">Optimize model parameters automatically</p>
</div>
<Switch id="hyperparameter-tuning" defaultChecked />
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="cross-validation">Cross-Validation</Label>
<p className="text-xs text-muted-foreground">Use k-fold cross-validation</p>
</div>
<Switch id="cross-validation" defaultChecked />
</div>
</div>
</div>
</CardContent>
</Card>
</div>
<div className="md:col-span-2">
<Card className="mb-6">
<CardHeader>
<CardTitle>Select Data Source</CardTitle>
<CardDescription>Choose a data pipeline to train your model</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{dataPipelines.map((pipeline) => (
<div
key={pipeline.id}
className={`p-4 border rounded-lg cursor-pointer transition-all ${
selectedPipeline === pipeline.id ? "border-primary bg-primary/5" : "hover:border-primary/50"
}`}
onClick={() => setSelectedPipeline(pipeline.id)}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Database className="h-5 w-5 text-primary" />
<div>
<h3 className="font-medium">{pipeline.name}</h3>
<p className="text-xs text-muted-foreground">
{pipeline.records.toLocaleString()} records Updated {pipeline.lastUpdated}
</p>
</div>
</div>
{selectedPipeline === pipeline.id && <Check className="h-5 w-5 text-primary" />}
</div>
</div>
))}
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Training Process</CardTitle>
<CardDescription>Monitor and control the training process</CardDescription>
</CardHeader>
<CardContent>
{isTraining ? (
<div className="space-y-4">
<div className="space-y-2">
<div className="flex justify-between items-center text-sm">
<span>Training Progress</span>
<span>{trainingProgress}%</span>
</div>
<Progress value={trainingProgress} className="h-2" />
</div>
<div className="space-y-1">
<div className="text-sm font-medium">Current Step:</div>
<div className="text-sm">
{trainingProgress < 20
? "Preparing data..."
: trainingProgress < 40
? "Feature engineering..."
: trainingProgress < 60
? "Training model..."
: trainingProgress < 80
? "Evaluating performance..."
: trainingProgress < 100
? "Finalizing model..."
: "Training complete!"}
</div>
</div>
{trainingProgress < 100 && (
<Button variant="outline" className="w-full" onClick={() => setIsTraining(false)}>
Cancel Training
</Button>
)}
{trainingProgress === 100 && (
<div className="space-y-3">
<div className="p-3 bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400 rounded-md flex items-center gap-2">
<Check className="h-5 w-5" />
<span>Training completed successfully!</span>
</div>
<div className="flex gap-2">
<Button variant="outline" className="flex-1">
View Details
</Button>
<Button className="flex-1">Use Model</Button>
</div>
</div>
)}
</div>
) : (
<div className="space-y-4">
<div className="p-4 border border-dashed rounded-lg">
<div className="flex items-center gap-3 mb-3">
<BrainCircuit className="h-8 w-8 text-primary" />
<div>
<h3 className="font-medium">Ready to Train</h3>
<p className="text-sm text-muted-foreground">Configure your settings and start training</p>
</div>
</div>
{!selectedPipeline && (
<div className="p-3 bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-400 rounded-md flex items-center gap-2 text-sm">
<AlertTriangle className="h-4 w-4" />
<span>Please select a data pipeline first</span>
</div>
)}
{!modelName && (
<div className="p-3 bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-400 rounded-md flex items-center gap-2 text-sm mt-2">
<AlertTriangle className="h-4 w-4" />
<span>Please enter a model name</span>
</div>
)}
</div>
<div className="flex gap-2">
<Button variant="outline" className="flex-1" onClick={() => setActiveTab("my-models")}>
Cancel
</Button>
<Button
className="flex-1 gap-2"
onClick={handleStartTraining}
disabled={!selectedPipeline || !modelName}>
<Play className="h-4 w-4" />
Start Training
</Button>
</div>
</div>
)}
</CardContent>
</Card>
</div>
</div>
</TabsContent>
</Tabs>
</div>
);
}
interface ModelCardProps {
model: {
id: string;
name: string;
type: string;
hyperparameters: {
[key: string]: string;
};
dataSource: string;
status: string;
lastUpdated: string;
isSystem: boolean;
};
}
function ModelCard({ model }: ModelCardProps) {
return (
<Card className={model.isSystem ? "border-primary/20" : ""}>
<CardHeader className="pb-2">
<div className="flex justify-between items-start">
<CardTitle className="text-lg">{model.name}</CardTitle>
<Badge variant={model.status === "active" ? "default" : "secondary"}>
{model.status === "active" ? "Active" : "Inactive"}
</Badge>
</div>
<CardDescription>{model.type} Model</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div>
<div className="flex items-center gap-1 mb-1">
<Database className="h-4 w-4 text-primary" />
<span className="text-sm font-medium">Data Source:</span>
</div>
{model.isSystem ? (
<div className="flex items-center gap-1 text-sm">
<Badge variant="outline" className="bg-primary/5">
System Base Model
</Badge>
<Info
className="h-4 w-4 text-muted-foreground cursor-help"
title="This is a pre-trained system model"
/>
</div>
) : (
<span className="text-sm">{model.dataSource}</span>
)}
</div>
<div>
<div className="flex items-center gap-1 mb-1">
<Sliders className="h-4 w-4 text-primary" />
<span className="text-sm font-medium">Hyperparameters:</span>
</div>
<div className="grid grid-cols-1 gap-1">
{Object.entries(model.hyperparameters).map(([key, value]) => (
<div key={key} className="flex justify-between text-xs">
<span className="text-muted-foreground">{key}:</span>
<span>{value}</span>
</div>
))}
</div>
</div>
<div className="flex items-center text-sm">
<Clock className="h-4 w-4 mr-2 text-muted-foreground" />
<span className="text-muted-foreground">Last updated:</span>
<span className="ml-1 font-medium">{model.lastUpdated}</span>
</div>
</div>
</CardContent>
<CardFooter className="flex justify-between">
<Button variant="outline" size="sm" asChild>
<Link href={model.isSystem ? "/documentation/models" : "/models/details"}>View Details</Link>
</Button>
<div className="flex gap-2">
<Button variant="outline" size="icon" className="h-8 w-8 text-primary border-primary/20 hover:border-primary">
<Settings className="h-4 w-4" />
</Button>
{!model.isSystem && (
<Button variant="outline" size="icon" className="h-8 w-8 border-primary/20 hover:border-primary">
<Trash2 className="h-4 w-4" />
</Button>
)}
<Button variant="outline" size="icon" className="h-8 w-8 border-primary/20 hover:border-primary">
<ArrowRight className="h-4 w-4" />
</Button>
</div>
</CardFooter>
</Card>
);
}

View File

@ -0,0 +1,704 @@
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Badge } from "@/components/ui/badge";
import { Progress } from "@/components/ui/progress";
import { Slider } from "@/components/ui/slider";
import {
ChevronRight,
Home,
BarChart2,
LineChart,
Droplets,
Wind,
Sun,
MapPin,
Bus,
School,
ShoppingBag,
Building,
ArrowRight,
Info,
FileText,
RefreshCw,
Check,
ChevronDown,
} from "lucide-react";
import Link from "next/link";
import PageHeader from "@/components/page-header";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
export default function PricePredictionPage() {
const [propertySize, setPropertySize] = useState(150);
const [propertyAge, setPropertyAge] = useState(5);
const [adjustedPrice, setAdjustedPrice] = useState(15000000);
const [activeTab, setActiveTab] = useState("property-details");
const [selectedModel, setSelectedModel] = useState("Standard ML Model v2.4");
const models = [
"Standard ML Model v2.4",
"Enhanced Neural Network v1.8",
"Geospatial Regression v3.1",
"Time Series Forecast v2.0",
"Custom Model (User #1242)",
];
const handleSizeChange = (value: number[]) => {
setPropertySize(value[0]);
// Simple calculation for demo purposes
const newPrice = 15000000 + (value[0] - 150) * 50000;
setAdjustedPrice(newPrice);
};
const handleAgeChange = (value: number[]) => {
setPropertyAge(value[0]);
// Simple calculation for demo purposes
const newPrice = 15000000 - (value[0] - 5) * 200000;
setAdjustedPrice(newPrice);
};
const handleGenerateReport = () => {
// In a real implementation, this would generate and download a PDF
alert("Generating PDF report with the selected model: " + selectedModel);
};
return (
<div className="container mx-auto p-6">
<div className="flex items-center text-sm text-muted-foreground mb-4">
<Link href="/maps" className="hover:text-foreground transition-colors">
Map
</Link>
<ChevronRight className="h-4 w-4 mx-1" />
<span className="font-medium text-foreground">Price Prediction Model</span>
</div>
<div className="flex justify-between items-center">
<PageHeader
title="Explainable Price Prediction Model"
description="Understand how our AI model predicts property prices and what factors influence the valuation."
/>
<div className="flex items-center gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="gap-2">
<BarChart2 className="h-4 w-4" />
{selectedModel}
<ChevronDown className="h-4 w-4 ml-2" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[240px]">
{models.map((model) => (
<DropdownMenuItem
key={model}
onClick={() => setSelectedModel(model)}
className="flex items-center gap-2">
{model === selectedModel && <Check className="h-4 w-4 text-primary" />}
<span className={model === selectedModel ? "font-medium" : ""}>{model}</span>
</DropdownMenuItem>
))}
<DropdownMenuItem>
<Link href="/models" className="flex items-center w-full">
<span className="text-primary">Manage Models...</span>
</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Button variant="outline" className="gap-2" onClick={() => setSelectedModel(selectedModel)}>
<RefreshCw className="h-4 w-4" />
Refresh
</Button>
</div>
</div>
<Tabs defaultValue="property-details" className="mt-6" onValueChange={setActiveTab}>
<TabsList className="grid grid-cols-5 mb-8">
<TabsTrigger value="property-details" className="flex items-center gap-2">
<Home className="h-4 w-4" />
<span>Property Details</span>
</TabsTrigger>
<TabsTrigger value="feature-analysis" className="flex items-center gap-2">
<BarChart2 className="h-4 w-4" />
<span>Feature Analysis</span>
</TabsTrigger>
<TabsTrigger value="market-comparison" className="flex items-center gap-2">
<LineChart className="h-4 w-4" />
<span>Market Comparison</span>
</TabsTrigger>
<TabsTrigger value="environmental-factors" className="flex items-center gap-2">
<Wind className="h-4 w-4" />
<span>Environmental Factors</span>
</TabsTrigger>
<TabsTrigger value="final-prediction" className="flex items-center gap-2">
<ArrowRight className="h-4 w-4" />
<span>Final Prediction</span>
</TabsTrigger>
</TabsList>
<div className="grid gap-6 md:grid-cols-3">
<div className="md:col-span-1">
<Card>
<CardHeader>
<CardTitle>Property Details</CardTitle>
<CardDescription>123 Sukhumvit Road, Bangkok</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex justify-between items-center">
<span className="text-muted-foreground">Type</span>
<span className="font-medium">Condominium</span>
</div>
<div className="flex justify-between items-center">
<span className="text-muted-foreground">Size</span>
<span className="font-medium">{propertySize} sqm</span>
</div>
<div className="flex justify-between items-center">
<span className="text-muted-foreground">Bedrooms</span>
<span className="font-medium">3</span>
</div>
<div className="flex justify-between items-center">
<span className="text-muted-foreground">Bathrooms</span>
<span className="font-medium">2</span>
</div>
<div className="flex justify-between items-center">
<span className="text-muted-foreground">Age</span>
<span className="font-medium">{propertyAge} years</span>
</div>
<div className="flex justify-between items-center">
<span className="text-muted-foreground">Floor</span>
<span className="font-medium">15</span>
</div>
</div>
</CardContent>
</Card>
<Card className="mt-6">
<CardHeader>
<CardTitle>Adjust Parameters</CardTitle>
<CardDescription>See how changes affect the prediction</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm font-medium">Property Size</span>
<span className="text-sm text-muted-foreground">{propertySize} sqm</span>
</div>
<Slider
defaultValue={[150]}
max={300}
min={50}
step={10}
onValueChange={handleSizeChange}
className="py-2"
/>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm font-medium">Property Age</span>
<span className="text-sm text-muted-foreground">{propertyAge} years</span>
</div>
<Slider
defaultValue={[5]}
max={20}
min={0}
step={1}
onValueChange={handleAgeChange}
className="py-2"
/>
</div>
<div className="pt-4 border-t">
<div className="flex justify-between items-center">
<span className="text-sm font-medium">Adjusted Price</span>
<span className="text-xl font-bold">฿{adjustedPrice.toLocaleString()}</span>
</div>
<p className="text-xs text-muted-foreground mt-1">
{adjustedPrice > 15000000 ? "+" : ""}
{adjustedPrice - 15000000 === 0 ? "±0" : (adjustedPrice - 15000000).toLocaleString()} THB from
original prediction
</p>
</div>
</div>
</CardContent>
</Card>
</div>
<div className="md:col-span-2">
<TabsContent value="property-details" className="mt-0">
<Card>
<CardHeader>
<CardTitle>Property Overview</CardTitle>
<CardDescription>Basic information used in our prediction model</CardDescription>
</CardHeader>
<CardContent>
<p className="mb-6">
Our AI model begins by analyzing the core attributes of your property. These fundamental
characteristics form the baseline for our prediction.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card>
<CardContent className="p-4">
<div className="flex items-start gap-3">
<Home className="h-5 w-5 text-primary mt-0.5" />
<div>
<h4 className="font-medium">Property Type</h4>
<p className="text-sm text-muted-foreground">
Condominium properties in this area have specific market dynamics
</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-start gap-3">
<BarChart2 className="h-5 w-5 text-primary mt-0.5" />
<div>
<h4 className="font-medium">Size & Layout</h4>
<p className="text-sm text-muted-foreground">150 sqm with 3 bedrooms and 2 bathrooms</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-start gap-3">
<Building className="h-5 w-5 text-primary mt-0.5" />
<div>
<h4 className="font-medium">Property Age</h4>
<p className="text-sm text-muted-foreground">
Built 5 years ago, affecting depreciation calculations
</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-start gap-3">
<MapPin className="h-5 w-5 text-primary mt-0.5" />
<div>
<h4 className="font-medium">Floor & View</h4>
<p className="text-sm text-muted-foreground">
Located on floor 15, impacting value and desirability
</p>
</div>
</div>
</CardContent>
</Card>
</div>
<div className="flex justify-end mt-6">
<Button onClick={() => setActiveTab("feature-analysis")}>
Next Step <ArrowRight className="ml-2 h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="feature-analysis" className="mt-0">
<Card>
<CardHeader>
<CardTitle>Feature Analysis</CardTitle>
<CardDescription>How different features impact the predicted price</CardDescription>
</CardHeader>
<CardContent>
<p className="mb-6">
Our model analyzes various features of your property and determines how each one contributes to the
final price prediction. Below is a breakdown of the most important factors.
</p>
<div className="space-y-4 mb-6">
<div className="flex items-center justify-between">
<span className="w-32 text-sm">Location</span>
<div className="flex-1 mx-4">
<Progress value={35} className="h-6 bg-muted" indicatorClassName="bg-emerald-500" />
</div>
<span className="w-12 text-right text-sm">+35%</span>
</div>
<div className="flex items-center justify-between">
<span className="w-32 text-sm">Size</span>
<div className="flex-1 mx-4">
<Progress value={25} className="h-6 bg-muted" indicatorClassName="bg-emerald-500" />
</div>
<span className="w-12 text-right text-sm">+25%</span>
</div>
<div className="flex items-center justify-between">
<span className="w-32 text-sm">Age</span>
<div className="flex-1 mx-4">
<Progress value={15} className="h-6 bg-muted" indicatorClassName="bg-red-500" />
</div>
<span className="w-12 text-right text-sm">-15%</span>
</div>
<div className="flex items-center justify-between">
<span className="w-32 text-sm">Amenities</span>
<div className="flex-1 mx-4">
<Progress value={10} className="h-6 bg-muted" indicatorClassName="bg-emerald-500" />
</div>
<span className="w-12 text-right text-sm">+10%</span>
</div>
<div className="flex items-center justify-between">
<span className="w-32 text-sm">Floor</span>
<div className="flex-1 mx-4">
<Progress value={8} className="h-6 bg-muted" indicatorClassName="bg-emerald-500" />
</div>
<span className="w-12 text-right text-sm">+8%</span>
</div>
<div className="flex items-center justify-between">
<span className="w-32 text-sm">Air Quality</span>
<div className="flex-1 mx-4">
<Progress value={7} className="h-6 bg-muted" indicatorClassName="bg-red-500" />
</div>
<span className="w-12 text-right text-sm">-7%</span>
</div>
</div>
<div className="bg-muted/30 p-4 rounded-lg border mb-6">
<div className="flex items-start gap-2">
<Info className="h-5 w-5 text-primary mt-0.5 flex-shrink-0" />
<div>
<h4 className="font-medium">Key Insights</h4>
<ul className="text-sm text-muted-foreground mt-1 space-y-1 list-disc list-inside">
<li>Location is the strongest factor, contributing +35% to the price</li>
<li>The property's size (150 sqm) positively impacts the valuation</li>
<li>The age of the property (5 years) has a moderate negative impact (-15%)</li>
<li>Poor air quality in the area reduces the value by 7%</li>
<li>High floor (15) adds a premium of 8% to the property value</li>
</ul>
</div>
</div>
</div>
<div className="flex justify-end">
<Button onClick={() => setActiveTab("market-comparison")}>
Next Step <ArrowRight className="ml-2 h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="market-comparison" className="mt-0">
<Card>
<CardHeader>
<CardTitle>Market Comparison</CardTitle>
<CardDescription>How your property compares to similar properties in the area</CardDescription>
</CardHeader>
<CardContent>
<p className="mb-6">
Our model analyzes comparable properties in the same area to provide context for your property's
valuation. This helps ensure the prediction is aligned with current market conditions.
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full border-collapse">
<thead>
<tr className="bg-muted/50">
<th className="px-4 py-2 text-left text-sm font-medium">Property</th>
<th className="px-4 py-2 text-left text-sm font-medium">Size</th>
<th className="px-4 py-2 text-left text-sm font-medium">Bedrooms</th>
<th className="px-4 py-2 text-left text-sm font-medium">Age</th>
<th className="px-4 py-2 text-left text-sm font-medium">Floor</th>
<th className="px-4 py-2 text-left text-sm font-medium">Price</th>
<th className="px-4 py-2 text-left text-sm font-medium">Price/sqm</th>
</tr>
</thead>
<tbody className="divide-y">
<tr className="bg-primary/5 font-medium">
<td className="px-4 py-2 text-sm">Your Property</td>
<td className="px-4 py-2 text-sm">150 sqm</td>
<td className="px-4 py-2 text-sm">3</td>
<td className="px-4 py-2 text-sm">5 years</td>
<td className="px-4 py-2 text-sm">15</td>
<td className="px-4 py-2 text-sm">฿15,000,000</td>
<td className="px-4 py-2 text-sm">฿100,000</td>
</tr>
<tr>
<td className="px-4 py-2 text-sm">Comp #1</td>
<td className="px-4 py-2 text-sm">145 sqm</td>
<td className="px-4 py-2 text-sm">3</td>
<td className="px-4 py-2 text-sm">4 years</td>
<td className="px-4 py-2 text-sm">12</td>
<td className="px-4 py-2 text-sm">฿14,500,000</td>
<td className="px-4 py-2 text-sm">฿100,000</td>
</tr>
<tr>
<td className="px-4 py-2 text-sm">Comp #2</td>
<td className="px-4 py-2 text-sm">160 sqm</td>
<td className="px-4 py-2 text-sm">3</td>
<td className="px-4 py-2 text-sm">6 years</td>
<td className="px-4 py-2 text-sm">18</td>
<td className="px-4 py-2 text-sm">฿15,800,000</td>
<td className="px-4 py-2 text-sm">฿98,750</td>
</tr>
<tr>
<td className="px-4 py-2 text-sm">Comp #3</td>
<td className="px-4 py-2 text-sm">140 sqm</td>
<td className="px-4 py-2 text-sm">2</td>
<td className="px-4 py-2 text-sm">3 years</td>
<td className="px-4 py-2 text-sm">10</td>
<td className="px-4 py-2 text-sm">฿13,800,000</td>
<td className="px-4 py-2 text-sm">฿98,571</td>
</tr>
<tr>
<td className="px-4 py-2 text-sm">Comp #4</td>
<td className="px-4 py-2 text-sm">155 sqm</td>
<td className="px-4 py-2 text-sm">3</td>
<td className="px-4 py-2 text-sm">7 years</td>
<td className="px-4 py-2 text-sm">14</td>
<td className="px-4 py-2 text-sm">฿14,700,000</td>
<td className="px-4 py-2 text-sm">฿94,839</td>
</tr>
</tbody>
</table>
</div>
<div className="bg-muted/30 p-4 rounded-lg border mb-6">
<div className="flex items-start gap-2">
<Info className="h-5 w-5 text-primary mt-0.5 flex-shrink-0" />
<div>
<h4 className="font-medium">Market Analysis</h4>
<ul className="text-sm text-muted-foreground mt-1 space-y-1 list-disc list-inside">
<li>Your property's predicted price is in line with comparable properties</li>
<li>The price per square meter (฿100,000) is slightly above the area average</li>
<li>Properties on higher floors command a 3-5% premium in this building</li>
<li>Recent sales in this area show a stable market with slight appreciation</li>
</ul>
</div>
</div>
</div>
<div className="flex justify-end">
<Button onClick={() => setActiveTab("environmental-factors")}>
Next Step <ArrowRight className="ml-2 h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="environmental-factors" className="mt-0">
<Card>
<CardHeader>
<CardTitle>Environmental Factors</CardTitle>
<CardDescription>How environmental conditions affect the property value</CardDescription>
</CardHeader>
<CardContent>
<p className="mb-6">
Environmental factors can significantly impact property values. Our model considers various
environmental conditions to provide a more accurate prediction.
</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<Card>
<CardContent className="p-4">
<div className="flex flex-col items-center text-center">
<Droplets className="h-8 w-8 text-blue-500 mb-2" />
<h4 className="font-medium">Flood Risk</h4>
<Badge className="mt-2 bg-amber-500">Moderate</Badge>
<p className="text-xs text-muted-foreground mt-2">
Historical data shows moderate flood risk in this area
</p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex flex-col items-center text-center">
<Wind className="h-8 w-8 text-purple-500 mb-2" />
<h4 className="font-medium">Air Quality</h4>
<Badge className="mt-2 bg-destructive">Poor</Badge>
<p className="text-xs text-muted-foreground mt-2">
Air quality is below average, affecting property value
</p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex flex-col items-center text-center">
<Sun className="h-8 w-8 text-amber-500 mb-2" />
<h4 className="font-medium">Noise Level</h4>
<Badge className="mt-2 bg-green-500">Low</Badge>
<p className="text-xs text-muted-foreground mt-2">
The area has relatively low noise pollution
</p>
</div>
</CardContent>
</Card>
</div>
<h4 className="font-medium text-lg mb-3">Proximity to Amenities</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<Card>
<CardContent className="p-4 flex items-center gap-3">
<Bus className="h-5 w-5 text-primary" />
<div>
<h4 className="font-medium">Public Transport</h4>
<p className="text-sm text-muted-foreground">300m</p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 flex items-center gap-3">
<School className="h-5 w-5 text-primary" />
<div>
<h4 className="font-medium">Schools</h4>
<p className="text-sm text-muted-foreground">1.2km</p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 flex items-center gap-3">
<ShoppingBag className="h-5 w-5 text-primary" />
<div>
<h4 className="font-medium">Shopping</h4>
<p className="text-sm text-muted-foreground">500m</p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 flex items-center gap-3">
<Building className="h-5 w-5 text-primary" />
<div>
<h4 className="font-medium">Hospitals</h4>
<p className="text-sm text-muted-foreground">2.5km</p>
</div>
</CardContent>
</Card>
</div>
<div className="bg-muted/30 p-4 rounded-lg border mb-6">
<div className="flex items-start gap-2">
<Info className="h-5 w-5 text-primary mt-0.5 flex-shrink-0" />
<div>
<h4 className="font-medium">Environmental Impact Analysis</h4>
<ul className="text-sm text-muted-foreground mt-1 space-y-1 list-disc list-inside">
<li>The moderate flood risk reduces the property value by approximately 3%</li>
<li>Poor air quality has a negative impact of about 7% on the valuation</li>
<li>Excellent proximity to public transport adds a 4% premium</li>
<li>Overall, environmental factors have a -6% net impact on the property value</li>
</ul>
</div>
</div>
</div>
<div className="flex justify-end">
<Button onClick={() => setActiveTab("final-prediction")}>
Next Step <ArrowRight className="ml-2 h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="final-prediction" className="mt-0">
<Card>
<CardHeader>
<CardTitle>Final Prediction</CardTitle>
<CardDescription>The predicted price and confidence level</CardDescription>
</CardHeader>
<CardContent>
<div className="bg-muted/30 p-6 rounded-lg text-center mb-6">
<h3 className="text-lg text-muted-foreground mb-1">Predicted Price</h3>
<div className="text-4xl font-bold mb-2">฿15,000,000</div>
<p className="text-sm text-muted-foreground">Confidence Level: 92%</p>
</div>
<Card className="mb-6">
<CardHeader className="pb-2">
<div className="flex items-center gap-2">
<Info className="h-5 w-5 text-primary" />
<CardTitle className="text-base">Price Range</CardTitle>
</div>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground mb-4">
Based on our model's confidence level, the price could range between:
</p>
<div className="grid grid-cols-3 gap-4 text-center">
<div>
<h4 className="text-sm font-medium text-muted-foreground">Lower Bound</h4>
<p className="font-medium">฿14,250,000</p>
</div>
<div>
<h4 className="text-sm font-medium text-muted-foreground">Prediction</h4>
<p className="font-bold text-primary">฿15,000,000</p>
</div>
<div>
<h4 className="text-sm font-medium text-muted-foreground">Upper Bound</h4>
<p className="font-medium">฿15,750,000</p>
</div>
</div>
</CardContent>
</Card>
<h3 className="text-lg font-medium mb-3">Summary of Factors</h3>
<p className="text-sm text-muted-foreground mb-4">
The final prediction is based on a combination of all factors analyzed in previous steps:
</p>
<ul className="space-y-2 mb-6">
<li className="flex items-start gap-2">
<div className="h-5 w-5 rounded-full bg-blue-500 flex-shrink-0 mt-0.5"></div>
<span>Property characteristics (size, age, layout)</span>
</li>
<li className="flex items-start gap-2">
<div className="h-5 w-5 rounded-full bg-green-500 flex-shrink-0 mt-0.5"></div>
<span>Location and neighborhood analysis</span>
</li>
<li className="flex items-start gap-2">
<div className="h-5 w-5 rounded-full bg-amber-500 flex-shrink-0 mt-0.5"></div>
<span>Market trends and comparable properties</span>
</li>
<li className="flex items-start gap-2">
<div className="h-5 w-5 rounded-full bg-red-500 flex-shrink-0 mt-0.5"></div>
<span>Environmental factors and amenities</span>
</li>
</ul>
<div className="flex flex-col md:flex-row gap-4 justify-between">
<Button variant="outline" onClick={() => setActiveTab("environmental-factors")}>
Previous
</Button>
<div className="flex gap-2">
<Button variant="outline" className="gap-2" onClick={handleGenerateReport}>
<FileText className="h-4 w-4" />
Generate PDF Report
</Button>
<Button variant="default" asChild>
<Link href="/maps">Back to Map</Link>
</Button>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
</div>
</div>
</Tabs>
</div>
);
}

View File

@ -0,0 +1,609 @@
"use client"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import {
MapPin,
Building,
Bath,
BedDouble,
Share,
ChevronRight,
Info,
Ruler,
Clock,
Star,
Droplets,
Wind,
Sun,
BarChart2,
LineChart,
Calendar,
Download,
FileText,
} from "lucide-react"
import Link from "next/link"
export default function PropertyDetailPage({ params }: { params: { id: string } }) {
const [liked, setLiked] = useState(false)
// This would normally come from an API call using the ID
const property = {
id: params.id,
title: "Modern Condominium in Sukhumvit",
price: 15000000,
location: "Sukhumvit Soi 24, Bangkok",
bedrooms: 3,
bathrooms: 2,
area: 150,
type: "Condominium",
description:
"This stunning modern condominium is located in the heart of Sukhumvit, one of Bangkok's most vibrant neighborhoods. The property features 3 spacious bedrooms, 2 bathrooms, and a large living area with floor-to-ceiling windows offering panoramic city views. The unit comes fully furnished with high-end appliances and fixtures. The building amenities include a swimming pool, fitness center, sauna, and 24-hour security.",
features: [
"Floor-to-ceiling windows",
"Fully furnished",
"High-end appliances",
"Marble countertops",
"Hardwood floors",
"Central air conditioning",
"Walk-in closet",
"Balcony with city view",
],
amenities: [
"Swimming pool",
"Fitness center",
"Sauna",
"24-hour security",
"Parking",
"Garden",
"Playground",
"BBQ area",
],
images: [
"/placeholder.svg?height=500&width=800",
"/placeholder.svg?height=500&width=800",
"/placeholder.svg?height=500&width=800",
"/placeholder.svg?height=500&width=800",
],
yearBuilt: 2018,
floorLevel: 15,
totalFloors: 32,
parkingSpaces: 1,
furnished: "Fully Furnished",
ownership: "Freehold",
availableFrom: "Immediate",
premium: true,
priceHistory: [
{ date: "2018", price: 12000000 },
{ date: "2020", price: 13500000 },
{ date: "2022", price: 14800000 },
{ date: "2024", price: 15000000 },
],
marketTrends: {
areaGrowth: 5.2,
similarProperties: 8,
averagePrice: 14500000,
pricePerSqm: 100000,
},
environmentalFactors: {
floodRisk: "Moderate",
airQuality: "Poor",
noiseLevel: "Low",
},
nearbyFacilities: [
{ name: "BTS Phrom Phong Station", distance: 300 },
{ name: "EmQuartier Shopping Mall", distance: 500 },
{ name: "Benchasiri Park", distance: 700 },
{ name: "Samitivej Hospital", distance: 1200 },
],
}
return (
<div className="container mx-auto p-6">
<div className="flex items-center text-sm text-muted-foreground mb-4">
<Link href="/" className="hover:text-foreground transition-colors">
Home
</Link>
<ChevronRight className="h-4 w-4 mx-1" />
<Link href="/properties" className="hover:text-foreground transition-colors">
Properties
</Link>
<ChevronRight className="h-4 w-4 mx-1" />
<span className="font-medium text-foreground">{property.title}</span>
</div>
<div className="flex flex-col lg:flex-row gap-6">
<div className="lg:w-2/3">
{/* Property Images */}
<div className="relative mb-6 rounded-lg overflow-hidden">
<img
src={property.images[0] || "/placeholder.svg"}
alt={property.title}
className="w-full h-[400px] object-cover"
/>
<div className="absolute bottom-4 right-4 flex gap-2">
<Button variant="secondary" size="sm" className="h-8 gap-1">
<Share className="h-4 w-4" />
<span>Share</span>
</Button>
<Button variant="secondary" size="sm" className="h-8 gap-1">
<Download className="h-4 w-4" />
<span>Export Data</span>
</Button>
</div>
<div className="absolute top-4 left-4 flex gap-1">
<Badge className="bg-primary">{property.type}</Badge>
{property.premium && (
<Badge className="bg-amber-500">
<Star className="h-3 w-3 mr-1" /> Premium
</Badge>
)}
</div>
</div>
{/* Thumbnail Images */}
<div className="grid grid-cols-4 gap-2 mb-6">
{property.images.map((image, index) => (
<div key={index} className="rounded-md overflow-hidden">
<img
src={image || "/placeholder.svg"}
alt={`${property.title} - Image ${index + 1}`}
className="w-full h-24 object-cover cursor-pointer hover:opacity-80 transition-opacity"
/>
</div>
))}
</div>
{/* Property Details */}
<div className="mb-6">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-2 mb-2">
<h1 className="text-2xl font-semibold">{property.title}</h1>
<div className="text-2xl font-bold">฿{property.price.toLocaleString()}</div>
</div>
<div className="flex items-center text-muted-foreground text-sm mb-4">
<MapPin className="h-4 w-4 mr-1 flex-shrink-0" />
<span>{property.location}</span>
</div>
<div className="flex flex-wrap items-center gap-6 mb-6">
<div className="flex items-center">
<BedDouble className="h-5 w-5 mr-2 text-primary" />
<div>
<div className="font-medium">{property.bedrooms}</div>
<div className="text-xs text-muted-foreground">Bedrooms</div>
</div>
</div>
<div className="flex items-center">
<Bath className="h-5 w-5 mr-2 text-primary" />
<div>
<div className="font-medium">{property.bathrooms}</div>
<div className="text-xs text-muted-foreground">Bathrooms</div>
</div>
</div>
<div className="flex items-center">
<Ruler className="h-5 w-5 mr-2 text-primary" />
<div>
<div className="font-medium">{property.area} m²</div>
<div className="text-xs text-muted-foreground">Area</div>
</div>
</div>
<div className="flex items-center">
<Building className="h-5 w-5 mr-2 text-primary" />
<div>
<div className="font-medium">Floor {property.floorLevel}</div>
<div className="text-xs text-muted-foreground">of {property.totalFloors}</div>
</div>
</div>
<div className="flex items-center">
<Calendar className="h-5 w-5 mr-2 text-primary" />
<div>
<div className="font-medium">{property.yearBuilt}</div>
<div className="text-xs text-muted-foreground">Year Built</div>
</div>
</div>
</div>
<Tabs defaultValue="analytics">
<TabsList className="grid grid-cols-4 mb-4">
<TabsTrigger value="description">Description</TabsTrigger>
<TabsTrigger value="features">Features</TabsTrigger>
<TabsTrigger value="location">Location</TabsTrigger>
<TabsTrigger value="analytics">Analytics</TabsTrigger>
</TabsList>
<TabsContent value="description" className="mt-0">
<p className="text-sm leading-relaxed mb-4">{property.description}</p>
<div className="grid grid-cols-2 gap-x-8 gap-y-2 mt-6">
<div className="flex justify-between py-2 border-b text-sm">
<span className="text-muted-foreground">Property Type</span>
<span className="font-medium">{property.type}</span>
</div>
<div className="flex justify-between py-2 border-b text-sm">
<span className="text-muted-foreground">Furnishing</span>
<span className="font-medium">{property.furnished}</span>
</div>
<div className="flex justify-between py-2 border-b text-sm">
<span className="text-muted-foreground">Ownership</span>
<span className="font-medium">{property.ownership}</span>
</div>
<div className="flex justify-between py-2 border-b text-sm">
<span className="text-muted-foreground">Available From</span>
<span className="font-medium">{property.availableFrom}</span>
</div>
<div className="flex justify-between py-2 border-b text-sm">
<span className="text-muted-foreground">Parking Spaces</span>
<span className="font-medium">{property.parkingSpaces}</span>
</div>
</div>
</TabsContent>
<TabsContent value="features" className="mt-0">
<div className="mb-4">
<h3 className="font-medium mb-2">Property Features</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{property.features.map((feature, index) => (
<div key={index} className="flex items-center text-sm">
<div className="h-1.5 w-1.5 rounded-full bg-primary mr-2"></div>
{feature}
</div>
))}
</div>
</div>
<div>
<h3 className="font-medium mb-2">Building Amenities</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{property.amenities.map((amenity, index) => (
<div key={index} className="flex items-center text-sm">
<div className="h-1.5 w-1.5 rounded-full bg-primary mr-2"></div>
{amenity}
</div>
))}
</div>
</div>
</TabsContent>
<TabsContent value="location" className="mt-0">
<div className="bg-muted h-[300px] rounded-lg flex items-center justify-center mb-4">
<Link href={`/maps?property=${property.id}`}>
<Button>View on Map</Button>
</Link>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h3 className="font-medium mb-2">Nearby Facilities</h3>
<div className="space-y-2">
{property.nearbyFacilities.map((facility, index) => (
<div key={index} className="flex items-center justify-between text-sm">
<span>{facility.name}</span>
<span className="text-muted-foreground">{facility.distance}m</span>
</div>
))}
</div>
</div>
<div>
<h3 className="font-medium mb-2">Environmental Factors</h3>
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center">
<Droplets className="h-4 w-4 text-blue-500 mr-2" />
<span className="text-sm">Flood Risk</span>
</div>
<Badge className="bg-amber-500">{property.environmentalFactors.floodRisk}</Badge>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center">
<Wind className="h-4 w-4 text-purple-500 mr-2" />
<span className="text-sm">Air Quality</span>
</div>
<Badge className="bg-destructive">{property.environmentalFactors.airQuality}</Badge>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center">
<Sun className="h-4 w-4 text-amber-500 mr-2" />
<span className="text-sm">Noise Level</span>
</div>
<Badge className="bg-green-500">{property.environmentalFactors.noiseLevel}</Badge>
</div>
</div>
</div>
</div>
</TabsContent>
<TabsContent value="analytics" className="mt-0">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-base flex items-center">
<BarChart2 className="h-4 w-4 mr-2 text-primary" />
Price Prediction
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold mb-1">฿15,000,000</div>
<p className="text-sm text-muted-foreground mb-3">The estimated price based on various factors</p>
<Link href="/price-prediction">
<Button size="sm" className="gap-1">
<Info className="h-4 w-4" />
<span>View Detailed Analysis</span>
</Button>
</Link>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-base flex items-center">
<LineChart className="h-4 w-4 mr-2 text-primary" />
Market Trends
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between mb-1">
<span className="text-sm">Area Price Trend</span>
<Badge className="bg-green-500">Rising</Badge>
</div>
<p className="text-sm text-muted-foreground mb-3">
Prices in this area have increased by {property.marketTrends.areaGrowth}% in the last year
</p>
<div className="flex items-center justify-between text-sm mb-1">
<span>Average Price in Area</span>
<span className="font-medium">฿{property.marketTrends.averagePrice.toLocaleString()}</span>
</div>
<div className="flex items-center justify-between text-sm">
<span>Price per sqm</span>
<span className="font-medium">฿{property.marketTrends.pricePerSqm.toLocaleString()}</span>
</div>
</CardContent>
</Card>
</div>
<Card className="mb-6">
<CardHeader className="pb-2">
<CardTitle className="text-base flex items-center">
<LineChart className="h-4 w-4 mr-2 text-primary" />
Price History
</CardTitle>
</CardHeader>
<CardContent>
<div className="h-40 w-full relative mb-4">
{/* Simple line chart simulation */}
<div className="absolute bottom-0 left-0 w-full h-px bg-border"></div>
<div className="absolute bottom-0 left-0 h-full flex items-end w-full">
{property.priceHistory.map((item, index) => (
<div key={index} className="flex-1 flex flex-col items-center">
<div
className="w-full border-b-2 border-r-2 border-primary rounded-br"
style={{
height: `${(item.price / 15000000) * 100}%`,
maxHeight: "90%",
}}
></div>
<div className="text-xs mt-1">{item.date}</div>
<div className="text-xs text-muted-foreground">฿{(item.price / 1000000).toFixed(1)}M</div>
</div>
))}
</div>
</div>
<div className="flex items-center text-xs text-muted-foreground">
<Clock className="h-3 w-3 mr-1" />
<span>Last updated: 2 days ago</span>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-base flex items-center">
<FileText className="h-4 w-4 mr-2 text-primary" />
Data Reports
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<Button variant="outline" size="sm" className="justify-start">
<Download className="h-4 w-4 mr-2" />
Property Analysis Report
</Button>
<Button variant="outline" size="sm" className="justify-start">
<Download className="h-4 w-4 mr-2" />
Market Comparison Data
</Button>
<Button variant="outline" size="sm" className="justify-start">
<Download className="h-4 w-4 mr-2" />
Environmental Assessment
</Button>
<Button variant="outline" size="sm" className="justify-start">
<Download className="h-4 w-4 mr-2" />
Historical Price Data
</Button>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
</div>
<div className="lg:w-1/3">
{/* Analytics Summary Card */}
<Card className="mb-6 sticky top-6">
<CardHeader>
<CardTitle className="text-lg">Analytics Summary</CardTitle>
<CardDescription>Key insights about this property</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div>
<h3 className="text-sm font-medium mb-2">Price Analysis</h3>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm">Current Price</span>
<span className="font-medium">฿{property.price.toLocaleString()}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm">Price per sqm</span>
<span className="font-medium">฿{property.marketTrends.pricePerSqm.toLocaleString()}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm">Area Average</span>
<span className="font-medium">฿{property.marketTrends.averagePrice.toLocaleString()}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm">Price Trend</span>
<Badge className="bg-green-500">+{property.marketTrends.areaGrowth}%</Badge>
</div>
</div>
</div>
<div>
<h3 className="text-sm font-medium mb-2">Environmental Assessment</h3>
<div className="grid grid-cols-3 gap-2">
<div className="flex flex-col items-center p-2 border rounded-md">
<Droplets className="h-5 w-5 text-blue-500 mb-1" />
<span className="text-xs font-medium">Flood Risk</span>
<Badge className="mt-1 text-xs bg-amber-500">{property.environmentalFactors.floodRisk}</Badge>
</div>
<div className="flex flex-col items-center p-2 border rounded-md">
<Wind className="h-5 w-5 text-purple-500 mb-1" />
<span className="text-xs font-medium">Air Quality</span>
<Badge className="mt-1 text-xs bg-destructive">{property.environmentalFactors.airQuality}</Badge>
</div>
<div className="flex flex-col items-center p-2 border rounded-md">
<Sun className="h-5 w-5 text-amber-500 mb-1" />
<span className="text-xs font-medium">Noise</span>
<Badge className="mt-1 text-xs bg-green-500">{property.environmentalFactors.noiseLevel}</Badge>
</div>
</div>
</div>
<div>
<h3 className="text-sm font-medium mb-2">Nearby Facilities</h3>
<div className="space-y-1 text-sm">
{property.nearbyFacilities.map((facility, index) => (
<div key={index} className="flex justify-between">
<span>{facility.name}</span>
<span className="text-muted-foreground">{facility.distance}m</span>
</div>
))}
</div>
</div>
<div className="pt-2">
<Link href="/price-prediction">
<Button className="w-full gap-2">
<BarChart2 className="h-4 w-4" />
View Full Price Analysis
</Button>
</Link>
</div>
<div>
<Link href={`/maps?property=${property.id}`}>
<Button variant="outline" className="w-full gap-2">
<MapPin className="h-4 w-4" />
View on Map
</Button>
</Link>
</div>
</CardContent>
</Card>
{/* Similar Properties */}
<div>
<h3 className="font-medium mb-3">Similar Properties</h3>
<div className="space-y-3">
<SimilarPropertyCard
id="sim1"
title="Luxury Condo in Asoke"
price={13500000}
location="Asoke, Bangkok"
bedrooms={2}
bathrooms={2}
area={120}
image="/placeholder.svg?height=80&width=120"
/>
<SimilarPropertyCard
id="sim2"
title="Modern Apartment in Thonglor"
price={16800000}
location="Thonglor, Bangkok"
bedrooms={3}
bathrooms={2}
area={160}
image="/placeholder.svg?height=80&width=120"
/>
<SimilarPropertyCard
id="sim3"
title="Spacious Condo with City View"
price={14200000}
location="Phrom Phong, Bangkok"
bedrooms={2}
bathrooms={2}
area={135}
image="/placeholder.svg?height=80&width=120"
/>
</div>
</div>
</div>
</div>
</div>
)
}
interface SimilarPropertyCardProps {
id: string
title: string
price: number
location: string
bedrooms: number
bathrooms: number
area: number
image: string
}
function SimilarPropertyCard({
id,
title,
price,
location,
bedrooms,
bathrooms,
area,
image,
}: SimilarPropertyCardProps) {
return (
<Link href={`/properties/${id}`}>
<div className="property-card">
<div className="flex">
<div className="w-24 h-20 flex-shrink-0">
<img src={image || "/placeholder.svg"} alt={title} className="w-full h-full object-cover rounded-l-lg" />
</div>
<div className="p-2 flex-1">
<h4 className="font-medium text-sm line-clamp-1">{title}</h4>
<div className="text-xs text-muted-foreground mb-1">{location}</div>
<div className="flex items-center gap-2 text-xs">
<span>{bedrooms} bd</span>
<span></span>
<span>{bathrooms} ba</span>
<span></span>
<span>{area} m²</span>
</div>
<div className="font-medium text-sm mt-1">฿{price.toLocaleString()}</div>
</div>
</div>
</div>
</Link>
)
}

View File

@ -0,0 +1,4 @@
export default function Loading() {
return null
}

View File

@ -0,0 +1,457 @@
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Slider } from "@/components/ui/slider"
import { MapPin, Home, Bath, BedDouble, ArrowRight, Search, Star } from "lucide-react"
import Link from "next/link"
import PageHeader from "@/components/page-header"
export default function PropertiesPage() {
return (
<div className="container mx-auto p-6">
<PageHeader
title="Property Listings"
description="Browse and filter available properties"
breadcrumb={[
{ title: "Home", href: "/" },
{ title: "Properties", href: "/properties" },
]}
/>
<div className="flex flex-col lg:flex-row gap-6 mt-6">
{/* Filters Sidebar */}
<div className="w-full lg:w-72 flex-shrink-0">
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between mb-4">
<h3 className="font-medium">Filters</h3>
<Button variant="ghost" size="sm" className="h-8 px-2 text-xs">
Reset All
</Button>
</div>
<div className="space-y-4">
<div>
<label className="text-sm font-medium mb-1.5 block">Price Range</label>
<div className="flex items-center justify-between text-xs text-muted-foreground mb-2">
<span>฿5,000,000</span>
<span>฿20,000,000</span>
</div>
<Slider
defaultValue={[5000000, 20000000]}
max={50000000}
min={1000000}
step={1000000}
className="mb-6"
/>
</div>
<div>
<label className="text-sm font-medium mb-1.5 block">Property Type</label>
<Select defaultValue="any">
<SelectTrigger>
<SelectValue placeholder="Any Type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="any">Any Type</SelectItem>
<SelectItem value="house">House</SelectItem>
<SelectItem value="condo">Condominium</SelectItem>
<SelectItem value="townhouse">Townhouse</SelectItem>
<SelectItem value="land">Land</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<label className="text-sm font-medium mb-1.5 block">Bedrooms</label>
<div className="grid grid-cols-5 gap-1">
{["Any", "1", "2", "3", "4+"].map((num) => (
<Button
key={num}
variant={num === "Any" ? "default" : "outline"}
size="sm"
className="text-xs h-8"
>
{num}
</Button>
))}
</div>
</div>
<div>
<label className="text-sm font-medium mb-1.5 block">Bathrooms</label>
<div className="grid grid-cols-5 gap-1">
{["Any", "1", "2", "3", "4+"].map((num) => (
<Button
key={num}
variant={num === "Any" ? "default" : "outline"}
size="sm"
className="text-xs h-8"
>
{num}
</Button>
))}
</div>
</div>
<div>
<label className="text-sm font-medium mb-1.5 block">Area (sqm)</label>
<div className="grid grid-cols-2 gap-2">
<Input placeholder="Min" className="h-9" />
<Input placeholder="Max" className="h-9" />
</div>
</div>
<div>
<label className="text-sm font-medium mb-1.5 block">Location</label>
<Select defaultValue="bangkok">
<SelectTrigger>
<SelectValue placeholder="Select Location" />
</SelectTrigger>
<SelectContent>
<SelectItem value="bangkok">Bangkok</SelectItem>
<SelectItem value="phuket">Phuket</SelectItem>
<SelectItem value="chiangmai">Chiang Mai</SelectItem>
<SelectItem value="pattaya">Pattaya</SelectItem>
</SelectContent>
</Select>
</div>
<div className="pt-2">
<Button className="w-full">Apply Filters</Button>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Main Content */}
<div className="flex-1">
{/* Search and Sort Bar */}
<div className="flex flex-col sm:flex-row gap-3 mb-4">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input placeholder="Search properties..." className="pl-9" />
</div>
<div className="flex gap-2">
<Select defaultValue="recommended">
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Sort by" />
</SelectTrigger>
<SelectContent>
<SelectItem value="recommended">Recommended</SelectItem>
<SelectItem value="price-asc">Price: Low to High</SelectItem>
<SelectItem value="price-desc">Price: High to Low</SelectItem>
<SelectItem value="newest">Newest First</SelectItem>
</SelectContent>
</Select>
<Link href="/maps">
<Button variant="outline" className="gap-2">
<MapPin className="h-4 w-4" />
<span>Map View</span>
</Button>
</Link>
</div>
</div>
{/* View Tabs */}
<Tabs defaultValue="grid" className="mb-6">
<div className="flex items-center justify-between">
<div className="text-sm text-muted-foreground">
Showing <span className="font-medium text-foreground">156</span> properties
</div>
<TabsList>
<TabsTrigger value="grid" className="text-xs px-3">
Grid
</TabsTrigger>
<TabsTrigger value="list" className="text-xs px-3">
List
</TabsTrigger>
</TabsList>
</div>
<TabsContent value="grid" className="mt-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{/* Property Cards */}
<PropertyCard
id="prop1"
title="Modern Condominium"
price={15000000}
location="Sukhumvit, Bangkok"
bedrooms={3}
bathrooms={2}
area={150}
type="Condominium"
image="/placeholder.svg?height=200&width=300"
premium={true}
/>
<PropertyCard
id="prop2"
title="Luxury Villa with Pool"
price={25000000}
location="Phuket Beach, Phuket"
bedrooms={4}
bathrooms={3}
area={320}
type="House"
image="/placeholder.svg?height=200&width=300"
/>
<PropertyCard
id="prop3"
title="City Center Apartment"
price={8500000}
location="Silom, Bangkok"
bedrooms={2}
bathrooms={1}
area={85}
type="Condominium"
image="/placeholder.svg?height=200&width=300"
/>
<PropertyCard
id="prop4"
title="Riverside Townhouse"
price={12000000}
location="Chao Phraya, Bangkok"
bedrooms={3}
bathrooms={3}
area={180}
type="Townhouse"
image="/placeholder.svg?height=200&width=300"
/>
<PropertyCard
id="prop5"
title="Mountain View Villa"
price={18000000}
location="Doi Suthep, Chiang Mai"
bedrooms={3}
bathrooms={2}
area={250}
type="House"
image="/placeholder.svg?height=200&width=300"
/>
<PropertyCard
id="prop6"
title="Beachfront Condo"
price={9800000}
location="Jomtien, Pattaya"
bedrooms={1}
bathrooms={1}
area={65}
type="Condominium"
image="/placeholder.svg?height=200&width=300"
premium={true}
/>
</div>
<div className="mt-8 flex justify-center">
<Button variant="outline" className="gap-2">
Load More <ArrowRight className="h-4 w-4" />
</Button>
</div>
</TabsContent>
<TabsContent value="list" className="mt-4">
<div className="space-y-4">
{/* List View Property Cards */}
<PropertyCardList
id="prop1"
title="Modern Condominium"
price={15000000}
location="Sukhumvit, Bangkok"
bedrooms={3}
bathrooms={2}
area={150}
type="Condominium"
image="/placeholder.svg?height=120&width=180"
premium={true}
/>
<PropertyCardList
id="prop2"
title="Luxury Villa with Pool"
price={25000000}
location="Phuket Beach, Phuket"
bedrooms={4}
bathrooms={3}
area={320}
type="House"
image="/placeholder.svg?height=120&width=180"
/>
<PropertyCardList
id="prop3"
title="City Center Apartment"
price={8500000}
location="Silom, Bangkok"
bedrooms={2}
bathrooms={1}
area={85}
type="Condominium"
image="/placeholder.svg?height=120&width=180"
/>
<PropertyCardList
id="prop4"
title="Riverside Townhouse"
price={12000000}
location="Chao Phraya, Bangkok"
bedrooms={3}
bathrooms={3}
area={180}
type="Townhouse"
image="/placeholder.svg?height=120&width=180"
/>
</div>
<div className="mt-8 flex justify-center">
<Button variant="outline" className="gap-2">
Load More <ArrowRight className="h-4 w-4" />
</Button>
</div>
</TabsContent>
</Tabs>
</div>
</div>
</div>
)
}
interface PropertyCardProps {
id: string
title: string
price: number
location: string
bedrooms: number
bathrooms: number
area: number
type: string
image: string
premium?: boolean
}
function PropertyCard({
id,
title,
price,
location,
bedrooms,
bathrooms,
area,
type,
image,
premium,
}: PropertyCardProps) {
return (
<Link href={`/properties/${id}`}>
<div className={`property-card ${premium ? "property-card-premium" : ""} h-full flex flex-col`}>
<div className="relative">
<img src={image || "/placeholder.svg"} alt={title} className="w-full h-48 object-cover" />
<div className="absolute top-2 left-2 flex gap-1">
<Badge className="bg-primary">{type}</Badge>
{premium && (
<Badge className="bg-amber-500">
<Star className="h-3 w-3 mr-1" /> Premium
</Badge>
)}
</div>
</div>
<div className="p-4 flex-1 flex flex-col">
<h3 className="font-medium text-lg mb-1">{title}</h3>
<div className="flex items-center text-muted-foreground text-sm mb-2">
<MapPin className="h-3.5 w-3.5 mr-1 flex-shrink-0" />
<span className="truncate">{location}</span>
</div>
<div className="flex items-center gap-3 text-sm mb-3">
<div className="flex items-center">
<BedDouble className="h-4 w-4 mr-1 text-primary" />
<span>{bedrooms}</span>
</div>
<div className="flex items-center">
<Bath className="h-4 w-4 mr-1 text-primary" />
<span>{bathrooms}</span>
</div>
<div className="flex items-center">
<Home className="h-4 w-4 mr-1 text-primary" />
<span>{area} m²</span>
</div>
</div>
<div className="mt-auto pt-2 border-t flex items-center justify-between">
<div className="font-semibold text-lg">฿{price.toLocaleString()}</div>
<Button size="sm" className="h-8">
View Details
</Button>
</div>
</div>
</div>
</Link>
)
}
function PropertyCardList({
id,
title,
price,
location,
bedrooms,
bathrooms,
area,
type,
image,
premium,
}: PropertyCardProps) {
return (
<Link href={`/properties/${id}`}>
<div className={`property-card ${premium ? "property-card-premium" : ""}`}>
<div className="flex flex-col sm:flex-row">
<div className="relative sm:w-48 flex-shrink-0">
<img src={image || "/placeholder.svg"} alt={title} className="w-full h-48 sm:h-full object-cover" />
<div className="absolute top-2 left-2 flex gap-1">
<Badge className="bg-primary">{type}</Badge>
{premium && (
<Badge className="bg-amber-500">
<Star className="h-3 w-3 mr-1" /> Premium
</Badge>
)}
</div>
</div>
<div className="p-4 flex-1 flex flex-col">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 mb-2">
<h3 className="font-medium text-lg">{title}</h3>
<div className="font-semibold text-lg">฿{price.toLocaleString()}</div>
</div>
<div className="flex items-center text-muted-foreground text-sm mb-3">
<MapPin className="h-3.5 w-3.5 mr-1 flex-shrink-0" />
<span>{location}</span>
</div>
<div className="flex items-center gap-4 text-sm mb-4">
<div className="flex items-center">
<BedDouble className="h-4 w-4 mr-1 text-primary" />
<span>{bedrooms} Beds</span>
</div>
<div className="flex items-center">
<Bath className="h-4 w-4 mr-1 text-primary" />
<span>{bathrooms} Baths</span>
</div>
<div className="flex items-center">
<Home className="h-4 w-4 mr-1 text-primary" />
<span>{area} m²</span>
</div>
</div>
<div className="mt-auto flex justify-end">
<Button size="sm">View Details</Button>
</div>
</div>
</div>
</div>
</Link>
)
}

View File

@ -1,101 +1,9 @@
@import 'tailwindcss' prefix(tw);
@plugin "tailwindcss-animate";
@import "tailwindcss";
@plugin 'tailwindcss-animate';
@custom-variant dark (&:is(.dark *));
@custom-variant dark (&:is(.dark *));
@theme {
--font-sans:
var(--font-sans), ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--color-border: hsl(var(--border));
--color-input: hsl(var(--input));
--color-ring: hsl(var(--ring));
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
--color-secondary: hsl(var(--secondary));
--color-secondary-foreground: hsl(var(--secondary-foreground));
--color-destructive: hsl(var(--destructive));
--color-destructive-foreground: hsl(var(--destructive-foreground));
--color-muted: hsl(var(--muted));
--color-muted-foreground: hsl(var(--muted-foreground));
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
--color-popover: hsl(var(--popover));
--color-popover-foreground: hsl(var(--popover-foreground));
--color-card: hsl(var(--card));
--color-card-foreground: hsl(var(--card-foreground));
--color-sidebar: hsl(var(--sidebar-background));
--color-sidebar-foreground: hsl(var(--sidebar-foreground));
--color-sidebar-border: hsl(var(--sidebar-border));
--color-sidebar-ring: hsl(var(--sidebar-ring));
--color-sidebar-accent: hsl(var(--sidebar-accent));
--color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground));
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
--animate-caret-blink: caret-blink 1s ease-in-out infinite;
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--radix-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
}
}
@keyframes caret-blink {
0%,
50%,
100% {
opacity: 1;
}
25%,
75% {
opacity: 0;
}
}
}
@utility container {
margin-inline: auto;
padding-inline: 2rem;
@media (width >= --theme(--breakpoint-sm)) {
max-width: none;
}
@media (width >= 1400px) {
max-width: 1400px;
}
}
@theme {
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
@ -165,24 +73,6 @@
}
}
/*
The default border color has changed to `currentcolor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
/*
The default border color has changed to `currentColor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still

View File

@ -1,22 +1,19 @@
/*
========================================
File: frontend/app/layout.tsx
========================================
*/
import type React from "react";
import type { Metadata } from "next";
import { Inter } from "next/font/google"; // Example using Inter font
import { Poppins } from "next/font/google";
import "./globals.css";
import { ThemeProvider } from "@/components/common/ThemeProvider"; // Correct path
import { ThemeController } from "@/components/common/ThemeController"; // Correct path
import { Toaster as SonnerToaster } from "@/components/ui/sonner"; // Sonner for notifications
import { Toaster as RadixToaster } from "@/components/ui/toaster"; // Shadcn Toaster (if using useToast hook)
import { ThemeProvider } from "@/components/theme-provider";
import Sidebar from "@/components/sidebar";
// Setup font
const inter = Inter({ subsets: ["latin"], variable: "--font-sans" }); // Define CSS variable
const poppins = Poppins({
subsets: ["latin"],
weight: ["300", "400", "500", "600", "700"],
variable: "--font-poppins",
});
export const metadata: Metadata = {
title: "Borbann - Data Platform", // More specific title
description: "Data integration, analysis, and visualization platform.",
title: "BorBann - Property Analytics",
description: "Automated data integration pipeline and property analytics for real estate",
};
export default function RootLayout({
@ -26,19 +23,12 @@ export default function RootLayout({
}>) {
return (
<html lang="en" suppressHydrationWarning>
{" "}
{/* suppressHydrationWarning needed for next-themes */}
<body className={`${inter.variable} font-sans`}>
{" "}
{/* Apply font class */}
{/* ThemeProvider should wrap everything for theme context */}
<body className={poppins.className}>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
{/* ThemeController can wrap specific parts or the whole app */}
{/* If placed here, it controls the base theme and color scheme */}
<ThemeController defaultColorScheme="Blue">{children}</ThemeController>
{/* Include Toaster components for notifications */}
<SonnerToaster />
<RadixToaster /> {/* Include if using the useToast hook */}
<div className="flex h-screen">
<Sidebar />
<div className="flex-1 overflow-auto">{children}</div>
</div>
</ThemeProvider>
</body>
</html>

40
frontend/app/loading.tsx Normal file
View File

@ -0,0 +1,40 @@
/* === src/app/loading.tsx === */
import { Skeleton } from "@/components/ui/skeleton";
// A more detailed full-page loading skeleton mimicking the layout
export default function Loading() {
return (
<div className="flex-1 flex flex-col p-6 space-y-6">
{/* Page Header Skeleton */}
<div className="flex justify-between items-center">
<div className="space-y-2">
<Skeleton className="h-4 w-48" />
<Skeleton className="h-8 w-64" />
<Skeleton className="h-4 w-80" />
</div>
<Skeleton className="h-9 w-9 rounded-full" />
</div>
{/* Content Area Skeleton - Adjust based on typical page content */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Left Column / Filters Skeleton (Example) */}
<div className="md:col-span-1 space-y-4">
<Skeleton className="h-64 w-full rounded-lg" />
<Skeleton className="h-48 w-full rounded-lg" />
</div>
{/* Right Column / Main Content Skeleton (Example) */}
<div className="md:col-span-2 space-y-4">
<div className="flex justify-between items-center">
<Skeleton className="h-6 w-1/3" />
<Skeleton className="h-9 w-24" />
</div>
<Skeleton className="h-96 w-full rounded-lg" />
<div className="flex justify-center">
<Skeleton className="h-10 w-32" />
</div>
</div>
</div>
</div>
);
}

View File

@ -1,36 +1,204 @@
/*
========================================
File: frontend/app/page.tsx
========================================
*/
import Image from "next/image";
import Link from "next/link"; // Import Link
import { Button } from "@/components/ui/button"; // Import common UI
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { ArrowRight, BarChart2, Building, Database, LineChart, MapPin, Zap } from "lucide-react";
import Link from "next/link";
export default function Home() {
export default function LandingPage() {
return (
<div className="flex flex-col min-h-screen items-center justify-center p-8 sm:p-20 text-center">
{/* Replace with your actual logo/branding */}
<h1 className="text-4xl font-bold mb-4">Welcome to Borbann</h1>
<p className="text-lg text-muted-foreground mb-8">Your data integration and analysis platform.</p>
<div className="min-h-screen">
{/* Hero Section */}
<section className="bg-gradient-to-b from-background to-muted py-20">
<div className="container mx-auto px-6 text-center">
<Badge className="mb-4 bg-primary/10 text-primary hover:bg-primary/20 border-primary/20">
Property Analytics Platform
</Badge>
<h1 className="text-4xl md:text-5xl font-bold mb-6">Data-Driven Property Intelligence</h1>
<p className="text-xl text-muted-foreground max-w-3xl mx-auto mb-10">
Leverage AI and machine learning to analyze property data, predict prices, and gain valuable insights for
better decision making.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button size="lg" asChild>
<Link href="/maps">
Explore Map <ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
<Button size="lg" variant="outline" asChild>
<Link href="/data-pipeline">Manage Data Pipelines</Link>
</Button>
</div>
</div>
</section>
<div className="flex flex-col sm:flex-row gap-4 items-center">
<Link href="/map">
<Button size="lg">Go to Map</Button>
</Link>
<Link href="/documentation">
{" "}
{/* Example link */}
<Button size="lg" variant="outline">
Read Docs
{/* Features Section */}
<section className="py-20">
<div className="container mx-auto px-6">
<div className="text-center mb-16">
<h2 className="text-3xl font-bold mb-4">Powerful Analytics Features</h2>
<p className="text-muted-foreground max-w-2xl mx-auto">
Our platform provides comprehensive tools to analyze property data from multiple angles
</p>
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
<Card>
<CardHeader>
<div className="h-12 w-12 rounded-lg bg-primary/10 flex items-center justify-center mb-4">
<MapPin className="h-6 w-6 text-primary" />
</div>
<CardTitle>Geospatial Visualization</CardTitle>
<CardDescription>
Interactive maps with property data overlays and environmental factors
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-muted-foreground">
Visualize properties on an interactive map with customizable overlays for flood risk, air quality, and
more. Analyze properties within a specific radius.
</p>
</CardContent>
<CardFooter>
<Button variant="ghost" className="gap-1" asChild>
<Link href="/maps">
Explore Maps <ArrowRight className="h-4 w-4" />
</Link>
</Button>
</CardFooter>
</Card>
<Card>
<CardHeader>
<div className="h-12 w-12 rounded-lg bg-primary/10 flex items-center justify-center mb-4">
<BarChart2 className="h-6 w-6 text-primary" />
</div>
<CardTitle>Price Prediction</CardTitle>
<CardDescription>AI-powered price prediction with explainable features</CardDescription>
</CardHeader>
<CardContent>
<p className="text-muted-foreground">
Understand how different factors affect property prices with our explainable AI models. Adjust
parameters to see how they impact predictions.
</p>
</CardContent>
<CardFooter>
<Button variant="ghost" className="gap-1" asChild>
<Link href="/price-prediction">
View Predictions <ArrowRight className="h-4 w-4" />
</Link>
</Button>
</CardFooter>
</Card>
<Card>
<CardHeader>
<div className="h-12 w-12 rounded-lg bg-primary/10 flex items-center justify-center mb-4">
<Database className="h-6 w-6 text-primary" />
</div>
<CardTitle>Data Pipelines</CardTitle>
<CardDescription>Automated data collection and processing</CardDescription>
</CardHeader>
<CardContent>
<p className="text-muted-foreground">
Set up automated data collection from multiple sources. Our AI-powered pipelines clean, normalize, and
prepare data for analysis.
</p>
</CardContent>
<CardFooter>
<Button variant="ghost" className="gap-1" asChild>
<Link href="/data-pipeline">
Manage Pipelines <ArrowRight className="h-4 w-4" />
</Link>
</Button>
</CardFooter>
</Card>
<Card>
<CardHeader>
<div className="h-12 w-12 rounded-lg bg-primary/10 flex items-center justify-center mb-4">
<Building className="h-6 w-6 text-primary" />
</div>
<CardTitle>Property Analytics</CardTitle>
<CardDescription>Comprehensive property data analysis</CardDescription>
</CardHeader>
<CardContent>
<p className="text-muted-foreground">
Analyze property details, historical price trends, and environmental factors. Generate reports and
export data for further analysis.
</p>
</CardContent>
<CardFooter>
<Button variant="ghost" className="gap-1" asChild>
<Link href="/properties">
Browse Properties <ArrowRight className="h-4 w-4" />
</Link>
</Button>
</CardFooter>
</Card>
<Card>
<CardHeader>
<div className="h-12 w-12 rounded-lg bg-primary/10 flex items-center justify-center mb-4">
<LineChart className="h-6 w-6 text-primary" />
</div>
<CardTitle>Market Trends</CardTitle>
<CardDescription>Real-time market analysis and trends</CardDescription>
</CardHeader>
<CardContent>
<p className="text-muted-foreground">
Track property market trends over time. Identify emerging patterns and make data-driven investment
decisions.
</p>
</CardContent>
<CardFooter>
<Button variant="ghost" className="gap-1" asChild>
<Link href="/maps">
View Trends <ArrowRight className="h-4 w-4" />
</Link>
</Button>
</CardFooter>
</Card>
<Card>
<CardHeader>
<div className="h-12 w-12 rounded-lg bg-primary/10 flex items-center justify-center mb-4">
<Zap className="h-6 w-6 text-primary" />
</div>
<CardTitle>Custom ML Models</CardTitle>
<CardDescription>Train and deploy custom machine learning models</CardDescription>
</CardHeader>
<CardContent>
<p className="text-muted-foreground">
Create and train custom machine learning models using your own data. Deploy models for specific
analysis needs.
</p>
</CardContent>
<CardFooter>
<Button variant="ghost" className="gap-1" asChild>
<Link href="/models">
Manage Models <ArrowRight className="h-4 w-4" />
</Link>
</Button>
</CardFooter>
</Card>
</div>
</div>
</section>
{/* CTA Section */}
<section className="py-20 bg-muted">
<div className="container mx-auto px-6 text-center">
<h2 className="text-3xl font-bold mb-4">Ready to Get Started?</h2>
<p className="text-muted-foreground max-w-2xl mx-auto mb-8">
Explore our platform and discover how data-driven property analytics can transform your decision making.
</p>
<Button size="lg" asChild>
<Link href="/maps">
Start Exploring <ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</Link>
</div>
{/* Optional: Add more introductory content or links */}
<footer className="absolute bottom-8 text-sm text-muted-foreground">
© {new Date().getFullYear()} Borbann Project.
</footer>
</div>
</section>
</div>
);
}

View File

@ -1,24 +1,26 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
"css": "src/styles/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"@": "@/src",
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks",
"features": "@/features",
"types": "@/types",
"services": "@/services"
"services": "@/services",
"store": "@/store",
"types": "@/types"
},
"iconLibrary": "lucide"
}

View File

@ -1,29 +0,0 @@
/*
========================================
File: frontend/components/common/PageLayout.tsx (NEW - Example)
========================================
*/
import React, { ReactNode } from "react";
import { cn } from "@/lib/utils";
// Example: Assuming you might have a common Header/Footer/Sidebar structure
// import { AppHeader } from './AppHeader';
// import { AppFooter } from './AppFooter';
interface PageLayoutProps {
children: ReactNode;
className?: string;
}
/**
* DUMMY: A basic layout component for pages.
* Might include common headers, footers, or side navigation structures.
*/
export function PageLayout({ children, className }: PageLayoutProps) {
return (
<div className={cn("flex flex-col min-h-screen", className)}>
{/* <AppHeader /> */} {/* Example: Shared Header */}
<main className="flex-1">{children}</main>
{/* <AppFooter /> */} {/* Example: Shared Footer */}
</div>
);
}

View File

@ -1,144 +0,0 @@
/*
========================================
File: frontend/components/common/ThemeController.tsx
========================================
*/
"use client";
import { useState, useEffect, useRef, type ReactNode } from "react";
import { useTheme } from "next-themes";
import { Sun, Moon, Laptop, Palette, Check } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
DropdownMenuLabel,
} from "@/components/ui/dropdown-menu";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
// Define available color schemes (these affect CSS variables)
const colorSchemes = [
{ name: "Blue", primary: "221.2 83.2% 53.3%" }, // Default blue
{ name: "Green", primary: "142.1 76.2% 36.3%" },
{ name: "Purple", primary: "262.1 83.3% 57.8%" },
{ name: "Orange", primary: "24.6 95% 53.1%" },
{ name: "Teal", primary: "173 80.4% 40%" },
];
interface ThemeControllerProps {
children: ReactNode;
defaultColorScheme?: string;
}
export function ThemeController({ children, defaultColorScheme = "Blue" }: ThemeControllerProps) {
const { setTheme, theme } = useTheme();
const [colorScheme, setColorScheme] = useState(defaultColorScheme);
// State for overlay boundaries removed, as overlay context now manages positioning/constraints.
// const [overlayBoundaries, setOverlayBoundaries] = useState({ width: 0, height: 0 });
const containerRef = useRef<HTMLDivElement>(null);
// Update overlay boundaries - This logic might be better placed within the OverlayProvider
// or removed if not strictly necessary for the controller's function.
// Kept here for now as per original code structure, but consider moving it.
// useEffect(() => {
// const updateBoundaries = () => {
// if (containerRef.current) {
// const width = containerRef.current.clientWidth;
// const height = containerRef.current.clientHeight;
// document.documentElement.style.setProperty("--max-overlay-width", `${width - 32}px`);
// document.documentElement.style.setProperty("--max-overlay-height", `${height - 32}px`);
// }
// };
// updateBoundaries();
// window.addEventListener("resize", updateBoundaries);
// return () => window.removeEventListener("resize", updateBoundaries);
// }, []);
// Apply color scheme by setting the '--primary' CSS variable
useEffect(() => {
const scheme = colorSchemes.find((s) => s.name === colorScheme);
if (scheme) {
document.documentElement.style.setProperty("--primary", scheme.primary);
// You might need to set --ring as well if it depends on primary
// document.documentElement.style.setProperty("--ring", scheme.primary);
}
}, [colorScheme]);
return (
<div ref={containerRef} className="relative h-full w-full overflow-hidden" data-theme-controller="true">
{children}
{/* Theme Controller UI */}
<div className="fixed bottom-4 left-4 z-999 flex items-center gap-2">
{" "}
{/* Ensure high z-index */}
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="icon"
className="h-10 w-10 rounded-full bg-background/90 backdrop-blur-xs shadow-md">
<Palette className="h-5 w-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-56">
<DropdownMenuLabel>Theme Options</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => setTheme("light")} className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Sun className="h-4 w-4" />
<span>Light</span>
</div>
{theme === "light" && <Check className="h-4 w-4" />}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")} className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Moon className="h-4 w-4" />
<span>Dark</span>
</div>
{theme === "dark" && <Check className="h-4 w-4" />}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")} className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Laptop className="h-4 w-4" />
<span>System</span>
</div>
{theme === "system" && <Check className="h-4 w-4" />}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuLabel>Color Scheme</DropdownMenuLabel>
{colorSchemes.map((scheme) => (
<DropdownMenuItem
key={scheme.name}
onClick={() => setColorScheme(scheme.name)}
className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="h-4 w-4 rounded-full" style={{ backgroundColor: `hsl(${scheme.primary})` }} />
<span>{scheme.name}</span>
</div>
{colorScheme === scheme.name && <Check className="h-4 w-4" />}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</TooltipTrigger>
<TooltipContent side="right">
<p>Theme Settings</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
);
}

View File

@ -1,23 +0,0 @@
/*
========================================
File: frontend/components/common/ThemeProvider.tsx
========================================
*/
"use client";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import type { ThemeProviderProps } from "next-themes";
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return (
<NextThemesProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
{...props} // Pass through any other props
>
{children}
</NextThemesProvider>
);
}

View File

@ -1,57 +0,0 @@
/*
========================================
File: frontend/components/common/ThemeToggle.tsx
========================================
*/
"use client";
import React from "react";
import { Moon, Sun, Laptop } from "lucide-react";
import { useTheme } from "next-themes";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
export function ThemeToggle() {
const { setTheme, theme } = useTheme();
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
<Sun className="mr-2 h-4 w-4" />
<span>Light</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
<Moon className="mr-2 h-4 w-4" />
<span>Dark</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
<Laptop className="mr-2 h-4 w-4" />
<span>System</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TooltipTrigger>
<TooltipContent>
<p>Change theme</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}

View File

@ -0,0 +1,28 @@
"use client"
import { Moon, Sun } from "lucide-react"
import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
export function ModeToggle() {
const { setTheme } = useTheme()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon" className="h-9 w-9">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>Light</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>Dark</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>System</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}

View File

@ -0,0 +1,51 @@
"use client"
import { ChevronRight } from "lucide-react"
import Link from "next/link"
import { ModeToggle } from "./mode-toggle"
interface BreadcrumbItem {
title: string
href: string
}
interface PageHeaderProps {
title: string
description?: string
breadcrumb?: BreadcrumbItem[]
}
export default function PageHeader({ title, description, breadcrumb = [] }: PageHeaderProps) {
return (
<div className="flex flex-col space-y-2">
{breadcrumb.length > 0 && (
<nav className="flex items-center text-sm text-muted-foreground">
{breadcrumb.map((item, index) => (
<div key={item.href} className="flex items-center">
{index > 0 && <ChevronRight className="h-4 w-4 mx-1" />}
<Link href={item.href} className="hover:text-foreground transition-colors">
{item.title}
</Link>
</div>
))}
{title && (
<>
<ChevronRight className="h-4 w-4 mx-1" />
<span className="font-medium text-foreground">{title}</span>
</>
)}
<div className="ml-auto">
<ModeToggle />
</div>
</nav>
)}
<div>
<h1 className="text-2xl font-bold tracking-tight">{title}</h1>
{description && <p className="text-muted-foreground">{description}</p>}
</div>
</div>
)
}

View File

@ -0,0 +1,114 @@
"use client"
import type React from "react"
import { cn } from "@/lib/utils"
import { Map, Database, FileText, Users, ChevronDown, ChevronUp, BrainCircuit } from "lucide-react"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { useState } from "react"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
interface SidebarItemProps {
icon: React.ReactNode
label: string
href: string
active?: boolean
badge?: React.ReactNode
}
const SidebarItem = ({ icon, label, href, active, badge }: SidebarItemProps) => {
return (
<Link
href={href}
className={cn(
"flex items-center gap-3 px-3 py-2 rounded-md transition-colors",
active
? "bg-primary/10 text-primary border-l-2 border-primary"
: "text-foreground/80 hover:bg-muted hover:text-foreground hover:border-l-2 hover:border-primary/50",
)}
>
<div className={cn("text-primary", active ? "text-primary" : "text-muted-foreground")}>{icon}</div>
<span className="flex-1">{label}</span>
{badge}
</Link>
)
}
export default function Sidebar() {
const pathname = usePathname()
const [expanded, setExpanded] = useState(false)
return (
<div className="w-64 border-r bg-background flex flex-col h-full">
<div className="p-4 border-b flex items-center gap-2">
<div className="h-8 w-8 rounded-full bg-primary flex items-center justify-center text-primary-foreground font-bold">
B
</div>
<span className="font-semibold text-xl">BorBann</span>
</div>
<div className="flex-1 py-4 px-3 space-y-1 overflow-y-auto">
<SidebarItem icon={<Map size={20} />} label="Maps" href="/maps" active={pathname.startsWith("/maps")} />
<SidebarItem
icon={<Database size={20} />}
label="Data Pipeline"
href="/data-pipeline"
active={pathname.startsWith("/data-pipeline")}
/>
<SidebarItem
icon={<BrainCircuit size={20} />}
label="Models"
href="/models"
active={pathname.startsWith("/models")}
/>
<SidebarItem
icon={<FileText size={20} />}
label="Documentation"
href="/documentation"
active={pathname.startsWith("/documentation")}
badge={<span className="text-xs bg-primary text-primary-foreground px-1.5 py-0.5 rounded-md">NEW</span>}
/>
</div>
<div className="mt-auto border-t p-4">
<div className="flex items-center gap-3">
<Avatar>
<AvatarImage src="/placeholder.svg?height=40&width=40" />
<AvatarFallback>GG</AvatarFallback>
</Avatar>
<div className="flex-1 overflow-hidden">
<p className="font-medium truncate">GG_WPX</p>
<p className="text-xs text-muted-foreground truncate">garfield.wpx@gmail.com</p>
</div>
</div>
<Button
variant="outline"
className="w-full mt-4 justify-start gap-2 border-primary/20 hover:border-primary"
onClick={() => setExpanded(!expanded)}
>
<Users size={18} />
<span className="flex-1 text-left">Users</span>
{expanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
</Button>
{expanded && (
<div className="mt-2 pl-2 space-y-1 text-sm">
<Link href="/users/manage" className="block py-1 px-2 rounded hover:bg-muted">
Manage Users
</Link>
<Link href="/users/roles" className="block py-1 px-2 rounded hover:bg-muted">
Roles & Permissions
</Link>
</div>
)}
</div>
</div>
)
}

View File

@ -0,0 +1,11 @@
'use client'
import * as React from 'react'
import {
ThemeProvider as NextThemesProvider,
type ThemeProviderProps,
} from 'next-themes'
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

View File

@ -1,65 +0,0 @@
/*
========================================
File: frontend/features/map/api/mapApi.ts
========================================
*/
import apiClient from "@/services/apiClient";
import type { APIResponse, PointOfInterest } from "@/types/api"; // Shared types
import type { MapBounds } from "../types"; // Feature-specific types
interface FetchPOIsParams {
bounds: MapBounds;
filters?: Record<string, any>;
}
/**
* DUMMY: Fetches Points of Interest based on map bounds and filters.
*/
export async function fetchPointsOfInterest(
params: FetchPOIsParams
): Promise<APIResponse<PointOfInterest[]>> {
console.log("DUMMY API: Fetching POIs with params:", params);
// Simulate building query parameters
const queryParams = new URLSearchParams({
north: params.bounds.north.toString(),
south: params.bounds.south.toString(),
east: params.bounds.east.toString(),
west: params.bounds.west.toString(),
});
if (params.filters) {
Object.entries(params.filters).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
queryParams.set(key, String(value));
}
});
}
// Use the dummy apiClient
const response = await apiClient.get<PointOfInterest[]>(`/map/pois?${queryParams.toString()}`);
if (response.success) {
// Simulate adding more data if needed for testing
const dummyData: PointOfInterest[] = [
{ id: "poi1", lat: params.bounds.north - 0.01, lng: params.bounds.west + 0.01, name: "Dummy Cafe", type: "cafe" },
{ id: "poi2", lat: params.bounds.south + 0.01, lng: params.bounds.east - 0.01, name: "Dummy Park", type: "park" },
...(response.data || []) // Include data if apiClient simulation provides it
];
return { success: true, data: dummyData };
} else {
return response; // Forward the error response
}
}
// Add other map-related API functions here
export async function fetchMapAnalytics(bounds: MapBounds): Promise<APIResponse<any>> {
console.log("DUMMY API: Fetching Map Analytics with params:", bounds);
// Simulate building query parameters
const queryParams = new URLSearchParams({
north: bounds.north.toString(),
south: bounds.south.toString(),
east: bounds.east.toString(),
west: bounds.west.toString(),
});
return apiClient.get(`/map/analytics?${queryParams.toString()}`);
}

View File

@ -1,120 +0,0 @@
/*
========================================
File: frontend/features/map/components/analytics-overlay.tsx
========================================
*/
"use client";
import { LineChart, Wind, Droplets, Sparkles, Bot } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { AreaChart } from "./area-chart";
import { Overlay } from "./overlay-system/overlay"; // Import local overlay system
import { useOverlay } from "./overlay-system/overlay-context";
export function AnalyticsOverlay() {
const { toggleOverlay } = useOverlay();
const handleChatClick = () => {
toggleOverlay("chat");
};
return (
<Overlay
id="analytics"
title="Analytics"
icon={<Sparkles className="h-5 w-5" />}
initialPosition="top-right"
initialIsOpen={true}
width="350px">
<div className="h-[calc(min(70vh,600px))] overflow-auto">
<div className="p-4">
<div className="flex flex-col gap-4">
<p className="text-xs text-muted-foreground">Information in radius will be analyzed</p>
{/* Area Price History Card */}
<Card className="bg-card/50 border border-border/50 shadow-xs">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<LineChart className="h-4 w-4 text-primary" />
Area Price History
</CardTitle>
<div className="flex items-baseline justify-between">
<div className="text-2xl font-bold">10,000,000 Baht</div>
</div>
<p className="text-xs text-muted-foreground">Overall Price History of this area</p>
</CardHeader>
<CardContent className="pt-0">
<AreaChart
data={[8500000, 9000000, 8800000, 9200000, 9500000, 9800000, 10000000]}
color="rgba(59, 130, 246, 0.5)"
/>
</CardContent>
</Card>
{/* Price Prediction Card */}
<Card className="bg-card/50 border border-border/50 shadow-xs">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<LineChart className="h-4 w-4 text-primary" />
Price Prediction
</CardTitle>
<div className="flex items-baseline justify-between">
<div className="text-2xl font-bold">15,000,000 Baht</div>
</div>
<p className="text-xs text-muted-foreground">The estimated price based on various factors.</p>
</CardHeader>
<CardContent className="pt-0">
<AreaChart
data={[10000000, 11000000, 12000000, 13000000, 14000000, 14500000, 15000000]}
color="rgba(16, 185, 129, 0.5)"
/>
</CardContent>
</Card>
{/* Environmental Factors Cards */}
<div className="grid grid-cols-2 gap-4">
<Card className="bg-card/50 border border-border/50 shadow-xs">
<CardHeader className="p-4">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<Droplets className="h-4 w-4 text-blue-500" />
Flood Factor
</CardTitle>
<div className="flex items-center gap-2 mt-1">
<div className="h-3 w-3 rounded-full bg-yellow-500"></div>
<span className="text-sm">Moderate</span>
</div>
</CardHeader>
</Card>
<Card className="bg-card/50 border border-border/50 shadow-xs">
<CardHeader className="p-4">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<Wind className="h-4 w-4 text-purple-500" />
Air Factor
</CardTitle>
<div className="flex items-center gap-2 mt-1">
<div className="h-3 w-3 rounded-full bg-red-500"></div>
<span className="text-sm">Bad</span>
</div>
</CardHeader>
</Card>
</div>
{/* Chat With AI Card */}
<Card
className="bg-card/50 border border-border/50 shadow-xs cursor-pointer hover:bg-muted/50 transition-colors"
onClick={handleChatClick}>
<CardHeader className="p-4">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<Bot className="h-4 w-4 text-teal-500" />
Chat With AI
</CardTitle>
<p className="text-xs text-muted-foreground">Want to ask specific question?</p>
</CardHeader>
</Card>
</div>
</div>
</div>
</Overlay>
);
}

View File

@ -1,10 +0,0 @@
/*
========================================
File: frontend/features/map/components/analytics-panel.tsx
========================================
*/
// This component seems redundant with analytics-overlay.tsx in the new structure.
// If it has unique logic or display variation, keep it and refactor its imports.
// Otherwise, it can likely be removed, and its functionality merged into analytics-overlay.tsx.
// For this rewrite, assuming it's removed or its distinct logic is integrated elsewhere.
// If needed, its structure would be similar to analytics-overlay.tsx but maybe without the <Overlay> wrapper.

View File

@ -1,73 +0,0 @@
/*
========================================
File: frontend/features/map/components/area-chart.tsx
========================================
*/
"use client";
import { LineChart, Line, ResponsiveContainer, XAxis, YAxis, Tooltip } from "@/components/ui/chart"; // Using shared ui chart components
import { useTheme } from "next-themes";
interface AreaChartProps {
data: number[];
color: string;
}
export function AreaChart({ data, color }: AreaChartProps) {
const { theme } = useTheme();
const isDark = theme === "dark";
// Generate labels (e.g., months or simple indices)
const labels = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"]; // Example labels
// Format the data for the chart
const chartData = data.map((value, index) => ({
name: labels[index % labels.length] || `Point ${index + 1}`, // Use labels or fallback
value: value,
}));
// Format the price for display in tooltip
const formatPrice = (value: number) => {
return new Intl.NumberFormat("th-TH", {
style: "currency",
currency: "THB",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(value);
};
return (
<div className="h-[80px] w-full">
{" "}
{/* Adjust height as needed */}
<ResponsiveContainer width="100%" height="100%">
<LineChart data={chartData}>
<XAxis dataKey="name" hide />
<YAxis hide domain={[(dataMin: number) => dataMin * 0.95, (dataMax: number) => dataMax * 1.05]} />
<Tooltip
formatter={(value: number) => [formatPrice(value), "Price"]}
contentStyle={{
backgroundColor: isDark ? "#1f2937" : "white",
borderRadius: "0.375rem",
border: isDark ? "1px solid #374151" : "1px solid #e2e8f0",
fontSize: "0.75rem",
color: isDark ? "#e5e7eb" : "#1f2937",
}}
/>
<Line
type="monotone"
dataKey="value"
stroke={color.replace("rgba", "rgb").replace(/,[^,]*\)/, ")")} // Ensure valid RGB for stroke
strokeWidth={2}
dot={{ r: 3, strokeWidth: 1 }}
activeDot={{ r: 5, strokeWidth: 0 }}
// Area charts typically use <Area>, but keeping <Line> based on original code
// If area fill is desired:
// fill={color}
// fillOpacity={0.5}
/>
</LineChart>
</ResponsiveContainer>
</div>
);
}

View File

@ -1,8 +0,0 @@
/*
========================================
File: frontend/features/map/components/chat-bot.tsx
========================================
*/
// This component seems redundant with chat-overlay.tsx.
// Assuming it's removed or its logic is integrated into chat-overlay.tsx.
// If needed, its structure would be similar to chat-overlay.tsx but maybe without the <Overlay> wrapper.

View File

@ -1,164 +0,0 @@
/*
========================================
File: frontend/features/map/components/filters-overlay.tsx
========================================
*/
"use client";
import { useState } from "react";
import { Filter } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Slider } from "@/components/ui/slider";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Overlay } from "./overlay-system/overlay"; // Import local overlay system
export function FiltersOverlay() {
const [area, setArea] = useState("< 30 km");
const [timePeriod, setTimePeriod] = useState("All Time");
const [propertyType, setPropertyType] = useState("House");
const [priceRange, setPriceRange] = useState([5_000_000, 20_000_000]);
const [activeTab, setActiveTab] = useState("basic");
const handleApplyFilters = () => {
console.log("DUMMY: Applying filters:", {
area,
timePeriod,
propertyType,
priceRange, // Include advanced filters state here
});
// In real app: trigger data refetch with these filters
};
return (
<Overlay
id="filters"
title="Property Filters"
icon={<Filter className="h-5 w-5" />}
initialPosition="bottom-left"
initialIsOpen={true}
width="350px">
<ScrollArea className="h-[calc(min(70vh,500px))]">
{" "}
{/* Scrollable content */}
<div className="p-4">
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-2 mb-4">
<TabsTrigger value="basic">Basic</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList>
<TabsContent value="basic" className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1">
<Label htmlFor="area-radius" className="text-xs font-medium">
Area Radius
</Label>
<Select value={area} onValueChange={setArea}>
<SelectTrigger id="area-radius">
<SelectValue placeholder="Select area" />
</SelectTrigger>
<SelectContent>
<SelectItem value="< 10 km">{"< 10 km"}</SelectItem>
<SelectItem value="< 20 km">{"< 20 km"}</SelectItem>
<SelectItem value="< 30 km">{"< 30 km"}</SelectItem>
<SelectItem value="< 50 km">{"< 50 km"}</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<Label htmlFor="time-period" className="text-xs font-medium">
Time Period
</Label>
<Select value={timePeriod} onValueChange={setTimePeriod}>
<SelectTrigger id="time-period">
<SelectValue placeholder="Select time period" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Last Month">Last Month</SelectItem>
<SelectItem value="Last 3 Months">Last 3 Months</SelectItem>
<SelectItem value="Last Year">Last Year</SelectItem>
<SelectItem value="All Time">All Time</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-1">
<Label htmlFor="property-type" className="text-xs font-medium">
Property Type
</Label>
<Select value={propertyType} onValueChange={setPropertyType}>
<SelectTrigger id="property-type">
<SelectValue placeholder="Select property type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="House">House</SelectItem>
<SelectItem value="Condo">Condo</SelectItem>
<SelectItem value="Townhouse">Townhouse</SelectItem>
<SelectItem value="Land">Land</SelectItem>
<SelectItem value="Commercial">Commercial</SelectItem>
</SelectContent>
</Select>
</div>
</TabsContent>
<TabsContent value="advanced" className="space-y-4">
<div className="space-y-3">
<div>
<div className="flex justify-between mb-1">
<Label htmlFor="price-range" className="text-xs font-medium">
Price Range
</Label>
<span className="text-xs text-muted-foreground">
{new Intl.NumberFormat("th-TH", { notation: "compact" }).format(priceRange[0])} -{" "}
{new Intl.NumberFormat("th-TH", { notation: "compact" }).format(priceRange[1])} ฿
</span>
</div>
<Slider
id="price-range"
value={priceRange}
min={1_000_000}
max={50_000_000}
step={100_000} // Finer step
onValueChange={setPriceRange}
/>
</div>
<div className="space-y-2">
<Label className="text-xs font-medium">Environmental Factors</Label>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor="low-flood" className="text-xs">
Low Flood Risk
</Label>
<Switch id="low-flood" />
</div>
<div className="flex items-center justify-between">
<Label htmlFor="good-air" className="text-xs">
Good Air Quality
</Label>
<Switch id="good-air" />
</div>
<div className="flex items-center justify-between">
<Label htmlFor="low-noise" className="text-xs">
Low Noise Pollution
</Label>
<Switch id="low-noise" />
</div>
</div>
</div>
</div>
</TabsContent>
</Tabs>
<Button className="mt-4 w-full" size="sm" onClick={handleApplyFilters}>
Apply Filters
</Button>
</div>
</ScrollArea>
</Overlay>
);
}

View File

@ -1,75 +0,0 @@
/*
========================================
File: frontend/features/map/components/map-container.tsx
========================================
*/
"use client";
import React, { useEffect, useRef } from "react";
import type { MapLocation } from "../types"; // Feature-specific type
interface MapContainerProps {
selectedLocation: MapLocation;
}
export function MapContainer({ selectedLocation }: MapContainerProps) {
const mapRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const mapElement = mapRef.current;
console.log("DUMMY MAP: Initializing/updating for:", selectedLocation);
if (mapElement) {
// Placeholder for actual map library integration (e.g., Leaflet, Mapbox GL JS, Google Maps API)
mapElement.innerHTML = `
<div style="
width: 100%; height: 100%;
background-image: url('/placeholder.svg?height=800&width=1200'); /* Using placeholder */
background-size: cover;
background-position: center;
display: flex; align-items: center; justify-content: center;
font-family: sans-serif; color: #555;
border: 1px dashed #aaa;
position: relative; /* Needed for marker positioning */
">
Map Placeholder: Centered on ${selectedLocation.name || "location"} (${selectedLocation.lat.toFixed(
4
)}, ${selectedLocation.lng.toFixed(4)})
<div style="
position: absolute;
left: 50%; top: 50%;
transform: translate(-50%, -50%);
width: 24px; height: 24px;
border-radius: 50%;
background-color: red;
border: 4px solid rgba(255, 100, 100, 0.5);
animation: pulse 1.5s infinite;
"></div>
</div>
<style>
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(255, 0, 0, 0.7); }
70% { box-shadow: 0 0 0 10px rgba(255, 0, 0, 0); }
100% { box-shadow: 0 0 0 0 rgba(255, 0, 0, 0); }
}
</style>
`;
// In a real app, you'd initialize the map library here, set view, add layers/markers.
}
// Cleanup function
return () => {
console.log("DUMMY MAP: Cleaning up map instance");
if (mapElement) {
mapElement.innerHTML = ""; // Clear placeholder
// In a real app, you'd properly destroy the map instance here.
}
};
}, [selectedLocation]); // Re-run effect if location changes
return (
<div ref={mapRef} className="absolute inset-0 h-full w-full bg-muted/20 dark:bg-muted/10">
{/* The map library will render into this div */}
</div>
);
}

View File

@ -1,40 +0,0 @@
/*
========================================
File: frontend/features/map/components/map-header.tsx
========================================
*/
"use client";
import { ChevronRight } from "lucide-react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { ThemeToggle } from "@/components/common/ThemeToggle"; // Import from common
export function MapHeader() {
// Add any map-specific header logic here if needed
return (
<header className="flex h-14 items-center justify-between border-b px-4 bg-background shrink-0">
{/* Breadcrumbs or Title */}
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Link href="/tools" className="hover:text-foreground">
{" "}
{/* Example link */}
Tools
</Link>
<ChevronRight className="h-4 w-4" />
<span className="font-medium text-foreground">Map</span>
</div>
{/* Header Actions */}
<div className="flex items-center gap-2">
<ThemeToggle />
<Button variant="outline" size="sm" className="ml-2">
Dummy Action 1
</Button>
<Button variant="ghost" size="sm">
Dummy Action 2
</Button>
</div>
</header>
);
}

View File

@ -1,144 +0,0 @@
/*
========================================
File: frontend/features/map/components/map-sidebar.tsx
========================================
*/
"use client";
import React from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import {
Home,
Map, // Changed from Clock
BarChart3, // Changed from Map
Layers, // Changed from FileText
Settings,
SlidersHorizontal, // Changed from PenTool
MessageCircle, // Changed from BarChart3
Info, // Changed from Plane
LineChart,
DollarSign,
MoreHorizontal,
Gift, // Added Gift icon component below
} from "lucide-react";
import { cn } from "@/lib/utils";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarMenu,
SidebarMenuItem,
SidebarMenuButton,
} from "@/components/ui/sidebar"; // Assuming sidebar is a shared UI component structure
export function MapSidebar() {
const pathname = usePathname();
// Define navigation items relevant to the map context or general app navigation shown here
const mainNavItems = [
{ name: "Map View", icon: Map, href: "/map" },
{ name: "Analytics", icon: BarChart3, href: "/map/analytics" }, // Example sub-route
{ name: "Filters", icon: SlidersHorizontal, href: "/map/filters" }, // Example sub-route
{ name: "Data Layers", icon: Layers, href: "/map/layers" }, // Example sub-route
{ name: "Chat", icon: MessageCircle, href: "/map/chat" }, // Example sub-route
{ name: "Model Info", icon: Info, href: "/model-explanation" }, // Link to other feature
{ name: "Settings", icon: Settings, href: "/settings" }, // Example general setting
{ name: "More", icon: MoreHorizontal, href: "/more" }, // Example general setting
];
// Example project-specific items (if sidebar is shared)
const projectNavItems = [
{ name: "Market Trends", icon: LineChart, href: "/projects/trends" },
{ name: "Investment", icon: DollarSign, href: "/projects/investment" },
];
return (
// Using the shared Sidebar component structure
<Sidebar side="left" variant="sidebar" collapsible="icon">
<SidebarHeader>
<Link href="/" className="flex items-center gap-2 font-semibold px-2">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary text-primary-foreground">
B
</div>
{/* Hide text when collapsed */}
<span className="text-xl font-bold group-data-collapsed:hidden">BorBann</span>
</Link>
</SidebarHeader>
<SidebarContent className="p-2">
<SidebarMenu>
{mainNavItems.map((item) => (
<SidebarMenuItem key={item.name}>
<SidebarMenuButton
asChild // Use asChild if the button itself is a Link or wraps one
variant="default"
size="default"
isActive={pathname === item.href}
tooltip={item.name} // Tooltip shown when collapsed
>
<Link href={item.href}>
<item.icon />
{/* Hide text when collapsed */}
<span className="group-data-collapsed:hidden">{item.name}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
{/* Optional Project Section */}
{/* <SidebarSeparator />
<SidebarGroup>
<SidebarGroupLabel>Projects</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{projectNavItems.map((item) => (
<SidebarMenuItem key={item.name}>
<SidebarMenuButton >...</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup> */}
</SidebarContent>
<SidebarFooter>
{/* Footer content like user profile, settings shortcut etc. */}
<div className="flex items-center gap-3 p-2">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-muted text-muted-foreground text-xs">
GG
</div>
<div className="group-data-collapsed:hidden">
<div className="font-medium text-sm">GG_WPX</div>
<div className="text-xs text-muted-foreground">gg@example.com</div>
</div>
</div>
</SidebarFooter>
</Sidebar>
);
}
// Example Gift Icon (if not using lucide-react)
function GiftIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
{...props}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round">
<polyline points="20 12 20 22 4 22 4 12"></polyline>
<rect x="2" y="7" width="20" height="5"></rect>
<line x1="12" y1="22" x2="12" y2="7"></line>
<path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z"></path>
<path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z"></path>
</svg>
);
}

View File

@ -1,230 +0,0 @@
/*
========================================
File: frontend/features/map/components/overlay-system/overlay-context.tsx
========================================
*/
"use client";
import React, { createContext, useContext, useState, useCallback, useRef, type ReactNode } from "react";
// Define overlay types and positions
export type OverlayId = string;
export type OverlayPosition = "top-left" | "top-right" | "bottom-left" | "bottom-right" | "center";
// Interface for overlay state
export interface OverlayState {
id: OverlayId;
isOpen: boolean;
isMinimized: boolean;
position: OverlayPosition;
zIndex: number;
title: string;
icon?: React.ReactNode;
}
// Interface for the overlay context
interface OverlayContextType {
overlays: Record<OverlayId, OverlayState>;
registerOverlay: (id: OverlayId, initialState: Partial<Omit<OverlayState, "id" | "zIndex">>) => void;
unregisterOverlay: (id: OverlayId) => void;
openOverlay: (id: OverlayId) => void;
closeOverlay: (id: OverlayId) => void;
toggleOverlay: (id: OverlayId) => void;
minimizeOverlay: (id: OverlayId) => void;
maximizeOverlay: (id: OverlayId) => void;
setPosition: (id: OverlayId, position: OverlayPosition) => void;
bringToFront: (id: OverlayId) => void;
getNextZIndex: () => number;
}
// Create the context
const OverlayContext = createContext<OverlayContextType | undefined>(undefined);
// Default values for overlay state
const defaultOverlayState: Omit<OverlayState, "id" | "title" | "icon"> = {
isOpen: false,
isMinimized: false,
position: "bottom-right", // Default position
zIndex: 10, // Starting z-index
};
export function OverlayProvider({ children }: { children: ReactNode }) {
const [overlays, setOverlays] = useState<Record<OverlayId, OverlayState>>({});
const maxZIndexRef = useRef(10); // Start z-index from 10
// Get the next z-index value
const getNextZIndex = useCallback(() => {
maxZIndexRef.current += 1;
return maxZIndexRef.current;
}, []);
// Register a new overlay
const registerOverlay = useCallback(
(id: OverlayId, initialState: Partial<Omit<OverlayState, "id" | "zIndex">>) => {
setOverlays((prev) => {
if (prev[id]) {
console.warn(`Overlay with id "${id}" already registered.`);
return prev;
}
const newZIndex = initialState.isOpen ? getNextZIndex() : defaultOverlayState.zIndex;
return {
...prev,
[id]: {
...defaultOverlayState,
id,
title: id, // Default title to id
...initialState,
zIndex: newZIndex, // Set initial z-index
},
};
});
},
[getNextZIndex]
);
// Unregister an overlay
const unregisterOverlay = useCallback((id: OverlayId) => {
setOverlays((prev) => {
const { [id]: _, ...rest } = prev; // Use destructuring to remove the key
return rest;
});
}, []);
// Open an overlay
const openOverlay = useCallback(
(id: OverlayId) => {
setOverlays((prev) => {
if (!prev[id] || prev[id].isOpen) return prev;
return {
...prev,
[id]: {
...prev[id],
isOpen: true,
isMinimized: false, // Ensure not minimized when opened
zIndex: getNextZIndex(), // Bring to front
},
};
});
},
[getNextZIndex]
);
// Close an overlay
const closeOverlay = useCallback((id: OverlayId) => {
setOverlays((prev) => {
if (!prev[id] || !prev[id].isOpen) return prev;
return {
...prev,
[id]: { ...prev[id], isOpen: false },
};
});
}, []);
// Toggle an overlay's open/closed state
const toggleOverlay = useCallback(
(id: OverlayId) => {
setOverlays((prev) => {
if (!prev[id]) return prev; // Don't toggle non-existent overlays
const willBeOpen = !prev[id].isOpen;
const newZIndex = willBeOpen ? getNextZIndex() : prev[id].zIndex; // Bring to front only if opening
return {
...prev,
[id]: {
...prev[id],
isOpen: willBeOpen,
isMinimized: willBeOpen ? false : prev[id].isMinimized, // Maximize when toggling open
zIndex: newZIndex,
},
};
});
},
[getNextZIndex]
);
// Minimize an overlay
const minimizeOverlay = useCallback((id: OverlayId) => {
setOverlays((prev) => {
if (!prev[id] || !prev[id].isOpen || prev[id].isMinimized) return prev; // Only minimize open, non-minimized overlays
return {
...prev,
[id]: {
...prev[id],
isMinimized: true,
// Optionally send to back when minimized: zIndex: defaultOverlayState.zIndex
},
};
});
}, []);
// Maximize an overlay
const maximizeOverlay = useCallback(
(id: OverlayId) => {
setOverlays((prev) => {
if (!prev[id] || !prev[id].isOpen || !prev[id].isMinimized) return prev; // Only maximize minimized overlays
return {
...prev,
[id]: {
...prev[id],
isMinimized: false,
zIndex: getNextZIndex(), // Bring to front when maximized
},
};
});
},
[getNextZIndex]
);
// Set the position of an overlay
const setPosition = useCallback((id: OverlayId, position: OverlayPosition) => {
setOverlays((prev) => {
if (!prev[id]) return prev;
return {
...prev,
[id]: { ...prev[id], position },
};
});
}, []);
// Bring an overlay to the front
const bringToFront = useCallback(
(id: OverlayId) => {
setOverlays((prev) => {
if (!prev[id] || !prev[id].isOpen) return prev; // Only bring open overlays to front
// Avoid getting new zIndex if already on top
if (prev[id].zIndex === maxZIndexRef.current) return prev;
return {
...prev,
[id]: { ...prev[id], zIndex: getNextZIndex() },
};
});
},
[getNextZIndex]
);
const value = {
overlays,
registerOverlay,
unregisterOverlay,
openOverlay,
closeOverlay,
toggleOverlay,
minimizeOverlay,
maximizeOverlay,
setPosition,
bringToFront,
getNextZIndex,
};
return <OverlayContext.Provider value={value}>{children}</OverlayContext.Provider>;
}
// Hook to use the overlay context
export function useOverlay() {
const context = useContext(OverlayContext);
if (context === undefined) {
throw new Error("useOverlay must be used within an OverlayProvider");
}
return context;
}

View File

@ -1,67 +0,0 @@
/*
========================================
File: frontend/features/map/components/overlay-system/overlay-dock.tsx
========================================
*/
"use client";
import React from "react";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { useOverlay } from "./overlay-context";
import { cn } from "@/lib/utils";
interface OverlayDockProps {
position?: "bottom" | "right" | "left" | "top"; // Added more positions
className?: string;
}
export function OverlayDock({ position = "bottom", className }: OverlayDockProps) {
const { overlays, toggleOverlay } = useOverlay();
// Filter overlays that have icons defined (and potentially are registered)
const dockableOverlays = Object.values(overlays).filter((overlay) => overlay.icon);
// No need to render if there are no dockable overlays
if (dockableOverlays.length === 0) return null;
// Define CSS classes for different positions
const positionClasses = {
bottom: "fixed bottom-4 left-1/2 -translate-x-1/2 flex flex-row gap-2 z-50",
right: "fixed right-4 top-1/2 -translate-y-1/2 flex flex-col gap-2 z-50",
left: "fixed left-4 top-1/2 -translate-y-1/2 flex flex-col gap-2 z-50",
top: "fixed top-4 left-1/2 -translate-x-1/2 flex flex-row gap-2 z-50",
};
const tooltipSide = {
bottom: "top",
top: "bottom",
left: "right",
right: "left",
} as const;
return (
<TooltipProvider>
<div className={cn(positionClasses[position], className)}>
{dockableOverlays.map((overlay) => (
<div key={overlay.id}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={overlay.isOpen && !overlay.isMinimized ? "default" : "outline"} // Highlight if open and not minimized
size="icon"
className="h-10 w-10 rounded-full bg-background/90 backdrop-blur-xs shadow-md"
onClick={() => toggleOverlay(overlay.id)}>
{overlay.icon}
</Button>
</TooltipTrigger>
<TooltipContent side={tooltipSide[position]}>
{overlay.isOpen ? `Hide ${overlay.title}` : `Show ${overlay.title}`}
</TooltipContent>
</Tooltip>
</div>
))}
</div>
</TooltipProvider>
);
}

View File

@ -1,219 +0,0 @@
/*
========================================
File: frontend/features/map/components/overlay-system/overlay.tsx
========================================
*/
"use client";
import React, { useEffect, useState, useRef, useCallback } from "react";
import { X, Minimize2, Maximize2, Move } from "lucide-react";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { useOverlay, type OverlayId, type OverlayPosition } from "./overlay-context";
interface OverlayProps {
id: OverlayId;
title: string;
icon?: React.ReactNode;
initialPosition?: OverlayPosition;
initialIsOpen?: boolean;
className?: string;
children: React.ReactNode;
onClose?: () => void;
showMinimize?: boolean;
width?: string;
height?: string; // Can be 'auto' or specific value like '400px'
maxHeight?: string; // e.g., '80vh'
}
export function Overlay({
id,
title,
icon,
initialPosition = "bottom-right",
initialIsOpen = false,
className,
children,
onClose,
showMinimize = true,
width = "350px",
height = "auto",
maxHeight = "80vh", // Default max height
}: OverlayProps) {
const {
overlays,
registerOverlay,
unregisterOverlay,
closeOverlay,
minimizeOverlay,
maximizeOverlay,
bringToFront,
// Add setPosition if dragging is implemented
} = useOverlay();
const overlayRef = useRef<HTMLDivElement>(null);
// State for dragging logic (Optional, basic example commented out)
// const [isDragging, setIsDragging] = useState(false);
// const [offset, setOffset] = useState({ x: 0, y: 0 });
// Register overlay on mount
useEffect(() => {
registerOverlay(id, {
title,
icon,
position: initialPosition,
isOpen: initialIsOpen,
// Add initial zIndex if needed, otherwise context handles it
});
// Unregister on unmount
return () => unregisterOverlay(id);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, registerOverlay, unregisterOverlay]); // Only run once on mount/unmount
// Get the current state of this overlay
const overlay = overlays[id];
// --- Optional Dragging Logic ---
// const handleMouseDown = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
// if (!overlayRef.current) return;
// bringToFront(id);
// setIsDragging(true);
// const rect = overlayRef.current.getBoundingClientRect();
// setOffset({
// x: e.clientX - rect.left,
// y: e.clientY - rect.top,
// });
// // Prevent text selection during drag
// e.preventDefault();
// }, [bringToFront, id]);
// const handleMouseMove = useCallback((e: MouseEvent) => {
// if (!isDragging || !overlayRef.current) return;
// overlayRef.current.style.left = `${e.clientX - offset.x}px`;
// overlayRef.current.style.top = `${e.clientY - offset.y}px`;
// // Remove fixed positioning classes if dragging manually
// overlayRef.current.classList.remove(...Object.values(positionClasses));
// }, [isDragging, offset]);
// const handleMouseUp = useCallback(() => {
// if (isDragging) {
// setIsDragging(false);
// // Optional: Snap to edge or update position state in context
// }
// }, [isDragging]);
// useEffect(() => {
// if (isDragging) {
// window.addEventListener("mousemove", handleMouseMove);
// window.addEventListener("mouseup", handleMouseUp);
// } else {
// window.removeEventListener("mousemove", handleMouseMove);
// window.removeEventListener("mouseup", handleMouseUp);
// }
// return () => {
// window.removeEventListener("mousemove", handleMouseMove);
// window.removeEventListener("mouseup", handleMouseUp);
// };
// }, [isDragging, handleMouseMove, handleMouseUp]);
// --- End Optional Dragging Logic ---
// If the overlay isn't registered yet or isn't open, don't render anything
if (!overlay || !overlay.isOpen) return null;
const handleCloseClick = (e: React.MouseEvent) => {
e.stopPropagation(); // Prevent triggering bringToFront if clicking close
closeOverlay(id);
if (onClose) onClose();
};
const handleMinimizeClick = (e: React.MouseEvent) => {
e.stopPropagation();
minimizeOverlay(id);
};
const handleMaximizeClick = (e: React.MouseEvent) => {
e.stopPropagation();
maximizeOverlay(id);
};
const handleHeaderMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
bringToFront(id);
// handleMouseDown(e); // Uncomment if implementing dragging
};
// Define position classes based on the current state
const positionClasses = {
"top-left": "top-4 left-4",
"top-right": "top-4 right-4",
"bottom-left": "bottom-4 left-4",
"bottom-right": "bottom-4 right-4",
center: "top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2",
};
// Render minimized state in the dock (handled by OverlayDock now)
if (overlay.isMinimized) {
// Minimized state is now handled by the OverlayDock component based on context state
// This component only renders the full overlay or nothing.
return null;
}
// Render full overlay
return (
<div
ref={overlayRef}
className={cn(
"fixed z-10", // z-index is managed by inline style
positionClasses[overlay.position], // Apply position classes
// Add transition for position changes if needed: 'transition-all duration-300 ease-out'
className
)}
style={{ zIndex: overlay.zIndex }} // Apply dynamic z-index
onClick={() => bringToFront(id)} // Bring to front on any click within the overlay
aria-labelledby={`${id}-title`}
role="dialog" // Use appropriate role
>
<Card
className={cn(
"shadow-lg bg-card/95 backdrop-blur-xs border border-border/50 overflow-hidden flex flex-col", // Added flex flex-col
className
)}
style={{
width,
height, // Use height directly
maxHeight, // Use maxHeight
maxWidth: "calc(100vw - 32px)", // Prevent overlay exceeding viewport width
}}>
{/* Make header draggable */}
<CardHeader
className="pb-2 flex flex-row items-center justify-between cursor-move shrink-0" // Added shrink-0
// onMouseDown={handleHeaderMouseDown} // Uncomment if implementing dragging
>
<CardTitle id={`${id}-title`} className="text-sm font-medium flex items-center gap-2">
{icon && <span className="text-primary">{icon}</span>}
{title}
{/* <Move className="h-3 w-3 text-muted-foreground ml-1 cursor-move" /> */} {/* Optional move icon */}
</CardTitle>
<div className="flex items-center gap-1">
{showMinimize && (
<Button
variant="ghost"
size="icon"
className="h-6 w-6 p-0"
onClick={handleMinimizeClick}
title="Minimize">
<Minimize2 className="h-4 w-4" />
</Button>
)}
<Button variant="ghost" size="icon" className="h-6 w-6 p-0" onClick={handleCloseClick} title="Close">
<X className="h-4 w-4" />
</Button>
</div>
</CardHeader>
{/* Ensure content area takes remaining space and scrolls if needed */}
<CardContent className="p-0 flex-1 min-h-0 overflow-auto">{children}</CardContent>
</Card>
</div>
);
}

View File

@ -1,8 +0,0 @@
/*
========================================
File: frontend/features/map/components/property-filters.tsx
========================================
*/
// This component seems redundant with filters-overlay.tsx.
// Assuming it's removed or its logic is integrated into filters-overlay.tsx.
// If needed, its structure would be similar to filters-overlay.tsx but maybe without the <Overlay> wrapper.

View File

@ -1,38 +0,0 @@
/*
========================================
File: frontend/features/map/hooks/useMapInteractions.ts (NEW - Dummy)
========================================
*/
import { useState, useCallback } from "react";
import type { MapLocation } from "../types";
/**
* DUMMY Hook: Manages map interaction state like selected markers, zoom level etc.
*/
export function useMapInteractions(initialLocation: MapLocation) {
const [currentLocation, setCurrentLocation] = useState<MapLocation>(initialLocation);
const [selectedMarkerId, setSelectedMarkerId] = useState<string | null>(null);
const handleMapMove = useCallback((newLocation: MapLocation) => {
console.log("DUMMY Hook: Map moved to", newLocation);
setCurrentLocation(newLocation);
}, []);
const handleMarkerClick = useCallback((markerId: string) => {
console.log("DUMMY Hook: Marker clicked", markerId);
setSelectedMarkerId(markerId);
// Potentially fetch details for this marker via API
}, []);
const clearSelection = useCallback(() => {
setSelectedMarkerId(null);
}, []);
return {
currentLocation,
selectedMarkerId,
handleMapMove,
handleMarkerClick,
clearSelection,
};
}

View File

@ -1,41 +0,0 @@
/*
========================================
File: frontend/features/map/types/index.ts (NEW - Dummy)
========================================
*/
// Types specific only to the Map feature
export interface MapBounds {
north: number;
south: number;
east: number;
west: number;
}
export interface MapLocation {
lat: number;
lng: number;
name?: string;
zoom?: number;
}
// Example type for map layer configuration
export interface MapLayerConfig {
id: string;
name: string;
url: string; // e.g., Tile server URL template
type: "raster" | "vector" | "geojson";
visible: boolean;
opacity?: number;
}
// Example type for data displayed on the map
export interface MapPropertyData {
id: string;
coordinates: [number, number]; // [lng, lat]
price: number;
type: string;
}
// Re-export relevant shared types if needed for convenience
// export type { PointOfInterest } from '@/types/api';

View File

@ -1,26 +0,0 @@
/*
========================================
File: frontend/features/map/utils/mapHelpers.ts (NEW - Dummy)
========================================
*/
import type { MapBounds, MapLocation } from "../types";
/**
* DUMMY Utility: Calculates the center of given map bounds.
*/
export function calculateBoundsCenter(bounds: MapBounds): MapLocation {
const centerLat = (bounds.north + bounds.south) / 2;
const centerLng = (bounds.east + bounds.west) / 2;
console.log("DUMMY Util: Calculating center for bounds:", bounds);
return { lat: centerLat, lng: centerLng };
}
/**
* DUMMY Utility: Formats coordinates for display.
*/
export function formatCoords(location: MapLocation): string {
return `${location.lat.toFixed(4)}, ${location.lng.toFixed(4)}`;
}
// Add other map-specific utility functions here

View File

@ -1,51 +0,0 @@
/*
========================================
File: frontend/features/model-explanation/api/explanationApi.ts (NEW - Dummy)
========================================
*/
import apiClient from "@/services/apiClient";
import type { APIResponse } from "@/types/api";
import type { ModelExplanationData, FeatureImportance } from "../types";
/**
* DUMMY: Fetches data needed for the model explanation page.
*/
export async function fetchModelExplanation(propertyId: string): Promise<APIResponse<ModelExplanationData>> {
console.log(`DUMMY API: Fetching model explanation for property ID: ${propertyId}`);
// return apiClient.get<ModelExplanationData>(`/properties/${propertyId}/explanation`);
// Simulate response
await new Promise((resolve) => setTimeout(resolve, 700));
const dummyExplanation: ModelExplanationData = {
propertyDetails: {
address: `Dummy Property ${propertyId}`,
type: "Condo",
size: 120,
bedrooms: 2,
bathrooms: 2,
age: 3,
floor: 10,
amenities: ["Pool", "Gym"],
predictedPrice: 12500000,
},
similarProperties: [
{ address: "Comp 1", price: 12000000, size: 115, age: 4 },
{ address: "Comp 2", price: 13500000, size: 130, age: 2 },
],
features: [
{ name: "Location", importance: 40, value: "Near BTS", impact: "positive" },
{ name: "Size", importance: 30, value: "120 sqm", impact: "positive" },
{ name: "Age", importance: 15, value: "3 years", impact: "neutral" },
{ name: "Amenities", importance: 10, value: "Pool, Gym", impact: "positive" },
{ name: "Floor", importance: 5, value: "10th", impact: "positive" },
],
environmentalFactors: {
floodRisk: "low",
airQuality: "moderate",
noiseLevel: "low",
},
confidence: 0.91,
priceRange: { lower: 11800000, upper: 13200000 },
};
return { success: true, data: dummyExplanation };
}

View File

@ -1,71 +0,0 @@
/*
========================================
File: frontend/features/model-explanation/components/feature-importance-chart.tsx
========================================
*/
"use client";
import { useTheme } from "next-themes";
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Cell } from "@/components/ui/chart"; // Using shared ui chart components
import type { FeatureImportance } from "../types"; // Feature specific type
interface FeatureImportanceChartProps {
features: FeatureImportance[];
}
export function FeatureImportanceChart({ features }: FeatureImportanceChartProps) {
const { theme } = useTheme();
const isDark = theme === "dark";
// Sort features by importance for consistent display
const sortedFeatures = [...features].sort((a, b) => b.importance - a.importance);
// Define colors based on impact
const getBarColor = (impact: "positive" | "negative" | "neutral") => {
if (impact === "positive") return "#10b981"; // Green
if (impact === "negative") return "#ef4444"; // Red
return "#f59e0b"; // Amber/Yellow for neutral
};
return (
<ResponsiveContainer width="100%" height="100%">
<BarChart data={sortedFeatures} layout="vertical" margin={{ top: 5, right: 30, left: 80, bottom: 5 }}>
<CartesianGrid
strokeDasharray="3 3"
horizontal={true}
vertical={false} // Typically vertical grid lines are less useful for horizontal bar charts
stroke={isDark ? "#374151" : "#e5e7eb"}
/>
<XAxis
type="number"
domain={[0, 100]} // Assuming importance is a percentage
tickFormatter={(value) => `${value}%`}
stroke={isDark ? "#9ca3af" : "#6b7280"}
/>
<YAxis
dataKey="name"
type="category"
width={80} // Adjust width based on longest label
stroke={isDark ? "#9ca3af" : "#6b7280"}
tick={{ fontSize: 12 }}
/>
<Tooltip
formatter={(value: number) => [`${value.toFixed(1)}%`, "Importance"]}
labelFormatter={(label: string) => `Feature: ${label}`} // Show feature name in tooltip
contentStyle={{
backgroundColor: isDark ? "#1f2937" : "white",
borderRadius: "0.375rem",
border: isDark ? "1px solid #374151" : "1px solid #e2e8f0",
fontSize: "0.75rem",
color: isDark ? "#e5e7eb" : "#1f2937",
}}
/>
<Bar dataKey="importance" radius={[0, 4, 4, 0]}>
{sortedFeatures.map((entry, index) => (
<Cell key={`cell-${index}`} fill={getBarColor(entry.impact)} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
);
}

View File

@ -1,94 +0,0 @@
/*
========================================
File: frontend/features/model-explanation/components/price-comparison-chart.tsx
========================================
*/
"use client";
import { useTheme } from "next-themes";
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Legend,
Cell,
} from "@/components/ui/chart"; // Using shared ui chart components
import type { ComparableProperty, PropertyBaseDetails } from "../types"; // Feature specific types
interface PriceComparisonChartProps {
property: PropertyBaseDetails & { name: string }; // Add name for the primary property
comparisons: ComparableProperty[];
}
export function PriceComparisonChart({ property, comparisons }: PriceComparisonChartProps) {
const { theme } = useTheme();
const isDark = theme === "dark";
// Combine property and comparisons for chart data
// Ensure the property being explained is included and identifiable
const data = [
{ ...property }, // Keep all details for tooltip if needed
...comparisons.map((c) => ({ ...c, name: c.address })), // Use address as name for comparisons
];
// Format the price for display
const formatPrice = (value: number) => {
return new Intl.NumberFormat("th-TH", {
style: "currency",
currency: "THB",
notation: "compact", // Use compact notation like 15M
minimumFractionDigits: 0,
maximumFractionDigits: 1,
}).format(value);
};
// Custom tooltip content
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
const data = payload[0].payload; // Get the data point for this bar
return (
<div className="p-2 bg-background border rounded shadow-lg text-xs">
<p className="font-bold">{label}</p>
<p>Price: {formatPrice(data.price)}</p>
<p>Size: {data.size} sqm</p>
<p>Age: {data.age} years</p>
</div>
);
}
return null;
};
return (
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data} margin={{ top: 5, right: 10, left: 40, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke={isDark ? "#374151" : "#e5e7eb"} />
<XAxis dataKey="name" stroke={isDark ? "#9ca3af" : "#6b7280"} fontSize={10} interval={0} />
<YAxis
tickFormatter={(value) => formatPrice(value)}
stroke={isDark ? "#9ca3af" : "#6b7280"}
fontSize={10}
width={40}
/>
<Tooltip
cursor={{ fill: isDark ? "rgba(107, 114, 128, 0.2)" : "rgba(209, 213, 219, 0.4)" }} // Subtle hover effect
content={<CustomTooltip />} // Use custom tooltip
wrapperStyle={{ zIndex: 100 }} // Ensure tooltip is on top
/>
{/* <Legend /> // Legend might be redundant if XAxis labels are clear */}
<Bar dataKey="price" name="Price" radius={[4, 4, 0, 0]}>
{data.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={entry.name === property.name ? "#3b82f6" : "#6b7280"} // Highlight the main property
fillOpacity={entry.name === property.name ? 1 : 0.7}
/>
))}
</Bar>
</BarChart>
</ResponsiveContainer>
);
}

View File

@ -1,50 +0,0 @@
/*
========================================
File: frontend/features/model-explanation/types/index.ts (NEW - Dummy)
========================================
*/
// Types specific to the Model Explanation feature
export interface FeatureImportance {
name: string;
importance: number; // e.g., percentage 0-100
value: string | number; // The actual value for the property being explained
impact: "positive" | "negative" | "neutral";
}
export interface ComparableProperty {
address: string;
price: number;
size: number;
age: number;
// Add other relevant comparison fields if needed
}
export interface PropertyBaseDetails {
address: string;
type: string;
size: number;
bedrooms?: number;
bathrooms?: number;
age: number;
floor?: number;
amenities?: string[];
predictedPrice: number;
}
export interface EnvironmentalFactors {
floodRisk: "low" | "moderate" | "high" | "unknown";
airQuality: "good" | "moderate" | "poor" | "unknown";
noiseLevel: "low" | "moderate" | "high" | "unknown";
// Add other factors like proximity scores etc.
}
export interface ModelExplanationData {
propertyDetails: PropertyBaseDetails;
similarProperties: ComparableProperty[];
features: FeatureImportance[];
environmentalFactors: EnvironmentalFactors;
confidence: number; // e.g., 0.92 for 92%
priceRange: { lower: number; upper: number };
}

View File

@ -1,37 +1,19 @@
/*
========================================
File: frontend/hooks/use-mobile.tsx
========================================
*/
import * as React from "react";
import * as React from "react"
const MOBILE_BREAKPOINT = 768; // Standard md breakpoint
const MOBILE_BREAKPOINT = 768
export function useIsMobile(): boolean {
// Initialize state based on current window size (or undefined if SSR)
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
typeof window !== "undefined" ? window.innerWidth < MOBILE_BREAKPOINT : undefined
);
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
React.useEffect(() => {
// Ensure this runs only client-side
if (typeof window === "undefined") {
return;
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange)
}, [])
const handleResize = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
// Set initial state correctly after mount
handleResize();
window.addEventListener("resize", handleResize);
// Cleanup listener on unmount
return () => window.removeEventListener("resize", handleResize);
}, []); // Empty dependency array ensures this runs only once on mount and cleanup on unmount
// Return false during SSR or initial client render before effect runs
return isMobile ?? false;
return !!isMobile
}

View File

@ -1,106 +1,106 @@
/*
========================================
File: frontend/hooks/use-toast.ts
========================================
*/
"use client";
"use client"
// Inspired by react-hot-toast library
import * as React from "react";
// Import types from the actual Toast component location
import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
import * as React from "react"
const TOAST_LIMIT = 1; // Show only one toast at a time
const TOAST_REMOVE_DELAY = 1000000; // A very long time (effectively manual dismiss only)
import type {
ToastActionElement,
ToastProps,
} from "@/components/ui/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
id: string;
title?: React.ReactNode;
description?: React.ReactNode;
action?: ToastActionElement;
};
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const;
} as const
let count = 0;
let count = 0
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER;
return count.toString();
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
}
type ActionType = typeof actionTypes;
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType["ADD_TOAST"];
toast: ToasterToast;
type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"];
toast: Partial<ToasterToast>;
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"];
toastId?: ToasterToast["id"];
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"];
toastId?: ToasterToast["id"];
};
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
interface State {
toasts: ToasterToast[];
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return;
return
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId);
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
});
}, TOAST_REMOVE_DELAY);
})
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout);
};
toastTimeouts.set(toastId, timeout)
}
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
// Slice ensures the limit is enforced
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
};
}
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
};
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
case "DISMISS_TOAST": {
const { toastId } = action;
const { toastId } = action
// Side effect: schedule removal for dismissed toasts
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId);
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id);
});
addToRemoveQueue(toast.id)
})
}
return {
@ -109,48 +109,48 @@ export const reducer = (state: State, action: Action): State => {
t.id === toastId || toastId === undefined
? {
...t,
open: false, // Trigger the close animation
open: false,
}
: t
),
};
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [], // Remove all toasts
};
toasts: [],
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
};
}
}
};
const listeners: Array<(state: State) => void> = [];
let memoryState: State = { toasts: [] }; // In-memory state
function dispatch(action: Action) {
memoryState = reducer(memoryState, action);
listeners.forEach((listener) => {
listener(memoryState);
});
}
type Toast = Omit<ToasterToast, "id">;
const listeners: Array<(state: State) => void> = []
let memoryState: State = { toasts: [] }
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
}
type Toast = Omit<ToasterToast, "id">
function toast({ ...props }: Toast) {
const id = genId();
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
});
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
dispatch({
type: "ADD_TOAST",
@ -159,37 +159,36 @@ function toast({ ...props }: Toast) {
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss(); // Ensure dismiss is called when the toast closes itself
if (!open) dismiss()
},
},
});
})
return {
id: id,
dismiss,
update,
};
}
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState);
const [state, setState] = React.useState<State>(memoryState)
React.useEffect(() => {
listeners.push(setState);
listeners.push(setState)
return () => {
// Clean up listener
const index = listeners.indexOf(setState);
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1);
listeners.splice(index, 1)
}
};
}, [state]); // Only re-subscribe if state instance changes (it shouldn't)
}
}, [state])
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
};
}
}
export { useToast, toast };
export { useToast, toast }

BIN
frontend/public/map.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 KiB

View File

@ -1,106 +1,135 @@
/*
========================================
File: frontend/services/apiClient.ts (NEW - Dummy)
========================================
*/
import type { APIResponse } from "@/types/api"; // Import shared response type
/* === src/services/apiClient.ts === */
/**
* API Client - Dummy Implementation
*
* This provides a basic structure for making API calls.
* - It includes an Authorization header in each request (assuming token-based auth).
* - Replace `getAuthToken` with your actual token retrieval logic.
* - Consider using libraries like axios or ky for more robust features in a real app.
*/
// --- Dummy Auth Token ---
// In a real app, this would come from localStorage, context, or a state manager after login
const DUMMY_AUTH_TOKEN = "Bearer dummy-jwt-token-12345";
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "/api/v1"; // Example API base URL
/**
* Base function for making API requests.
* Includes dummy authorization header.
* Retrieves the authentication token.
* Replace this with your actual implementation (e.g., from localStorage, context, state management).
* @returns {string | null} The auth token or null if not found.
*/
async function fetchApi<T = any>(endpoint: string, options: RequestInit = {}): Promise<APIResponse<T>> {
const url = `${process.env.NEXT_PUBLIC_API_URL || "/api/v1"}${endpoint}`;
const getAuthToken = (): string | null => {
// Dummy implementation: Replace with your actual logic
if (typeof window !== "undefined") {
// Example: return localStorage.getItem("authToken");
// For dummy purposes, returning a placeholder token
return "dummy-auth-token-12345";
}
return null;
};
const defaultHeaders: HeadersInit = {
"Content-Type": "application/json",
Authorization: DUMMY_AUTH_TOKEN, // Add dummy token here
};
interface FetchOptions extends RequestInit {
params?: Record<string, string | number | boolean>; // For query parameters
/** If true, Content-Type header will not be set (e.g., for FormData) */
skipContentType?: boolean;
}
const config: RequestInit = {
...options,
headers: {
...defaultHeaders,
...options.headers,
},
};
/**
* Generic fetch function for API calls.
* @template T The expected response type.
* @param {string} endpoint The API endpoint (e.g., '/users').
* @param {FetchOptions} options Fetch options (method, body, headers, params).
* @returns {Promise<T>} The response data.
*/
async function apiClient<T>(endpoint: string, options: FetchOptions = {}): Promise<T> {
const { params, headers: customHeaders, skipContentType, body, ...fetchOptions } = options;
const token = getAuthToken();
console.log(`DUMMY API Client: Requesting ${config.method || "GET"} ${url}`);
// Construct URL
let url = `${API_BASE_URL}${endpoint}`;
if (params) {
const queryParams = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
// Ensure value exists
queryParams.append(key, String(value));
}
});
const queryString = queryParams.toString();
if (queryString) {
url += `?${queryString}`;
}
}
// Simulate network delay
await new Promise((resolve) => setTimeout(resolve, Math.random() * 300 + 100));
// Prepare headers
const headers = new Headers(customHeaders);
if (!skipContentType && body && !(body instanceof FormData)) {
if (!headers.has("Content-Type")) {
headers.set("Content-Type", "application/json");
}
}
if (token && !headers.has("Authorization")) {
headers.set("Authorization", `Bearer ${token}`);
}
// Prepare body - Stringify JSON unless it's FormData
let processedBody = body;
if (body && headers.get("Content-Type") === "application/json" && typeof body !== "string") {
processedBody = JSON.stringify(body);
}
try {
// --- Simulate API Responses ---
// You can add more sophisticated simulation based on the endpoint
if (endpoint.includes("error")) {
console.warn(`DUMMY API Client: Simulating error for ${url}`);
return { success: false, error: "Simulated server error" };
const response = await fetch(url, {
...fetchOptions,
headers,
body: processedBody,
});
// Check for successful response
if (!response.ok) {
let errorData = { message: `HTTP error! status: ${response.status} ${response.statusText}` };
try {
// Try to parse specific error details from the API response
const jsonError = await response.json();
errorData = { ...errorData, ...jsonError };
} catch (e) {
// Ignore if the error response is not JSON
}
console.error("API Error:", errorData);
throw new Error(errorData.message);
}
if (config.method === "POST" || config.method === "PUT" || config.method === "DELETE") {
// Simulate simple success for modification requests
console.log(`DUMMY API Client: Simulating success for ${config.method} ${url}`);
return { success: true, data: { message: "Operation successful (simulated)" } as T };
// Handle responses with no content (e.g., 204 No Content)
if (response.status === 204) {
// For 204, there's no body, return undefined or null as appropriate for T
// Using 'as T' assumes the caller expects undefined/null for no content
return undefined as T;
}
// Simulate success for GET requests (return empty array or specific data based on endpoint)
console.log(`DUMMY API Client: Simulating success for GET ${url}`);
let responseData: any = [];
if (endpoint.includes("/map/pois")) responseData = []; // Let feature api add dummy data
if (endpoint.includes("/properties")) responseData = []; // Example
// Add more specific endpoint data simulation if needed
return { success: true, data: responseData as T };
// --- Real Fetch Logic (keep commented for dummy) ---
// const response = await fetch(url, config);
//
// if (!response.ok) {
// let errorData;
// try {
// errorData = await response.json();
// } catch (e) {
// errorData = { detail: response.statusText || "Unknown error" };
// }
// const errorMessage = errorData?.detail || `HTTP error ${response.status}`;
// console.error(`API Error (${response.status}) for ${url}:`, errorMessage);
// return { success: false, error: errorMessage, details: errorData };
// }
//
// // Handle cases with no content
// if (response.status === 204) {
// return { success: true, data: null as T };
// }
//
// const data: T = await response.json();
// return { success: true, data };
// --- End Real Fetch Logic ---
// Parse the JSON response body for other successful responses
const data: T = await response.json();
return data;
} catch (error) {
console.error(`Network or other error for ${url}:`, error);
const errorMessage = error instanceof Error ? error.message : "Network error or invalid response";
return { success: false, error: errorMessage };
console.error("API Client Fetch Error:", error);
// Re-throw the error for handling by the calling code (e.g., React Query, component)
throw error;
}
}
// --- Convenience Methods ---
const apiClient = {
get: <T = any>(endpoint: string, options?: RequestInit) => fetchApi<T>(endpoint, { ...options, method: "GET" }),
// --- Specific HTTP Method Helpers ---
post: <T = any>(endpoint: string, body: any, options?: RequestInit) =>
fetchApi<T>(endpoint, { ...options, method: "POST", body: JSON.stringify(body) }),
export const api = {
get: <T>(endpoint: string, options?: FetchOptions) => apiClient<T>(endpoint, { ...options, method: "GET" }),
put: <T = any>(endpoint: string, body: any, options?: RequestInit) =>
fetchApi<T>(endpoint, { ...options, method: "PUT", body: JSON.stringify(body) }),
post: <T>(endpoint: string, body: any, options?: FetchOptions) =>
apiClient<T>(endpoint, { ...options, method: "POST", body }),
delete: <T = any>(endpoint: string, options?: RequestInit) => fetchApi<T>(endpoint, { ...options, method: "DELETE" }),
put: <T>(endpoint: string, body: any, options?: FetchOptions) =>
apiClient<T>(endpoint, { ...options, method: "PUT", body }),
patch: <T = any>(endpoint: string, body: any, options?: RequestInit) =>
fetchApi<T>(endpoint, { ...options, method: "PATCH", body: JSON.stringify(body) }),
patch: <T>(endpoint: string, body: any, options?: FetchOptions) =>
apiClient<T>(endpoint, { ...options, method: "PATCH", body }),
delete: <T>(endpoint: string, options?: FetchOptions) => apiClient<T>(endpoint, { ...options, method: "DELETE" }),
};
export default apiClient;
export default api;

View File

@ -1,41 +1,175 @@
/*
========================================
File: frontend/types/api.ts (NEW - Dummy Shared Types)
========================================
*/
/** Generic API Response Structure */
export type APIResponse<T> = { success: true; data: T } | { success: false; error: string; details?: any };
/** Represents a Point of Interest (can be property, cafe, etc.) */
export interface PointOfInterest {
id: string;
lat: number;
lng: number;
name: string;
type: string; // e.g., 'property', 'cafe', 'park', 'station'
price?: number; // Optional: for properties
rating?: number; // Optional: for amenities
// Add other relevant shared fields
/* === src/types/api.ts === */
/**
* General API Response Types
*/
export interface PaginatedResponse<T> {
count: number;
next: string | null;
previous: string | null;
results: T[];
}
/** Basic Property Information */
export interface PropertySummary {
id: string;
address: string;
lat: number;
lng: number;
export interface ApiErrorResponse {
message: string;
details?: Record<string, any> | string[]; // Can be an object or array of strings
code?: string; // Optional error code
}
/**
* Represents a generic successful API operation, possibly with no specific data.
*/
export interface SuccessResponse {
success: boolean;
message?: string;
}
/**
* Property Data Types
*/
export type PropertyType = "Condominium" | "House" | "Townhouse" | "Land" | "Apartment" | "Other";
export type OwnershipType = "Freehold" | "Leasehold";
export type FurnishingStatus = "Unfurnished" | "Partly Furnished" | "Fully Furnished";
export interface PriceHistoryEntry {
date: string; // Consider using Date object after parsing
price: number;
type: string; // e.g., 'Condo', 'House'
size?: number; // sqm
}
/** User representation */
export interface User {
export interface MarketTrendData {
areaGrowthPercentage: number;
similarPropertiesCount: number;
averagePrice: number;
averagePricePerSqm: number;
priceTrend?: "Rising" | "Falling" | "Stable"; // Optional trend indicator
}
export interface EnvironmentalFactor {
type: "Flood Risk" | "Air Quality" | "Noise Level";
level: "Low" | "Moderate" | "High" | "Good" | "Fair" | "Poor"; // Adjust levels as needed
details?: string;
}
export interface NearbyFacility {
name: string;
type: "Transport" | "Shopping" | "Park" | "Hospital" | "School" | "Other";
distanceMeters: number;
}
export interface Property {
id: string;
username: string;
email: string;
// Add roles or other relevant fields
title: string;
description: string;
price: number;
currency?: string; // e.g., 'THB', 'USD'
location: {
address: string;
city: string;
district?: string;
postalCode?: string;
latitude?: number;
longitude?: number;
};
bedrooms: number;
bathrooms: number;
areaSqm: number;
propertyType: PropertyType;
images: string[]; // Array of image URLs
yearBuilt?: number;
floorLevel?: number;
totalFloors?: number;
parkingSpaces?: number;
furnishing: FurnishingStatus;
ownership: OwnershipType;
availabilityDate?: string; // Consider using Date object
isPremium: boolean;
features: string[];
amenities: string[];
priceHistory?: PriceHistoryEntry[];
marketTrends?: MarketTrendData;
environmentalFactors?: EnvironmentalFactor[];
nearbyFacilities?: NearbyFacility[];
agent?: {
// Optional agent info
id: string;
name: string;
contact: string;
};
dataSource?: string; // Origin of the data
createdAt: string; // ISO 8601 date string
updatedAt: string; // ISO 8601 date string
}
// Add other globally shared types (e.g., PipelineStatus, DataSourceType if needed FE side)
/**
* Data Pipeline Types
*/
export type PipelineStatus = "active" | "paused" | "error" | "idle" | "running";
export type SourceType = "Website" | "API" | "File Upload" | "Database";
export interface DataSource {
id: string;
name: string;
type: SourceType;
url?: string; // For Website/API
lastUpdated: string;
recordCount: number;
status: "connected" | "error" | "pending";
}
export interface DataPipeline {
id: string;
name: string;
description: string;
status: PipelineStatus;
lastRunAt: string | null;
nextRunAt: string | null;
runFrequency: string; // e.g., "Daily", "Hourly", "Manual"
sources: DataSource[];
totalRecords: number;
aiPowered: boolean;
errorDetails?: string;
createdAt: string;
updatedAt: string;
}
/**
* Model Types
*/
export type ModelType = "Regression" | "Neural Network" | "Geospatial" | "Time Series" | "Ensemble" | "Classification";
export type ModelStatus = "active" | "inactive" | "training" | "error" | "pending";
export interface Model {
id: string;
name: string;
description?: string;
type: ModelType;
version: string;
status: ModelStatus;
isSystemModel: boolean; // Distinguishes system models from custom ones
dataSourceId?: string; // ID of the DataPipeline used for training (if custom)
dataSourceName?: string; // Name of the source for display
hyperparameters: Record<string, string | number | boolean>;
performanceMetrics?: Record<string, number>; // e.g., { accuracy: 0.92, mae: 150000 }
lastTrainedAt?: string;
createdAt: string;
updatedAt: string;
}
/**
* Map Related Types
*/
export interface MapLayer {
id: string;
name: string;
type: "property" | "heatmap" | "environmental";
isVisible: boolean;
dataUrl?: string; // URL to fetch layer data
style?: Record<string, any>; // Mapbox style properties
}
export interface MapViewState {
longitude: number;
latitude: number;
zoom: number;
pitch?: number;
bearing?: number;
}

View File

@ -1,12 +1,22 @@
/*
========================================
File: frontend/types/index.ts (NEW - Barrel File)
========================================
*/
/* === src/types/index.ts === */
// Barrel file for exporting shared types
// Re-export shared types for easier importing
export * from "./api";
export * from "./user";
// You can add other shared types here or export from other files in this directory
// export * from './user';
// Example of another shared type definition
export interface SelectOption<T = string | number> {
value: T;
label: string;
icon?: React.ComponentType<{ className?: string }>; // Optional icon component
disabled?: boolean;
}
// Generic type for UI components requiring an icon
export interface IconProps {
className?: string;
}
// Add other shared types as the application grows
// export * from './settings';
// export * from './notifications';

56
frontend/types/user.ts Normal file
View File

@ -0,0 +1,56 @@
/* === src/types/user.ts === */
/**
* User Profile and Authentication Types
*/
export interface UserProfile {
id: string;
username: string;
email: string;
firstName?: string;
lastName?: string;
avatarUrl?: string; // URL to the user's avatar image
roles: UserRole[]; // Array of roles assigned to the user
preferences?: UserPreferences; // User-specific settings
isActive: boolean;
lastLogin?: string; // ISO 8601 date string
createdAt: string; // ISO 8601 date string
}
export type UserRole = "admin" | "analyst" | "viewer" | "data_manager"; // Example roles
export interface UserPreferences {
theme?: "light" | "dark" | "system";
defaultMapLocation?: {
latitude: number;
longitude: number;
zoom: number;
};
notifications?: {
pipelineSuccess?: boolean;
pipelineError?: boolean;
newReports?: boolean;
};
// Add other user-specific preferences
}
export interface AuthState {
user: UserProfile | null;
token: string | null; // The authentication token (e.g., JWT)
isAuthenticated: boolean;
isLoading: boolean; // Tracks loading state during auth checks/login/logout
error: string | null; // Stores any authentication errors
}
// Type for login credentials
export interface LoginCredentials {
emailOrUsername: string;
password?: string; // Password might not be needed for SSO flows
}
// Type for the response after successful login
export interface LoginResponse {
user: UserProfile;
token: string;
refreshToken?: string; // Optional refresh token
}