mirror of
https://github.com/arkorty/B.Tech-Project-III.git
synced 2026-04-19 20:51:49 +00:00
438 lines
14 KiB
TypeScript
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 [];
|
|
}
|
|
}
|