| // src/components/authoring/AIWritingAssistantPanel.tsx | |
| import { useState } from "react"; | |
| import { Sparkles, Loader2, Check, X, ChevronDown, Wand2, List, RefreshCw, FileText, Table2, Lightbulb } from "lucide-react"; | |
| import type { AISuggestionType } from "@/types/authoring"; | |
| interface AIWritingAssistantPanelProps { | |
| suggestions: any[]; | |
| loading: boolean; | |
| activeType: AISuggestionType | null; | |
| appliedCount: number; | |
| requestSuggestion: (type: AISuggestionType, prompt?: string) => void; | |
| applySuggestion: (s: any) => void; | |
| dismissSuggestion: (id: string) => void; | |
| clearSuggestions: () => void; | |
| } | |
| const QUICK_ACTIONS: { type: AISuggestionType; icon: typeof Wand2; label: string; description: string }[] = [ | |
| { type: "expand_bullets", icon: List, label: "Expand Bullets", description: "Convert bullet points to full paragraphs" }, | |
| { type: "rephrase", icon: RefreshCw, label: "Rephrase", description: "Improve clarity of selected text" }, | |
| { type: "generate_table", icon: Table2, label: "Generate Table", description: "Create a LaTeX table from description" }, | |
| { type: "improve_clarity", icon: Wand2, label: "Improve Clarity", description: "Polish language and flow" }, | |
| { type: "suggest_title", icon: Lightbulb, label: "Suggest Title", description: "Generate alternative titles" }, | |
| { type: "generate_abstract", icon: FileText, label: "Generate Abstract", description: "Create an abstract from content" }, | |
| ]; | |
| export function AIWritingAssistantPanel({ suggestions, loading, activeType, appliedCount, requestSuggestion, applySuggestion, dismissSuggestion, clearSuggestions }: AIWritingAssistantPanelProps) { | |
| const [customPrompt, setCustomPrompt] = useState(""); | |
| const [selectedType, setSelectedType] = useState<AISuggestionType>("rephrase"); | |
| return ( | |
| <div className="grid grid-cols-1 lg:grid-cols-3 gap-4 h-full"> | |
| {/* Quick actions */} | |
| <div className="space-y-2"> | |
| <h4 className="text-sm font-semibold flex items-center gap-2"><Wand2 className="w-4 h-4 text-purple-500"/> Quick Actions</h4> | |
| <div className="grid gap-1.5"> | |
| {QUICK_ACTIONS.map(action => ( | |
| <button key={action.type} onClick={() => requestSuggestion(action.type)} disabled={loading} | |
| className="flex items-start gap-3 p-2.5 rounded-xl text-left hover:bg-white/10 transition disabled:opacity-50 border border-white/10"> | |
| <action.icon className="w-4 h-4 text-purple-500 mt-0.5 shrink-0"/> | |
| <div> | |
| <p className="text-sm font-medium">{action.label}</p> | |
| <p className="text-[10px] text-gray-500">{action.description}</p> | |
| </div> | |
| </button> | |
| ))} | |
| </div> | |
| <div className="flex gap-1.5 pt-2"> | |
| <input type="text" value={customPrompt} onChange={e => setCustomPrompt(e.target.value)} | |
| placeholder="Custom instruction..." className="flex-1 text-xs px-3 py-1.5 bg-white/10 rounded-lg" /> | |
| <button onClick={() => { if (customPrompt.trim()) requestSuggestion("rephrase", customPrompt.trim()); setCustomPrompt(""); }} | |
| disabled={!customPrompt.trim() || loading} className="btn btn-primary btn-xs gap-1"> | |
| {loading ? <Loader2 className="w-3 h-3 animate-spin"/> : <Wand2 className="w-3 h-3"/>} Go | |
| </button> | |
| </div> | |
| </div> | |
| {/* Suggestions list */} | |
| <div className="lg:col-span-2 space-y-2"> | |
| <div className="flex items-center justify-between"> | |
| <h4 className="text-sm font-semibold flex items-center gap-2"> | |
| <Sparkles className="w-4 h-4 text-purple-500"/> Suggestions ({suggestions.length}) | |
| {appliedCount > 0 && <span className="text-[10px] text-green-500">({appliedCount} applied)</span>} | |
| </h4> | |
| {suggestions.length > 0 && ( | |
| <button onClick={clearSuggestions} className="text-xs text-gray-400 hover:text-red-500">Clear all</button> | |
| )} | |
| </div> | |
| {loading && ( | |
| <div className="flex items-center gap-3 p-4 rounded-xl bg-purple-50/30 dark:bg-purple-900/10 border border-purple-200 dark:border-purple-800"> | |
| <Loader2 className="w-5 h-5 animate-spin text-purple-500"/> | |
| <div> | |
| <p className="text-sm font-medium text-purple-700 dark:text-purple-400">AI is thinking...</p> | |
| <p className="text-xs text-purple-500">{activeType ? activeType.replace(/_/g, " ") : "Processing"}</p> | |
| </div> | |
| </div> | |
| )} | |
| <div className="space-y-2 max-h-64 overflow-y-auto"> | |
| {suggestions.map(s => ( | |
| <div key={s.id} className={`rounded-xl border p-3 transition ${s.applied ? "opacity-60 border-green-200 dark:border-green-800 bg-green-50/30 dark:bg-green-900/5" : "border-white/20 bg-white/5"}`}> | |
| <div className="flex items-center justify-between mb-2"> | |
| <span className="text-[10px] px-2 py-0.5 rounded-full bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-400">{s.type.replace(/_/g, " ")}</span> | |
| <span className="text-[9px] text-gray-400">{(s.confidence * 100).toFixed(0)}% confidence</span> | |
| </div> | |
| {s.originalText && ( | |
| <p className="text-xs text-gray-500 line-through mb-1.5">{s.originalText.substring(0, 100)}...</p> | |
| )} | |
| <p className="text-sm text-gray-800 dark:text-gray-200">{s.suggestedText.substring(0, 200)}</p> | |
| {!s.applied && ( | |
| <div className="flex gap-1.5 mt-2"> | |
| <button onClick={() => applySuggestion(s)} className="btn btn-primary btn-xs gap-1"><Check className="w-3 h-3"/> Apply</button> | |
| <button onClick={() => dismissSuggestion(s.id)} className="btn btn-ghost btn-xs gap-1"><X className="w-3 h-3"/> Dismiss</button> | |
| </div> | |
| )} | |
| </div> | |
| ))} | |
| {suggestions.length === 0 && !loading && ( | |
| <div className="text-center py-8 text-gray-400"> | |
| <Sparkles className="w-8 h-8 mx-auto mb-2 opacity-30"/> | |
| <p className="text-sm">No suggestions yet. Select an action or select text and ask for help.</p> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } |
Xet Storage Details
- Size:
- 6.28 kB
- Xet hash:
- 16f390af603af69d364c3223cd34e8f014125073e68fd6646a402c0fc413dbb4
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.