メール処理履歴画面にフィードバック機能を追加
- 全メール(重要・通常問わず)に対してフィードバックボタンを追加 - PATCH /api/mail/emails/<pk>/feedback/ エンドポイントを追加(JWT認証) - フィードバックモーダル: 重要/普通/今後通知しない/常に通知 の4択 - never_notify/always_notify 選択時はアドレス/ドメインの適用範囲を選択可能 - gmail_service アカウントのフィルタオプションを追加 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { api } from '@/lib/api';
|
||||
import Navbar from '@/components/Navbar';
|
||||
import { Mail, Loader2, ExternalLink, CheckCircle, XCircle, Clock, Shield } from 'lucide-react';
|
||||
import { Mail, Loader2, CheckCircle, XCircle, Clock, Shield, MessageSquare } from 'lucide-react';
|
||||
|
||||
interface MailEmail {
|
||||
id: number;
|
||||
@@ -34,6 +34,13 @@ const ACCOUNT_LABELS: Record<string, string> = {
|
||||
gmail_service: 'Gmail (サービス用)',
|
||||
};
|
||||
|
||||
const FEEDBACK_OPTIONS = [
|
||||
{ value: 'important', label: '重要だった', color: 'bg-green-50 text-green-700 border-green-200 hover:bg-green-100' },
|
||||
{ value: 'not_important',label: '普通のメール', color: 'bg-gray-50 text-gray-600 border-gray-200 hover:bg-gray-100' },
|
||||
{ value: 'never_notify', label: '今後通知しない', color: 'bg-orange-50 text-orange-700 border-orange-200 hover:bg-orange-100' },
|
||||
{ value: 'always_notify',label: '常に通知してほしい', color: 'bg-blue-50 text-blue-700 border-blue-200 hover:bg-blue-100' },
|
||||
];
|
||||
|
||||
export default function MailHistoryPage() {
|
||||
const router = useRouter();
|
||||
const [emails, setEmails] = useState<MailEmail[]>([]);
|
||||
@@ -41,6 +48,12 @@ export default function MailHistoryPage() {
|
||||
const [filterAccount, setFilterAccount] = useState('');
|
||||
const [filterVerdict, setFilterVerdict] = useState('');
|
||||
|
||||
// フィードバックモーダル
|
||||
const [feedbackModal, setFeedbackModal] = useState<MailEmail | null>(null);
|
||||
const [pendingFeedback, setPendingFeedback] = useState('');
|
||||
const [pendingScope, setPendingScope] = useState<'address' | 'domain'>('address');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchEmails = async () => {
|
||||
setLoading(true);
|
||||
@@ -68,6 +81,41 @@ export default function MailHistoryPage() {
|
||||
});
|
||||
};
|
||||
|
||||
const openFeedbackModal = (email: MailEmail) => {
|
||||
setFeedbackModal(email);
|
||||
setPendingFeedback(email.feedback ?? '');
|
||||
setPendingScope('address');
|
||||
};
|
||||
|
||||
const closeFeedbackModal = () => {
|
||||
setFeedbackModal(null);
|
||||
setPendingFeedback('');
|
||||
};
|
||||
|
||||
const submitFeedback = async () => {
|
||||
if (!feedbackModal || !pendingFeedback) return;
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const body: Record<string, string> = { feedback: pendingFeedback };
|
||||
if (['never_notify', 'always_notify'].includes(pendingFeedback)) {
|
||||
body.scope = pendingScope;
|
||||
}
|
||||
await api.patch(`/mail/emails/${feedbackModal.id}/feedback/`, body);
|
||||
setEmails(prev => prev.map(e =>
|
||||
e.id === feedbackModal.id
|
||||
? { ...e, feedback: pendingFeedback as MailEmail['feedback'], feedback_at: new Date().toISOString() }
|
||||
: e
|
||||
));
|
||||
closeFeedbackModal();
|
||||
} catch (error) {
|
||||
console.error('Failed to submit feedback:', error);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const needsScope = ['never_notify', 'always_notify'].includes(pendingFeedback);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<Navbar />
|
||||
@@ -128,7 +176,6 @@ export default function MailHistoryPage() {
|
||||
<th className="text-left px-4 py-3 text-gray-500 font-medium">件名</th>
|
||||
<th className="text-center px-4 py-3 text-gray-500 font-medium whitespace-nowrap">LLM判定</th>
|
||||
<th className="text-center px-4 py-3 text-gray-500 font-medium whitespace-nowrap">フィードバック</th>
|
||||
<th className="text-center px-4 py-3 text-gray-500 font-medium"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -156,35 +203,31 @@ export default function MailHistoryPage() {
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
{email.feedback ? (
|
||||
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
|
||||
email.feedback === 'important' ? 'bg-green-50 text-green-700' :
|
||||
email.feedback === 'not_important' ? 'bg-gray-100 text-gray-600' :
|
||||
email.feedback === 'never_notify' ? 'bg-orange-50 text-orange-700' :
|
||||
'bg-blue-50 text-blue-700'
|
||||
}`}>
|
||||
{FEEDBACK_LABELS[email.feedback] ?? email.feedback}
|
||||
</span>
|
||||
) : email.llm_verdict === 'important' ? (
|
||||
<span className="inline-flex items-center gap-1 text-amber-500 text-xs">
|
||||
<Clock className="h-3 w-3" />未回答
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-gray-400 text-xs">—</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
{email.feedback_token && (
|
||||
<a
|
||||
href={`/mail/feedback/${email.feedback_token}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 text-blue-600 hover:text-blue-800 text-xs"
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
{email.feedback ? (
|
||||
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
|
||||
email.feedback === 'important' ? 'bg-green-50 text-green-700' :
|
||||
email.feedback === 'not_important'? 'bg-gray-100 text-gray-600' :
|
||||
email.feedback === 'never_notify' ? 'bg-orange-50 text-orange-700' :
|
||||
'bg-blue-50 text-blue-700'
|
||||
}`}>
|
||||
{FEEDBACK_LABELS[email.feedback] ?? email.feedback}
|
||||
</span>
|
||||
) : email.llm_verdict === 'important' ? (
|
||||
<span className="inline-flex items-center gap-1 text-amber-500 text-xs">
|
||||
<Clock className="h-3 w-3" />未回答
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-gray-400 text-xs">—</span>
|
||||
)}
|
||||
<button
|
||||
onClick={() => openFeedbackModal(email)}
|
||||
className="inline-flex items-center gap-1 text-gray-400 hover:text-blue-600 text-xs transition-colors"
|
||||
title="フィードバックを設定"
|
||||
>
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
回答
|
||||
</a>
|
||||
)}
|
||||
<MessageSquare className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
@@ -196,6 +239,75 @@ export default function MailHistoryPage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* フィードバックモーダル */}
|
||||
{feedbackModal && (
|
||||
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50" onClick={closeFeedbackModal}>
|
||||
<div className="bg-white rounded-xl shadow-xl p-6 w-80 mx-4" onClick={(e) => e.stopPropagation()}>
|
||||
<h3 className="font-semibold text-gray-900 mb-1">フィードバック</h3>
|
||||
<p className="text-xs text-gray-500 mb-1 truncate">{feedbackModal.sender_email}</p>
|
||||
<p className="text-xs text-gray-700 mb-4 truncate">{feedbackModal.subject}</p>
|
||||
|
||||
<div className="space-y-2 mb-3">
|
||||
{FEEDBACK_OPTIONS.map(opt => (
|
||||
<button
|
||||
key={opt.value}
|
||||
onClick={() => setPendingFeedback(opt.value)}
|
||||
className={`w-full text-left px-3 py-2 rounded-lg text-sm border transition-colors ${
|
||||
pendingFeedback === opt.value
|
||||
? opt.color + ' font-medium'
|
||||
: 'border-gray-200 text-gray-600 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{needsScope && (
|
||||
<div className="mb-4 p-3 bg-gray-50 rounded-lg text-sm">
|
||||
<p className="text-xs text-gray-500 mb-2">適用範囲:</p>
|
||||
<div className="flex gap-4">
|
||||
<label className="flex items-center gap-1.5 cursor-pointer text-gray-700">
|
||||
<input
|
||||
type="radio"
|
||||
value="address"
|
||||
checked={pendingScope === 'address'}
|
||||
onChange={() => setPendingScope('address')}
|
||||
/>
|
||||
このアドレス
|
||||
</label>
|
||||
<label className="flex items-center gap-1.5 cursor-pointer text-gray-700">
|
||||
<input
|
||||
type="radio"
|
||||
value="domain"
|
||||
checked={pendingScope === 'domain'}
|
||||
onChange={() => setPendingScope('domain')}
|
||||
/>
|
||||
このドメイン
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={closeFeedbackModal}
|
||||
className="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 text-gray-700"
|
||||
>
|
||||
キャンセル
|
||||
</button>
|
||||
<button
|
||||
onClick={submitFeedback}
|
||||
disabled={!pendingFeedback || submitting}
|
||||
className="flex-1 px-3 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{submitting ? '送信中...' : '確定'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user