/** * 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; 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; 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(url: string, options?: RequestInit): Promise { const res = await fetch(url, options); if (!res.ok) { throw new Error(`API error ${res.status}: ${await res.text()}`); } return res.json() as Promise; } // ─── Groups ─────────────────────────────────────────────────────────────────── export async function fetchGroups(): Promise { const data = await fetchJSON<{ groups: Group[] }>(`${API_BASE}/groups`); return data.groups; } // ─── Signals ────────────────────────────────────────────────────────────────── export async function fetchSignals( groupId: string, signalType?: string ): Promise { 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 { const data = await fetchJSON<{ patterns: Pattern[] }>( `${API_BASE}/groups/${groupId}/patterns` ); return data.patterns; } export async function fetchAllPatterns(): Promise { const groups = await fetchGroups(); const results = await Promise.allSettled(groups.map((g) => fetchPatterns(g.group_id))); return results .filter((r): r is PromiseFulfilledResult => r.status === "fulfilled") .flatMap((r) => r.value); } // ─── Cross-Group Insights ───────────────────────────────────────────────────── export async function fetchCrossGroupInsights( timeoutMs = 30000 ): Promise { 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 { const data = await fetchJSON<{ meetings: Meeting[] }>(`${API_BASE}/meet/meetings`); return data.meetings; } export async function fetchMeetingDetail(meetingId: string): Promise { return fetchJSON(`${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 { 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 { 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 { return fetchJSON(`${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 { 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( `${API_BASE}/knowledge/browse/${encodeURIComponent(groupId)}${qs ? `?${qs}` : ""}` ); } // ─── Health ─────────────────────────────────────────────────────────────────── export async function checkHealth(): Promise { 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 = { 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 []; } }