Files
B.Tech-Project-III/thirdeye/dashboard/app/lib/api.ts
2026-04-05 00:43:23 +05:30

438 lines
14 KiB
TypeScript

/**
* ThirdEye API Client
* Connects to the FastAPI backend at localhost:8000 (proxied via /api)
*/
const API_BASE = "/api";
// ─── Types ────────────────────────────────────────────────────────────────────
export interface SignalMetadata {
type: string;
severity: "low" | "medium" | "high" | "critical";
status: string;
sentiment: string;
urgency: string;
entities: string; // JSON string
keywords: string; // JSON string
raw_quote: string;
timestamp: string;
group_id: string;
lens: string;
meeting_id?: string;
}
export interface Signal {
document: string;
metadata: SignalMetadata;
id: string;
}
export interface Group {
group_id: string;
group_name: string;
signal_count: number;
lens: string;
}
export interface Pattern {
id: string;
group_id: string;
type: string;
description: string;
severity: "info" | "warning" | "critical";
evidence_signal_ids: string[];
recommendation: string;
detected_at: string;
is_active: boolean;
}
export interface CrossGroupInsight {
id: string;
type: string;
description: string;
group_a: { name?: string; group_id?: string; evidence?: string };
group_b: { name?: string; group_id?: string; evidence?: string };
severity: string;
recommendation: string;
detected_at: string;
is_resolved: boolean;
}
export interface Meeting {
meeting_id: string;
signal_count: number;
types: Record<string, number>;
started_at?: string;
speaker?: string;
group_id?: string;
}
export interface MeetingDetail {
meeting_id: string;
started_at: string;
speaker: string;
group_id: string;
total_signals: number;
signal_counts: Record<string, number>;
summary: string;
}
export interface TranscriptChunk {
id: string;
text: string;
speaker: string;
timestamp: string;
summary: string;
}
export interface JiraTicket {
id: string;
jira_key: string;
jira_url: string;
jira_summary: string;
jira_priority: string;
original_signal_id: string;
group_id: string;
raised_at: string;
status: string;
assignee?: string;
}
export interface JiraConfig {
configured: boolean;
connected?: boolean;
base_url?: string;
default_project?: string;
projects?: { key: string; name: string; id: string }[];
}
export interface TimelineSignal extends Signal {
group_name?: string;
}
// ─── Helpers ──────────────────────────────────────────────────────────────────
async function fetchJSON<T>(url: string, options?: RequestInit): Promise<T> {
const res = await fetch(url, options);
if (!res.ok) {
throw new Error(`API error ${res.status}: ${await res.text()}`);
}
return res.json() as Promise<T>;
}
// ─── Groups ───────────────────────────────────────────────────────────────────
export async function fetchGroups(): Promise<Group[]> {
const data = await fetchJSON<{ groups: Group[] }>(`${API_BASE}/groups`);
return data.groups;
}
// ─── Signals ──────────────────────────────────────────────────────────────────
export async function fetchSignals(
groupId: string,
signalType?: string
): Promise<Signal[]> {
const url = signalType
? `${API_BASE}/groups/${groupId}/signals?signal_type=${signalType}`
: `${API_BASE}/groups/${groupId}/signals`;
const data = await fetchJSON<{ signals: Signal[]; count: number }>(url);
return data.signals;
}
export async function fetchAllSignals(): Promise<{ group_id: string; signals: Signal[] }[]> {
const groups = await fetchGroups();
const results = await Promise.allSettled(
groups.map(async (g) => ({
group_id: g.group_id,
signals: await fetchSignals(g.group_id),
}))
);
return results
.filter(
(r): r is PromiseFulfilledResult<{ group_id: string; signals: Signal[] }> =>
r.status === "fulfilled"
)
.map((r) => r.value);
}
// ─── Patterns ─────────────────────────────────────────────────────────────────
export async function fetchPatterns(groupId: string): Promise<Pattern[]> {
const data = await fetchJSON<{ patterns: Pattern[] }>(
`${API_BASE}/groups/${groupId}/patterns`
);
return data.patterns;
}
export async function fetchAllPatterns(): Promise<Pattern[]> {
const groups = await fetchGroups();
const results = await Promise.allSettled(groups.map((g) => fetchPatterns(g.group_id)));
return results
.filter((r): r is PromiseFulfilledResult<Pattern[]> => r.status === "fulfilled")
.flatMap((r) => r.value);
}
// ─── Cross-Group Insights ─────────────────────────────────────────────────────
export async function fetchCrossGroupInsights(
timeoutMs = 30000
): Promise<CrossGroupInsight[]> {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
try {
const data = await fetchJSON<{ insights: CrossGroupInsight[]; message?: string }>(
`${API_BASE}/cross-group/insights`,
{ signal: ctrl.signal }
);
return data.insights;
} finally {
clearTimeout(timer);
}
}
// ─── Knowledge Base Query ─────────────────────────────────────────────────────
export async function queryKnowledge(
groupId: string,
question: string
): Promise<{ answer: string; question: string }> {
return fetchJSON(`${API_BASE}/groups/${groupId}/query`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ question }),
});
}
// ─── Meetings ─────────────────────────────────────────────────────────────────
export async function fetchMeetings(): Promise<Meeting[]> {
const data = await fetchJSON<{ meetings: Meeting[] }>(`${API_BASE}/meet/meetings`);
return data.meetings;
}
export async function fetchMeetingDetail(meetingId: string): Promise<MeetingDetail> {
return fetchJSON<MeetingDetail>(`${API_BASE}/meet/meetings/${encodeURIComponent(meetingId)}`);
}
export async function fetchMeetingTranscript(
meetingId: string
): Promise<{ transcript: TranscriptChunk[]; chunk_count: number }> {
return fetchJSON(`${API_BASE}/meet/meetings/${encodeURIComponent(meetingId)}/transcript`);
}
export async function fetchMeetingSignals(meetingId: string): Promise<Signal[]> {
const data = await fetchJSON<{ signals: Signal[]; count: number }>(
`${API_BASE}/meet/meetings/${encodeURIComponent(meetingId)}/signals`
);
return data.signals;
}
// ─── Jira ─────────────────────────────────────────────────────────────────────
export interface JiraTicketFilters {
group_id?: string;
date_from?: string;
date_to?: string;
live?: boolean;
}
export async function fetchJiraTickets(filters: JiraTicketFilters = {}): Promise<JiraTicket[]> {
const params = new URLSearchParams();
if (filters.group_id) params.set("group_id", filters.group_id);
if (filters.date_from) params.set("date_from", filters.date_from);
if (filters.date_to) params.set("date_to", filters.date_to);
if (filters.live) params.set("live", "true");
const url = params.toString()
? `${API_BASE}/jira/tickets?${params}`
: `${API_BASE}/jira/tickets`;
const data = await fetchJSON<{ tickets: JiraTicket[]; count: number }>(url);
return data.tickets;
}
export async function fetchJiraTicketStatus(
ticketKey: string
): Promise<{ key: string; status: string; assignee: string; summary: string; url: string }> {
return fetchJSON(`${API_BASE}/jira/tickets/${encodeURIComponent(ticketKey)}/status`);
}
export async function raiseJiraTicket(
signalId: string,
groupId: string,
projectKey?: string,
force = false
): Promise<{ ok: boolean; key?: string; url?: string; summary?: string; reason?: string; error?: string }> {
return fetchJSON(`${API_BASE}/jira/raise`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ signal_id: signalId, group_id: groupId, project_key: projectKey, force }),
});
}
export async function createJiraTicket(data: {
summary: string;
description?: string;
project_key?: string;
issue_type?: string;
priority?: string;
labels?: string[];
assignee_account_id?: string;
}): Promise<{ ok: boolean; key?: string; url?: string; error?: string; details?: unknown }> {
return fetchJSON(`${API_BASE}/jira/create`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
}
export async function searchJiraUsers(
query: string
): Promise<{ account_id: string; display_name: string; email: string; active: boolean }[]> {
const data = await fetchJSON<{ users: { account_id: string; display_name: string; email: string; active: boolean }[] }>(
`${API_BASE}/jira/users/search?q=${encodeURIComponent(query)}`
);
return data.users;
}
export async function fetchJiraConfig(): Promise<JiraConfig> {
return fetchJSON<JiraConfig>(`${API_BASE}/jira/config`);
}
// ─── Timeline (cross-group signals) ──────────────────────────────────────────
export interface TimelineFilters {
group_id?: string;
severity?: string;
lens?: string;
signal_type?: string;
date_from?: string;
date_to?: string;
limit?: number;
}
export async function fetchTimeline(
filters: TimelineFilters = {}
): Promise<{ signals: TimelineSignal[]; total: number; truncated: boolean }> {
const params = new URLSearchParams();
Object.entries(filters).forEach(([k, v]) => {
if (v !== undefined && v !== "") params.set(k, String(v));
});
const url = params.toString()
? `${API_BASE}/signals/timeline?${params}`
: `${API_BASE}/signals/timeline`;
return fetchJSON(url);
}
// ─── Knowledge Browser ───────────────────────────────────────────────────────
export interface KnowledgeTopicSummary {
name: string;
signal_count: number;
latest: string;
sample_signals: string[];
}
export interface KnowledgeDayEntry {
date: string;
signals: Signal[];
topics: string[];
signal_count: number;
}
export interface KnowledgeBrowseResponse {
group_id: string;
group_name: string;
total_signals: number;
date_range: { earliest: string; latest: string };
topics: KnowledgeTopicSummary[];
timeline: KnowledgeDayEntry[];
}
export async function fetchKnowledgeBrowse(
groupId: string,
options: { dateFrom?: string; dateTo?: string; topic?: string } = {}
): Promise<KnowledgeBrowseResponse> {
const params = new URLSearchParams();
if (options.dateFrom) params.set("date_from", options.dateFrom);
if (options.dateTo) params.set("date_to", options.dateTo);
if (options.topic) params.set("topic", options.topic);
const qs = params.toString();
return fetchJSON<KnowledgeBrowseResponse>(
`${API_BASE}/knowledge/browse/${encodeURIComponent(groupId)}${qs ? `?${qs}` : ""}`
);
}
// ─── Health ───────────────────────────────────────────────────────────────────
export async function checkHealth(): Promise<boolean> {
try {
await fetchJSON("/health");
return true;
} catch {
return false;
}
}
// ─── Utility ──────────────────────────────────────────────────────────────────
/** Format ISO timestamp to a relative "T-Xm ago" style string */
export function formatRelativeTime(isoString: string): string {
const now = Date.now();
const ts = new Date(isoString).getTime();
const diffMs = now - ts;
const diffS = Math.floor(diffMs / 1000);
if (diffS < 60) return `T-${diffS}s ago`;
const diffM = Math.floor(diffS / 60);
if (diffM < 60) return `T-${diffM}m ago`;
const diffH = Math.floor(diffM / 60);
if (diffH < 24) return `T-${diffH}h ago`;
const diffD = Math.floor(diffH / 24);
return `T-${diffD}d ago`;
}
/** Get a severity color for a signal */
export function getSeverityColor(severity: string): string {
switch (severity) {
case "critical":
return "#ff6f78";
case "high":
return "#ffb300";
case "medium":
return "#A78BFA";
default:
return "#A78BFA";
}
}
/** Get an icon for a signal type */
export function getSignalIcon(type: string): string {
const iconMap: Record<string, string> = {
architecture_decision: "architecture",
tech_debt: "construction",
security_concern: "security",
consensus: "check_circle",
blocker: "block",
risk: "warning",
sentiment_spike: "mood",
knowledge_gap: "help",
decision: "gavel",
action_item: "task_alt",
trend: "trending_up",
jira_raised: "bug_report",
meet_started: "videocam",
meet_transcript: "record_voice_over",
};
return iconMap[type] ?? "sensors";
}
/** Parse a JSON string field from signal metadata safely */
export function parseMetaList(str: string): string[] {
try {
const parsed = JSON.parse(str);
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
}