1610 lines
87 KiB
JavaScript
1610 lines
87 KiB
JavaScript
"use client";
|
||
|
||
import React, { useState, useEffect, useRef } from "react";
|
||
import {
|
||
Activity,
|
||
Terminal,
|
||
User,
|
||
Cpu,
|
||
ShieldAlert,
|
||
CheckCircle,
|
||
XCircle,
|
||
Database,
|
||
FileText,
|
||
Sparkles,
|
||
Send,
|
||
RefreshCw,
|
||
Play,
|
||
GitPullRequest,
|
||
Layers,
|
||
Settings,
|
||
AlertCircle,
|
||
Trash2,
|
||
HeartPulse,
|
||
LogOut,
|
||
Maximize2,
|
||
CloudSun,
|
||
Wind,
|
||
Droplets,
|
||
Thermometer,
|
||
Compass,
|
||
MessageSquare,
|
||
Mail
|
||
} from "lucide-react";
|
||
|
||
import WhatsAppAgentUI from "../components/WhatsAppAgentUI";
|
||
import GmailAgentUI from "../components/GmailAgentUI";
|
||
|
||
export default function Page() {
|
||
const [activeTab, setActiveTab] = useState("dashboard");
|
||
|
||
// Claw Chat State
|
||
const [messages, setMessages] = useState([
|
||
{
|
||
role: "assistant",
|
||
content: "Hello! I am Claw, your local agent. Try:\n• `weather` — live weather for Mars, PA\n• `whatsapp me: message` — send now to your phone\n• `remind me at 3pm: call doctor` — schedule for later\n• `remind me tomorrow at 6am: gym` — future reminders\n• Or give me a list with times and I'll schedule them all!",
|
||
timestamp: "Just now"
|
||
}
|
||
]);
|
||
const [inputValue, setInputValue] = useState("");
|
||
const [isClawTyping, setIsClawTyping] = useState(false);
|
||
const [clawTerminalLogs, setClawTerminalLogs] = useState([]);
|
||
const [weatherCard, setWeatherCard] = useState(null);
|
||
const [weatherSending, setWeatherSending] = useState(false);
|
||
const [weatherSent, setWeatherSent] = useState(false);
|
||
const [scheduledReminders, setScheduledReminders] = useState([]);
|
||
|
||
// OpsAgent State
|
||
const [pendingApprovals, setPendingApprovals] = useState([
|
||
{
|
||
id: "ops-req-102",
|
||
targetType: "proxmox",
|
||
targetId: "102",
|
||
action: "clean_disk",
|
||
details: "LXC container root filesystem is at 94% disk usage. Recommending apt cache clean and temporary file removal.",
|
||
timestamp: "5 mins ago",
|
||
status: "pending"
|
||
},
|
||
{
|
||
id: "ops-req-201",
|
||
targetType: "kubernetes",
|
||
targetId: "curio-app-xyz",
|
||
action: "restart_pod",
|
||
details: "Pod curio-app-xyz is in CrashLoopBackOff state. Logs indicate: 'Database connection failed: Connection refused by 10.96.20.10:5432'. Recommending pod deletion to trigger k8s reschedule.",
|
||
timestamp: "10 mins ago",
|
||
status: "pending"
|
||
}
|
||
]);
|
||
const [opsLogs, setOpsLogs] = useState([
|
||
"2026-05-29 01:10:00 [OpsAgent] Initialized diagnostic run on K8s cluster...",
|
||
"2026-05-29 01:10:15 [OpsAgent] Found Pod 'curio-app-xyz' in CrashLoopBackOff. Emitting warning.",
|
||
"2026-05-29 01:12:02 [OpsAgent] Proxmox VMID 102 root usage checked: 94%. Action required."
|
||
]);
|
||
|
||
// Curio HMS State
|
||
const [dictationText, setDictationText] = useState(
|
||
"Patient is a 45-year-old male presenting with severe, crushing chest pain radiating to his left arm. He has a past medical history of hypertension. Admitting to Cardiac ICU."
|
||
);
|
||
const [clinicalNote, setClinicalNote] = useState(null);
|
||
const [billingCodes, setBillingCodes] = useState([]);
|
||
const [isProcessingClinical, setIsProcessingClinical] = useState(false);
|
||
|
||
// Workflow logs state
|
||
const [workflows, setWorkflows] = useState([
|
||
{ id: "wf-bernard-001", name: "PRReviewWorkflow", agent: "Bernard", target: "pr-401", status: "completed", result: "Approved with suggestions" },
|
||
{ id: "wf-gumbo-002", name: "GumboSummarizeWorkflow", agent: "Gumbo", target: "api-spec.md", status: "completed", result: "Summary persisted in PG" },
|
||
{ id: "wf-ops-003", name: "OpsRemediateWorkflow", agent: "OpsAgent", target: "VMID 102", status: "running", result: "Awaiting approval signal" }
|
||
]);
|
||
|
||
// Trips State
|
||
const [tripsList, setTripsList] = useState([]);
|
||
const [selectedTripId, setSelectedTripId] = useState(null);
|
||
const [tripEditorJson, setTripEditorJson] = useState("");
|
||
const [isSavingTrip, setIsSavingTrip] = useState(false);
|
||
const [isTestingTrip, setIsTestingTrip] = useState(false);
|
||
|
||
// Config State
|
||
const [configData, setConfigData] = useState({ defaultGroupUrl: "", tripGroupUrl: "" });
|
||
const [isSavingConfig, setIsSavingConfig] = useState(false);
|
||
const [configLoaded, setConfigLoaded] = useState(false);
|
||
|
||
const messagesEndRef = useRef(null);
|
||
const terminalEndRef = useRef(null);
|
||
|
||
useEffect(() => {
|
||
if (activeTab === "config" && !configLoaded) {
|
||
fetch('/api/config/whatsapp')
|
||
.then(res => res.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
setConfigData({
|
||
defaultGroupUrl: data.data['default-group-url'] || "",
|
||
tripGroupUrl: data.data['trip-group-url'] || ""
|
||
});
|
||
setConfigLoaded(true);
|
||
}
|
||
})
|
||
.catch(console.error);
|
||
}
|
||
}, [activeTab, configLoaded]);
|
||
|
||
const handleSaveConfig = async () => {
|
||
setIsSavingConfig(true);
|
||
try {
|
||
const res = await fetch('/api/config/whatsapp', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(configData)
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
alert("Configuration saved successfully to Kubernetes manifest!");
|
||
} else {
|
||
alert("Failed to save: " + data.error);
|
||
}
|
||
} catch (e) {
|
||
alert("Error saving config: " + e.message);
|
||
} finally {
|
||
setIsSavingConfig(false);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||
}, [messages, isClawTyping]);
|
||
|
||
useEffect(() => {
|
||
terminalEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||
}, [clawTerminalLogs]);
|
||
|
||
useEffect(() => {
|
||
if (activeTab === "trips") {
|
||
fetch('/api/trips').then(res => res.json()).then(data => {
|
||
if(Array.isArray(data)) setTripsList(data);
|
||
}).catch(() => {});
|
||
}
|
||
}, [activeTab]);
|
||
|
||
// Simulated Claw response logic
|
||
const handleSendMessage = async (e) => {
|
||
e.preventDefault();
|
||
if (!inputValue.trim()) return;
|
||
|
||
const userText = inputValue;
|
||
setMessages(prev => [...prev, { role: "user", content: userText, timestamp: "Just now" }]);
|
||
setInputValue("");
|
||
setIsClawTyping(true);
|
||
setWeatherCard(null);
|
||
setWeatherSent(false);
|
||
setClawTerminalLogs(prev => [...prev, `[Claw] Received instruction: "${userText}"`]);
|
||
|
||
let responseText = "";
|
||
let logs = [];
|
||
|
||
const cleanInput = userText.toLowerCase();
|
||
|
||
// ── WhatsApp Send Command ────────────────────────────────────────────────
|
||
// Matches: "whatsapp me: ...", "send to whatsapp ...", "message me ...",
|
||
// "text me ...", "wa me ...", "send 'X' to my whatsapp"
|
||
const waIntent = /(?:whatsapp\s+me|message\s+me|text\s+me|send\s+(?:to\s+)?(?:my\s+)?whatsapp|wa\s+me|remind\s+me\s+via\s+whatsapp)[:\s]+(.+)/i;
|
||
const waMatch = userText.match(waIntent);
|
||
|
||
if (waMatch) {
|
||
const waMsg = waMatch[1].replace(/^["'`]|["'`]$/g, '').trim(); // strip surrounding quotes
|
||
|
||
setClawTerminalLogs(prev => [...prev,
|
||
`[Claw] WhatsApp send detected. Message: "${waMsg}"`,
|
||
`[Claw] Calling /api/send-whatsapp (personalOnly) ...`
|
||
]);
|
||
|
||
try {
|
||
const res = await fetch('/api/send-whatsapp', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ message: `📌 *Claw Reminder*\n\n${waMsg}`, personalOnly: true }),
|
||
});
|
||
const data = await res.json();
|
||
|
||
if (data.success) {
|
||
setClawTerminalLogs(prev => [...prev, `[Claw] ✅ Delivered to +14085505485. MsgID: ${data.messageId}`]);
|
||
responseText = `✅ Done! I've sent that to your WhatsApp (+14085505485):\n\n> "${waMsg}"\n\nYou should see it arrive shortly.`;
|
||
} else {
|
||
throw new Error(data.error || 'Gateway error');
|
||
}
|
||
} catch (err) {
|
||
setClawTerminalLogs(prev => [...prev, `[Claw] ❌ WhatsApp send failed: ${err.message}`]);
|
||
responseText = `❌ Couldn't send to WhatsApp: ${err.message}. Make sure the gateway is running.`;
|
||
}
|
||
|
||
// ── Scheduled Reminder ─────────────────────────────────────────────────────
|
||
// Patterns: "remind me at X: msg", "schedule (whatsapp) at X: msg",
|
||
// "send to whatsapp at X: msg", multi-bullet lists
|
||
} else if (
|
||
/(?:remind\s+me|schedule\s+(?:a\s+)?(?:whatsapp\s+)?(?:reminder|message)?)\s+(?:at|on|for|in|tomorrow|monday|tuesday|wednesday|thursday|friday|saturday|sunday)/i.test(userText) ||
|
||
/send\s+to\s+whatsapp\s+(?:at|in|tomorrow|on\s+)/i.test(userText) ||
|
||
// multi-bullet list: "schedule these:\n- msg at 3pm\n- msg2 at 6am"
|
||
(/(?:schedule|remind\s+me)/i.test(userText) && userText.includes('\n'))
|
||
) {
|
||
// Check for multi-item list (lines starting with - or •)
|
||
const lines = userText.split('\n').map(l => l.trim()).filter(Boolean);
|
||
const bulletLines = lines.filter(l => /^[-•*]\s+/.test(l) || (lines.length > 2 && l !== lines[0]));
|
||
const isBatch = bulletLines.length > 1;
|
||
|
||
if (isBatch) {
|
||
// Batch scheduling
|
||
const items = [];
|
||
const skipped = [];
|
||
for (const line of bulletLines) {
|
||
const clean = line.replace(/^[-•*]\s*/, '').trim();
|
||
// Try to split into message + time: "call doctor at 2pm" or "2pm: call doctor"
|
||
// Strategy: extract time, rest is the message
|
||
const sendAt = parseScheduleDateTime(clean);
|
||
if (!sendAt) { skipped.push(clean); continue; }
|
||
// Remove the time expression from the label
|
||
const label = clean.replace(/(at|by|on|for)?\s*\d{1,2}(?::\d{2})?\s*(am|pm)/gi, '').replace(/tomorrow|today|monday|tuesday|wednesday|thursday|friday|saturday|sunday/gi, '').replace(/in\s+\d+\s+(hour|hr|minute|min)s?/gi, '').trim().replace(/\s+/g, ' ');
|
||
const formatted = `⏰ *Claw Reminder*\n\n${label}\n\n_Scheduled for ${sendAt.toLocaleString('en-US', { timeZone: 'America/New_York', weekday: 'short', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', hour12: true })} ET_`;
|
||
items.push({ message: formatted, sendAt: sendAt.toISOString(), label });
|
||
}
|
||
|
||
if (items.length === 0) {
|
||
responseText = `❌ I couldn't parse any valid times from your list. Make sure each item includes a time like "at 3pm", "tomorrow at 9am", or "in 2 hours".`;
|
||
} else {
|
||
setClawTerminalLogs(prev => [...prev, `[Claw] Scheduling ${items.length} reminders...`]);
|
||
try {
|
||
const res = await fetch('/api/reminders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ items }) });
|
||
const data = await res.json();
|
||
const lines2 = data.created?.map(c => `✅ "${c.label}" → ${formatReminderTime(c.sendAt)} ET`) || [];
|
||
if (skipped.length) lines2.push(...skipped.map(s => `⚠️ Skipped (no time): "${s}"`));
|
||
responseText = `📅 Scheduled ${data.created?.length || 0} reminder(s):\n\n${lines2.join('\n')}`;
|
||
// Refresh reminder panel
|
||
fetch('/api/reminders?status=pending').then(r => r.json()).then(d => { if (Array.isArray(d)) setScheduledReminders(d); });
|
||
setClawTerminalLogs(prev => [...prev, `[Claw] ✅ Batch scheduled: ${items.length} reminders`]);
|
||
} catch (err) {
|
||
responseText = `❌ Failed to schedule: ${err.message}`;
|
||
}
|
||
}
|
||
|
||
} else {
|
||
// Single reminder
|
||
// Parse message after colon: "remind me at 3pm: call doctor" → msg = "call doctor"
|
||
const colonSplit = userText.match(/[:\u2014]\s*(.+)$/s);
|
||
const msgText = colonSplit ? colonSplit[1].trim() : userText;
|
||
const sendAt = parseScheduleDateTime(userText);
|
||
|
||
if (!sendAt) {
|
||
responseText = `❌ I couldn't find a valid time in your message. Try: "remind me at 3pm: call doctor" or "remind me tomorrow at 9am: gym".`;
|
||
} else {
|
||
const etLabel = sendAt.toLocaleString('en-US', { timeZone: 'America/New_York', weekday: 'short', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', hour12: true });
|
||
const waMsg = `⏰ *Claw Reminder*\n\n${msgText}\n\n_Scheduled for ${etLabel} ET_`;
|
||
setClawTerminalLogs(prev => [...prev, `[Claw] Scheduling: "${msgText}" → ${sendAt.toISOString()}`]);
|
||
try {
|
||
const res = await fetch('/api/schedule-reminder', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ message: waMsg, sendAt: sendAt.toISOString(), label: msgText }),
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
responseText = `✅ Reminder scheduled!\n\n📌 **"${msgText}"**\n🕐 Delivers at **${etLabel} ET** to your WhatsApp (+14085505485)`;
|
||
fetch('/api/reminders?status=pending').then(r => r.json()).then(d => { if (Array.isArray(d)) setScheduledReminders(d); });
|
||
setClawTerminalLogs(prev => [...prev, `[Claw] ✅ Scheduled ID: ${data.id}`]);
|
||
} else {
|
||
throw new Error(data.error || 'Scheduling failed');
|
||
}
|
||
} catch (err) {
|
||
responseText = `❌ Couldn't schedule: ${err.message}`;
|
||
setClawTerminalLogs(prev => [...prev, `[Claw] ❌ ${err.message}`]);
|
||
}
|
||
}
|
||
}
|
||
|
||
} else if (cleanInput.includes("weather")) {
|
||
|
||
// Extract optional location override
|
||
const locMatch = userText.match(/(?:weather\s+(?:in|for|at)?\s*)(.+)/i);
|
||
const location = locMatch ? locMatch[1].trim() : '16046';
|
||
|
||
logs = [
|
||
`[Claw] Fetching live weather data for "${location}"...`,
|
||
"[Claw] Connecting to wttr.in weather service...",
|
||
];
|
||
setClawTerminalLogs(prev => [...prev, ...logs]);
|
||
|
||
try {
|
||
const res = await fetch(`/api/weather?location=${encodeURIComponent(location)}`);
|
||
const wx = await res.json();
|
||
|
||
if (wx.error) throw new Error(wx.error);
|
||
|
||
setWeatherCard(wx);
|
||
setClawTerminalLogs(prev => [...prev,
|
||
`[Claw] Got weather for ${wx.location}: ${wx.current.tempF}°F, ${wx.current.desc}`,
|
||
"[Claw] Rendering weather card. Use the button to push to WhatsApp."
|
||
]);
|
||
|
||
const emoji = getWeatherEmoji(wx.current.desc);
|
||
responseText = `${emoji} Here's the live weather for **${wx.location}**:\n\n🌡️ **${wx.current.tempF}°F** (feels like ${wx.current.feelsLikeF}°F) — ${wx.current.desc}\n💧 Humidity: ${wx.current.humidity}% | 💨 Wind: ${wx.current.windMph} mph ${wx.current.windDir}\n📊 Today: ${wx.today.minF}°F – ${wx.today.maxF}°F | 🌅 ${wx.today.sunrise} / 🌇 ${wx.today.sunset}\n\nI've generated the full report card below. Hit **Send to WhatsApp** to push it to your group!`;
|
||
|
||
} catch (err) {
|
||
responseText = `❌ Could not fetch weather: ${err.message}. Check your internet or try a city name like \`weather Toronto\`.`;
|
||
setClawTerminalLogs(prev => [...prev, `[Claw] Error: ${err.message}`]);
|
||
}
|
||
|
||
} else if (cleanInput.includes("pod") || cleanInput.includes("k8s") || cleanInput.includes("kubectl")) {
|
||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||
logs = [
|
||
"Connecting to Kubernetes API server...",
|
||
"Executing: kubectl get pods -n default",
|
||
"Found 2 pods in namespace 'default':",
|
||
" - web-pod-xyz 0/1 CrashLoopBackOff 5 2h",
|
||
" - db-pod-abc 1/1 Running 0 5d",
|
||
"Fetching logs for 'web-pod-xyz'...",
|
||
" Error detected: Database connection refused by 10.96.20.10:5432"
|
||
];
|
||
responseText = "I've checked the Kubernetes pods. Pod `web-pod-xyz` is currently failing due to a database connection refusal. You can use the **OpsAgent** tab to approve a pod restart or scale the DB deployment.";
|
||
} else if (cleanInput.includes("proxmox") || cleanInput.includes("vmid") || cleanInput.includes("disk")) {
|
||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||
logs = [
|
||
"Establishing SSH connection to Proxmox cluster host 192.168.8.225...",
|
||
"Running: pct status 102",
|
||
" status: running",
|
||
"Running: pct exec 102 -- df -h /",
|
||
" Filesystem Size Used Avail Use% Mounted on",
|
||
" /dev/loop0 20G 19G 0.2G 94% /",
|
||
"Disk usage critically high! Proposed action: clean_disk"
|
||
];
|
||
responseText = "Proxmox Container ID `102` is running out of disk space (94% used). I have created a pending action under the **OpsAgent Center** to perform automated cleanup. Would you like me to execute it?";
|
||
} else if (cleanInput.includes("hello") || cleanInput.includes("hi")) {
|
||
await new Promise(resolve => setTimeout(resolve, 800));
|
||
responseText = "Hi there! I am Claw, your local personal assistant. Try asking me to `check k8s pods`, `diagnose proxmox container 102`, or just type `weather` for a live report!";
|
||
} else {
|
||
await new Promise(resolve => setTimeout(resolve, 1200));
|
||
responseText = `I've analyzed your query: "${userText}". I recommend initiating an **OpsRemediateWorkflow** to check the status or scanning your configuration files. Let me know if you would like me to trigger that for you.`;
|
||
}
|
||
|
||
setClawTerminalLogs(prev => [...prev, ...logs, "[Claw] Response formulated successfully."]);
|
||
setMessages(prev => [...prev, { role: "assistant", content: responseText, timestamp: "Just now" }]);
|
||
setIsClawTyping(false);
|
||
};
|
||
|
||
// Weather emoji helper
|
||
const getWeatherEmoji = (desc) => {
|
||
const d = (desc || '').toLowerCase();
|
||
if (d.includes('sunny') || d.includes('clear')) return '☀️';
|
||
if (d.includes('partly cloudy')) return '⛅';
|
||
if (d.includes('cloud') || d.includes('overcast')) return '☁️';
|
||
if (d.includes('thunder') || d.includes('storm')) return '⛈️';
|
||
if (d.includes('rain') || d.includes('drizzle')) return '🌧️';
|
||
if (d.includes('snow') || d.includes('blizzard')) return '❄️';
|
||
if (d.includes('fog') || d.includes('mist')) return '🌫️';
|
||
return '🌤️';
|
||
};
|
||
|
||
// Send weather to WhatsApp
|
||
const handleSendWeatherToWhatsApp = async () => {
|
||
if (!weatherCard) return;
|
||
setWeatherSending(true);
|
||
const wx = weatherCard;
|
||
const emoji = getWeatherEmoji(wx.current.desc);
|
||
const msg = [
|
||
`${emoji} *Good Morning — Daily Weather Report*`,
|
||
`📍 *${wx.location}* (ZIP: ${wx.zip})`,
|
||
``,
|
||
`${emoji} ${wx.current.desc}`,
|
||
`🌡️ *Temp:* ${wx.current.tempF}°F (feels like ${wx.current.feelsLikeF}°F)`,
|
||
`💧 *Humidity:* ${wx.current.humidity}%`,
|
||
`💨 *Wind:* ${wx.current.windMph} mph ${wx.current.windDir}`,
|
||
`👁️ *Visibility:* ${wx.current.visibility} mi`,
|
||
`☀️ *UV Index:* ${wx.current.uvIndex}`,
|
||
``,
|
||
`📊 *Today:* ${wx.today.minF}°F – ${wx.today.maxF}°F`,
|
||
`🌅 Sunrise: ${wx.today.sunrise} 🌇 Sunset: ${wx.today.sunset}`,
|
||
wx.tomorrow ? `🔮 *Tomorrow:* ${wx.tomorrow.minF}°F – ${wx.tomorrow.maxF}°F ${getWeatherEmoji(wx.tomorrow.desc)} ${wx.tomorrow.desc}` : '',
|
||
``,
|
||
`_Agentic OS HQ • hq.applaude.net_ 🏠`,
|
||
].filter(Boolean).join('\n');
|
||
|
||
try {
|
||
const res = await fetch('/api/send-whatsapp', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ message: msg })
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
setWeatherSent(true);
|
||
setMessages(prev => [...prev, { role: "assistant", content: "✅ Weather report sent to your WhatsApp group!", timestamp: "Just now" }]);
|
||
} else {
|
||
setMessages(prev => [...prev, { role: "assistant", content: `❌ Failed to send: ${data.error}`, timestamp: "Just now" }]);
|
||
}
|
||
} catch (err) {
|
||
setMessages(prev => [...prev, { role: "assistant", content: `❌ Error: ${err.message}`, timestamp: "Just now" }]);
|
||
} finally {
|
||
setWeatherSending(false);
|
||
}
|
||
};
|
||
|
||
// Poll scheduled reminders every 30s when on claw tab
|
||
useEffect(() => {
|
||
if (activeTab !== 'claw') return;
|
||
const fetchReminders = async () => {
|
||
try {
|
||
const res = await fetch('/api/reminders?status=pending');
|
||
if (res.ok) {
|
||
const data = await res.json();
|
||
if (Array.isArray(data)) setScheduledReminders(data);
|
||
}
|
||
} catch { /* reminder service may not be up yet */ }
|
||
};
|
||
fetchReminders();
|
||
const interval = setInterval(fetchReminders, 30000);
|
||
return () => clearInterval(interval);
|
||
}, [activeTab]);
|
||
|
||
// ── Natural-language date/time parser ────────────────────────────────────────
|
||
const parseScheduleDateTime = (text) => {
|
||
const t = text.toLowerCase();
|
||
const now = new Date();
|
||
|
||
// "in X hours/minutes"
|
||
const inMatch = t.match(/\bin\s+(\d+)\s+(hour|hr|minute|min)s?\b/);
|
||
if (inMatch) {
|
||
const n = parseInt(inMatch[1]);
|
||
const ms = inMatch[2].startsWith('h') ? n * 3600000 : n * 60000;
|
||
return new Date(now.getTime() + ms);
|
||
}
|
||
|
||
// Extract time component: "3pm", "6:30am", "14:00"
|
||
let hours = null, minutes = 0;
|
||
const ampm = t.match(/(\d{1,2})(?::(\d{2}))?\s*(am|pm)/);
|
||
const h24 = t.match(/(\d{2}):(\d{2})(?!\s*(am|pm))/);
|
||
if (ampm) {
|
||
hours = parseInt(ampm[1]);
|
||
minutes = ampm[2] ? parseInt(ampm[2]) : 0;
|
||
if (ampm[3] === 'pm' && hours !== 12) hours += 12;
|
||
if (ampm[3] === 'am' && hours === 12) hours = 0;
|
||
} else if (h24) {
|
||
hours = parseInt(h24[1]);
|
||
minutes = parseInt(h24[2]);
|
||
}
|
||
|
||
// Build date
|
||
let date = new Date(now);
|
||
let explicitDate = false;
|
||
|
||
if (t.includes('tomorrow')) {
|
||
date.setDate(date.getDate() + 1);
|
||
explicitDate = true;
|
||
} else {
|
||
const days = ['sunday','monday','tuesday','wednesday','thursday','friday','saturday'];
|
||
const dayIdx = days.findIndex(d => t.includes(d));
|
||
if (dayIdx !== -1) {
|
||
const diff = ((dayIdx - now.getDay()) + 7) % 7 || 7; // next occurrence
|
||
date.setDate(date.getDate() + diff);
|
||
explicitDate = true;
|
||
} else {
|
||
// Named month: "May 30", "June 1"
|
||
const months = ['january','february','march','april','may','june','july','august','september','october','november','december'];
|
||
const shortM = ['jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec'];
|
||
let mIdx = months.findIndex(m => t.includes(m));
|
||
if (mIdx === -1) mIdx = shortM.findIndex(m => t.match(new RegExp('\\b' + m + '\\b')));
|
||
if (mIdx !== -1) {
|
||
const dayNum = t.match(/(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\w*\s+(\d{1,2})/i);
|
||
if (dayNum) {
|
||
date.setMonth(mIdx, parseInt(dayNum[1]));
|
||
if (date < now) date.setFullYear(date.getFullYear() + 1);
|
||
explicitDate = true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (hours === null) return null; // no time found
|
||
|
||
date.setHours(hours, minutes, 0, 0);
|
||
|
||
// If the computed time is in the past and no explicit date was given, push to tomorrow
|
||
if (date <= now && !explicitDate) date.setDate(date.getDate() + 1);
|
||
|
||
return date;
|
||
};
|
||
|
||
// ── Format a Date nicely for display ─────────────────────────────────────────
|
||
const formatReminderTime = (isoStr) => {
|
||
const d = new Date(isoStr);
|
||
return d.toLocaleString('en-US', {
|
||
timeZone: 'America/New_York',
|
||
weekday: 'short', month: 'short', day: 'numeric',
|
||
hour: 'numeric', minute: '2-digit', hour12: true
|
||
});
|
||
};
|
||
|
||
// ── Cancel a reminder ─────────────────────────────────────────────────────────
|
||
const handleCancelReminder = async (id) => {
|
||
try {
|
||
const res = await fetch(`/api/reminders?id=${id}`, { method: 'DELETE' });
|
||
if (res.ok) setScheduledReminders(prev => prev.filter(r => r.id !== id));
|
||
} catch { /* silent */ }
|
||
};
|
||
const handleApprove = async (id, action, target) => {
|
||
setPendingApprovals(prev => prev.map(item => item.id === id ? { ...item, status: "approved" } : item));
|
||
setOpsLogs(prev => [...prev, `2026-05-29 01:14:10 [OpsAgent] RECEIVED APPROVAL SIGNAL for request ${id}`]);
|
||
|
||
// Simulate execution log
|
||
let execLogs = [
|
||
`[OpsAgent] Initiating execution of '${action}' on target '${target}'...`,
|
||
`[OpsAgent] Sending signal to Temporal workflow...`,
|
||
];
|
||
|
||
if (action === "clean_disk") {
|
||
execLogs.push(
|
||
"[OpsAgent] SSH into Proxmox node VMID 102...",
|
||
"[OpsAgent] running: apt-get clean && rm -rf /tmp/*",
|
||
"[OpsAgent] Disk space cleared. Root filesystem usage reduced from 94% to 71%."
|
||
);
|
||
} else {
|
||
execLogs.push(
|
||
`[OpsAgent] Running: kubectl delete pod ${target} -n default`,
|
||
`[OpsAgent] Pod deleted. K8s controller successfully scheduled a replica.`
|
||
);
|
||
}
|
||
|
||
// Delay log write to simulate real execution
|
||
for (const log of execLogs) {
|
||
await new Promise(resolve => setTimeout(resolve, 800));
|
||
setOpsLogs(prev => [...prev, log]);
|
||
}
|
||
|
||
setPendingApprovals(prev => prev.filter(item => item.id !== id));
|
||
setWorkflows(prev => prev.map(wf => wf.target === (action === "clean_disk" ? "VMID 102" : target) ? { ...wf, status: "completed", result: "Remediated successfully" } : wf));
|
||
};
|
||
|
||
const handleReject = (id) => {
|
||
setPendingApprovals(prev => prev.filter(item => item.id !== id));
|
||
setOpsLogs(prev => [...prev, `2026-05-29 01:14:30 [OpsAgent] Request ${id} REJECTED by operator. Action aborted.`]);
|
||
};
|
||
|
||
// Clinical note simulation
|
||
const handleProcessClinical = async () => {
|
||
setIsProcessingClinical(true);
|
||
setClinicalNote(null);
|
||
setBillingCodes([]);
|
||
|
||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||
|
||
setClinicalNote({
|
||
subjective: "Patient is a 45-year-old male presenting with severe, crushing chest pain radiating to his left arm. He reports shortness of breath.",
|
||
objective: "Alert and oriented. Reports pain scale 8/10.",
|
||
assessment: "Acute Myocardial Infarction suspected.",
|
||
plan: "Order EKG, troponin levels. Administer Aspirin 324mg immediately. Admit to Cardiac ICU."
|
||
});
|
||
|
||
setBillingCodes([
|
||
{ code: "I21.9", desc: "Acute myocardial infarction, unspecified" },
|
||
{ code: "93000", desc: "Electrocardiogram, tracing and report" },
|
||
{ code: "99223", desc: "Initial hospital care, high level severity" }
|
||
]);
|
||
setIsProcessingClinical(false);
|
||
};
|
||
|
||
const handleSaveTrip = async () => {
|
||
try {
|
||
setIsSavingTrip(true);
|
||
let parsed = JSON.parse(tripEditorJson);
|
||
if (selectedTripId) {
|
||
await fetch(`/api/trips/${selectedTripId}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(parsed)
|
||
});
|
||
} else {
|
||
await fetch('/api/trips', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(parsed)
|
||
});
|
||
}
|
||
const data = await fetch('/api/trips').then(r => r.json());
|
||
setTripsList(Array.isArray(data) ? data : []);
|
||
setSelectedTripId(null);
|
||
setTripEditorJson("");
|
||
} catch (e) {
|
||
alert("Invalid JSON or save failed: " + e.message);
|
||
} finally {
|
||
setIsSavingTrip(false);
|
||
}
|
||
};
|
||
|
||
const handleTestTrip = async (id) => {
|
||
setIsTestingTrip(true);
|
||
try {
|
||
const res = await fetch(`/api/trips/${id}/test`, { method: 'POST' });
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
alert("Test briefing fired to WhatsApp successfully!");
|
||
} else {
|
||
alert("Test failed: " + JSON.stringify(data));
|
||
}
|
||
} catch (e) {
|
||
alert("Error triggering test: " + e.message);
|
||
} finally {
|
||
setIsTestingTrip(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="app-container">
|
||
{/* Sidebar Navigation */}
|
||
<aside className="sidebar">
|
||
<div style={{ display: "flex", flexDirection: "column", gap: "32px" }}>
|
||
{/* Dashboard Title */}
|
||
<div style={{ display: "flex", alignItems: "center", gap: "10px", padding: "0 8px" }}>
|
||
<div style={{ width: "32px", height: "32px", borderRadius: "8px", background: "linear-gradient(135deg, var(--accent-violet), var(--accent-cyan))", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||
<Terminal size={18} style={{ color: "#fff" }} />
|
||
</div>
|
||
<div>
|
||
<h2 style={{ fontSize: "16px", color: "var(--text-primary)" }}>Agentic OS</h2>
|
||
<span style={{ fontSize: "11px", color: "var(--text-muted)", letterSpacing: "1px", textTransform: "uppercase" }}>Control HQ</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Nav Items */}
|
||
<nav style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
|
||
<button
|
||
onClick={() => setActiveTab("dashboard")}
|
||
style={{
|
||
display: "flex", alignItems: "center", gap: "12px", width: "100%", padding: "12px 16px", border: "none", borderRadius: "8px", cursor: "pointer",
|
||
background: activeTab === "dashboard" ? "rgba(139, 92, 246, 0.15)" : "transparent",
|
||
color: activeTab === "dashboard" ? "var(--accent-violet)" : "var(--text-secondary)",
|
||
fontWeight: activeTab === "dashboard" ? "600" : "500",
|
||
transition: "all 0.2s"
|
||
}}
|
||
>
|
||
<Cpu size={18} />
|
||
Dashboard Home
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => setActiveTab("claw")}
|
||
style={{
|
||
display: "flex", alignItems: "center", gap: "12px", width: "100%", padding: "12px 16px", border: "none", borderRadius: "8px", cursor: "pointer",
|
||
background: activeTab === "claw" ? "rgba(6, 182, 212, 0.15)" : "transparent",
|
||
color: activeTab === "claw" ? "var(--accent-cyan)" : "var(--text-secondary)",
|
||
fontWeight: activeTab === "claw" ? "600" : "500",
|
||
transition: "all 0.2s"
|
||
}}
|
||
>
|
||
<Sparkles size={18} />
|
||
Claw Assistant
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => setActiveTab("ops")}
|
||
style={{
|
||
display: "flex", alignItems: "center", gap: "12px", width: "100%", padding: "12px 16px", border: "none", borderRadius: "8px", cursor: "pointer",
|
||
background: activeTab === "ops" ? "rgba(245, 158, 11, 0.15)" : "transparent",
|
||
color: activeTab === "ops" ? "var(--accent-amber)" : "var(--text-secondary)",
|
||
fontWeight: activeTab === "ops" ? "600" : "500",
|
||
transition: "all 0.2s"
|
||
}}
|
||
>
|
||
<ShieldAlert size={18} />
|
||
OpsAgent Center
|
||
{pendingApprovals.length > 0 && (
|
||
<span style={{ fontSize: "10px", padding: "2px 6px", borderRadius: "10px", background: "var(--accent-amber)", color: "#000", fontWeight: "700", marginLeft: "auto" }}>
|
||
{pendingApprovals.length}
|
||
</span>
|
||
)}
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => setActiveTab("curio")}
|
||
style={{
|
||
display: "flex", alignItems: "center", gap: "12px", width: "100%", padding: "12px 16px", border: "none", borderRadius: "8px", cursor: "pointer",
|
||
background: activeTab === "curio" ? "rgba(16, 185, 129, 0.15)" : "transparent",
|
||
color: activeTab === "curio" ? "var(--accent-emerald)" : "var(--text-secondary)",
|
||
fontWeight: activeTab === "curio" ? "600" : "500",
|
||
transition: "all 0.2s"
|
||
}}
|
||
>
|
||
<HeartPulse size={18} />
|
||
Clinical Workspace
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => setActiveTab("trips")}
|
||
style={{
|
||
display: "flex", alignItems: "center", gap: "12px", width: "100%", padding: "12px 16px", border: "none", borderRadius: "8px", cursor: "pointer",
|
||
background: activeTab === "trips" ? "rgba(236, 72, 153, 0.15)" : "transparent",
|
||
color: activeTab === "trips" ? "var(--accent-pink, #ec4899)" : "var(--text-secondary)",
|
||
fontWeight: activeTab === "trips" ? "600" : "500",
|
||
transition: "all 0.2s"
|
||
}}
|
||
>
|
||
<Compass size={18} />
|
||
Trips & Briefings
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => setActiveTab("logs")}
|
||
style={{
|
||
display: "flex", alignItems: "center", gap: "12px", width: "100%", padding: "12px 16px", border: "none", borderRadius: "8px", cursor: "pointer",
|
||
background: activeTab === "logs" ? "rgba(255, 255, 255, 0.05)" : "transparent",
|
||
color: activeTab === "logs" ? "var(--text-primary)" : "var(--text-secondary)",
|
||
fontWeight: activeTab === "logs" ? "600" : "500",
|
||
transition: "all 0.2s"
|
||
}}
|
||
>
|
||
<Layers size={18} />
|
||
Workflows & Logs
|
||
</button>
|
||
|
||
<div style={{ margin: "8px 0", borderTop: "1px solid rgba(255,255,255,0.1)" }}></div>
|
||
|
||
<button
|
||
onClick={() => setActiveTab("whatsapp")}
|
||
style={{
|
||
display: "flex", alignItems: "center", gap: "12px", width: "100%", padding: "12px 16px", border: "none", borderRadius: "8px", cursor: "pointer",
|
||
background: activeTab === "whatsapp" ? "rgba(37, 211, 102, 0.15)" : "transparent",
|
||
color: activeTab === "whatsapp" ? "#25D366" : "var(--text-secondary)",
|
||
fontWeight: activeTab === "whatsapp" ? "600" : "500",
|
||
transition: "all 0.2s"
|
||
}}
|
||
>
|
||
<MessageSquare size={18} />
|
||
WhatsApp Gateway
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => setActiveTab("gmail")}
|
||
style={{
|
||
display: "flex", alignItems: "center", gap: "12px", width: "100%", padding: "12px 16px", border: "none", borderRadius: "8px", cursor: "pointer",
|
||
background: activeTab === "gmail" ? "rgba(234, 67, 53, 0.15)" : "transparent",
|
||
color: activeTab === "gmail" ? "#EA4335" : "var(--text-secondary)",
|
||
fontWeight: activeTab === "gmail" ? "600" : "500",
|
||
transition: "all 0.2s"
|
||
}}
|
||
>
|
||
<Mail size={18} />
|
||
Gmail Agent
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => setActiveTab("config")}
|
||
style={{
|
||
display: "flex", alignItems: "center", gap: "12px", width: "100%", padding: "12px 16px", border: "none", borderRadius: "8px", cursor: "pointer",
|
||
background: activeTab === "config" ? "rgba(168, 85, 247, 0.15)" : "transparent",
|
||
color: activeTab === "config" ? "var(--accent-purple, #a855f7)" : "var(--text-secondary)",
|
||
fontWeight: activeTab === "config" ? "600" : "500",
|
||
transition: "all 0.2s"
|
||
}}
|
||
>
|
||
<Settings size={18} />
|
||
Settings & Config
|
||
</button>
|
||
</nav>
|
||
</div>
|
||
|
||
{/* Footer info */}
|
||
<div style={{ display: "flex", flexDirection: "column", gap: "8px", borderTop: "1px solid var(--border-color)", paddingTop: "16px" }}>
|
||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||
<span style={{ width: "8px", height: "8px", borderRadius: "50%" }} className="pulse-emerald"></span>
|
||
<span style={{ fontSize: "12px", color: "var(--text-secondary)" }}>Temporal Worker Connected</span>
|
||
</div>
|
||
<span style={{ fontSize: "11px", color: "var(--text-muted)" }}>v1.2.0-Production</span>
|
||
</div>
|
||
</aside>
|
||
|
||
{/* Main Content Area */}
|
||
<main className="main-content">
|
||
<header className="header">
|
||
<div style={{ display: "flex", alignItems: "center", gap: "12px" }}>
|
||
<h1 style={{ fontSize: "18px", textTransform: "capitalize" }}>{activeTab.replace("curio", "Clinical Curio").replace("ops", "OpsAgent Hub")}</h1>
|
||
<div style={{ fontSize: "12px", color: "var(--text-muted)", padding: "2px 8px", background: "rgba(255,255,255,0.05)", borderRadius: "4px" }}>
|
||
Namespace: ai-agents
|
||
</div>
|
||
</div>
|
||
<div style={{ display: "flex", alignItems: "center", gap: "16px" }}>
|
||
<div style={{ display: "flex", flexDirection: "column", alignItems: "flex-end" }}>
|
||
<span style={{ fontSize: "12px", color: "var(--text-primary)" }}>Sysadmin Console</span>
|
||
<span style={{ fontSize: "10px", color: "var(--accent-cyan)" }}>192.168.8.250</span>
|
||
</div>
|
||
<div style={{ width: "36px", height: "36px", borderRadius: "50%", background: "#1f293d", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||
<User size={18} style={{ color: "var(--text-secondary)" }} />
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<div className="content-body">
|
||
{/* TAB 1: DASHBOARD HOME */}
|
||
{activeTab === "dashboard" && (
|
||
<div className="fade-in" style={{ display: "flex", flexDirection: "column", gap: "24px" }}>
|
||
{/* Stat Cards */}
|
||
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))", gap: "20px" }}>
|
||
<div className="glass glass-interactive" style={{ padding: "20px" }}>
|
||
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: "12px" }}>
|
||
<span style={{ color: "var(--text-secondary)", fontSize: "14px" }}>Total Active Agents</span>
|
||
<Cpu style={{ color: "var(--accent-violet)" }} size={20} />
|
||
</div>
|
||
<h2 style={{ fontSize: "28px" }}>5 / 8</h2>
|
||
<div style={{ fontSize: "12px", color: "var(--text-muted)", marginTop: "6px" }}>Active in namespace</div>
|
||
</div>
|
||
|
||
<div className="glass glass-interactive" style={{ padding: "20px" }}>
|
||
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: "12px" }}>
|
||
<span style={{ color: "var(--text-secondary)", fontSize: "14px" }}>Temporal Workflows</span>
|
||
<RefreshCw style={{ color: "var(--accent-cyan)" }} size={20} />
|
||
</div>
|
||
<h2 style={{ fontSize: "28px" }}>12 Active</h2>
|
||
<div style={{ fontSize: "12px", color: "var(--text-emerald)", marginTop: "6px" }}>98.8% success SLA</div>
|
||
</div>
|
||
|
||
<div className="glass glass-interactive" style={{ padding: "20px" }}>
|
||
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: "12px" }}>
|
||
<span style={{ color: "var(--text-secondary)", fontSize: "14px" }}>Diagnostic Logs</span>
|
||
<Database style={{ color: "var(--accent-emerald)" }} size={20} />
|
||
</div>
|
||
<h2 style={{ fontSize: "28px" }}>142 MB</h2>
|
||
<div style={{ fontSize: "12px", color: "var(--text-muted)", marginTop: "6px" }}>Persisted in PostgreSQL</div>
|
||
</div>
|
||
|
||
<div className="glass glass-interactive" style={{ padding: "20px" }}>
|
||
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: "12px" }}>
|
||
<span style={{ color: "var(--text-secondary)", fontSize: "14px" }}>Action Approvals</span>
|
||
<ShieldAlert style={{ color: "var(--accent-amber)" }} size={20} />
|
||
</div>
|
||
<h2 style={{ fontSize: "28px", color: "var(--accent-amber)" }}>{pendingApprovals.length} Awaiting</h2>
|
||
<div style={{ fontSize: "12px", color: "var(--accent-rose)", marginTop: "6px" }}>Require administrator override</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Agent Status Panel */}
|
||
<div className="glass" style={{ padding: "24px" }}>
|
||
<h3 style={{ fontSize: "16px", marginBottom: "16px" }}>Core Agent Ecosystem Status</h3>
|
||
<div style={{ display: "flex", flexDirection: "column", gap: "12px" }}>
|
||
{[
|
||
{ name: "Bernard", role: "GitOps Reviewer & Workflow Monitor", status: "Idle", desc: "Awaiting PR push events on Gitea hooks.", type: "system" },
|
||
{ name: "Gumbo", role: "Documentation Summarizer", status: "Active", desc: "Indexing recent technical wiki docs via MCP filesystem.", type: "assistant" },
|
||
{ name: "OpsAgent", role: "SRE Diagnostics & Self-Healing", status: "Pending approval", desc: "Awaiting administrator decision for remediation action.", type: "remediator" },
|
||
{ name: "Claw", role: "Local Assistant CLI (OpenClaw clone)", status: "Active", desc: "Awaiting operator prompt input.", type: "assistant" },
|
||
{ name: "Curio Billing/Triage", role: "HMS AI Assist", status: "Idle", desc: "Standing by for clinical intake recording inputs.", type: "clinical" },
|
||
].map((agent, i) => (
|
||
<div key={i} style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "12px 16px", border: "1px solid var(--border-color)", borderRadius: "8px", background: "rgba(255,255,255,0.01)" }}>
|
||
<div>
|
||
<h4 style={{ fontSize: "14px", fontWeight: "600" }}>{agent.name}</h4>
|
||
<p style={{ fontSize: "12px", color: "var(--text-secondary)" }}>{agent.role} — <span style={{ color: "var(--text-muted)" }}>{agent.desc}</span></p>
|
||
</div>
|
||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||
<span style={{ width: "8px", height: "8px", borderRadius: "50%" }} className={agent.status.includes("Pending") ? "pulse-amber" : "pulse-emerald"}></span>
|
||
<span style={{ fontSize: "12px", fontWeight: "600", color: agent.status.includes("Pending") ? "var(--accent-amber)" : "var(--text-secondary)" }}>{agent.status}</span>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Quick Trigger Block */}
|
||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "20px" }}>
|
||
<div className="glass" style={{ padding: "20px" }}>
|
||
<h3 style={{ fontSize: "14px", marginBottom: "12px" }}>Diagnostics & Ops Triggers</h3>
|
||
<div style={{ display: "flex", gap: "10px" }}>
|
||
<button
|
||
onClick={() => {
|
||
setActiveTab("ops");
|
||
}}
|
||
style={{ padding: "10px 16px", border: "none", borderRadius: "6px", background: "linear-gradient(135deg, var(--accent-amber), #d97706)", color: "#000", fontWeight: "600", cursor: "pointer", display: "flex", alignItems: "center", gap: "8px" }}
|
||
>
|
||
<ShieldAlert size={16} />
|
||
Go to Approval Center
|
||
</button>
|
||
<button
|
||
onClick={() => {
|
||
setActiveTab("claw");
|
||
setMessages(prev => [...prev, { role: "assistant", content: "I am ready. Tell me to `check k8s pods` to test system diagnostic logic.", timestamp: "Just now" }]);
|
||
}}
|
||
style={{ padding: "10px 16px", border: "1px solid var(--border-color)", borderRadius: "6px", background: "rgba(255,255,255,0.05)", color: "var(--text-primary)", fontWeight: "600", cursor: "pointer" }}
|
||
>
|
||
Diagnose Pods
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="glass" style={{ padding: "20px" }}>
|
||
<h3 style={{ fontSize: "14px", marginBottom: "12px" }}>Knowledge Intake Trigger</h3>
|
||
<div style={{ display: "flex", gap: "10px" }}>
|
||
<button
|
||
onClick={() => {
|
||
alert("Triggered GumboSummarizeWorkflow for api-spec.md");
|
||
}}
|
||
style={{ padding: "10px 16px", border: "none", borderRadius: "6px", background: "linear-gradient(135deg, var(--accent-violet), var(--accent-cyan))", color: "#fff", fontWeight: "600", cursor: "pointer", display: "flex", alignItems: "center", gap: "8px" }}
|
||
>
|
||
<Sparkles size={16} />
|
||
Summarize Documentation (Gumbo)
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* TAB 2: CLAW PERSONAL ASSISTANT */}
|
||
{activeTab === "claw" && (
|
||
<div className="fade-in" style={{ display: "grid", gridTemplateColumns: "1fr 340px", gap: "24px", height: "calc(100vh - 140px)", overflow: "hidden" }}>
|
||
{/* Chat Panel */}
|
||
<div className="glass" style={{ display: "flex", flexDirection: "column", height: "100%", overflow: "hidden" }}>
|
||
{/* Chat Header */}
|
||
<div style={{ padding: "16px 24px", borderBottom: "1px solid var(--border-color)", display: "flex", alignItems: "center", gap: "12px", background: "rgba(255,255,255,0.01)" }}>
|
||
<div style={{ width: "10px", height: "10px", borderRadius: "50%" }} className="pulse-emerald"></div>
|
||
<div>
|
||
<h3 style={{ fontSize: "15px" }}>Claw Personal Assistant</h3>
|
||
<span style={{ fontSize: "11px", color: "var(--text-muted)" }}>Runs locally. Accesses file tools & system terminal.</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Chat Messages */}
|
||
<div style={{ flex: 1, padding: "24px", overflowY: "auto", display: "flex", flexDirection: "column", gap: "16px" }}>
|
||
{messages.map((msg, i) => (
|
||
<div
|
||
key={i}
|
||
style={{
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
alignItems: msg.role === "user" ? "flex-end" : "flex-start",
|
||
maxWidth: "85%",
|
||
alignSelf: msg.role === "user" ? "flex-end" : "flex-start"
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
padding: "12px 16px",
|
||
borderRadius: "12px",
|
||
fontSize: "14px",
|
||
lineHeight: "1.5",
|
||
background: msg.role === "user" ? "var(--accent-cyan)" : "rgba(255, 255, 255, 0.04)",
|
||
color: msg.role === "user" ? "#000" : "var(--text-primary)",
|
||
border: msg.role === "user" ? "none" : "1px solid var(--border-color)"
|
||
}}
|
||
>
|
||
{msg.content}
|
||
</div>
|
||
<span style={{ fontSize: "10px", color: "var(--text-muted)", marginTop: "4px", padding: "0 4px" }}>{msg.timestamp}</span>
|
||
</div>
|
||
))}
|
||
|
||
{isClawTyping && (
|
||
<div style={{ alignSelf: "flex-start", display: "flex", alignItems: "center", gap: "4px", padding: "12px 16px", background: "rgba(255, 255, 255, 0.04)", borderRadius: "12px", border: "1px solid var(--border-color)" }}>
|
||
<span style={{ width: "6px", height: "6px", background: "var(--text-secondary)", borderRadius: "50%", display: "inline-block", animation: "bounce 1.4s infinite ease-in-out both" }}></span>
|
||
<span style={{ width: "6px", height: "6px", background: "var(--text-secondary)", borderRadius: "50%", display: "inline-block", animation: "bounce 1.4s infinite ease-in-out both 0.2s" }}></span>
|
||
<span style={{ width: "6px", height: "6px", background: "var(--text-secondary)", borderRadius: "50%", display: "inline-block", animation: "bounce 1.4s infinite ease-in-out both 0.4s" }}></span>
|
||
</div>
|
||
)}
|
||
|
||
{/* Weather Card */}
|
||
{weatherCard && (
|
||
<div style={{ alignSelf: "flex-start", width: "100%", maxWidth: "480px" }} className="fade-in">
|
||
<div style={{
|
||
borderRadius: "16px",
|
||
overflow: "hidden",
|
||
border: "1px solid rgba(6,182,212,0.3)",
|
||
background: "linear-gradient(135deg, rgba(6,182,212,0.08) 0%, rgba(139,92,246,0.08) 100%)",
|
||
backdropFilter: "blur(12px)"
|
||
}}>
|
||
{/* Card Header */}
|
||
<div style={{
|
||
padding: "16px 20px",
|
||
background: "linear-gradient(135deg, rgba(6,182,212,0.15), rgba(139,92,246,0.1))",
|
||
borderBottom: "1px solid rgba(6,182,212,0.2)",
|
||
display: "flex", alignItems: "center", justifyContent: "space-between"
|
||
}}>
|
||
<div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
|
||
<span style={{ fontSize: "28px" }}>{getWeatherEmoji(weatherCard.current.desc)}</span>
|
||
<div>
|
||
<div style={{ fontSize: "13px", fontWeight: "600", color: "var(--accent-cyan)" }}>📍 {weatherCard.location}</div>
|
||
<div style={{ fontSize: "11px", color: "var(--text-muted)" }}>Live conditions • ZIP {weatherCard.zip}</div>
|
||
</div>
|
||
</div>
|
||
<div style={{ textAlign: "right" }}>
|
||
<div style={{ fontSize: "32px", fontWeight: "700", color: "var(--text-primary)", lineHeight: 1 }}>{weatherCard.current.tempF}°F</div>
|
||
<div style={{ fontSize: "11px", color: "var(--text-secondary)", marginTop: "2px" }}>feels like {weatherCard.current.feelsLikeF}°F</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Condition */}
|
||
<div style={{ padding: "10px 20px", borderBottom: "1px solid rgba(255,255,255,0.05)", fontSize: "13px", color: "var(--text-secondary)" }}>
|
||
{weatherCard.current.desc}
|
||
</div>
|
||
|
||
{/* Stats Grid */}
|
||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: "0", padding: "0" }}>
|
||
{[
|
||
{ icon: "💧", label: "Humidity", value: `${weatherCard.current.humidity}%` },
|
||
{ icon: "💨", label: "Wind", value: `${weatherCard.current.windMph} mph ${weatherCard.current.windDir}` },
|
||
{ icon: "☀️", label: `UV Index`, value: `${weatherCard.current.uvIndex}` },
|
||
{ icon: "👁️", label: "Visibility", value: `${weatherCard.current.visibility} mi` },
|
||
{ icon: "🔺", label: "High", value: `${weatherCard.today.maxF}°F` },
|
||
{ icon: "🔻", label: "Low", value: `${weatherCard.today.minF}°F` },
|
||
].map((stat, i) => (
|
||
<div key={i} style={{
|
||
padding: "12px 16px",
|
||
borderRight: i % 3 !== 2 ? "1px solid rgba(255,255,255,0.05)" : "none",
|
||
borderBottom: i < 3 ? "1px solid rgba(255,255,255,0.05)" : "none"
|
||
}}>
|
||
<div style={{ fontSize: "16px", marginBottom: "2px" }}>{stat.icon}</div>
|
||
<div style={{ fontSize: "13px", fontWeight: "600", color: "var(--text-primary)" }}>{stat.value}</div>
|
||
<div style={{ fontSize: "10px", color: "var(--text-muted)" }}>{stat.label}</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{/* Sunrise/Sunset & Tomorrow */}
|
||
<div style={{ padding: "12px 20px", borderTop: "1px solid rgba(255,255,255,0.05)", display: "flex", justifyContent: "space-between", fontSize: "12px", color: "var(--text-secondary)" }}>
|
||
<span>🌅 {weatherCard.today.sunrise}</span>
|
||
<span>🌇 {weatherCard.today.sunset}</span>
|
||
{weatherCard.tomorrow && (
|
||
<span>{getWeatherEmoji(weatherCard.tomorrow.desc)} Tomorrow: {weatherCard.tomorrow.minF}–{weatherCard.tomorrow.maxF}°F</span>
|
||
)}
|
||
</div>
|
||
|
||
{/* Send to WhatsApp CTA */}
|
||
<div style={{ padding: "14px 20px", borderTop: "1px solid rgba(6,182,212,0.2)", display: "flex", gap: "10px", alignItems: "center" }}>
|
||
<button
|
||
id="send-weather-whatsapp"
|
||
onClick={handleSendWeatherToWhatsApp}
|
||
disabled={weatherSending || weatherSent}
|
||
style={{
|
||
flex: 1,
|
||
padding: "10px 16px",
|
||
border: "none",
|
||
borderRadius: "8px",
|
||
background: weatherSent
|
||
? "linear-gradient(135deg, #10b981, #059669)"
|
||
: "linear-gradient(135deg, #25D366, #128C7E)",
|
||
color: "#fff",
|
||
fontWeight: "600",
|
||
cursor: weatherSending || weatherSent ? "default" : "pointer",
|
||
display: "flex", alignItems: "center", justifyContent: "center", gap: "8px",
|
||
fontSize: "13px",
|
||
opacity: weatherSending ? 0.7 : 1,
|
||
transition: "all 0.3s"
|
||
}}
|
||
>
|
||
{weatherSent ? "✅ Sent to WhatsApp!" : weatherSending ? "⏳ Sending..." : "📱 Send to WhatsApp Group"}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div ref={messagesEndRef} />
|
||
|
||
</div>
|
||
|
||
{/* Chat Input */}
|
||
<form onSubmit={handleSendMessage} style={{ padding: "20px", borderTop: "1px solid var(--border-color)", display: "flex", gap: "10px", background: "rgba(8,10,16,0.3)" }}>
|
||
<input
|
||
type="text"
|
||
value={inputValue}
|
||
onChange={(e) => setInputValue(e.target.value)}
|
||
placeholder="e.g. 'weather', 'whatsapp me: call doctor at 3pm', 'check k8s pods'..."
|
||
style={{ flex: 1, padding: "12px 16px", border: "1px solid var(--border-color)", borderRadius: "8px", background: "var(--bg-secondary)", color: "var(--text-primary)", outline: "none", fontSize: "14px" }}
|
||
/>
|
||
<button
|
||
type="submit"
|
||
style={{ padding: "12px 20px", border: "none", borderRadius: "8px", background: "var(--accent-cyan)", color: "#000", fontWeight: "600", cursor: "pointer", display: "flex", alignItems: "center", gap: "8px" }}
|
||
>
|
||
<Send size={16} />
|
||
Send
|
||
</button>
|
||
</form>
|
||
</div>
|
||
|
||
{/* Claw Tool & Console Log Panel */}
|
||
<div className="glass" style={{ display: "flex", flexDirection: "column", height: "100%", overflow: "hidden", borderLeft: "1px solid var(--border-color)" }}>
|
||
{/* Terminal Console */}
|
||
<div style={{ padding: "16px 20px", borderBottom: "1px solid var(--border-color)", background: "rgba(255,255,255,0.01)" }}>
|
||
<h3 style={{ fontSize: "14px", display: "flex", alignItems: "center", gap: "8px" }}>
|
||
<Terminal size={16} style={{ color: "var(--accent-cyan)" }} />
|
||
Claw Tool Console
|
||
</h3>
|
||
</div>
|
||
|
||
<div style={{ flex: 1, minHeight: 0, padding: "16px", background: "#05070a", fontFamily: "var(--font-mono)", fontSize: "11px", color: "#a5f3fc", overflowY: "auto", display: "flex", flexDirection: "column", gap: "8px" }}>
|
||
{clawTerminalLogs.length === 0 ? (
|
||
<div style={{ color: "var(--text-muted)", fontStyle: "italic" }}>Console logs will stream here as Claw calls CLI and API tools...</div>
|
||
) : (
|
||
clawTerminalLogs.map((log, i) => (
|
||
<div key={i} style={{ whiteSpace: "pre-wrap", wordBreak: "break-all" }}>
|
||
<span style={{ color: "var(--text-muted)" }}>></span> {log}
|
||
</div>
|
||
))
|
||
)}
|
||
<div ref={terminalEndRef} />
|
||
</div>
|
||
|
||
{/* Scheduled Reminders Panel */}
|
||
<div style={{ borderTop: "1px solid var(--border-color)", display: "flex", flexDirection: "column", maxHeight: "300px" }}>
|
||
<div style={{ padding: "12px 16px", borderBottom: "1px solid var(--border-color)", display: "flex", alignItems: "center", justifyContent: "space-between", background: "rgba(139,92,246,0.05)" }}>
|
||
<h3 style={{ fontSize: "13px", display: "flex", alignItems: "center", gap: "8px", color: "var(--text-primary)" }}>
|
||
<span>⏰</span>
|
||
Scheduled Reminders
|
||
{scheduledReminders.length > 0 && (
|
||
<span style={{ fontSize: "10px", padding: "1px 6px", borderRadius: "10px", background: "rgba(139,92,246,0.3)", color: "#c4b5fd", fontWeight: "700" }}>
|
||
{scheduledReminders.length}
|
||
</span>
|
||
)}
|
||
</h3>
|
||
<span style={{ fontSize: "10px", color: "var(--text-muted)" }}>auto-refreshes</span>
|
||
</div>
|
||
|
||
<div style={{ overflowY: "auto", flex: 1 }}>
|
||
{scheduledReminders.length === 0 ? (
|
||
<div style={{ padding: "20px 16px", textAlign: "center", color: "var(--text-muted)", fontSize: "12px" }}>
|
||
No reminders scheduled.<br />
|
||
<span style={{ color: "var(--accent-violet)", fontSize: "11px" }}>Try: "remind me at 3pm: call doctor"</span>
|
||
</div>
|
||
) : (
|
||
scheduledReminders.map((r) => (
|
||
<div key={r.id} style={{
|
||
display: "flex", alignItems: "flex-start", justifyContent: "space-between",
|
||
padding: "10px 14px",
|
||
borderBottom: "1px solid rgba(255,255,255,0.04)",
|
||
gap: "8px"
|
||
}}>
|
||
<div style={{ flex: 1, minWidth: 0 }}>
|
||
<div style={{ fontSize: "12px", fontWeight: "600", color: "var(--text-primary)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
|
||
{r.label}
|
||
</div>
|
||
<div style={{ fontSize: "10px", color: "var(--accent-violet)", marginTop: "2px" }}>
|
||
🕐 {formatReminderTime(r.sendAt)} ET
|
||
</div>
|
||
</div>
|
||
<button
|
||
onClick={() => handleCancelReminder(r.id)}
|
||
title="Cancel reminder"
|
||
style={{
|
||
flexShrink: 0,
|
||
padding: "3px 8px",
|
||
border: "1px solid rgba(239,68,68,0.3)",
|
||
borderRadius: "4px",
|
||
background: "rgba(239,68,68,0.1)",
|
||
color: "#f87171",
|
||
fontSize: "10px",
|
||
cursor: "pointer"
|
||
}}
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
|
||
{/* TAB 3: OPSAGENT HUB */}
|
||
{activeTab === "ops" && (
|
||
<div className="fade-in" style={{ display: "grid", gridTemplateColumns: "1fr 400px", gap: "24px", height: "calc(100vh - 140px)" }}>
|
||
{/* Left Column: Diagnostics and Approvals */}
|
||
<div style={{ display: "flex", flexDirection: "column", gap: "20px", overflowY: "auto" }}>
|
||
{/* Pending Actions */}
|
||
<div className="glass" style={{ padding: "24px" }}>
|
||
<h2 style={{ fontSize: "16px", marginBottom: "16px", display: "flex", alignItems: "center", gap: "8px" }}>
|
||
<ShieldAlert size={20} style={{ color: "var(--accent-amber)" }} />
|
||
Awaiting System Approvals (Approval Gate)
|
||
</h2>
|
||
|
||
{pendingApprovals.length === 0 ? (
|
||
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", padding: "40px", gap: "12px", border: "1px dashed var(--border-color)", borderRadius: "8px" }}>
|
||
<CheckCircle size={32} style={{ color: "var(--accent-emerald)" }} />
|
||
<h4 style={{ color: "var(--text-primary)" }}>No approvals pending</h4>
|
||
<p style={{ fontSize: "12px", color: "var(--text-muted)" }}>All systems are running clean and within normal parameters.</p>
|
||
</div>
|
||
) : (
|
||
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
|
||
{pendingApprovals.map((req) => (
|
||
<div key={req.id} style={{ border: "1px solid var(--border-color)", borderRadius: "8px", background: "rgba(255,255,255,0.02)", overflow: "hidden" }}>
|
||
<div style={{ padding: "16px", borderBottom: "1px solid var(--border-color)", display: "flex", justifyContent: "space-between", alignItems: "center", background: "rgba(245,158,11,0.03)" }}>
|
||
<div>
|
||
<span style={{ fontSize: "11px", fontWeight: "700", textTransform: "uppercase", background: "rgba(245,158,11,0.15)", color: "var(--accent-amber)", padding: "2px 6px", borderRadius: "4px", marginRight: "8px" }}>
|
||
{req.action.replace("_", " ")}
|
||
</span>
|
||
<span style={{ fontSize: "13px", color: "var(--text-primary)", fontWeight: "600" }}>Target: {req.targetType.toUpperCase()} ({req.targetId})</span>
|
||
</div>
|
||
<span style={{ fontSize: "11px", color: "var(--text-muted)" }}>{req.timestamp}</span>
|
||
</div>
|
||
|
||
<div style={{ padding: "16px" }}>
|
||
<p style={{ fontSize: "13px", color: "var(--text-secondary)", lineHeight: "1.5", marginBottom: "16px" }}>{req.details}</p>
|
||
|
||
<div style={{ display: "flex", gap: "10px", justifyContent: "flex-end" }}>
|
||
<button
|
||
onClick={() => handleReject(req.id)}
|
||
style={{ padding: "8px 16px", border: "1px solid var(--accent-rose)", borderRadius: "6px", background: "transparent", color: "var(--accent-rose)", fontWeight: "600", cursor: "pointer", display: "flex", alignItems: "center", gap: "6px" }}
|
||
>
|
||
<XCircle size={14} />
|
||
Reject
|
||
</button>
|
||
<button
|
||
onClick={() => handleApprove(req.id, req.action, req.targetId)}
|
||
style={{ padding: "8px 16px", border: "none", borderRadius: "6px", background: "linear-gradient(135deg, var(--accent-emerald), #059669)", color: "#fff", fontWeight: "600", cursor: "pointer", display: "flex", alignItems: "center", gap: "6px" }}
|
||
>
|
||
<CheckCircle size={14} />
|
||
Approve & Execute
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Diagnostics Panel */}
|
||
<div className="glass" style={{ padding: "24px" }}>
|
||
<h3 style={{ fontSize: "15px", marginBottom: "12px" }}>OpsAgent Diagnostics Tools</h3>
|
||
<p style={{ fontSize: "13px", color: "var(--text-secondary)", marginBottom: "16px" }}>Manually run automated checks across your container nodes.</p>
|
||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "12px" }}>
|
||
<div style={{ padding: "12px", border: "1px solid var(--border-color)", borderRadius: "8px", background: "rgba(255,255,255,0.01)" }}>
|
||
<span style={{ fontSize: "12px", fontWeight: "600" }}>K8s Diagnostic</span>
|
||
<button
|
||
onClick={() => {
|
||
setOpsLogs(prev => [...prev, `[Manual Trigger] Initiated Kubernetes diagnostics check for namespace 'default'...`]);
|
||
setTimeout(() => {
|
||
setOpsLogs(prev => [...prev, `[OpsAgent] web-pod-xyz logs scanned. DB connection failed.`]);
|
||
}, 1000);
|
||
}}
|
||
style={{ width: "100%", padding: "8px", border: "none", borderRadius: "4px", background: "rgba(6, 182, 212, 0.15)", color: "var(--accent-cyan)", fontWeight: "600", fontSize: "11px", cursor: "pointer", marginTop: "8px" }}
|
||
>
|
||
Scan Namespace
|
||
</button>
|
||
</div>
|
||
|
||
<div style={{ padding: "12px", border: "1px solid var(--border-color)", borderRadius: "8px", background: "rgba(255,255,255,0.01)" }}>
|
||
<span style={{ fontSize: "12px", fontWeight: "600" }}>Proxmox Auditor</span>
|
||
<button
|
||
onClick={() => {
|
||
setOpsLogs(prev => [...prev, `[Manual Trigger] Querying Proxmox host 192.168.8.225 for LXC disk metrics...`]);
|
||
setTimeout(() => {
|
||
setOpsLogs(prev => [...prev, `[OpsAgent] VMID 102 checked: 94% storage load. VMID 105 checked: 52% load.`]);
|
||
}, 1000);
|
||
}}
|
||
style={{ width: "100%", padding: "8px", border: "none", borderRadius: "4px", background: "rgba(139, 92, 246, 0.15)", color: "var(--accent-violet)", fontWeight: "600", fontSize: "11px", cursor: "pointer", marginTop: "8px" }}
|
||
>
|
||
Audit Disk Usage
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Right Column: Execution Live Terminal */}
|
||
<div className="glass" style={{ display: "flex", flexDirection: "column", height: "100%", overflow: "hidden" }}>
|
||
<div style={{ padding: "16px 20px", borderBottom: "1px solid var(--border-color)", background: "rgba(255,255,255,0.01)" }}>
|
||
<h3 style={{ fontSize: "14px" }}>OpsAgent Execution Terminal Logs</h3>
|
||
</div>
|
||
<div style={{ flex: 1, padding: "16px", background: "#05070a", fontFamily: "var(--font-mono)", fontSize: "11px", color: "#a3e635", overflowY: "auto", display: "flex", flexDirection: "column", gap: "8px" }}>
|
||
{opsLogs.map((log, i) => (
|
||
<div key={i} style={{ lineBreak: "anywhere" }}>{log}</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* TAB 4: CLINICAL WORKSPACE */}
|
||
{activeTab === "curio" && (
|
||
<div className="fade-in" style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "24px", height: "calc(100vh - 140px)" }}>
|
||
{/* Doctor Dictation Section */}
|
||
<div className="glass" style={{ padding: "24px", display: "flex", flexDirection: "column", gap: "16px", height: "100%", overflowY: "auto" }}>
|
||
<div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
|
||
<HeartPulse style={{ color: "var(--accent-emerald)" }} size={24} />
|
||
<div>
|
||
<h2 style={{ fontSize: "16px" }}>Clinical Scribe Workspace</h2>
|
||
<span style={{ fontSize: "11px", color: "var(--text-muted)" }}>Uses ClinicalIntakeWorkflow (Temporal Task Queue)</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
|
||
<label style={{ fontSize: "12px", color: "var(--text-secondary)", fontWeight: "600" }}>Raw Dictation Input</label>
|
||
<textarea
|
||
value={dictationText}
|
||
onChange={(e) => setDictationText(e.target.value)}
|
||
style={{ width: "100%", height: "200px", padding: "16px", border: "1px solid var(--border-color)", borderRadius: "8px", background: "var(--bg-secondary)", color: "var(--text-primary)", outline: "none", resize: "none", fontSize: "14px", lineHeight: "1.5" }}
|
||
/>
|
||
</div>
|
||
|
||
<button
|
||
onClick={handleProcessClinical}
|
||
disabled={isProcessingClinical || !dictationText}
|
||
style={{ width: "100%", padding: "12px", border: "none", borderRadius: "8px", background: "var(--accent-emerald)", color: "#000", fontWeight: "600", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", gap: "8px" }}
|
||
>
|
||
{isProcessingClinical ? (
|
||
<>
|
||
<RefreshCw size={16} className="spin" style={{ animation: "spin 2s linear infinite" }} />
|
||
Structuring Dictation via LLM...
|
||
</>
|
||
) : (
|
||
<>
|
||
<Play size={16} />
|
||
Submit to ClinicalIntake Workflow
|
||
</>
|
||
)}
|
||
</button>
|
||
</div>
|
||
|
||
{/* Structuring Result Output */}
|
||
<div className="glass" style={{ padding: "24px", display: "flex", flexDirection: "column", gap: "16px", height: "100%", overflowY: "auto" }}>
|
||
<h3 style={{ fontSize: "15px", borderBottom: "1px solid var(--border-color)", paddingBottom: "12px" }}>Structured Output (SOAP Note)</h3>
|
||
|
||
{clinicalNote ? (
|
||
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
|
||
<div style={{ display: "grid", gridTemplateColumns: "1fr", gap: "12px" }}>
|
||
<div>
|
||
<span style={{ fontSize: "11px", fontWeight: "700", color: "var(--accent-emerald)" }}>SUBJECTIVE</span>
|
||
<p style={{ fontSize: "13px", color: "var(--text-secondary)", marginTop: "4px" }}>{clinicalNote.subjective}</p>
|
||
</div>
|
||
<div>
|
||
<span style={{ fontSize: "11px", fontWeight: "700", color: "var(--accent-emerald)" }}>OBJECTIVE</span>
|
||
<p style={{ fontSize: "13px", color: "var(--text-secondary)", marginTop: "4px" }}>{clinicalNote.objective}</p>
|
||
</div>
|
||
<div>
|
||
<span style={{ fontSize: "11px", fontWeight: "700", color: "var(--accent-emerald)" }}>ASSESSMENT</span>
|
||
<p style={{ fontSize: "13px", color: "var(--text-secondary)", marginTop: "4px" }}>{clinicalNote.assessment}</p>
|
||
</div>
|
||
<div>
|
||
<span style={{ fontSize: "11px", fontWeight: "700", color: "var(--accent-emerald)" }}>PLAN</span>
|
||
<p style={{ fontSize: "13px", color: "var(--text-secondary)", marginTop: "4px" }}>{clinicalNote.plan}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ borderTop: "1px solid var(--border-color)", paddingTop: "16px" }}>
|
||
<h4 style={{ fontSize: "12px", color: "var(--text-primary)", marginBottom: "8px" }}>Generated Billing Codes (ICD-10 / CPT)</h4>
|
||
<table style={{ width: "100%", borderCollapse: "collapse", fontSize: "12px" }}>
|
||
<thead>
|
||
<tr style={{ textAlign: "left", color: "var(--text-muted)", borderBottom: "1px solid var(--border-color)" }}>
|
||
<th style={{ padding: "8px" }}>Code</th>
|
||
<th style={{ padding: "8px" }}>Description</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{billingCodes.map((bc, idx) => (
|
||
<tr key={idx} style={{ borderBottom: "1px solid rgba(255,255,255,0.02)" }}>
|
||
<td style={{ padding: "8px", fontWeight: "600", color: "var(--accent-cyan)" }}>{bc.code}</td>
|
||
<td style={{ padding: "8px", color: "var(--text-secondary)" }}>{bc.desc}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div style={{ display: "flex", flex: 1, alignItems: "center", justifyContent: "center", color: "var(--text-muted)", fontSize: "13px", fontStyle: "italic", border: "1px dashed var(--border-color)", borderRadius: "8px" }}>
|
||
No workflow submitted yet. Enter dictation and click submit.
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* TAB: CONFIG */}
|
||
{activeTab === "config" && (
|
||
<div className="fade-in" style={{ display: "flex", flexDirection: "column", gap: "24px", maxWidth: "800px" }}>
|
||
<div className="glass" style={{ padding: "24px" }}>
|
||
<div style={{ display: "flex", alignItems: "center", gap: "12px", marginBottom: "20px" }}>
|
||
<Settings size={24} style={{ color: "var(--accent-purple, #a855f7)" }} />
|
||
<h2 style={{ fontSize: "20px", fontWeight: "600" }}>WhatsApp Routing Configuration</h2>
|
||
</div>
|
||
|
||
<p style={{ color: "var(--text-secondary)", fontSize: "14px", marginBottom: "24px", lineHeight: "1.6" }}>
|
||
Manage the WhatsApp group invite links used by the various Agentic OS microservices.
|
||
Changes here are written directly to the Kubernetes <code>whatsapp-config.yaml</code> ConfigMap.
|
||
</p>
|
||
|
||
<div style={{ display: "flex", flexDirection: "column", gap: "20px" }}>
|
||
{/* Default Group */}
|
||
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
|
||
<label style={{ fontSize: "14px", fontWeight: "600", color: "var(--text-primary)" }}>Default Group URL</label>
|
||
<input
|
||
type="text"
|
||
value={configData.defaultGroupUrl}
|
||
onChange={(e) => setConfigData({...configData, defaultGroupUrl: e.target.value})}
|
||
placeholder="https://chat.whatsapp.com/..."
|
||
style={{
|
||
width: "100%", padding: "12px", borderRadius: "8px",
|
||
background: "rgba(0,0,0,0.2)", border: "1px solid var(--border-color)",
|
||
color: "var(--text-primary)", fontSize: "14px", fontFamily: "monospace"
|
||
}}
|
||
/>
|
||
<span style={{ fontSize: "12px", color: "var(--text-muted)" }}>
|
||
Used by <strong>Reminder Service</strong>, <strong>Weather CronJob</strong>, and general notifications.
|
||
</span>
|
||
</div>
|
||
|
||
{/* Trip Group */}
|
||
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
|
||
<label style={{ fontSize: "14px", fontWeight: "600", color: "var(--text-primary)" }}>Trip & Briefings Group URL</label>
|
||
<input
|
||
type="text"
|
||
value={configData.tripGroupUrl}
|
||
onChange={(e) => setConfigData({...configData, tripGroupUrl: e.target.value})}
|
||
placeholder="https://chat.whatsapp.com/..."
|
||
style={{
|
||
width: "100%", padding: "12px", borderRadius: "8px",
|
||
background: "rgba(0,0,0,0.2)", border: "1px solid var(--border-color)",
|
||
color: "var(--text-primary)", fontSize: "14px", fontFamily: "monospace"
|
||
}}
|
||
/>
|
||
<span style={{ fontSize: "12px", color: "var(--text-muted)" }}>
|
||
Used exclusively by the <strong>Trip Service</strong> to isolate travel insights.
|
||
</span>
|
||
</div>
|
||
|
||
{/* Save Button */}
|
||
<div style={{ marginTop: "16px", display: "flex", justifyContent: "flex-end" }}>
|
||
<button
|
||
onClick={handleSaveConfig}
|
||
disabled={isSavingConfig}
|
||
style={{
|
||
padding: "10px 24px", borderRadius: "8px", border: "none",
|
||
background: "var(--accent-purple, #a855f7)", color: "#fff",
|
||
fontWeight: "600", cursor: isSavingConfig ? "not-allowed" : "pointer",
|
||
opacity: isSavingConfig ? 0.7 : 1, transition: "opacity 0.2s"
|
||
}}
|
||
>
|
||
{isSavingConfig ? "Saving to K8s..." : "Save & Apply"}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* TAB 5: WORKFLOW MONITOR */}
|
||
{activeTab === "logs" && (
|
||
<div className="fade-in" style={{ display: "flex", flexDirection: "column", gap: "20px" }}>
|
||
<div className="glass" style={{ padding: "24px" }}>
|
||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "16px" }}>
|
||
<div>
|
||
<h3 style={{ fontSize: "16px" }}>Durable Execution Logs</h3>
|
||
<p style={{ fontSize: "12px", color: "var(--text-muted)" }}>Temporal workflows audit records inside the local postgres cluster.</p>
|
||
</div>
|
||
<button
|
||
onClick={() => {
|
||
setWorkflows(prev => [
|
||
...prev,
|
||
{ id: `wf-manual-${Math.floor(Math.random()*900+100)}`, name: "GumboSummarizeWorkflow", agent: "Gumbo", target: "wiki-summary.txt", status: "completed", result: "Completed" }
|
||
]);
|
||
}}
|
||
style={{ padding: "8px 14px", border: "none", borderRadius: "6px", background: "rgba(255,255,255,0.05)", color: "var(--text-primary)", fontWeight: "600", cursor: "pointer", fontSize: "12px" }}
|
||
>
|
||
Refresh List
|
||
</button>
|
||
</div>
|
||
|
||
<div style={{ overflowX: "auto" }}>
|
||
<table style={{ width: "100%", borderCollapse: "collapse", fontSize: "13px" }}>
|
||
<thead>
|
||
<tr style={{ borderBottom: "1px solid var(--border-color)", color: "var(--text-muted)", textAlign: "left" }}>
|
||
<th style={{ padding: "12px 16px" }}>Workflow ID</th>
|
||
<th style={{ padding: "12px 16px" }}>Workflow Name</th>
|
||
<th style={{ padding: "12px 16px" }}>Assigned Agent</th>
|
||
<th style={{ padding: "12px 16px" }}>Target</th>
|
||
<th style={{ padding: "12px 16px" }}>Status</th>
|
||
<th style={{ padding: "12px 16px" }}>Result</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{workflows.map((wf) => (
|
||
<tr key={wf.id} style={{ borderBottom: "1px solid var(--border-color)", background: "rgba(255,255,255,0.005)" }}>
|
||
<td style={{ padding: "12px 16px", fontFamily: "var(--font-mono)", fontSize: "12px" }}>{wf.id}</td>
|
||
<td style={{ padding: "12px 16px", fontWeight: "600" }}>{wf.name}</td>
|
||
<td style={{ padding: "12px 16px" }}>{wf.agent}</td>
|
||
<td style={{ padding: "12px 16px", color: "var(--text-secondary)" }}>{wf.target}</td>
|
||
<td style={{ padding: "12px 16px" }}>
|
||
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
|
||
<span style={{ width: "6px", height: "6px", borderRadius: "50%" }} className={wf.status === "running" ? "pulse-amber" : "pulse-emerald"}></span>
|
||
<span style={{ textTransform: "capitalize", fontWeight: "600", color: wf.status === "running" ? "var(--accent-amber)" : "var(--text-secondary)" }}>
|
||
{wf.status}
|
||
</span>
|
||
</div>
|
||
</td>
|
||
<td style={{ padding: "12px 16px", color: wf.status === "running" ? "var(--text-muted)" : "var(--text-primary)" }}>{wf.result}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* TAB 6: TRIPS & CONFIG */}
|
||
{activeTab === "trips" && (
|
||
<div className="fade-in" style={{ display: "grid", gridTemplateColumns: "300px 1fr", gap: "24px", height: "calc(100vh - 140px)" }}>
|
||
{/* Trip List Panel */}
|
||
<div className="glass" style={{ display: "flex", flexDirection: "column", height: "100%", overflow: "hidden" }}>
|
||
<div style={{ padding: "16px 20px", borderBottom: "1px solid var(--border-color)", background: "rgba(255,255,255,0.01)" }}>
|
||
<h3 style={{ fontSize: "14px", display: "flex", alignItems: "center", gap: "8px" }}>
|
||
<Compass size={16} style={{ color: "var(--accent-pink)" }} />
|
||
Managed Trips
|
||
</h3>
|
||
</div>
|
||
<div style={{ flex: 1, overflowY: "auto", padding: "16px", display: "flex", flexDirection: "column", gap: "12px" }}>
|
||
<button
|
||
onClick={() => {
|
||
setSelectedTripId(null);
|
||
setTripEditorJson("{\n \"name\": \"New Trip\",\n \"briefingTime\": \"07:00\",\n \"sendToGroup\": true,\n \"groupUrls\": [],\n \"llmModel\": \"qwen2.5:0.5b\",\n \"days\": []\n}");
|
||
}}
|
||
style={{ padding: "12px", border: "1px dashed var(--border-color)", borderRadius: "8px", background: "rgba(255,255,255,0.02)", color: "var(--text-primary)", cursor: "pointer", fontSize: "13px" }}
|
||
>
|
||
+ Add New Trip
|
||
</button>
|
||
{tripsList.map(t => (
|
||
<div
|
||
key={t.id}
|
||
onClick={() => {
|
||
setSelectedTripId(t.id);
|
||
setTripEditorJson(JSON.stringify(t, null, 2));
|
||
}}
|
||
style={{ padding: "12px", border: `1px solid ${selectedTripId === t.id ? 'var(--accent-pink)' : 'var(--border-color)'}`, borderRadius: "8px", background: selectedTripId === t.id ? "rgba(236,72,153,0.05)" : "var(--bg-secondary)", cursor: "pointer" }}
|
||
>
|
||
<h4 style={{ fontSize: "14px", fontWeight: "600", marginBottom: "4px" }}>{t.name}</h4>
|
||
<div style={{ fontSize: "11px", color: "var(--text-muted)", display: "flex", justifyContent: "space-between" }}>
|
||
<span>⏰ {t.briefingTime} ET</span>
|
||
<span>{t.days?.length} Days</span>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Editor Panel */}
|
||
<div className="glass" style={{ display: "flex", flexDirection: "column", height: "100%", overflow: "hidden" }}>
|
||
<div style={{ padding: "16px 20px", borderBottom: "1px solid var(--border-color)", background: "rgba(255,255,255,0.01)", display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||
<h3 style={{ fontSize: "14px" }}>{selectedTripId ? "Edit Trip Configuration" : "New Trip Configuration"}</h3>
|
||
<div style={{ display: "flex", gap: "8px" }}>
|
||
{selectedTripId && (
|
||
<button
|
||
onClick={() => handleTestTrip(selectedTripId)}
|
||
disabled={isTestingTrip}
|
||
style={{ padding: "8px 14px", border: "1px solid var(--border-color)", borderRadius: "6px", background: "rgba(255,255,255,0.05)", color: "var(--text-primary)", fontSize: "12px", cursor: "pointer" }}
|
||
>
|
||
{isTestingTrip ? "Firing..." : "Test Briefing"}
|
||
</button>
|
||
)}
|
||
<button
|
||
onClick={handleSaveTrip}
|
||
disabled={isSavingTrip}
|
||
style={{ padding: "8px 14px", border: "none", borderRadius: "6px", background: "linear-gradient(135deg, #ec4899, #be185d)", color: "#fff", fontWeight: "600", fontSize: "12px", cursor: "pointer" }}
|
||
>
|
||
{isSavingTrip ? "Saving..." : "Save Trip Config"}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div style={{ flex: 1, display: "flex", flexDirection: "column", padding: "16px", background: "#05070a" }}>
|
||
<textarea
|
||
value={tripEditorJson}
|
||
onChange={(e) => setTripEditorJson(e.target.value)}
|
||
placeholder="Paste JSON config here..."
|
||
style={{ flex: 1, width: "100%", padding: "16px", border: "none", background: "transparent", color: "#a5f3fc", fontFamily: "var(--font-mono)", fontSize: "12px", resize: "none", outline: "none" }}
|
||
spellCheck={false}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* TAB: WHATSAPP */}
|
||
{activeTab === "whatsapp" && <WhatsAppAgentUI />}
|
||
|
||
{/* TAB: GMAIL */}
|
||
{activeTab === "gmail" && <GmailAgentUI />}
|
||
</div>
|
||
</main>
|
||
</div>
|
||
);
|
||
}
|