source: chapterx-frontend/src/components/writer/AISuggestionPanel.tsx@ a6e33d1

main
Last change on this file since a6e33d1 was a6e33d1, checked in by kikisrbinoska <srbinoskakristina07@…>, 3 days ago

AI suggestions fixed

  • Property mode set to 100644
File size: 6.8 KB
Line 
1import React, { useState, useEffect } from 'react'
2import { Sparkles, Check, X, Clock, ChevronDown, ChevronUp } from 'lucide-react'
3import { useStoryStore } from '../../store/storyStore'
4import { useUIStore } from '../../store/uiStore'
5import { AISuggestion, SuggestionType } from '../../types'
6import { Button } from '../ui/Button'
7
8interface AISuggestionPanelProps {
9 chapterId: number
10 storyId?: number
11}
12
13const typeColors: Record<SuggestionType, string> = {
14 grammar: 'bg-blue-500/20 text-blue-300 border-blue-500/30',
15 style: 'bg-violet-500/20 text-violet-300 border-violet-500/30',
16 plot: 'bg-amber-500/20 text-amber-300 border-amber-500/30',
17 character: 'bg-emerald-500/20 text-emerald-300 border-emerald-500/30',
18 pacing: 'bg-pink-500/20 text-pink-300 border-pink-500/30',
19}
20
21const SuggestionCard: React.FC<{
22 suggestion: AISuggestion
23 onAccept: () => void
24 onReject: () => void
25}> = ({ suggestion, onAccept, onReject }) => {
26 const [expanded, setExpanded] = useState(false)
27
28 return (
29 <div className="bg-slate-800 border border-slate-700 rounded-xl p-4">
30 <div className="flex items-start justify-between gap-3 mb-3">
31 <div className="flex items-center gap-2">
32 <span className={`px-2 py-0.5 text-xs font-medium rounded-full border ${typeColors[suggestion.suggestion_type]}`}>
33 {suggestion.suggestion_type}
34 </span>
35 {suggestion.accepted === true && (
36 <span className="flex items-center gap-1 text-xs text-emerald-400">
37 <Check size={12} /> Accepted
38 </span>
39 )}
40 {suggestion.accepted === false && (
41 <span className="flex items-center gap-1 text-xs text-rose-400">
42 <X size={12} /> Rejected
43 </span>
44 )}
45 {suggestion.accepted === null && (
46 <span className="flex items-center gap-1 text-xs text-amber-400">
47 <Clock size={12} /> Pending
48 </span>
49 )}
50 </div>
51 <button
52 onClick={() => setExpanded(!expanded)}
53 className="text-slate-500 hover:text-slate-300 transition-colors"
54 >
55 {expanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
56 </button>
57 </div>
58
59 <div className="space-y-3">
60 <div>
61 <p className="text-xs text-slate-500 mb-1">Original:</p>
62 <p className="text-sm text-slate-400 bg-slate-900 rounded-lg p-3 italic border border-slate-700">
63 "{suggestion.original_text}"
64 </p>
65 </div>
66 <div>
67 <p className="text-xs text-slate-500 mb-1">Suggested:</p>
68 <p className="text-sm text-emerald-300 bg-emerald-500/5 rounded-lg p-3 border border-emerald-500/20">
69 "{suggestion.suggested_text}"
70 </p>
71 </div>
72
73 {expanded && (
74 <div>
75 <p className="text-xs text-slate-500 mb-1">Explanation:</p>
76 <p className="text-sm text-slate-400">{suggestion.explanation}</p>
77 {suggestion.applied_at && (
78 <p className="text-xs text-slate-600 mt-2">
79 Applied: {new Date(suggestion.applied_at).toLocaleString()}
80 </p>
81 )}
82 </div>
83 )}
84
85 {suggestion.accepted === null && (
86 <div className="flex gap-2 pt-1">
87 <Button size="sm" onClick={onAccept} className="flex-1">
88 <Check size={14} />
89 Accept
90 </Button>
91 <Button size="sm" variant="danger" onClick={onReject} className="flex-1">
92 <X size={14} />
93 Reject
94 </Button>
95 </div>
96 )}
97 </div>
98 </div>
99 )
100}
101
102export const AISuggestionPanel: React.FC<AISuggestionPanelProps> = ({ chapterId }) => {
103 const { aiSuggestions, acceptSuggestion, rejectSuggestion, fetchSuggestions } = useStoryStore()
104 const { addToast } = useUIStore()
105 const [tab, setTab] = useState<'pending' | 'accepted' | 'rejected'>('pending')
106 const [loading, setLoading] = useState(false)
107
108 useEffect(() => {
109 setLoading(true)
110 fetchSuggestions(chapterId).finally(() => setLoading(false))
111 }, [chapterId])
112
113 // Backend already scopes results to this chapter via NEED_APPROVAL
114 const chapterSuggestions = aiSuggestions
115 const pending = chapterSuggestions.filter(s => s.accepted === null)
116 const accepted = chapterSuggestions.filter(s => s.accepted === true)
117 const rejected = chapterSuggestions.filter(s => s.accepted === false)
118
119 const currentList = tab === 'pending' ? pending : tab === 'accepted' ? accepted : rejected
120
121 return (
122 <div className="bg-slate-900 border border-slate-700 rounded-2xl p-6">
123 <div className="flex items-center gap-2 mb-4">
124 <Sparkles size={18} className="text-violet-400" />
125 <h3 className="text-white font-semibold">AI Writing Suggestions</h3>
126 <span className="ml-auto text-xs text-slate-500">{chapterSuggestions.length} total</span>
127 </div>
128
129 {/* Tabs */}
130 <div className="flex gap-1 p-1 bg-slate-800 rounded-xl mb-4">
131 {([
132 { key: 'pending', label: 'Pending', count: pending.length },
133 { key: 'accepted', label: 'Accepted', count: accepted.length },
134 { key: 'rejected', label: 'Rejected', count: rejected.length },
135 ] as const).map(t => (
136 <button
137 key={t.key}
138 onClick={() => setTab(t.key)}
139 className={`flex-1 flex items-center justify-center gap-1.5 py-1.5 rounded-lg text-sm font-medium transition-colors ${
140 tab === t.key
141 ? 'bg-indigo-600 text-white'
142 : 'text-slate-400 hover:text-white'
143 }`}
144 >
145 {t.label}
146 <span className={`text-xs px-1.5 py-0.5 rounded-full ${
147 tab === t.key ? 'bg-indigo-500' : 'bg-slate-700'
148 }`}>
149 {t.count}
150 </span>
151 </button>
152 ))}
153 </div>
154
155 {/* Suggestions */}
156 <div className="space-y-3">
157 {loading ? (
158 <div className="text-center py-8 text-slate-500 text-sm">
159 <Sparkles size={32} className="mx-auto mb-2 opacity-30 animate-pulse" />
160 Loading suggestions...
161 </div>
162 ) : currentList.length === 0 ? (
163 <div className="text-center py-8 text-slate-500 text-sm">
164 <Sparkles size={32} className="mx-auto mb-2 opacity-30" />
165 No {tab} suggestions
166 </div>
167 ) : (
168 currentList.map(s => (
169 <SuggestionCard
170 key={s.suggestion_id}
171 suggestion={s}
172 onAccept={async () => { await acceptSuggestion(s.suggestion_id); addToast('Suggestion accepted!') }}
173 onReject={async () => { await rejectSuggestion(s.suggestion_id); addToast('Suggestion rejected', 'info') }}
174 />
175 ))
176 )}
177 </div>
178 </div>
179 )
180}
Note: See TracBrowser for help on using the repository browser.