RM / src /app /(dashboard) /editor /page.tsx
trretretret's picture
Initial commit: Add research assistant application
b708f13
"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";
// Enum-aligned Study Designs
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);
// Data for initialization
const [initData, setInitData] = React.useState({
title: "",
target_journal: "",
study_design: "Systematic Review",
pico_context_id: "", // Now expecting the ID from extraction
});
// 1. Initialize Manuscript (POST /api/v1/writesage/init)
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);
}
};
// 2. Section Composition (POST /api/v1/writesage/compose)
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
})
});
// Logic for re-fetching manuscript content goes here
} 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>
);
}