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,
|
||||
Wind,
|
||||
Droplets,
|
||||
Thermometer
|
||||
Thermometer,
|
||||
Compass
|
||||
} from "lucide-react";
|
||||
|
||||
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" }
|
||||
]);
|
||||
|
||||
// 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 terminalEndRef = useRef(null);
|
||||
|
||||
|
|
@ -101,6 +109,14 @@ export default function Page() {
|
|||
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();
|
||||
|
|
@ -523,6 +539,51 @@ export default function Page() {
|
|||
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 */}
|
||||
|
|
@ -602,6 +663,20 @@ export default function Page() {
|
|||
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={{
|
||||
|
|
@ -1280,6 +1355,82 @@ export default function Page() {
|
|||
</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>
|
||||
</main>
|
||||
</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) => {
|
||||
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 req = http.request({
|
||||
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 nps = await getNpsAlerts(trip.npsParkCode);
|
||||
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` +
|
||||
`*🌤️ Weather (${dayPlan.location || 'Pigeon Forge'}):* ${weather}\n\n` +
|
||||
|
|
@ -174,6 +174,24 @@ app.get('/trips', (req, res) => {
|
|||
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) => {
|
||||
const trips = loadTrips();
|
||||
const trip = trips.find(t => t.id === req.params.id);
|
||||
|
|
|
|||
Loading…
Reference in New Issue