GuestOps/web/src/ReplyActions.tsx

43 lines
5.9 KiB
TypeScript

import { useEffect } from 'react';
import { api, type Conversation, type Hotel, type Knowledge, type Mailboxes } from './api';
type Run = (action: () => Promise<void>) => Promise<void>;
export function ReplyActions({message,hotel,mailboxes,knowledge,dirty,busy,run,onUpdate}:{message:Conversation;hotel:Hotel;mailboxes:Mailboxes;knowledge:Knowledge[];dirty:boolean;busy:boolean;run:Run;onUpdate:(c:Conversation)=>void}) {
const delivery=message.delivery;
useEffect(()=>{
if(!delivery || !['Pending','Sending'].includes(delivery.state))return;
let active=true;
const timer=setInterval(()=>{api<Conversation>(`/conversations/${message.id}`).then(c=>{if(active)onUpdate(c);}).catch(()=>{});},5000);
return()=>{active=false;clearInterval(timer);};
},[message.id,delivery?.state,onUpdate]);
const generate=()=>run(async()=>{
if(message.draft && !window.confirm('Replace the saved draft with a new AI suggestion?'))return;
onUpdate(await api<Conversation>(`/conversations/${message.id}/generate`,'POST',{version:message.version}));
});
const send=()=>run(async()=>{
if(!window.confirm(`Send this saved reply to ${message.replyAddress}?\n\n${message.draft}\n\nThis submits the reply to Gmail. You cannot undo it here.`))return;
onUpdate(await api<Conversation>(`/conversations/${message.id}/send`,'POST',{version:message.version,recipient:message.replyAddress}));
});
const act=(action:string)=>run(async()=>onUpdate(await api<Conversation>(`/conversations/${message.id}/delivery/${action}`,'POST',{version:message.version})));
const canSend=hotel.staffSendingEnabled&&mailboxes.sendingConfigured&&mailboxes.items.some(m=>m.id===message.mailboxId&&m.canSend&&m.status==='Connected');
return <section className="reply-actions" aria-label="Reply review and delivery">
{message.autoReplyDetail&&<p className="staff-note">FAQ check: {message.autoReplyDetail}</p>}
{delivery?.automatic&&<p className="small muted">This reply uses an owner-approved FAQ rule.</p>}
{message.draftReviewNote&&<p className="staff-note">AI review: {message.draftReviewNote}</p>}
{!!message.draftSources?.length&&<details><summary>Hotel answers referenced by this draft</summary>{message.draftSources.map(id=>{const source=knowledge.find(k=>k.id===id);return <div key={id}><strong>{source?.title||'Answer no longer available'}</strong><p>{source?.answer||'Ask your hotel owner to check this information.'}</p>{source&&!source.approved&&<p>This answer is no longer approved. Check the draft before sending.</p>}</div>;})}</details>}
{delivery?<div role="status"><strong>Delivery: {delivery.state==='NeedsReview'?'Needs verification':delivery.state}</strong><p>{delivery.detail}</p><p className="small">To: {delivery.recipient}</p>{delivery.state==='Rejected'&&<button className="button secondary" disabled={busy} onClick={()=>act(delivery.automatic?'release':'retry')}>{delivery.automatic?'Return to staff review':'Retry approved reply'}</button>}{delivery.state==='NeedsReview'&&<><button className="button secondary" disabled={busy} onClick={()=>act('verify')}>Verify in Gmail Sent</button><p className="small">Reference: {delivery.messageId}. An uncertain reply is never automatically resent.</p></>}</div>:<>
<div className="form-actions"><button className="button secondary" disabled={busy||dirty||!hotel.aiDraftsEnabled||!mailboxes.aiConfigured} onClick={generate}>Generate AI draft</button><button className="button primary" disabled={busy||dirty||!canSend||!message.draft.trim()||!message.replyAddress} onClick={send}>Review and send</button></div>
<p className="small muted">{dirty?'Save your changes before generating or sending.':canSend?`Reply to: ${message.replyAddress||'Unavailable — open this message in Gmail'}. This manual reply requires your approval.`:'Staff sending is not enabled for this mailbox. Your hotel owner can configure it in Settings.'}</p>
{!hotel.aiDraftsEnabled&&<p className="small muted">AI drafts are off. Your hotel owner can enable them after the administrator configures AI.</p>}
</>}
</section>;
}
export function ReplyControls({hotel,mailboxes,owner,busy,run,onSave}:{hotel:Hotel;mailboxes:Mailboxes;owner:boolean;busy:boolean;run:Run;onSave:(hotel:Hotel)=>void}) {
const update=(field:'aiDraftsEnabled'|'staffSendingEnabled',value:boolean)=>run(async()=>{
if(value&&!window.confirm(field==='aiDraftsEnabled'?'Enable AI drafts? Message text and selected approved hotel answers will be sent to the configured OpenAI service when staff request a draft.':'Enable staff-approved Gmail sending for this hotel? Staff replies require review. If FAQ live mode is enabled separately, its approved rules can also send automatically.'))return;
onSave(await api<Hotel>('/reply-controls','PUT',{version:hotel.version,aiDraftsEnabled:hotel.aiDraftsEnabled,staffSendingEnabled:hotel.staffSendingEnabled,[field]:value}));
});
return <section className="settings-card"><h2>Reply controls</h2><p>Staff replies require review. FAQ automation is controlled separately on the FAQ automation page.</p><label className="checkbox-label"><input type="checkbox" checked={hotel.aiDraftsEnabled||false} disabled={!owner||busy||(!mailboxes.aiConfigured&&!hotel.aiDraftsEnabled)} onChange={e=>update('aiDraftsEnabled',e.target.checked)}/>AI drafts from approved hotel knowledge</label><p className="small muted">{mailboxes.aiConfigured?'Only requested drafts use AI. Check all facts before sending.':'Your administrator must configure the AI API key and model first.'}</p><label className="checkbox-label"><input type="checkbox" checked={hotel.staffSendingEnabled||false} disabled={!owner||busy||(!mailboxes.sendingConfigured&&!hotel.staffSendingEnabled)} onChange={e=>update('staffSendingEnabled',e.target.checked)}/>Allow staff to approve and send replies</label><p className="small muted">{mailboxes.sendingConfigured?'Reconnect Google to grant send permission if it was previously read-only.':'Your administrator must enable Google sending first.'}</p></section>;
}