| "use client"; |
|
|
| import * as React from "react"; |
| import { |
| FilePlus, |
| Sparkles, |
| Wand2, |
| Target, |
| FlaskConical, |
| Database, |
| ChevronRight, |
| AlertCircle |
| } from "lucide-react"; |
| import { DashboardTemplate } from "@/components/templates"; |
| import { Button } from "@/components/atoms/Button"; |
| import { Icon } from "@/components/atoms/Icon"; |
| import { Spinner } from "@/components/atoms/Spinner"; |
| import { Badge } from "@/components/atoms/Badge"; |
|
|
| |
| const STUDY_DESIGNS = ["RCT", "Systematic Review", "Meta-Analysis", "Cohort Study", "Case Report"]; |
| const MANUSCRIPT_SECTIONS = ["Abstract", "Introduction", "Methods", "Results", "Discussion", "Conclusion"]; |
|
|
| export default function EditorPage() { |
| const [step, setStep] = React.useState<"init" | "editor">("init"); |
| const [isLoading, setIsLoading] = React.useState(false); |
| const [manuscriptId, setManuscriptId] = React.useState<string | null>(null); |
| |
| |
| const [initData, setInitData] = React.useState({ |
| title: "", |
| target_journal: "", |
| study_design: "Systematic Review", |
| pico_context_id: "", |
| }); |
|
|
| |
| const handleInitialize = async (e: React.FormEvent) => { |
| e.preventDefault(); |
| if (!initData.pico_context_id) return alert("Please provide a PICO Context ID"); |
|
|
| setIsLoading(true); |
| try { |
| const token = localStorage.getItem("token"); |
| const res = await fetch("/api/v1/writesage/init", { |
| method: "POST", |
| headers: { |
| "Content-Type": "application/json", |
| "Authorization": `Bearer ${token}` |
| }, |
| body: JSON.stringify(initData) |
| }); |
| |
| if (res.ok) { |
| const data = await res.json(); |
| setManuscriptId(data.id); |
| setStep("editor"); |
| } |
| } catch (err) { |
| console.error("Initialization failed:", err); |
| } finally { |
| setIsLoading(false); |
| } |
| }; |
|
|
| |
| const handleComposeSection = async (sectionName: string) => { |
| if (!manuscriptId) return; |
| setIsLoading(true); |
| try { |
| const token = localStorage.getItem("token"); |
| await fetch("/api/v1/writesage/compose", { |
| method: "POST", |
| headers: { |
| "Content-Type": "application/json", |
| "Authorization": `Bearer ${token}` |
| }, |
| body: JSON.stringify({ |
| manuscript_id: manuscriptId, |
| section_name: sectionName |
| }) |
| }); |
| |
| } finally { |
| setIsLoading(false); |
| } |
| }; |
|
|
| return ( |
| <DashboardTemplate> |
| <div className="max-w-5xl mx-auto space-y-8 animate-in fade-in duration-500"> |
| |
| {step === "init" ? ( |
| <div className="max-w-2xl mx-auto space-y-8 py-10"> |
| <div className="text-center space-y-2"> |
| <div className="inline-flex p-3 rounded-2xl bg-primary/10 text-primary mb-2"> |
| <Icon icon={Sparkles} size={28} /> |
| </div> |
| <h1 className="text-3xl font-bold tracking-tight">Manuscript Genesis</h1> |
| <p className="text-muted-foreground text-sm"> |
| Ground your writing in existing PICO extractions. |
| </p> |
| </div> |
| |
| <form onSubmit={handleInitialize} className="space-y-6 rounded-2xl border bg-card p-8 shadow-sm"> |
| <div className="space-y-4"> |
| {/* ID-based Context Link */} |
| <div className="space-y-2"> |
| <label className="text-xs font-bold uppercase tracking-widest text-muted-foreground flex items-center gap-2"> |
| <Icon icon={Database} size={12} /> PICO Context ID |
| </label> |
| <input |
| required |
| placeholder="Enter the ID from your PICO extraction step" |
| className="w-full rounded-lg border bg-background px-4 py-3 text-sm font-mono focus:ring-2 focus:ring-primary/20 outline-none transition-all" |
| value={initData.pico_context_id} |
| onChange={e => setInitData({...initData, pico_context_id: e.target.value})} |
| /> |
| <p className="text-[10px] text-muted-foreground italic flex items-center gap-1"> |
| <Icon icon={AlertCircle} size={10} /> |
| Required to ground AI generation in specific evidence. |
| </p> |
| </div> |
| |
| <div className="space-y-2"> |
| <label className="text-xs font-bold uppercase tracking-widest text-muted-foreground">Title</label> |
| <input |
| required |
| placeholder="e.g., Impact of Metformin on COVID-19 Outcomes" |
| className="w-full rounded-lg border bg-background px-4 py-3 text-sm focus:ring-2 focus:ring-primary/20 outline-none transition-all" |
| onChange={e => setInitData({...initData, title: e.target.value})} |
| /> |
| </div> |
| |
| <div className="grid grid-cols-2 gap-4"> |
| <div className="space-y-2"> |
| <label className="text-xs font-bold uppercase tracking-widest text-muted-foreground flex items-center gap-2"> |
| <Icon icon={Target} size={12} /> Journal |
| </label> |
| <input |
| placeholder="e.g., BMJ" |
| className="w-full rounded-lg border bg-background px-4 py-2 text-sm outline-none" |
| onChange={e => setInitData({...initData, target_journal: e.target.value})} |
| /> |
| </div> |
| <div className="space-y-2"> |
| <label className="text-xs font-bold uppercase tracking-widest text-muted-foreground flex items-center gap-2"> |
| <Icon icon={FlaskConical} size={12} /> Study Design |
| </label> |
| <select |
| className="w-full rounded-lg border bg-background px-4 py-2 text-sm outline-none" |
| value={initData.study_design} |
| onChange={e => setInitData({...initData, study_design: e.target.value})} |
| > |
| {STUDY_DESIGNS.map(design => ( |
| <option key={design} value={design}>{design}</option> |
| ))} |
| </select> |
| </div> |
| </div> |
| </div> |
| |
| <Button type="submit" className="w-full py-6 text-md font-bold gap-2" disabled={isLoading}> |
| {isLoading ? <Spinner size={20} /> : <Icon icon={FilePlus} size={20} />} |
| Initialize Framework |
| </Button> |
| </form> |
| </div> |
| ) : ( |
| /* Editor Layout */ |
| <div className="grid lg:grid-cols-[1fr_320px] gap-8"> |
| <div className="space-y-6"> |
| <div className="flex items-center justify-between border-b pb-4"> |
| <div className="space-y-1"> |
| <h2 className="text-2xl font-bold tracking-tight">{initData.title}</h2> |
| <div className="flex items-center gap-3"> |
| <Badge variant="secondary" className="text-[10px]">{initData.study_design}</Badge> |
| <span className="text-xs text-muted-foreground font-medium flex items-center gap-1"> |
| ID: <span className="font-mono text-primary/70">{initData.pico_context_id}</span> |
| </span> |
| </div> |
| </div> |
| </div> |
| |
| <div className="space-y-4"> |
| {MANUSCRIPT_SECTIONS.map((section) => ( |
| <div key={section} className="group rounded-xl border bg-card p-6 transition-all hover:border-primary/20"> |
| <div className="flex items-center justify-between mb-4"> |
| <h3 className="font-bold text-lg">{section}</h3> |
| <Button |
| size="sm" |
| variant="outline" |
| className="gap-2 border-primary/20 text-primary hover:bg-primary/5" |
| onClick={() => handleComposeSection(section)} |
| disabled={isLoading} |
| > |
| <Icon icon={Wand2} size={14} /> |
| {isLoading ? "Drafting..." : "Generate"} |
| </Button> |
| </div> |
| <div className="min-h-[120px] rounded-lg bg-muted/10 border border-dashed flex items-center justify-center p-4"> |
| <p className="text-xs text-muted-foreground italic"> |
| Click "Generate" to synthesize this section from the PICO context. |
| </p> |
| </div> |
| </div> |
| ))} |
| </div> |
| </div> |
| |
| <aside className="space-y-6"> |
| <div className="rounded-xl border bg-muted/5 p-5 space-y-4"> |
| <h4 className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">Source Evidence</h4> |
| <p className="text-[11px] text-muted-foreground leading-relaxed italic"> |
| Drafting is grounded in PICO Extraction #<span className="font-mono">{initData.pico_context_id}</span>. |
| </p> |
| <Button variant="ghost" size="sm" className="w-full text-[11px] gap-2"> |
| View Extraction Details <ChevronRight size={12} /> |
| </Button> |
| </div> |
| </aside> |
| </div> |
| )} |
| </div> |
| </DashboardTemplate> |
| ); |
| } |
|
|