mirror of
https://github.com/arkorty/B.Tech-Project-III.git
synced 2026-04-19 20:51:49 +00:00
init
This commit is contained in:
533
thirdeye/dashboard/app/meetings/page.tsx
Normal file
533
thirdeye/dashboard/app/meetings/page.tsx
Normal file
@@ -0,0 +1,533 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
import Sidebar from "../components/Sidebar";
|
||||
import TopBar from "../components/TopBar";
|
||||
import {
|
||||
fetchMeetings,
|
||||
fetchMeetingDetail,
|
||||
fetchMeetingTranscript,
|
||||
fetchMeetingSignals,
|
||||
Meeting,
|
||||
MeetingDetail,
|
||||
TranscriptChunk,
|
||||
Signal,
|
||||
formatRelativeTime,
|
||||
getSeverityColor,
|
||||
getSignalIcon,
|
||||
parseMetaList,
|
||||
} from "../lib/api";
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function formatDate(iso: string): string {
|
||||
if (!iso) return "—";
|
||||
try {
|
||||
return new Date(iso).toLocaleString("en-GB", {
|
||||
day: "2-digit", month: "short", year: "numeric",
|
||||
hour: "2-digit", minute: "2-digit",
|
||||
});
|
||||
} catch {
|
||||
return iso;
|
||||
}
|
||||
}
|
||||
|
||||
function SignalTypeBadge({ type }: { type: string }) {
|
||||
const colors: Record<string, string> = {
|
||||
meet_decision: "#34D399",
|
||||
meet_action_item: "#60A5FA",
|
||||
meet_blocker: "#F87171",
|
||||
meet_risk: "#FBBF24",
|
||||
meet_open_q: "#A78BFA",
|
||||
meet_summary: "#A78BFA",
|
||||
meet_chunk_raw: "#71717A",
|
||||
};
|
||||
const color = colors[type] || "#A78BFA";
|
||||
return (
|
||||
<span
|
||||
className="text-[9px] font-bold uppercase tracking-[0.1em] px-2 py-0.5 rounded-full border"
|
||||
style={{ color, borderColor: `${color}40`, background: `${color}15` }}
|
||||
>
|
||||
{type.replace("meet_", "").replace(/_/g, " ")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Meeting List Item ─────────────────────────────────────────────────────────
|
||||
|
||||
function MeetingListItem({
|
||||
meeting,
|
||||
isSelected,
|
||||
onClick,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
}: {
|
||||
meeting: Meeting;
|
||||
isSelected: boolean;
|
||||
onClick: () => void;
|
||||
dateFrom: string;
|
||||
dateTo: string;
|
||||
}) {
|
||||
const ts = meeting.started_at || "";
|
||||
if (dateFrom && ts && ts < dateFrom) return null;
|
||||
if (dateTo && ts && ts > dateTo) return null;
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`w-full text-left px-4 py-3 rounded-xl border transition-all duration-200 mb-2 ${
|
||||
isSelected
|
||||
? "border-[#A78BFA]/60 bg-[rgba(167,139,250,0.12)]"
|
||||
: "border-white/5 bg-[rgba(22,16,36,0.5)] hover:border-[#A78BFA]/30 hover:bg-[rgba(167,139,250,0.06)]"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<div
|
||||
className="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0"
|
||||
style={{ background: "rgba(167,139,250,0.15)" }}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[#A78BFA]" style={{ fontSize: "16px" }}>
|
||||
video_camera_front
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-[12px] font-semibold text-zinc-200 truncate font-mono-data">
|
||||
{meeting.meeting_id}
|
||||
</p>
|
||||
<p className="text-[10px] text-zinc-500">
|
||||
{meeting.started_at ? formatDate(meeting.started_at) : "Date unknown"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className="text-[11px] font-bold text-[#A78BFA] flex-shrink-0 mt-0.5"
|
||||
>
|
||||
{meeting.signal_count}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{Object.entries(meeting.types || {})
|
||||
.filter(([t]) => t !== "meet_started" && t !== "meet_chunk_raw")
|
||||
.slice(0, 4)
|
||||
.map(([type, count]) => (
|
||||
<SignalTypeBadge key={type} type={type} />
|
||||
))}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Transcript Panel ──────────────────────────────────────────────────────────
|
||||
|
||||
function TranscriptPanel({ chunks }: { chunks: TranscriptChunk[] }) {
|
||||
if (chunks.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-64 text-zinc-600">
|
||||
<span className="material-symbols-outlined text-4xl mb-2">mic_off</span>
|
||||
<p className="text-[12px] uppercase tracking-wider">No transcript chunks stored</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{chunks.map((chunk, i) => (
|
||||
<div
|
||||
key={chunk.id || i}
|
||||
className="rounded-xl border border-white/5 bg-[rgba(22,16,36,0.5)] p-4"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-6 h-6 rounded-full flex items-center justify-center"
|
||||
style={{ background: "rgba(167,139,250,0.2)" }}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[#A78BFA]" style={{ fontSize: "12px" }}>
|
||||
record_voice_over
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[11px] font-semibold text-[#A78BFA]">
|
||||
{chunk.speaker || "Unknown Speaker"}
|
||||
</span>
|
||||
<span className="text-[9px] text-zinc-600 font-mono-data">
|
||||
CHUNK {String(i + 1).padStart(2, "0")}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[10px] text-zinc-600 font-mono-data">
|
||||
{formatDate(chunk.timestamp)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[13px] text-zinc-300 leading-relaxed">{chunk.text}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Signals Panel ─────────────────────────────────────────────────────────────
|
||||
|
||||
function SignalsPanel({ signals }: { signals: Signal[] }) {
|
||||
const filtered = signals.filter(
|
||||
(s) => !["meet_chunk_raw", "meet_started"].includes(s.metadata?.type || "")
|
||||
);
|
||||
|
||||
if (filtered.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-64 text-zinc-600">
|
||||
<span className="material-symbols-outlined text-4xl mb-2">sensors_off</span>
|
||||
<p className="text-[12px] uppercase tracking-wider">No signals extracted yet</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{filtered.map((sig, i) => {
|
||||
const meta = sig.metadata;
|
||||
const color = getSeverityColor(meta.severity);
|
||||
const icon = getSignalIcon(meta.type);
|
||||
return (
|
||||
<div
|
||||
key={sig.id || i}
|
||||
className="rounded-xl border border-white/5 bg-[rgba(22,16,36,0.5)] p-4 border-l-[3px]"
|
||||
style={{ borderLeftColor: color }}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3 mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1.5 rounded-lg" style={{ background: `${color}18` }}>
|
||||
<span className="material-symbols-outlined" style={{ fontSize: "14px", color }}>
|
||||
{icon}
|
||||
</span>
|
||||
</div>
|
||||
<SignalTypeBadge type={meta.type} />
|
||||
</div>
|
||||
<span className="text-[10px] text-zinc-600 font-mono-data flex-shrink-0">
|
||||
{formatRelativeTime(meta.timestamp)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[13px] text-zinc-200 leading-relaxed">{meta.summary || sig.document}</p>
|
||||
{meta.raw_quote && meta.raw_quote !== meta.summary && (
|
||||
<p className="text-[11px] text-zinc-500 mt-2 italic border-l-2 border-zinc-700 pl-3">
|
||||
"{meta.raw_quote.slice(0, 200)}"
|
||||
</p>
|
||||
)}
|
||||
{meta.entities && (
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{parseMetaList(meta.entities).map((e, j) => (
|
||||
<span
|
||||
key={j}
|
||||
className="text-[10px] text-[#A78BFA] bg-[rgba(167,139,250,0.1)] px-2 py-0.5 rounded-full"
|
||||
>
|
||||
{e}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Main Page ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export default function MeetingsPage() {
|
||||
const [meetings, setMeetings] = useState<Meeting[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const [detail, setDetail] = useState<MeetingDetail | null>(null);
|
||||
const [transcript, setTranscript] = useState<TranscriptChunk[]>([]);
|
||||
const [signals, setSignals] = useState<Signal[]>([]);
|
||||
const [activeTab, setActiveTab] = useState<"transcript" | "signals">("signals");
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
|
||||
// Filters
|
||||
const [dateFrom, setDateFrom] = useState("");
|
||||
const [dateTo, setDateTo] = useState("");
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
fetchMeetings()
|
||||
.then((data) => setMeetings(data))
|
||||
.catch(console.error)
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
const selectMeeting = useCallback(async (id: string) => {
|
||||
setSelectedId(id);
|
||||
setDetail(null);
|
||||
setTranscript([]);
|
||||
setSignals([]);
|
||||
setDetailLoading(true);
|
||||
try {
|
||||
const [det, trans, sigs] = await Promise.all([
|
||||
fetchMeetingDetail(id),
|
||||
fetchMeetingTranscript(id),
|
||||
fetchMeetingSignals(id),
|
||||
]);
|
||||
setDetail(det);
|
||||
setTranscript(trans.transcript);
|
||||
setSignals(sigs);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
setDetailLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const filteredMeetings = meetings.filter((m) => {
|
||||
if (search && !m.meeting_id.toLowerCase().includes(search.toLowerCase())) return false;
|
||||
if (dateFrom && m.started_at && m.started_at < dateFrom) return false;
|
||||
if (dateTo && m.started_at && m.started_at > dateTo) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
const totalSignals = meetings.reduce((sum, m) => sum + m.signal_count, 0);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex" style={{ backgroundColor: "#09090B" }}>
|
||||
<Sidebar />
|
||||
<div className="flex-1 ml-[240px] flex flex-col min-h-screen">
|
||||
<TopBar />
|
||||
<main className="flex-1 p-8">
|
||||
{/* Header */}
|
||||
<div className="mb-8 animate-fade-in-up opacity-0">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div
|
||||
className="w-10 h-10 rounded-xl flex items-center justify-center"
|
||||
style={{ background: "rgba(167,139,250,0.15)" }}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[#A78BFA]">video_camera_front</span>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white tracking-tight">Meeting History</h1>
|
||||
<p className="text-[11px] text-zinc-500 uppercase tracking-[0.2em]">
|
||||
Google Meet · Signal Intelligence
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Row */}
|
||||
<div className="grid grid-cols-3 gap-4 mb-6 animate-fade-in-up opacity-0 delay-100">
|
||||
{[
|
||||
{ label: "Total Meetings", value: meetings.length, icon: "video_camera_front" },
|
||||
{ label: "Total Signals", value: totalSignals, icon: "sensors" },
|
||||
{
|
||||
label: "Avg Signals / Meeting",
|
||||
value: meetings.length ? Math.round(totalSignals / meetings.length) : 0,
|
||||
icon: "analytics",
|
||||
},
|
||||
].map((stat) => (
|
||||
<div
|
||||
key={stat.label}
|
||||
className="neon-card-gradient rounded-xl p-4 border border-white/5 flex items-center gap-4"
|
||||
>
|
||||
<div
|
||||
className="w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0"
|
||||
style={{ background: "rgba(167,139,250,0.1)" }}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[#A78BFA]" style={{ fontSize: "18px" }}>
|
||||
{stat.icon}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-white">{stat.value}</p>
|
||||
<p className="text-[10px] text-zinc-500 uppercase tracking-wider">{stat.label}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Filter bar */}
|
||||
<div className="flex items-center gap-3 mb-6 animate-fade-in-up opacity-0 delay-200">
|
||||
<div className="relative flex-1 max-w-xs">
|
||||
<span
|
||||
className="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-zinc-500"
|
||||
style={{ fontSize: "16px" }}
|
||||
>
|
||||
search
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search meeting ID..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-full bg-[#141419] border border-white/10 rounded-lg pl-9 pr-4 py-2.5 text-[12px] text-zinc-200 placeholder-zinc-600 focus:outline-none focus:border-[#A78BFA]/50"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[10px] text-zinc-500 uppercase tracking-wider">From</span>
|
||||
<input
|
||||
type="date"
|
||||
value={dateFrom}
|
||||
onChange={(e) => setDateFrom(e.target.value ? new Date(e.target.value).toISOString() : "")}
|
||||
className="bg-[#141419] border border-white/10 rounded-lg px-3 py-2 text-[12px] text-zinc-300 focus:outline-none focus:border-[#A78BFA]/50"
|
||||
/>
|
||||
<span className="text-[10px] text-zinc-500 uppercase tracking-wider">To</span>
|
||||
<input
|
||||
type="date"
|
||||
value={dateTo}
|
||||
onChange={(e) => setDateTo(e.target.value ? new Date(e.target.value).toISOString() : "")}
|
||||
className="bg-[#141419] border border-white/10 rounded-lg px-3 py-2 text-[12px] text-zinc-300 focus:outline-none focus:border-[#A78BFA]/50"
|
||||
/>
|
||||
{(dateFrom || dateTo || search) && (
|
||||
<button
|
||||
onClick={() => { setDateFrom(""); setDateTo(""); setSearch(""); }}
|
||||
className="text-[10px] text-zinc-500 hover:text-zinc-300 uppercase tracking-wider px-3 py-2 rounded-lg border border-white/10 transition-colors"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Two-panel layout */}
|
||||
<div className="flex gap-6" style={{ minHeight: "calc(100vh - 400px)" }}>
|
||||
{/* Left: Meeting List */}
|
||||
<div className="w-[320px] flex-shrink-0">
|
||||
{loading ? (
|
||||
<div className="flex flex-col items-center justify-center h-64 text-zinc-600">
|
||||
<div className="w-8 h-8 border-2 border-[#A78BFA]/30 border-t-[#A78BFA] rounded-full animate-spin mb-3" />
|
||||
<p className="text-[11px] uppercase tracking-wider">Loading meetings...</p>
|
||||
</div>
|
||||
) : filteredMeetings.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-64 text-zinc-600">
|
||||
<span className="material-symbols-outlined text-4xl mb-2">video_camera_front</span>
|
||||
<p className="text-[12px] uppercase tracking-wider">No meetings found</p>
|
||||
<p className="text-[10px] text-zinc-700 mt-1">Meetings appear when Google Meet extension sends data</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="custom-scrollbar overflow-y-auto" style={{ maxHeight: "calc(100vh - 420px)" }}>
|
||||
{filteredMeetings.map((m) => (
|
||||
<MeetingListItem
|
||||
key={m.meeting_id}
|
||||
meeting={m}
|
||||
isSelected={selectedId === m.meeting_id}
|
||||
onClick={() => selectMeeting(m.meeting_id)}
|
||||
dateFrom={dateFrom}
|
||||
dateTo={dateTo}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right: Detail Panel */}
|
||||
<div className="flex-1 min-w-0">
|
||||
{!selectedId ? (
|
||||
<div className="neon-card-gradient rounded-2xl border border-white/5 h-full flex flex-col items-center justify-center text-zinc-600 p-12">
|
||||
<span className="material-symbols-outlined text-5xl mb-4 text-zinc-700">
|
||||
video_camera_front
|
||||
</span>
|
||||
<p className="text-[14px] uppercase tracking-wider text-zinc-500">Select a meeting</p>
|
||||
<p className="text-[11px] text-zinc-700 mt-2">to view transcript and signals</p>
|
||||
</div>
|
||||
) : detailLoading ? (
|
||||
<div className="neon-card-gradient rounded-2xl border border-white/5 h-full flex flex-col items-center justify-center">
|
||||
<div className="w-10 h-10 border-2 border-[#A78BFA]/30 border-t-[#A78BFA] rounded-full animate-spin mb-4" />
|
||||
<p className="text-[12px] text-zinc-500 uppercase tracking-wider">Loading meeting data...</p>
|
||||
</div>
|
||||
) : detail ? (
|
||||
<div className="neon-card-gradient rounded-2xl border border-white/5 flex flex-col h-full">
|
||||
{/* Meeting header */}
|
||||
<div className="p-6 border-b border-white/5">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-[10px] text-[#A78BFA] font-bold uppercase tracking-wider bg-[rgba(167,139,250,0.1)] px-2 py-0.5 rounded-full">
|
||||
MEETING
|
||||
</span>
|
||||
<span className="text-[10px] text-zinc-500 font-mono-data">{detail.group_id}</span>
|
||||
</div>
|
||||
<h2 className="text-[15px] font-bold text-white font-mono-data break-all">
|
||||
{detail.meeting_id}
|
||||
</h2>
|
||||
<div className="flex items-center gap-4 mt-2">
|
||||
<div className="flex items-center gap-1 text-zinc-400">
|
||||
<span className="material-symbols-outlined" style={{ fontSize: "14px" }}>schedule</span>
|
||||
<span className="text-[11px]">{formatDate(detail.started_at)}</span>
|
||||
</div>
|
||||
{detail.speaker && detail.speaker !== "Unknown" && (
|
||||
<div className="flex items-center gap-1 text-zinc-400">
|
||||
<span className="material-symbols-outlined" style={{ fontSize: "14px" }}>person</span>
|
||||
<span className="text-[11px]">{detail.speaker}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-2xl font-bold text-[#A78BFA]">{detail.total_signals}</p>
|
||||
<p className="text-[10px] text-zinc-500 uppercase tracking-wider">signals</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Signal type counts */}
|
||||
<div className="flex flex-wrap gap-2 mt-4">
|
||||
{Object.entries(detail.signal_counts)
|
||||
.filter(([t]) => t !== "meet_started" && t !== "meet_chunk_raw")
|
||||
.map(([type, count]) => (
|
||||
<div
|
||||
key={type}
|
||||
className="flex items-center gap-1.5 bg-[rgba(255,255,255,0.04)] border border-white/5 rounded-lg px-3 py-1"
|
||||
>
|
||||
<SignalTypeBadge type={type} />
|
||||
<span className="text-[11px] font-bold text-zinc-300">{count}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Summary */}
|
||||
{detail.summary && (
|
||||
<div className="mt-4 p-4 rounded-xl bg-[rgba(167,139,250,0.06)] border border-[#A78BFA]/15">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="material-symbols-outlined text-[#A78BFA]" style={{ fontSize: "14px" }}>
|
||||
summarize
|
||||
</span>
|
||||
<span className="text-[10px] font-bold text-[#A78BFA] uppercase tracking-wider">
|
||||
AI Summary
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[12px] text-zinc-300 leading-relaxed whitespace-pre-line">
|
||||
{detail.summary}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex border-b border-white/5">
|
||||
{(["signals", "transcript"] as const).map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={`px-6 py-3 text-[11px] font-semibold uppercase tracking-wider transition-colors ${
|
||||
activeTab === tab
|
||||
? "text-[#A78BFA] border-b-2 border-[#A78BFA]"
|
||||
: "text-zinc-500 hover:text-zinc-300"
|
||||
}`}
|
||||
>
|
||||
{tab === "signals" ? `Signals (${signals.filter(s => !["meet_chunk_raw","meet_started"].includes(s.metadata?.type)).length})` : `Transcript (${transcript.length} chunks)`}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tab content */}
|
||||
<div className="flex-1 p-6 overflow-y-auto custom-scrollbar">
|
||||
{activeTab === "signals" ? (
|
||||
<SignalsPanel signals={signals} />
|
||||
) : (
|
||||
<TranscriptPanel chunks={transcript} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user