This commit is contained in:
2026-02-04 01:05:00 +00:00
parent f9222627ef
commit d02d95e680
30 changed files with 2449 additions and 326 deletions

View File

@@ -0,0 +1,326 @@
"use client"
import * as React from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Textarea } from "@/components/ui/textarea"
import { Badge } from "@/components/ui/badge"
import { useMutation } from "convex/react"
import { api } from "@/convex/_generated/api"
type SectionKey =
| "profile"
| "features"
| "competitors"
| "keywords"
| "problems"
| "personas"
| "useCases"
| "dorkQueries"
function summarizeItem(item: any) {
if (typeof item === "string") return item
if (!item || typeof item !== "object") return String(item)
if (item.name && item.role) return `${item.name} · ${item.role}`
if (item.name) return item.name
if (item.term) return item.term
if (item.problem) return item.problem
if (item.scenario) return item.scenario
if (item.query) return item.query
return JSON.stringify(item)
}
export function SectionEditor({
analysisId,
sectionKey,
title,
items,
}: {
analysisId: string
sectionKey: SectionKey
title: string
items: any[]
}) {
const addItem = useMutation(api.analysisSections.addItem)
const removeItem = useMutation(api.analysisSections.removeItem)
const [isRepromptOpen, setIsRepromptOpen] = React.useState(false)
const [repromptText, setRepromptText] = React.useState("")
const [isAddOpen, setIsAddOpen] = React.useState(false)
const [addText, setAddText] = React.useState("")
const [isBusy, setIsBusy] = React.useState(false)
const handleAdd = async () => {
setIsBusy(true)
try {
let parsed: any = addText
if (addText.trim().startsWith("{") || addText.trim().startsWith("[")) {
parsed = JSON.parse(addText)
}
await addItem({ analysisId: analysisId as any, sectionKey, item: parsed })
setAddText("")
setIsAddOpen(false)
} finally {
setIsBusy(false)
}
}
const handleReprompt = async () => {
setIsBusy(true)
try {
const response = await fetch("/api/analysis/reprompt", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
analysisId,
sectionKey,
prompt: repromptText.trim() || undefined,
}),
})
if (!response.ok) {
const data = await response.json()
throw new Error(data.error || "Reprompt failed")
}
setRepromptText("")
setIsRepromptOpen(false)
} finally {
setIsBusy(false)
}
}
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between gap-3">
<CardTitle className="text-base">{title}</CardTitle>
<div className="flex flex-wrap items-center gap-2">
<Button variant="outline" size="sm" onClick={() => setIsAddOpen(true)}>
Add
</Button>
<Button variant="secondary" size="sm" onClick={() => setIsRepromptOpen(true)}>
Reprompt
</Button>
</div>
</CardHeader>
<CardContent className="space-y-2 text-sm">
{items.length === 0 ? (
<div className="text-muted-foreground">No items yet.</div>
) : (
items.map((item, index) => (
<div
key={`${sectionKey}-${index}`}
className="flex flex-wrap items-start justify-between gap-2 rounded-md border border-border/60 p-3"
>
<div className="space-y-2">
<div className="font-medium">{summarizeItem(item)}</div>
<Badge variant="outline">#{index + 1}</Badge>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => removeItem({ analysisId: analysisId as any, sectionKey, index })}
>
Remove
</Button>
</div>
))
)}
</CardContent>
<Dialog open={isAddOpen} onOpenChange={setIsAddOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Add to {title}</DialogTitle>
<DialogDescription>
Paste JSON for an item, or plain text for a string entry.
</DialogDescription>
</DialogHeader>
<Textarea
value={addText}
onChange={(event) => setAddText(event.target.value)}
placeholder='{"name":"...", "description":"..."}'
className="min-h-[160px]"
/>
<DialogFooter>
<Button variant="outline" onClick={() => setIsAddOpen(false)} disabled={isBusy}>
Cancel
</Button>
<Button onClick={handleAdd} disabled={isBusy || !addText.trim()}>
Save
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog open={isRepromptOpen} onOpenChange={setIsRepromptOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Reprompt {title}</DialogTitle>
<DialogDescription>
Provide guidance to regenerate just this section.
</DialogDescription>
</DialogHeader>
<Textarea
value={repromptText}
onChange={(event) => setRepromptText(event.target.value)}
placeholder="Focus on B2B teams in healthcare..."
className="min-h-[140px]"
/>
<DialogFooter>
<Button variant="outline" onClick={() => setIsRepromptOpen(false)} disabled={isBusy}>
Cancel
</Button>
<Button onClick={handleReprompt} disabled={isBusy}>
Reprompt
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Card>
)
}
export function ProfileSectionEditor({
analysisId,
items,
}: {
analysisId: string
items: Record<string, any>
}) {
const replaceSection = useMutation(api.analysisSections.replaceSection)
const [isRepromptOpen, setIsRepromptOpen] = React.useState(false)
const [isEditOpen, setIsEditOpen] = React.useState(false)
const [repromptText, setRepromptText] = React.useState("")
const [editText, setEditText] = React.useState(JSON.stringify(items, null, 2))
const [isBusy, setIsBusy] = React.useState(false)
React.useEffect(() => {
setEditText(JSON.stringify(items, null, 2))
}, [items])
const handleSave = async () => {
setIsBusy(true)
try {
const parsed = JSON.parse(editText)
await replaceSection({
analysisId: analysisId as any,
sectionKey: "profile",
items: parsed,
source: "mixed",
})
setIsEditOpen(false)
} finally {
setIsBusy(false)
}
}
const handleReprompt = async () => {
setIsBusy(true)
try {
const response = await fetch("/api/analysis/reprompt", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
analysisId,
sectionKey: "profile",
prompt: repromptText.trim() || undefined,
}),
})
if (!response.ok) {
const data = await response.json()
throw new Error(data.error || "Reprompt failed")
}
setRepromptText("")
setIsRepromptOpen(false)
} finally {
setIsBusy(false)
}
}
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between gap-3">
<CardTitle className="text-base">Product Profile</CardTitle>
<div className="flex flex-wrap items-center gap-2">
<Button variant="outline" size="sm" onClick={() => setIsEditOpen(true)}>
Edit
</Button>
<Button variant="secondary" size="sm" onClick={() => setIsRepromptOpen(true)}>
Reprompt
</Button>
</div>
</CardHeader>
<CardContent className="space-y-2 text-sm text-muted-foreground">
<div>
<span className="font-medium text-foreground">Product:</span>{" "}
{items.productName || "Not set"}
</div>
<div>
<span className="font-medium text-foreground">Tagline:</span>{" "}
{items.tagline || "Not set"}
</div>
<div>
<span className="font-medium text-foreground">Category:</span>{" "}
{items.category || "Not set"}
</div>
<div>
<span className="font-medium text-foreground">Positioning:</span>{" "}
{items.positioning || "Not set"}
</div>
</CardContent>
<Dialog open={isEditOpen} onOpenChange={setIsEditOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Profile</DialogTitle>
<DialogDescription>Update the profile JSON.</DialogDescription>
</DialogHeader>
<Textarea
value={editText}
onChange={(event) => setEditText(event.target.value)}
className="min-h-[200px]"
/>
<DialogFooter>
<Button variant="outline" onClick={() => setIsEditOpen(false)} disabled={isBusy}>
Cancel
</Button>
<Button onClick={handleSave} disabled={isBusy}>
Save
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog open={isRepromptOpen} onOpenChange={setIsRepromptOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Reprompt Profile</DialogTitle>
<DialogDescription>
Provide guidance to regenerate the profile.
</DialogDescription>
</DialogHeader>
<Textarea
value={repromptText}
onChange={(event) => setRepromptText(event.target.value)}
className="min-h-[140px]"
/>
<DialogFooter>
<Button variant="outline" onClick={() => setIsRepromptOpen(false)} disabled={isBusy}>
Cancel
</Button>
<Button onClick={handleReprompt} disabled={isBusy}>
Reprompt
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Card>
)
}