File size: 3,812 Bytes
2a5d15a
 
 
c86876a
 
 
2a5d15a
 
6737a45
2a5d15a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
/**
 * FinBot API Client
 * =================
 * Connects the Next.js frontend to the IRIS API proxy.
 * The proxy forwards to FastAPI when available and serves validated cached
 * Investor Relations responses when the backend process is warming up.
 */

export const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL || '/api/proxy';

export interface Source {
  id: number;
  doc_name: string;
  page: number;
  support: string;
  image_url?: string;
}

export interface KPIRow {
  metric: string;
  current: string;
  previous: string;
  change: string;
  interpretation: string;
  direction: 'positive' | 'negative' | 'neutral';
  period?: string;
  value?: string;
}

export interface Driver {
  title: string;
  detail: string;
}

export interface VisualItem {
  id: string;
  page: number;
  image_url: string;
  alt: string;
}

export interface ChatResponse {
  response_type: 'ir_response' | 'unsupported' | 'insufficient';
  question: string;
  executive_summary?: string;
  sources: Source[];
  financial_kpis: KPIRow[];
  key_drivers_summary?: string;
  key_drivers: Driver[];
  visual_evidence: VisualItem[];
  latency_ms: number;
  model_used: string;
}

export interface Document {
  doc_id: string;
  name: string;
  doc_type: string;
  period: string;
  institution?: string;
  total_pages: number;
  status: string;
  filename: string;
  chunks_indexed?: number;
  tables_indexed?: number;
  colpali_pages?: number;
  pagemap_file?: string;
  page_section_map?: Record<string, string>;
  page_metadata_map?: Record<string, {
    slide: number;
    section: string;
    mapping_items: string[];
    kpis: string[];
    kpi_tags: string[];
    description: string;
    period: string;
    synonyms: string[];
    visual_layout: string;
  }>;
  retrieval_config?: string;
}

/** Check if the backend is reachable */
export async function checkBackendHealth(): Promise<boolean> {
  try {
    const res = await fetch(`${BACKEND_URL}/api/health`, {
      signal: AbortSignal.timeout(2000),
    });
    return res.ok;
  } catch {
    return false;
  }
}

/** Submit a question to the RAG pipeline */
export async function submitQuestion(
  question: string,
  docIds: string[],
): Promise<ChatResponse> {
  const res = await fetch(`${BACKEND_URL}/api/chat/query`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ question, doc_ids: docIds }),
    signal: AbortSignal.timeout(120_000), // ColPali can take time
  });

  if (!res.ok) {
    const err = await res.text();
    throw new Error(`Backend error ${res.status}: ${err}`);
  }

  return res.json();
}

/** Get list of indexed documents */
export async function fetchDocuments(): Promise<Document[]> {
  const res = await fetch(`${BACKEND_URL}/api/documents/`, {
    signal: AbortSignal.timeout(5000),
  });
  if (!res.ok) throw new Error('Failed to fetch documents');
  return res.json();
}

/** Get the URL for a page image (rendered PDF page) */
export function getPageImageUrl(docId: string, pageNumber: number): string {
  return `${BACKEND_URL}/api/visuals/${docId}/${pageNumber}`;
}

/** Normalize legacy static page URLs to the backend visual route. */
export function normalizePageImageUrl(
  imageUrl: string | undefined,
  fallbackDocId: string,
  fallbackPage: number,
): string {
  if (!imageUrl) {
    return getPageImageUrl(fallbackDocId, fallbackPage);
  }

  const legacyPageMatch = imageUrl.match(
    /\/pages\/([^/]+)\/pages\/page_(\d{4})(?:_[^/.]+)?\.png$/,
  );
  if (legacyPageMatch) {
    return getPageImageUrl(legacyPageMatch[1], Number.parseInt(legacyPageMatch[2], 10));
  }

  if (imageUrl.startsWith('/api/visuals/')) {
    return `${BACKEND_URL}${imageUrl}`;
  }

  if (imageUrl.startsWith('/')) {
    return `${BACKEND_URL}${imageUrl}`;
  }

  return imageUrl;
}