a
This commit is contained in:
326
components/analysis-section-editor.tsx
Normal file
326
components/analysis-section-editor.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user