UI Updates

This commit is contained in:
Logan
2026-04-19 15:22:29 -04:00
parent 03212fca51
commit 0df53df92e
7 changed files with 181 additions and 13 deletions
+4 -2
View File
@@ -4,11 +4,13 @@ import { useState } from "react";
import { useCalls } from "@/lib/useCalls";
import { useSystems } from "@/lib/useSystems";
import { CallRow } from "@/components/CallRow";
import { useAuth } from "@/components/AuthProvider";
export default function CallsPage() {
const [limitCount, setLimitCount] = useState(100);
const { calls, loading } = useCalls(limitCount);
const { systems } = useSystems();
const { isAdmin } = useAuth();
const systemMap = Object.fromEntries(systems.map((s) => [s.system_id, s]));
const active = calls.filter((c) => c.status === "active");
@@ -41,7 +43,7 @@ export default function CallsPage() {
</thead>
<tbody>
{active.map((c) => (
<CallRow key={c.call_id} call={c} systemName={systemMap[c.system_id ?? ""]?.name} />
<CallRow key={c.call_id} call={c} systemName={systemMap[c.system_id ?? ""]?.name} isAdmin={isAdmin} />
))}
</tbody>
</table>
@@ -73,7 +75,7 @@ export default function CallsPage() {
</thead>
<tbody>
{ended.map((c) => (
<CallRow key={c.call_id} call={c} systemName={systemMap[c.system_id ?? ""]?.name} />
<CallRow key={c.call_id} call={c} systemName={systemMap[c.system_id ?? ""]?.name} isAdmin={isAdmin} />
))}
</tbody>
</table>
+3 -1
View File
@@ -8,6 +8,7 @@ import { CallRow } from "@/components/CallRow";
import { NodeConfigModal } from "@/components/NodeConfigModal";
import { useState } from "react";
import type { NodeRecord } from "@/lib/types";
import { useAuth } from "@/components/AuthProvider";
function StatCard({ label, value, accent }: { label: string; value: string | number; accent?: string }) {
return (
@@ -26,6 +27,7 @@ export default function DashboardPage() {
const { systems, error: systemsError } = useSystems();
const [configNode, setConfigNode] = useState<NodeRecord | null>(null);
const { isAdmin } = useAuth();
const systemMap = Object.fromEntries(systems.map((s) => [s.system_id, s]));
const onlineCount = nodes.filter((n) => n.status !== "offline").length;
@@ -98,7 +100,7 @@ export default function DashboardPage() {
</thead>
<tbody>
{calls.map((c) => (
<CallRow key={c.call_id} call={c} systemName={systemMap[c.system_id ?? ""]?.name} />
<CallRow key={c.call_id} call={c} systemName={systemMap[c.system_id ?? ""]?.name} isAdmin={isAdmin} />
))}
</tbody>
</table>
+81 -9
View File
@@ -2,10 +2,12 @@
import { useState } from "react";
import type { CallRecord } from "@/lib/types";
import { c2api } from "@/lib/c2api";
interface Props {
call: CallRecord;
systemName?: string;
isAdmin?: boolean;
}
function duration(started: string, ended: string | null): string {
@@ -22,13 +24,35 @@ const TAG_COLORS: Record<string, string> = {
accident: "bg-orange-900 text-orange-300",
};
export function CallRow({ call, systemName }: Props) {
export function CallRow({ call, systemName, isAdmin }: Props) {
const [expanded, setExpanded] = useState(false);
const [showOriginal, setShowOriginal] = useState(false);
const [editing, setEditing] = useState(false);
const [editText, setEditText] = useState("");
const [saving, setSaving] = useState(false);
const isActive = call.status === "active";
const hasDetails = call.transcript || call.transcript_corrected || (call.tags && call.tags.length > 0) || call.incident_id;
const displayTranscript = (!showOriginal && call.transcript_corrected) ? call.transcript_corrected : call.transcript;
const hasBoth = !!(call.transcript && call.transcript_corrected);
const hasSegments = call.segments && call.segments.length > 1;
function startEdit() {
setEditText(call.transcript_corrected ?? call.transcript ?? "");
setEditing(true);
}
async function saveEdit(e: React.MouseEvent) {
e.stopPropagation();
setSaving(true);
try {
await c2api.patchTranscript(call.call_id, editText);
setEditing(false);
} catch (err) {
console.error(err);
} finally {
setSaving(false);
}
}
return (
<>
@@ -104,16 +128,64 @@ export function CallRow({ call, systemName }: Props) {
)}
{/* Transcript */}
{displayTranscript ? (
{editing ? (
<div className="space-y-2" onClick={(e) => e.stopPropagation()}>
<textarea
value={editText}
onChange={(e) => setEditText(e.target.value)}
rows={4}
className="w-full bg-gray-800 border border-gray-600 rounded-lg px-3 py-2 text-xs text-gray-200 font-mono focus:outline-none focus:border-indigo-500 resize-none"
/>
<div className="flex gap-2">
<button
onClick={saveEdit}
disabled={saving}
className="text-xs bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 text-white px-3 py-1 rounded transition-colors"
>
{saving ? "Saving…" : "Save & reprocess"}
</button>
<button
onClick={(e) => { e.stopPropagation(); setEditing(false); }}
className="text-xs text-gray-500 hover:text-gray-300 px-3 py-1 transition-colors"
>
Cancel
</button>
</div>
</div>
) : hasSegments ? (
<div className="space-y-1">
<div className="flex items-center gap-2">
{hasBoth && (
<span className="text-xs text-gray-600 font-mono">
{showOriginal ? "original" : "corrected"}
</span>
<div className="flex items-center justify-between">
<span className="text-xs text-gray-600 font-mono">{call.segments!.length} transmissions</span>
{isAdmin && (
<button onClick={(e) => { e.stopPropagation(); startEdit(); }} className="text-xs text-gray-600 hover:text-gray-400 font-mono transition-colors">edit</button>
)}
{!hasBoth && call.transcript_corrected && (
<span className="text-xs text-gray-600 font-mono">corrected</span>
</div>
<div className="bg-gray-800 rounded-lg px-4 py-3 space-y-2 max-h-48 overflow-y-auto">
{call.segments!.map((seg, i) => (
<div key={i} className="flex gap-3 text-xs font-mono">
<span className="text-gray-600 shrink-0">{i + 1}. [{seg.start}s]</span>
<span className="text-gray-300">{seg.text}</span>
</div>
))}
</div>
{hasBoth && (
<button
onClick={(e) => { e.stopPropagation(); setShowOriginal((v) => !v); }}
className="text-xs text-gray-600 hover:text-gray-400 font-mono transition-colors"
>
{showOriginal ? "show corrected ↑" : "show original ↓"}
</button>
)}
</div>
) : displayTranscript ? (
<div className="space-y-1">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{hasBoth && <span className="text-xs text-gray-600 font-mono">{showOriginal ? "original" : "corrected"}</span>}
{!hasBoth && call.transcript_corrected && <span className="text-xs text-gray-600 font-mono">corrected</span>}
</div>
{isAdmin && (
<button onClick={(e) => { e.stopPropagation(); startEdit(); }} className="text-xs text-gray-600 hover:text-gray-400 font-mono transition-colors">edit</button>
)}
</div>
<pre className="text-xs text-gray-300 bg-gray-800 rounded-lg px-4 py-3 whitespace-pre-wrap font-mono leading-relaxed max-h-40 overflow-y-auto">
+2
View File
@@ -55,6 +55,8 @@ export const c2api = {
const qs = params ? "?" + new URLSearchParams(params).toString() : "";
return request<unknown[]>(`/calls${qs}`);
},
patchTranscript: (callId: string, transcript: string) =>
request(`/calls/${callId}/transcript`, { method: "PATCH", body: JSON.stringify({ transcript }) }),
// Incidents
getIncidents: (params?: { status?: string; type?: string }) => {
+7
View File
@@ -20,6 +20,12 @@ export interface SystemRecord {
config: Record<string, unknown>;
}
export interface TranscriptSegment {
start: number;
end: number;
text: string;
}
export interface CallRecord {
call_id: string;
node_id: string;
@@ -32,6 +38,7 @@ export interface CallRecord {
audio_url: string | null;
transcript: string | null;
transcript_corrected: string | null;
segments: TranscriptSegment[] | null;
incident_id: string | null;
location: { lat: number; lng: number } | null;
tags: string[];