1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212import { useEffect, useRef, useState, type FormEvent } from 'react';
import { Modal } from '@/components/ui/modal';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { X, Loader2, CheckCircle2 } from 'lucide-react';
import { apiFetch } from '@/auth/apiFetch';
const CATEGORIES = [
{ value: 'content', label: 'Content โ incorrect text, missing content, OCR errors' },
{ value: 'formatting', label: 'Formatting โ layout issues, broken tables, misplaced images' },
{ value: 'accessibility', label: 'Accessibility โ missing alt text, heading levels, labels' },
{ value: 'structure', label: 'Structure โ reading order, misplaced sections, broken lists' },
{ value: 'other', label: 'Other' },
] as const;
type Category = (typeof CATEGORIES)[number]['value'];
interface FeedbackModalProps {
onClose: () => void;
sessionId: string | null;
documentTitle: string | null;
currentPage: number | null;
currentStage: string | null;
}
export function FeedbackModal({
onClose,
sessionId,
documentTitle,
currentPage,
currentStage,
}: FeedbackModalProps) {
const [category, setCategory] = useState<Category>('content');
const [description, setDescription] = useState('');
const [page, setPage] = useState<string>(currentPage ? String(currentPage) : '');
const [website, setWebsite] = useState(''); // honeypot
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
const successRef = useRef<HTMLHeadingElement>(null);
// On success, move focus to the confirmation. Screen readers announce
// the heading because focus landed on it; the user closes the dialog
// themselves.
useEffect(() => {
if (success) successRef.current?.focus();
}, [success]);
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
if (description.trim().length < 10) {
setError('Please describe the issue in at least 10 characters.');
return;
}
setSubmitting(true);
setError(null);
try {
const res = await apiFetch('/api/v1/feedback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
category,
description: description.trim(),
session_id: sessionId,
document_title: documentTitle,
page: page ? parseInt(page, 10) : null,
section: currentStage,
website,
}),
});
if (!res.ok) {
const detail = await res.text();
throw new Error(`Submission failed (${res.status}): ${detail}`);
}
setSuccess(true);
} catch (err) {
setError(err instanceof Error ? err.message : 'Submission failed');
} finally {
setSubmitting(false);
}
};
return (
<Modal onClose={onClose} aria-label="Report an issue" className="bg-white rounded-lg shadow-xl w-full max-w-lg mx-4">
<div className="flex items-center justify-between px-5 py-3 border-b">
<h2 className="text-base font-semibold text-gray-900">Report an issue</h2>
<button
onClick={onClose}
className="p-1 rounded hover:bg-gray-100"
aria-label="Close dialog"
>
<X className="w-4 h-4 text-muted-foreground" />
</button>
</div>
{success ? (
<div className="flex flex-col items-center gap-3 px-5 py-10 text-center">
<CheckCircle2 className="w-10 h-10 text-green-600" aria-hidden="true" />
<h3
ref={successRef}
tabIndex={-1}
className="text-sm font-medium text-gray-900 outline-none focus:ring-2 focus:ring-uic-blue rounded"
>
Thanks โ feedback submitted.
</h3>
<p className="text-xs text-muted-foreground">
Reports like this help us prioritize pipeline improvements.
</p>
<Button variant="outline" onClick={onClose} className="mt-2">
Close
</Button>
</div>
) : (
<form onSubmit={handleSubmit} className="px-5 py-4 space-y-4">
<div>
<label htmlFor="feedback-category" className="block text-xs font-medium text-gray-700 mb-1">
Category
</label>
<select
id="feedback-category"
value={category}
onChange={(e) => setCategory(e.target.value as Category)}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
disabled={submitting}
>
{CATEGORIES.map((c) => (
<option key={c.value} value={c.value}>
{c.label}
</option>
))}
</select>
</div>
<div>
<label htmlFor="feedback-description" className="block text-xs font-medium text-gray-700 mb-1">
What went wrong?
</label>
<textarea
id="feedback-description"
value={description}
onChange={(e) => setDescription(e.target.value)}
rows={5}
minLength={10}
maxLength={5000}
required
disabled={submitting}
placeholder="Describe the issue โ be as specific as you can. The more detail the better."
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring resize-y"
/>
<p className="text-[11px] text-muted-foreground mt-1">
{description.length}/5000 characters ยท minimum 10
</p>
</div>
<div>
<label htmlFor="feedback-page" className="block text-xs font-medium text-gray-700 mb-1">
Page (optional)
</label>
<Input
id="feedback-page"
type="number"
min={1}
value={page}
onChange={(e) => setPage(e.target.value)}
disabled={submitting}
className="w-32"
/>
</div>
{/* Honeypot โ hidden from real users, bots will fill it. */}
<div aria-hidden="true" className="hidden">
<label htmlFor="feedback-website">Website</label>
<input
id="feedback-website"
type="text"
tabIndex={-1}
autoComplete="off"
value={website}
onChange={(e) => setWebsite(e.target.value)}
/>
</div>
{error && (
<div role="alert" className="text-sm text-red-700 bg-red-50 border border-red-200 rounded px-3 py-2">
{error}
</div>
)}
<div className="flex items-center justify-end gap-2 pt-2 border-t">
<Button type="button" variant="outline" onClick={onClose} disabled={submitting}>
Cancel
</Button>
<Button type="submit" disabled={submitting || description.trim().length < 10}>
{submitting ? (
<>
<Loader2 className="w-4 h-4 mr-1.5 animate-spin" />
Sending...
</>
) : (
'Submit feedback'
)}
</Button>
</div>
</form>
)}
</Modal>
);
}