feat: Add dynamic trip configuration UI to dashboard and backend
This commit is contained in:
parent
6eb126257f
commit
aab9e60100
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
const TRIP_SERVICE_URL = 'http://trip-service.ai-agents.svc.cluster.local:6002';
|
||||||
|
|
||||||
|
export async function PUT(req, { params }) {
|
||||||
|
try {
|
||||||
|
const body = await req.json();
|
||||||
|
const res = await fetch(`${TRIP_SERVICE_URL}/trips/${params.id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
return NextResponse.json(data);
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DELETE(req, { params }) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${TRIP_SERVICE_URL}/trips/${params.id}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
return NextResponse.json(data);
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
const TRIP_SERVICE_URL = 'http://trip-service.ai-agents.svc.cluster.local:6002';
|
||||||
|
|
||||||
|
export async function POST(req, { params }) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${TRIP_SERVICE_URL}/trips/${params.id}/test`, {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
return NextResponse.json(data);
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
const TRIP_SERVICE_URL = 'http://trip-service.ai-agents.svc.cluster.local:6002';
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${TRIP_SERVICE_URL}/trips`, { cache: 'no-store' });
|
||||||
|
const data = await res.json();
|
||||||
|
return NextResponse.json(data);
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(req) {
|
||||||
|
try {
|
||||||
|
const body = await req.json();
|
||||||
|
const res = await fetch(`${TRIP_SERVICE_URL}/trips`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
return NextResponse.json(data);
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -26,7 +26,8 @@ import {
|
||||||
CloudSun,
|
CloudSun,
|
||||||
Wind,
|
Wind,
|
||||||
Droplets,
|
Droplets,
|
||||||
Thermometer
|
Thermometer,
|
||||||
|
Compass
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
|
|
@ -90,6 +91,13 @@ export default function Page() {
|
||||||
{ id: "wf-ops-003", name: "OpsRemediateWorkflow", agent: "OpsAgent", target: "VMID 102", status: "running", result: "Awaiting approval signal" }
|
{ 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);
|
||||||
|
|
||||||
const messagesEndRef = useRef(null);
|
const messagesEndRef = useRef(null);
|
||||||
const terminalEndRef = useRef(null);
|
const terminalEndRef = useRef(null);
|
||||||
|
|
||||||
|
|
@ -101,6 +109,14 @@ export default function Page() {
|
||||||
terminalEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
terminalEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
}, [clawTerminalLogs]);
|
}, [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
|
// Simulated Claw response logic
|
||||||
const handleSendMessage = async (e) => {
|
const handleSendMessage = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -523,6 +539,51 @@ export default function Page() {
|
||||||
setIsProcessingClinical(false);
|
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 (
|
return (
|
||||||
<div className="app-container">
|
<div className="app-container">
|
||||||
{/* Sidebar Navigation */}
|
{/* Sidebar Navigation */}
|
||||||
|
|
@ -602,6 +663,20 @@ export default function Page() {
|
||||||
Clinical Workspace
|
Clinical Workspace
|
||||||
</button>
|
</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
|
<button
|
||||||
onClick={() => setActiveTab("logs")}
|
onClick={() => setActiveTab("logs")}
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -1280,6 +1355,82 @@ export default function Page() {
|
||||||
</div>
|
</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>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -88,10 +88,10 @@ async function getTdotTraffic() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAiInsight(activityDetails, location) {
|
async function getAiInsight(activityDetails, location, modelName = 'qwen2.5:0.5b') {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const prompt = `Provide one short, clever tip or insight for someone visiting ${location} today, doing: ${activityDetails}. Keep it under 2 sentences.`;
|
const prompt = `Provide one short, clever tip or insight for someone visiting ${location} today, doing: ${activityDetails}. Keep it under 2 sentences.`;
|
||||||
const body = JSON.stringify({ model: 'qwen2.5:0.5b', prompt, stream: false });
|
const body = JSON.stringify({ model: modelName, prompt, stream: false });
|
||||||
const url = new URL(OLLAMA_URL);
|
const url = new URL(OLLAMA_URL);
|
||||||
const req = http.request({
|
const req = http.request({
|
||||||
hostname: url.hostname, port: url.port, path: url.pathname, method: 'POST',
|
hostname: url.hostname, port: url.port, path: url.pathname, method: 'POST',
|
||||||
|
|
@ -131,7 +131,7 @@ async function generateAndSendBriefing(trip, dayPlan) {
|
||||||
const weather = await getWeather(dayPlan.location || 'Pigeon Forge, TN');
|
const weather = await getWeather(dayPlan.location || 'Pigeon Forge, TN');
|
||||||
const nps = await getNpsAlerts(trip.npsParkCode);
|
const nps = await getNpsAlerts(trip.npsParkCode);
|
||||||
const traffic = await getTdotTraffic();
|
const traffic = await getTdotTraffic();
|
||||||
const insight = await getAiInsight(dayPlan.activities, dayPlan.location || 'Smoky Mountains');
|
const insight = await getAiInsight(dayPlan.activities, dayPlan.location || 'Smoky Mountains', trip.llmModel);
|
||||||
|
|
||||||
const message = `*🏕️ ${trip.name} - ${dayPlan.date} Briefing*\n\n` +
|
const message = `*🏕️ ${trip.name} - ${dayPlan.date} Briefing*\n\n` +
|
||||||
`*🌤️ Weather (${dayPlan.location || 'Pigeon Forge'}):* ${weather}\n\n` +
|
`*🌤️ Weather (${dayPlan.location || 'Pigeon Forge'}):* ${weather}\n\n` +
|
||||||
|
|
@ -174,6 +174,24 @@ app.get('/trips', (req, res) => {
|
||||||
res.json(loadTrips());
|
res.json(loadTrips());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.put('/trips/:id', (req, res) => {
|
||||||
|
const trips = loadTrips();
|
||||||
|
const index = trips.findIndex(t => t.id === req.params.id);
|
||||||
|
if (index === -1) return res.status(404).json({ error: 'Trip not found' });
|
||||||
|
|
||||||
|
const updatedTrip = { ...req.body, id: req.params.id }; // preserve ID
|
||||||
|
trips[index] = updatedTrip;
|
||||||
|
saveTrips(trips);
|
||||||
|
res.json({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/trips/:id', (req, res) => {
|
||||||
|
let trips = loadTrips();
|
||||||
|
trips = trips.filter(t => t.id !== req.params.id);
|
||||||
|
saveTrips(trips);
|
||||||
|
res.json({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
app.post('/trips/:id/test', async (req, res) => {
|
app.post('/trips/:id/test', async (req, res) => {
|
||||||
const trips = loadTrips();
|
const trips = loadTrips();
|
||||||
const trip = trips.find(t => t.id === req.params.id);
|
const trip = trips.find(t => t.id === req.params.id);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue