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:
437
thirdeye/dashboard/app/lib/api.ts
Normal file
437
thirdeye/dashboard/app/lib/api.ts
Normal file
@@ -0,0 +1,437 @@
|
||||
/**
|
||||
* 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 [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user