feat: Implement AI classification and Web UI, refactor to IMAP

This commit is contained in:
hyungi
2025-12-29 15:50:35 +09:00
commit d3c9cd2c7f
13 changed files with 928 additions and 0 deletions

35
web/index.html Normal file
View File

@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Mail Manager - Rule Editor</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="container">
<header>
<h1>Mail Processing Rule Editor</h1>
<p>Edit the rules in JSON format below. Click 'Save Rules' to apply them.</p>
</header>
<main>
<div class="editor-container">
<textarea id="rules-editor" spellcheck="false"></textarea>
</div>
<div class="actions">
<button id="save-button">Save Rules</button>
<button id="run-button">Run Processing Now</button>
</div>
<div id="status-message"></div>
</main>
<footer>
<p>AI Mail Server</p>
</footer>
</div>
<script src="/static/script.js"></script>
</body>
</html>

85
web/script.js Normal file
View File

@@ -0,0 +1,85 @@
document.addEventListener('DOMContentLoaded', () => {
const editor = document.getElementById('rules-editor');
const saveButton = document.getElementById('save-button');
const runButton = document.getElementById('run-button');
const statusMessage = document.getElementById('status-message');
// Function to display status messages
const showStatus = (message, isError = false) => {
statusMessage.textContent = message;
statusMessage.className = isError ? 'error' : 'success';
};
// 1. Fetch initial rules on page load
fetch('/api/rules')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
if (data.error) {
showStatus(`Error loading rules: ${data.error}`, true);
} else {
// Pretty-print the JSON with 2 spaces
editor.value = JSON.stringify(data, null, 2);
}
})
.catch(error => {
showStatus('Failed to fetch rules. Is the server running?', true);
console.error('Fetch error:', error);
});
// 2. Add event listener for the Save button
saveButton.addEventListener('click', () => {
let rulesContent;
try {
// We parse it first to ensure it's valid JSON before sending
rulesContent = JSON.parse(editor.value);
} catch (error) {
showStatus('Invalid JSON format. Please correct it before saving.', true);
return;
}
fetch('/api/rules', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(rulesContent),
})
.then(response => response.json())
.then(data => {
if (data.error) {
showStatus(`Error saving rules: ${data.error}`, true);
} else {
showStatus(data.message || 'Rules saved successfully!', false);
}
})
.catch(error => {
showStatus('Failed to save rules. An unknown error occurred.', true);
console.error('Save error:', error);
});
});
// 3. Add event listener for the Run button
runButton.addEventListener('click', () => {
showStatus('Requesting to start email processing...', false);
fetch('/api/run-processing', {
method: 'POST',
})
.then(response => response.json())
.then(data => {
if (data.error) {
showStatus(`Error starting process: ${data.error}`, true);
} else {
showStatus(data.message || 'Processing started in the background.', false);
}
})
.catch(error => {
showStatus('Failed to start processing. An unknown error occurred.', true);
console.error('Run error:', error);
});
});
});

116
web/style.css Normal file
View File

@@ -0,0 +1,116 @@
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background-color: #f4f7f9;
color: #333;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.container {
width: 80%;
max-width: 900px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
padding: 2rem;
}
header {
text-align: center;
border-bottom: 1px solid #e0e0e0;
padding-bottom: 1rem;
margin-bottom: 1.5rem;
}
h1 {
color: #2c3e50;
margin: 0;
}
p {
color: #7f8c8d;
}
.editor-container {
border: 1px solid #ccc;
border-radius: 4px;
overflow: hidden;
}
#rules-editor {
width: 100%;
height: 400px;
border: none;
padding: 1rem;
font-family: "SF Mono", "Fira Code", "Courier New", monospace;
font-size: 14px;
line-height: 1.5;
resize: vertical;
box-sizing: border-box;
}
.actions {
display: flex;
justify-content: flex-end;
gap: 1rem;
margin-top: 1.5rem;
}
button {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.2s ease;
}
#save-button {
background-color: #3498db;
color: white;
}
#save-button:hover {
background-color: #2980b9;
}
#run-button {
background-color: #2ecc71;
color: white;
}
#run-button:hover {
background-color: #27ae60;
}
#status-message {
margin-top: 1rem;
padding: 0.75rem;
border-radius: 4px;
text-align: center;
font-weight: 500;
display: none; /* Hidden by default */
}
#status-message.success {
background-color: #e8f5e9;
color: #2e7d32;
display: block;
}
#status-message.error {
background-color: #ffebee;
color: #c62828;
display: block;
}
footer {
text-align: center;
margin-top: 2rem;
padding-top: 1rem;
border-top: 1px solid #e0e0e0;
color: #bdc3c7;
}