feat: Implement core application structure with new dashboard, settings, and help pages, and enhance opportunities management with persistence and filtering.
This commit is contained in:
132
app/(app)/dashboard/page.tsx
Normal file
132
app/(app)/dashboard/page.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
"use client"
|
||||
|
||||
import { useQuery } from "convex/react"
|
||||
import { api } from "@/convex/_generated/api"
|
||||
import { useProject } from "@/components/project-context"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
|
||||
export default function Page() {
|
||||
const { selectedProjectId } = useProject()
|
||||
const projects = useQuery(api.projects.getProjects)
|
||||
const analysis = useQuery(
|
||||
api.analyses.getLatestByProject,
|
||||
selectedProjectId ? { projectId: selectedProjectId as any } : "skip"
|
||||
)
|
||||
|
||||
const selectedProject = projects?.find((project) => project._id === selectedProjectId)
|
||||
const isLoading = selectedProjectId && analysis === undefined
|
||||
|
||||
if (!selectedProjectId && projects && projects.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-1 items-center justify-center p-10 text-center">
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-xl font-semibold">No projects yet</h2>
|
||||
<p className="text-muted-foreground">Complete onboarding to create your first project.</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex flex-1 items-center justify-center p-10 text-center text-muted-foreground">
|
||||
Loading analysis...
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!analysis) {
|
||||
return (
|
||||
<div className="flex flex-1 items-center justify-center p-10 text-center">
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-xl font-semibold">No analysis yet</h2>
|
||||
<p className="text-muted-foreground">Run onboarding to analyze a product for this project.</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col gap-6 p-4 lg:p-8">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-2xl font-semibold">{analysis.productName}</h1>
|
||||
{selectedProject?.name && (
|
||||
<Badge variant="outline">{selectedProject.name}</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-muted-foreground">{analysis.tagline}</p>
|
||||
<p className="max-w-3xl text-sm text-muted-foreground">{analysis.description}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-5">
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm">Features</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-2xl font-semibold">{analysis.features.length}</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm">Keywords</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-2xl font-semibold">{analysis.keywords.length}</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm">Personas</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-2xl font-semibold">{analysis.personas.length}</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm">Competitors</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-2xl font-semibold">{analysis.competitors.length}</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm">Use Cases</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-2xl font-semibold">{analysis.useCases.length}</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Top Features</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
{analysis.features.slice(0, 6).map((feature, index) => (
|
||||
<div key={`${feature.name}-${index}`} className="flex items-start gap-2">
|
||||
<span className="mt-1 h-2 w-2 rounded-full bg-primary" />
|
||||
<div>
|
||||
<p className="text-sm font-medium">{feature.name}</p>
|
||||
<p className="text-xs text-muted-foreground">{feature.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Top Problems Solved</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
{analysis.problemsSolved.slice(0, 6).map((problem, index) => (
|
||||
<div key={`${problem.problem}-${index}`} className="flex items-start gap-2">
|
||||
<span className="mt-1 h-2 w-2 rounded-full bg-primary" />
|
||||
<div>
|
||||
<p className="text-sm font-medium">{problem.problem}</p>
|
||||
<p className="text-xs text-muted-foreground">{problem.emotionalImpact}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
54
app/(app)/help/page.tsx
Normal file
54
app/(app)/help/page.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
"use client"
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
|
||||
export default function HelpPage() {
|
||||
return (
|
||||
<div className="flex flex-1 flex-col gap-6 p-4 lg:p-8">
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-2xl font-semibold">Help</h1>
|
||||
<p className="text-muted-foreground">Tips for getting the most out of Sanati.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Quickstart</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 text-sm text-muted-foreground">
|
||||
<p>1. Run onboarding with your product URL or manual details.</p>
|
||||
<p>2. Open Opportunities and pick platforms + strategies.</p>
|
||||
<p>3. Review matches, generate replies, and track status.</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Outreach Tips</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 text-sm text-muted-foreground">
|
||||
<p>Lead with empathy and context, not a hard pitch.</p>
|
||||
<p>Reference the specific problem the post mentions.</p>
|
||||
<p>Offer help or a quick walkthrough before asking for a call.</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Common Issues</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 text-sm text-muted-foreground">
|
||||
<div>
|
||||
<p className="font-medium text-foreground">Scrape fails</p>
|
||||
<p>Use manual input or try a different URL (homepage works best).</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-foreground">Search returns few results</p>
|
||||
<p>Enable Serper API key or broaden strategies in the search config.</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,22 +1,27 @@
|
||||
'use client'
|
||||
|
||||
import { Sidebar } from '@/components/sidebar'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import { AppSidebar } from "@/components/app-sidebar"
|
||||
import {
|
||||
SidebarInset,
|
||||
SidebarProvider,
|
||||
} from "@/components/ui/sidebar"
|
||||
import { ProjectProvider } from "@/components/project-context"
|
||||
|
||||
export default function AppLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const searchParams = useSearchParams()
|
||||
const productName = searchParams.get('product') || undefined
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-background">
|
||||
<Sidebar productName={productName} />
|
||||
<main className="flex-1 overflow-auto">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
<SidebarProvider>
|
||||
<ProjectProvider>
|
||||
<AppSidebar />
|
||||
<SidebarInset>
|
||||
<div className="flex min-h-svh flex-1 flex-col bg-background">
|
||||
{children}
|
||||
</div>
|
||||
</SidebarInset>
|
||||
</ProjectProvider>
|
||||
</SidebarProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useMutation, useQuery } from 'convex/react'
|
||||
import { api } from '@/convex/_generated/api'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Slider } from '@/components/ui/slider'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
|
||||
@@ -47,6 +50,7 @@ import {
|
||||
Eye,
|
||||
Copy
|
||||
} from 'lucide-react'
|
||||
import { useProject } from '@/components/project-context'
|
||||
import type {
|
||||
EnhancedProductAnalysis,
|
||||
Opportunity,
|
||||
@@ -66,6 +70,9 @@ const STRATEGY_INFO: Record<SearchStrategy, { name: string; description: string
|
||||
|
||||
export default function OpportunitiesPage() {
|
||||
const router = useRouter()
|
||||
const { selectedProjectId } = useProject()
|
||||
const upsertOpportunities = useMutation(api.opportunities.upsertBatch)
|
||||
const updateOpportunity = useMutation(api.opportunities.updateStatus)
|
||||
const [analysis, setAnalysis] = useState<EnhancedProductAnalysis | null>(null)
|
||||
const [platforms, setPlatforms] = useState<PlatformConfig[]>([])
|
||||
const [strategies, setStrategies] = useState<SearchStrategy[]>([
|
||||
@@ -81,6 +88,54 @@ export default function OpportunitiesPage() {
|
||||
const [selectedOpportunity, setSelectedOpportunity] = useState<Opportunity | null>(null)
|
||||
const [replyText, setReplyText] = useState('')
|
||||
const [stats, setStats] = useState<any>(null)
|
||||
const [searchError, setSearchError] = useState('')
|
||||
const [statusFilter, setStatusFilter] = useState('all')
|
||||
const [intentFilter, setIntentFilter] = useState('all')
|
||||
const [minScore, setMinScore] = useState(0)
|
||||
const [limit, setLimit] = useState(50)
|
||||
const [statusInput, setStatusInput] = useState('new')
|
||||
const [notesInput, setNotesInput] = useState('')
|
||||
const [tagsInput, setTagsInput] = useState('')
|
||||
|
||||
const latestAnalysis = useQuery(
|
||||
api.analyses.getLatestByProject,
|
||||
selectedProjectId ? { projectId: selectedProjectId as any } : 'skip'
|
||||
)
|
||||
|
||||
const savedOpportunities = useQuery(
|
||||
api.opportunities.listByProject,
|
||||
selectedProjectId
|
||||
? {
|
||||
projectId: selectedProjectId as any,
|
||||
status: statusFilter === 'all' ? undefined : statusFilter,
|
||||
intent: intentFilter === 'all' ? undefined : intentFilter,
|
||||
minScore: minScore > 0 ? minScore / 100 : undefined,
|
||||
limit,
|
||||
}
|
||||
: 'skip'
|
||||
)
|
||||
|
||||
const displayOpportunities = useMemo(() => {
|
||||
if (!savedOpportunities || savedOpportunities.length === 0) return opportunities
|
||||
return savedOpportunities.map((opp: any) => ({
|
||||
id: opp._id,
|
||||
title: opp.title,
|
||||
url: opp.url,
|
||||
snippet: opp.snippet,
|
||||
platform: opp.platform,
|
||||
source: opp.platform,
|
||||
relevanceScore: opp.relevanceScore,
|
||||
emotionalIntensity: 'low',
|
||||
intent: opp.intent,
|
||||
matchedKeywords: opp.matchedKeywords,
|
||||
matchedProblems: opp.matchedProblems,
|
||||
suggestedApproach: opp.suggestedApproach,
|
||||
softPitch: opp.softPitch,
|
||||
status: opp.status,
|
||||
notes: opp.notes,
|
||||
tags: opp.tags,
|
||||
})) as Opportunity[]
|
||||
}, [savedOpportunities, opportunities])
|
||||
|
||||
useEffect(() => {
|
||||
const stored = localStorage.getItem('productAnalysis')
|
||||
@@ -121,9 +176,11 @@ export default function OpportunitiesPage() {
|
||||
|
||||
const executeSearch = async () => {
|
||||
if (!analysis) return
|
||||
if (!selectedProjectId) return
|
||||
|
||||
setIsSearching(true)
|
||||
setOpportunities([])
|
||||
setSearchError('')
|
||||
|
||||
try {
|
||||
const config = {
|
||||
@@ -147,12 +204,34 @@ export default function OpportunitiesPage() {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
setOpportunities(data.data.opportunities)
|
||||
const mapped = data.data.opportunities.map((opp: Opportunity) => ({
|
||||
...opp,
|
||||
status: 'new',
|
||||
}))
|
||||
setOpportunities(mapped)
|
||||
setGeneratedQueries(data.data.queries)
|
||||
setStats(data.data.stats)
|
||||
|
||||
await upsertOpportunities({
|
||||
projectId: selectedProjectId as any,
|
||||
analysisId: latestAnalysis?._id,
|
||||
opportunities: mapped.map((opp: Opportunity) => ({
|
||||
url: opp.url,
|
||||
platform: opp.platform,
|
||||
title: opp.title,
|
||||
snippet: opp.snippet,
|
||||
relevanceScore: opp.relevanceScore,
|
||||
intent: opp.intent,
|
||||
suggestedApproach: opp.suggestedApproach,
|
||||
matchedKeywords: opp.matchedKeywords,
|
||||
matchedProblems: opp.matchedProblems,
|
||||
softPitch: opp.softPitch,
|
||||
})),
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('Search error:', error)
|
||||
setSearchError(error?.message || 'Search failed. Please try again.')
|
||||
} finally {
|
||||
setIsSearching(false)
|
||||
}
|
||||
@@ -174,6 +253,30 @@ export default function OpportunitiesPage() {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedOpportunity) {
|
||||
setStatusInput(selectedOpportunity.status || 'new')
|
||||
setNotesInput(selectedOpportunity.notes || '')
|
||||
setTagsInput(selectedOpportunity.tags?.join(', ') || '')
|
||||
}
|
||||
}, [selectedOpportunity])
|
||||
|
||||
const handleSaveOpportunity = async () => {
|
||||
if (!selectedOpportunity?.id) return
|
||||
|
||||
const tags = tagsInput
|
||||
.split(',')
|
||||
.map((tag) => tag.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
await updateOpportunity({
|
||||
id: selectedOpportunity.id as any,
|
||||
status: statusInput,
|
||||
notes: notesInput || undefined,
|
||||
tags: tags.length > 0 ? tags : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
if (!analysis) return null
|
||||
|
||||
return (
|
||||
@@ -280,6 +383,11 @@ export default function OpportunitiesPage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{searchError && (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>{searchError}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Generated Queries */}
|
||||
{generatedQueries.length > 0 && (
|
||||
@@ -307,20 +415,72 @@ export default function OpportunitiesPage() {
|
||||
)}
|
||||
|
||||
{/* Results Table */}
|
||||
{opportunities.length > 0 ? (
|
||||
{displayOpportunities.length > 0 ? (
|
||||
<Card>
|
||||
<div className="flex flex-wrap items-center gap-3 border-b border-border p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-xs text-muted-foreground">Status</Label>
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="h-8 rounded-md border border-input bg-background px-2 text-xs"
|
||||
>
|
||||
<option value="all">All</option>
|
||||
<option value="new">New</option>
|
||||
<option value="viewed">Viewed</option>
|
||||
<option value="contacted">Contacted</option>
|
||||
<option value="responded">Responded</option>
|
||||
<option value="converted">Converted</option>
|
||||
<option value="ignored">Ignored</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-xs text-muted-foreground">Intent</Label>
|
||||
<select
|
||||
value={intentFilter}
|
||||
onChange={(e) => setIntentFilter(e.target.value)}
|
||||
className="h-8 rounded-md border border-input bg-background px-2 text-xs"
|
||||
>
|
||||
<option value="all">All</option>
|
||||
<option value="frustrated">Frustrated</option>
|
||||
<option value="comparing">Comparing</option>
|
||||
<option value="learning">Learning</option>
|
||||
<option value="recommending">Recommending</option>
|
||||
<option value="looking">Looking</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-xs text-muted-foreground">Min Score</Label>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
value={minScore}
|
||||
onChange={(e) => setMinScore(Number(e.target.value))}
|
||||
className="h-8 w-20 rounded-md border border-input bg-background px-2 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setLimit((prev) => prev + 50)}
|
||||
>
|
||||
Load more
|
||||
</Button>
|
||||
</div>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Platform</TableHead>
|
||||
<TableHead>Intent</TableHead>
|
||||
<TableHead>Score</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead className="w-1/2">Post</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{opportunities.slice(0, 50).map((opp) => (
|
||||
{displayOpportunities.slice(0, limit).map((opp) => (
|
||||
<TableRow key={opp.id}>
|
||||
<TableCell><Badge variant="outline">{opp.platform}</Badge></TableCell>
|
||||
<TableCell>
|
||||
@@ -334,6 +494,11 @@ export default function OpportunitiesPage() {
|
||||
{Math.round(opp.relevanceScore * 100)}%
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="secondary" className="capitalize">
|
||||
{opp.status || 'new'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<p className="font-medium line-clamp-1">{opp.title}</p>
|
||||
<p className="text-sm text-muted-foreground line-clamp-2">{opp.snippet}</p>
|
||||
@@ -387,6 +552,43 @@ export default function OpportunitiesPage() {
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label>Status</Label>
|
||||
<select
|
||||
value={statusInput}
|
||||
onChange={(e) => setStatusInput(e.target.value)}
|
||||
className="h-10 w-full rounded-md border border-input bg-background px-3 text-sm"
|
||||
>
|
||||
<option value="new">New</option>
|
||||
<option value="viewed">Viewed</option>
|
||||
<option value="contacted">Contacted</option>
|
||||
<option value="responded">Responded</option>
|
||||
<option value="converted">Converted</option>
|
||||
<option value="ignored">Ignored</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Tags (comma separated)</Label>
|
||||
<input
|
||||
value={tagsInput}
|
||||
onChange={(e) => setTagsInput(e.target.value)}
|
||||
className="h-10 w-full rounded-md border border-input bg-background px-3 text-sm"
|
||||
placeholder="reddit, high-intent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Notes</Label>
|
||||
<Textarea
|
||||
value={notesInput}
|
||||
onChange={(e) => setNotesInput(e.target.value)}
|
||||
placeholder="Add notes about this lead..."
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{selectedOpportunity.matchedKeywords.length > 0 && (
|
||||
<div>
|
||||
<Label className="text-sm text-muted-foreground">Matched Keywords</Label>
|
||||
@@ -425,8 +627,11 @@ export default function OpportunitiesPage() {
|
||||
<Button variant="outline" onClick={() => window.open(selectedOpportunity.url, '_blank')}>
|
||||
<ExternalLink className="h-4 w-4 mr-2" /> View Post
|
||||
</Button>
|
||||
<Button onClick={() => setSelectedOpportunity(null)}>
|
||||
<CheckCircle2 className="h-4 w-4 mr-2" /> Mark as Viewed
|
||||
<Button onClick={async () => {
|
||||
await handleSaveOpportunity()
|
||||
setSelectedOpportunity(null)
|
||||
}}>
|
||||
<CheckCircle2 className="h-4 w-4 mr-2" /> Save Updates
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
|
||||
53
app/(app)/settings/page.tsx
Normal file
53
app/(app)/settings/page.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
"use client"
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
|
||||
export default function SettingsPage() {
|
||||
return (
|
||||
<div className="flex flex-1 flex-col gap-6 p-4 lg:p-8">
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-2xl font-semibold">Settings</h1>
|
||||
<p className="text-muted-foreground">Manage account details and API configuration.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Account</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 text-sm text-muted-foreground">
|
||||
<p>Signed in with Convex Auth.</p>
|
||||
<p>Profile management can be added here in a later phase.</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">API Keys</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 text-sm text-muted-foreground">
|
||||
<div className="space-y-1">
|
||||
<p className="font-medium text-foreground">OpenAI</p>
|
||||
<p>Set `OPENAI_API_KEY` in your `.env` file.</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="font-medium text-foreground">Serper (optional)</p>
|
||||
<p>Set `SERPER_API_KEY` in your `.env` file for more reliable search.</p>
|
||||
</div>
|
||||
<Badge variant="outline">Reload server after changes</Badge>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Billing</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm text-muted-foreground">
|
||||
Billing is not configured yet. Add Stripe or another provider when ready.
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user