@@ -79,6 +79,7 @@ interface SummaryResponse {
7979
8080type ParagraphState = 'normal' | 'deleted' | 'rewriting'
8181type PendingAgentAction = 'regenerate' | 'readMore' | 'reply' | null
82+ type PageStatusState = 'opened' | 'active' | 'hidden' | 'submitted'
8283
8384// Keys must match REASON_LABELS in preference.ts
8485const REASONS = [
@@ -230,6 +231,7 @@ export default function ReviewPage() {
230231 const [ filterOpen , setFilterOpen ] = useState ( false )
231232 const [ pendingAgentAction , setPendingAgentAction ] = useState < PendingAgentAction > ( null )
232233 const [ activeReplyRequestEmailId , setActiveReplyRequestEmailId ] = useState < string | null > ( null )
234+ const [ replyDraftOpen , setReplyDraftOpen ] = useState ( false )
233235
234236 const resetEditState = useCallback ( ( ) => {
235237 setActions ( [ ] )
@@ -247,6 +249,25 @@ export default function ReviewPage() {
247249 setSubmitting ( false )
248250 } , [ ] )
249251
252+ const postPageStatus = useCallback ( async (
253+ state : PageStatusState ,
254+ options ?: { stopMonitoring ?: boolean ; reason ?: string } ,
255+ ) => {
256+ try {
257+ await fetch ( `/api/sessions/${ id } /page-status` , {
258+ method : 'POST' ,
259+ headers : { 'Content-Type' : 'application/json' } ,
260+ body : JSON . stringify ( {
261+ state,
262+ stopMonitoring : options ?. stopMonitoring === true ,
263+ reason : options ?. reason ,
264+ } ) ,
265+ } )
266+ } catch {
267+ // ignore page status failures
268+ }
269+ } , [ id ] )
270+
250271 useEffect ( ( ) => {
251272 fetch ( `/api/sessions/${ id } ` )
252273 . then ( r => r . json ( ) )
@@ -259,6 +280,22 @@ export default function ReviewPage() {
259280 . catch ( ( ) => { setError ( true ) ; setLoading ( false ) } )
260281 } , [ id ] )
261282
283+ useEffect ( ( ) => {
284+ void postPageStatus ( 'opened' )
285+ const activeTimer = window . setInterval ( ( ) => {
286+ void postPageStatus ( document . visibilityState === 'visible' ? 'active' : 'hidden' )
287+ } , 10000 )
288+ const onVisibility = ( ) => { void postPageStatus ( document . visibilityState === 'visible' ? 'active' : 'hidden' ) }
289+ const onBeforeUnload = ( ) => { void postPageStatus ( 'hidden' , { reason : 'page_unload' } ) }
290+ document . addEventListener ( 'visibilitychange' , onVisibility )
291+ window . addEventListener ( 'beforeunload' , onBeforeUnload )
292+ return ( ) => {
293+ window . clearInterval ( activeTimer )
294+ document . removeEventListener ( 'visibilitychange' , onVisibility )
295+ window . removeEventListener ( 'beforeunload' , onBeforeUnload )
296+ }
297+ } , [ postPageStatus ] )
298+
262299 // Poll for payload updates while waiting for agent rewrite
263300 useEffect ( ( ) => {
264301 if ( ! waitingForRewrite ) return
@@ -553,6 +590,11 @@ export default function ReviewPage() {
553590 }
554591 }
555592
593+ const stopMonitoringAndLeave = async ( ) => {
594+ await postPageStatus ( 'hidden' , { stopMonitoring : true , reason : 'user_left_email_review' } )
595+ navigate ( '/' )
596+ }
597+
556598 const hasInbox = payload && 'inbox' in payload && Array . isArray ( ( payload as InboxPayload ) . inbox )
557599 const inboxPayloadForState = hasInbox ? payload as InboxPayload : null
558600 const selectedInboxEmail = inboxPayloadForState && selectedEmailId
@@ -717,7 +759,15 @@ export default function ReviewPage() {
717759 { /* Left Panel — Inbox List */ }
718760 < div className = "w-full md:w-72 md:shrink-0 bg-white dark:bg-zinc-900 border-b md:border-b-0 md:border-r border-gray-100 dark:border-zinc-800 overflow-y-auto max-h-[30vh] md:max-h-full" >
719761 { ! isCompleted && (
720- < button onClick = { ( ) => navigate ( '/' ) } className = "text-sm text-zinc-400 dark:text-slate-500 hover:text-zinc-600 dark:hover:text-slate-300 transition-colors p-4 block border-b border-gray-50 dark:border-zinc-800 w-full text-left" > ← Back</ button >
762+ < div className = "p-4 border-b border-gray-50 dark:border-zinc-800 space-y-2" >
763+ < button onClick = { stopMonitoringAndLeave } className = "text-sm text-zinc-400 dark:text-slate-500 hover:text-zinc-600 dark:hover:text-slate-300 transition-colors block w-full text-left" > ← Back</ button >
764+ < button
765+ onClick = { stopMonitoringAndLeave }
766+ className = "w-full text-xs font-medium px-3 py-2 rounded-lg border border-red-200 dark:border-red-900 text-red-600 dark:text-red-300 hover:bg-red-50 dark:hover:bg-red-950 transition-colors"
767+ >
768+ Stop Agent Monitor
769+ </ button >
770+ </ div >
721771 ) }
722772 < div className = "p-4 border-b border-gray-50 dark:border-zinc-800 space-y-3" >
723773 < div >
@@ -1025,98 +1075,111 @@ export default function ReviewPage() {
10251075
10261076 { activeDraft && (
10271077 < >
1028- < div className = "mb-6 p-4 bg-white dark:bg-zinc-900 border border-gray-100 dark:border-zinc-800 rounded-lg space-y-3 " >
1078+ < div className = "mb-6 p-4 bg-white dark:bg-zinc-900 border border-gray-100 dark:border-zinc-800 rounded-lg" >
10291079 < div className = "flex items-center justify-between" >
10301080 < p className = "text-xs text-zinc-400 dark:text-slate-500 uppercase tracking-wider" > Reply Draft</ p >
1031- { replyIsReady && (
1032- < span className = "text-xs font-medium text-blue-600 dark:text-blue-300" > Draft ready</ span >
1033- ) }
1034- </ div >
1035- < div >
1036- < label className = "block text-xs text-zinc-500 dark:text-slate-400 mb-1" > Reply To</ label >
1037- < div className = "w-full text-sm border border-gray-200 dark:border-zinc-700 rounded px-3 py-2 bg-zinc-50 dark:bg-zinc-950 text-zinc-500 dark:text-slate-400" >
1038- { activeDraft . replyTo }
1039- </ div >
1040- </ div >
1041- < div >
1042- < label className = "block text-xs text-zinc-500 dark:text-slate-400 mb-1" > To</ label >
1043- < div className = "w-full text-sm border border-gray-200 dark:border-zinc-700 rounded px-3 py-2 bg-zinc-50 dark:bg-zinc-950 text-zinc-500 dark:text-slate-400" >
1044- { draftTo }
1045- </ div >
1046- </ div >
1047- < div >
1048- < label className = "block text-xs text-zinc-500 dark:text-slate-400 mb-1" > Subject</ label >
1049- < input
1050- type = "text"
1051- value = { draftSubject }
1052- onChange = { e => setDraftSubject ( e . target . value ) }
1053- className = "w-full text-sm border border-gray-200 dark:border-zinc-700 rounded px-3 py-2 bg-white dark:bg-zinc-950 text-zinc-700 dark:text-slate-300 focus:outline-none focus:ring-2 focus:ring-blue-300"
1054- />
1055- </ div >
1056- < div >
1057- < label className = "block text-xs text-zinc-500 dark:text-slate-400 mb-1" > Add Recipient</ label >
1058- < div className = "flex gap-2" >
1059- < button
1060- type = "button"
1061- onClick = { ( ) => setNewRecipientType ( type => type === 'cc' ? 'bcc' : 'cc' ) }
1062- className = "px-3 py-2 text-sm rounded border border-gray-200 dark:border-zinc-700 text-zinc-600 dark:text-slate-300"
1063- title = "Toggle recipient type"
1064- >
1065- { newRecipientType . toUpperCase ( ) } +
1066- </ button >
1067- < input
1068- type = "text"
1069- value = { newRecipientValue }
1070- onChange = { e => setNewRecipientValue ( e . target . value ) }
1071- placeholder = { newRecipientType === 'cc' ? 'Add Cc address' : 'Add Bcc address' }
1072- className = "flex-1 text-sm border border-gray-200 dark:border-zinc-700 rounded px-3 py-2 bg-white dark:bg-zinc-950 text-zinc-700 dark:text-slate-300 focus:outline-none focus:ring-2 focus:ring-blue-300"
1073- />
1081+ < div className = "flex items-center gap-3" >
1082+ { replyIsReady && (
1083+ < span className = "text-xs font-medium text-blue-600 dark:text-blue-300" > Draft ready</ span >
1084+ ) }
10741085 < button
10751086 type = "button"
1076- onClick = { ( ) => {
1077- if ( newRecipientType === 'cc' ) setDraftCc ( current => addUniqueAddress ( current , newRecipientValue ) )
1078- else setDraftBcc ( current => addUniqueAddress ( current , newRecipientValue ) )
1079- setNewRecipientValue ( '' )
1080- } }
1081- className = "px-3 py-2 text-sm rounded border border-blue-200 dark:border-blue-800 text-blue-600 dark:text-blue-300"
1082- title = "Add recipient"
1087+ onClick = { ( ) => setReplyDraftOpen ( current => ! current ) }
1088+ className = "text-xs text-zinc-500 dark:text-slate-400 hover:text-zinc-700 dark:hover:text-slate-200"
10831089 >
1084- +
1090+ { replyDraftOpen ? 'Fold' : 'Unfold' }
10851091 </ button >
10861092 </ div >
10871093 </ div >
1088- { draftCc . length > 0 && (
1089- < div >
1090- < label className = "block text-xs text-zinc-500 dark:text-slate-400 mb-1" > Cc</ label >
1091- < div className = "flex flex-wrap gap-2" >
1092- { draftCc . map ( email => (
1094+ { replyDraftOpen && (
1095+ < div className = "space-y-3 mt-3" >
1096+ < div >
1097+ < label className = "block text-xs text-zinc-500 dark:text-slate-400 mb-1" > Reply To</ label >
1098+ < div className = "w-full text-sm border border-gray-200 dark:border-zinc-700 rounded px-3 py-2 bg-zinc-50 dark:bg-zinc-950 text-zinc-500 dark:text-slate-400" >
1099+ { activeDraft . replyTo }
1100+ </ div >
1101+ </ div >
1102+ < div >
1103+ < label className = "block text-xs text-zinc-500 dark:text-slate-400 mb-1" > To</ label >
1104+ < div className = "w-full text-sm border border-gray-200 dark:border-zinc-700 rounded px-3 py-2 bg-zinc-50 dark:bg-zinc-950 text-zinc-500 dark:text-slate-400" >
1105+ { draftTo }
1106+ </ div >
1107+ </ div >
1108+ < div >
1109+ < label className = "block text-xs text-zinc-500 dark:text-slate-400 mb-1" > Subject</ label >
1110+ < input
1111+ type = "text"
1112+ value = { draftSubject }
1113+ onChange = { e => setDraftSubject ( e . target . value ) }
1114+ className = "w-full text-sm border border-gray-200 dark:border-zinc-700 rounded px-3 py-2 bg-white dark:bg-zinc-950 text-zinc-700 dark:text-slate-300 focus:outline-none focus:ring-2 focus:ring-blue-300"
1115+ />
1116+ </ div >
1117+ < div >
1118+ < label className = "block text-xs text-zinc-500 dark:text-slate-400 mb-1" > Add Recipient</ label >
1119+ < div className = "flex gap-2" >
10931120 < button
1094- key = { `cc-${ email } ` }
10951121 type = "button"
1096- onClick = { ( ) => setDraftCc ( current => current . filter ( value => value !== email ) ) }
1097- className = "text-xs px-2 py-1 rounded-full border border-sky-200 dark:border-sky-800 bg-sky-50 dark:bg-sky-950 text-sky-700 dark:text-sky-300"
1122+ onClick = { ( ) => setNewRecipientType ( type => type === 'cc' ? 'bcc' : 'cc' ) }
1123+ className = "px-3 py-2 text-sm rounded border border-gray-200 dark:border-zinc-700 text-zinc-600 dark:text-slate-300"
1124+ title = "Toggle recipient type"
10981125 >
1099- { email } ×
1126+ { newRecipientType . toUpperCase ( ) } +
11001127 </ button >
1101- ) ) }
1102- </ div >
1103- </ div >
1104- ) }
1105- { draftBcc . length > 0 && (
1106- < div >
1107- < label className = "block text-xs text-zinc-500 dark:text-slate-400 mb-1" > Bcc</ label >
1108- < div className = "flex flex-wrap gap-2" >
1109- { draftBcc . map ( email => (
1128+ < input
1129+ type = "text"
1130+ value = { newRecipientValue }
1131+ onChange = { e => setNewRecipientValue ( e . target . value ) }
1132+ placeholder = { newRecipientType === 'cc' ? 'Add Cc address' : 'Add Bcc address' }
1133+ className = "flex-1 text-sm border border-gray-200 dark:border-zinc-700 rounded px-3 py-2 bg-white dark:bg-zinc-950 text-zinc-700 dark:text-slate-300 focus:outline-none focus:ring-2 focus:ring-blue-300"
1134+ />
11101135 < button
1111- key = { `bcc-${ email } ` }
11121136 type = "button"
1113- onClick = { ( ) => setDraftBcc ( current => current . filter ( value => value !== email ) ) }
1114- className = "text-xs px-2 py-1 rounded-full border border-violet-200 dark:border-violet-800 bg-violet-50 dark:bg-violet-950 text-violet-700 dark:text-violet-300"
1137+ onClick = { ( ) => {
1138+ if ( newRecipientType === 'cc' ) setDraftCc ( current => addUniqueAddress ( current , newRecipientValue ) )
1139+ else setDraftBcc ( current => addUniqueAddress ( current , newRecipientValue ) )
1140+ setNewRecipientValue ( '' )
1141+ } }
1142+ className = "px-3 py-2 text-sm rounded border border-blue-200 dark:border-blue-800 text-blue-600 dark:text-blue-300"
1143+ title = "Add recipient"
11151144 >
1116- { email } ×
1145+ +
11171146 </ button >
1118- ) ) }
1147+ </ div >
11191148 </ div >
1149+ { draftCc . length > 0 && (
1150+ < div >
1151+ < label className = "block text-xs text-zinc-500 dark:text-slate-400 mb-1" > Cc</ label >
1152+ < div className = "flex flex-wrap gap-2" >
1153+ { draftCc . map ( email => (
1154+ < button
1155+ key = { `cc-${ email } ` }
1156+ type = "button"
1157+ onClick = { ( ) => setDraftCc ( current => current . filter ( value => value !== email ) ) }
1158+ className = "text-xs px-2 py-1 rounded-full border border-sky-200 dark:border-sky-800 bg-sky-50 dark:bg-sky-950 text-sky-700 dark:text-sky-300"
1159+ >
1160+ { email } ×
1161+ </ button >
1162+ ) ) }
1163+ </ div >
1164+ </ div >
1165+ ) }
1166+ { draftBcc . length > 0 && (
1167+ < div >
1168+ < label className = "block text-xs text-zinc-500 dark:text-slate-400 mb-1" > Bcc</ label >
1169+ < div className = "flex flex-wrap gap-2" >
1170+ { draftBcc . map ( email => (
1171+ < button
1172+ key = { `bcc-${ email } ` }
1173+ type = "button"
1174+ onClick = { ( ) => setDraftBcc ( current => current . filter ( value => value !== email ) ) }
1175+ className = "text-xs px-2 py-1 rounded-full border border-violet-200 dark:border-violet-800 bg-violet-50 dark:bg-violet-950 text-violet-700 dark:text-violet-300"
1176+ >
1177+ { email } ×
1178+ </ button >
1179+ ) ) }
1180+ </ div >
1181+ </ div >
1182+ ) }
11201183 </ div >
11211184 ) }
11221185 </ div >
0 commit comments