diff --git a/package-lock.json b/package-lock.json index 39d05ef..8c13277 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@atproto/api": "^0.18.9", + "@google/generative-ai": "^0.24.1", "@steipete/bird": "^0.4.0", "axios": "^1.13.2", "bcryptjs": "^3.0.3", @@ -737,6 +738,15 @@ "node": ">=18" } }, + "node_modules/@google/generative-ai": { + "version": "0.24.1", + "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.1.tgz", + "integrity": "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@img/colour": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", diff --git a/package.json b/package.json index d45fb28..4e73e35 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "license": "MIT", "dependencies": { "@atproto/api": "^0.18.9", + "@google/generative-ai": "^0.24.1", "@steipete/bird": "^0.4.0", "axios": "^1.13.2", "bcryptjs": "^3.0.3", diff --git a/public/index.html b/public/index.html index b4d8228..45036cd 100644 --- a/public/index.html +++ b/public/index.html @@ -8,23 +8,86 @@ @@ -44,15 +107,15 @@ const [view, setView] = useState(localStorage.getItem('token') ? 'dashboard' : 'login'); const [mappings, setMappings] = useState([]); const [twitterConfig, setTwitterConfig] = useState({ authToken: '', ct0: '' }); + const [geminiApiKey, setGeminiApiKey] = useState(''); const [isAdmin, setIsAdmin] = useState(false); const [status, setStatus] = useState({}); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [countdown, setCountdown] = useState(''); - - // Edit Modal State const [editingMapping, setEditingMapping] = useState(null); + // ... (keep logic for logout, fetchStatus, etc.) ... const handleLogout = useCallback(() => { localStorage.removeItem('token'); setToken(null); @@ -86,6 +149,8 @@ if (meRes.data.isAdmin) { const twitRes = await axios.get('/api/twitter-config', { headers }); setTwitterConfig(twitRes.data); + const gemRes = await axios.get('/api/gemini-config', { headers }); + setGeminiApiKey(gemRes.data.apiKey); } fetchStatus(); } catch (err) { @@ -110,30 +175,28 @@ useEffect(() => { if (view !== 'dashboard' || !token) return; - - const statusTimer = setInterval(fetchStatus, 30000); + const statusTimer = setInterval(fetchStatus, 5000); // Faster status poll for better responsiveness return () => clearInterval(statusTimer); }, [view, token, fetchStatus]); useEffect(() => { if (!status.nextCheckTime) return; - const updateCountdown = () => { const diff = status.nextCheckTime - Date.now(); if (diff <= 0) { - setCountdown('Soon...'); + setCountdown('Checking...'); return; } const mins = Math.floor(diff / 60000); const secs = Math.floor((diff % 60000) / 1000); setCountdown(`${mins}m ${secs}s`); }; - updateCountdown(); const timer = setInterval(updateCountdown, 1000); return () => clearInterval(timer); }, [status.nextCheckTime]); + // ... Auth Handlers ... const handleLogin = async (e) => { e.preventDefault(); setError(''); @@ -163,6 +226,7 @@ } }; + // ... Action Handlers ... const addMapping = async (e) => { e.preventDefault(); setLoading(true); @@ -206,9 +270,7 @@ const deleteMapping = async (id) => { if (!confirm('Are you sure?')) return; try { - await axios.delete(`/api/mappings/${id}`, { - headers: { Authorization: `Bearer ${token}` } - }); + await axios.delete(`/api/mappings/${id}`, { headers: { Authorization: `Bearer ${token}` } }); fetchData(); } catch (err) { alert('Failed to delete'); @@ -216,32 +278,20 @@ }; const runNow = async () => { - if (!confirm('Run check now for all accounts?')) return; try { - await axios.post('/api/run-now', {}, { - headers: { Authorization: `Bearer ${token}` } - }); - alert('Check triggered!'); - setTimeout(fetchStatus, 1000); + await axios.post('/api/run-now', {}, { headers: { Authorization: `Bearer ${token}` } }); + fetchStatus(); } catch (err) { alert('Failed to trigger check'); } }; - const runBackfill = async (id, twitterUsernames) => { - // If multiple usernames, we could ask which one, but backend handles "all for this mapping" currently - // or we can prompt for a specific one if we want. - // For simplicity, let's just trigger backfill for the mapping, which iterates all usernames. - + const runBackfill = async (id) => { const limit = prompt(`How many tweets to backfill per account?`, "15"); if (limit === null) return; - try { - await axios.post(`/api/backfill/${id}`, { limit: parseInt(limit) || 15 }, { - headers: { Authorization: `Bearer ${token}` } - }); - alert(`Backfill queued`); - setTimeout(fetchStatus, 1000); + await axios.post(`/api/backfill/${id}`, { limit: parseInt(limit) || 15 }, { headers: { Authorization: `Bearer ${token}` } }); + fetchStatus(); } catch (err) { alert(err.response?.data?.error || 'Failed to queue backfill'); } @@ -249,9 +299,7 @@ const cancelBackfill = async (id) => { try { - await axios.delete(`/api/backfill/${id}`, { - headers: { Authorization: `Bearer ${token}` } - }); + await axios.delete(`/api/backfill/${id}`, { headers: { Authorization: `Bearer ${token}` } }); fetchStatus(); } catch (err) { alert('Failed to cancel backfill'); @@ -261,9 +309,7 @@ const clearAllBackfills = async () => { if (!confirm('Stop all pending and running backfills?')) return; try { - await axios.post('/api/backfill/clear-all', {}, { - headers: { Authorization: `Bearer ${token}` } - }); + await axios.post('/api/backfill/clear-all', {}, { headers: { Authorization: `Bearer ${token}` } }); fetchStatus(); } catch (err) { alert('Failed to clear backfills'); @@ -271,11 +317,9 @@ }; const clearCache = async (id) => { - if (!confirm(`Clear processed tweets cache for all accounts in this mapping?`)) return; + if (!confirm(`Clear processed tweets cache?`)) return; try { - await axios.delete(`/api/mappings/${id}/cache`, { - headers: { Authorization: `Bearer ${token}` } - }); + await axios.delete(`/api/mappings/${id}/cache`, { headers: { Authorization: `Bearer ${token}` } }); alert(`Cache cleared`); } catch (err) { alert('Failed to clear cache'); @@ -285,16 +329,10 @@ const resetAndBackfill = async (id) => { const limit = prompt(`Reset cache and backfill how many tweets?`, "15"); if (limit === null) return; - try { - await axios.delete(`/api/mappings/${id}/cache`, { - headers: { Authorization: `Bearer ${token}` } - }); - await axios.post(`/api/backfill/${id}`, { limit: parseInt(limit) || 15 }, { - headers: { Authorization: `Bearer ${token}` } - }); - alert(`Cache cleared and backfill queued`); - setTimeout(fetchStatus, 1000); + await axios.delete(`/api/mappings/${id}/cache`, { headers: { Authorization: `Bearer ${token}` } }); + await axios.post(`/api/backfill/${id}`, { limit: parseInt(limit) || 15 }, { headers: { Authorization: `Bearer ${token}` } }); + fetchStatus(); } catch (err) { alert('Reset & Backfill failed'); } @@ -315,32 +353,50 @@ } }; + const updateGemini = async (e) => { + e.preventDefault(); + const formData = new FormData(e.target); + try { + await axios.post('/api/gemini-config', { + apiKey: formData.get('apiKey') + }, { headers: { Authorization: `Bearer ${token}` } }); + alert('Gemini API Key updated!'); + fetchData(); + } catch (err) { + alert('Failed to update Gemini config'); + } + }; + if (view === 'login' || view === 'register' || !token) { return ( -
-
-

{view === 'login' ? 'Login' : 'Register'}

- {error &&
{error}
} +
+
+
+ swap_calls +

{view === 'login' ? 'Welcome Back' : 'Join Us'}

+
+ {error &&
{error}
}
- +
-
- +
+
- -
-
@@ -353,198 +409,197 @@ return (
-