GrantForge Bot
Deploy sha-565ad85979610064f6d1c18ab3b6404357d61073 — source build (no GHCR)
ce8f04a
Raw
History Blame Contribute Delete
43 kB
import axios from 'axios';
import toast from 'react-hot-toast';
import { safeSignInRedirectPath, shouldHardRedirectOn401 } from '../utils/authGate';
/** Strip trailing /api from env URLs without mangling hosts that contain "api" elsewhere. */
export function resolveApiBaseUrl(
apiUrl?: string,
langserveUrl?: string,
fallback = 'http://localhost:8000',
): string {
const raw = apiUrl || langserveUrl || fallback;
return raw.replace(/\/api\/?$/, '');
}
// Bazowy URL to backend
export const apiClient = axios.create({
baseURL: resolveApiBaseUrl(
import.meta.env.VITE_API_URL,
import.meta.env.VITE_LANGSERVE_URL,
),
headers: { 'Content-Type': 'application/json' },
});
// --- Interceptory ---
// Request: Clerk token is injected by ApiInterceptor; keep localStorage only as last-resort legacy fallback
apiClient.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token && !config.headers['Authorization']) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
});
// Response: obsługa błędów globalnych
apiClient.interceptors.response.use(
(response) => response,
(error) => {
const status = error?.response?.status;
if (status === 429) {
const retryAfter = error.response?.data?.retry_after;
const msg = retryAfter
? `Przekroczono limit zapytań. Spróbuj za ${retryAfter}s.`
: 'Przekroczono limit zapytań. Poczekaj chwilę.';
toast.error(`⏳ ${msg}`, { duration: 8000, id: 'rate-limit' });
}
// 401: only hard-redirect when Clerk says user is signed OUT.
// Signed-in + 401 is token/backend lag — bouncing to /sign-in causes
// sign-in → /dashboard → /projects → 401 loop (prod logs).
if (status === 401 && typeof window !== 'undefined') {
if (shouldHardRedirectOn401()) {
const target = safeSignInRedirectPath(
window.location.pathname || '',
window.location.search || '',
);
if (!window.location.pathname.startsWith('/sign-in')) {
window.location.assign(target);
}
}
}
return Promise.reject(error);
}
);
export const getSubscriptionStatus = async () => {
// /api/me zwraca {tier, wizard_iterations_today, tokens_used_month, limits}
const { data } = await apiClient.get('/api/me');
return data;
};
export const createCheckoutSession = async (plan: string = 'pro') => {
const { data } = await apiClient.post('/api/subscription/checkout', { plan });
return data as { checkout_url?: string; session_id?: string };
};
export const mintGeneratorStreamTicket = async (payload: {
project_id: string;
resume?: boolean;
full_autopilot?: boolean;
document_type?: string;
}) => {
const { data } = await apiClient.post('/api/generator/stream-ticket', payload);
return data as { ticket: string; expires_in: number; stream_url: string };
};
export const resumeGeneratorHil = async (projectId: string, userResponse: string) => {
const { data } = await apiClient.post('/api/generator/resume', {
project_id: projectId,
user_response: userResponse,
});
return data;
};
/** Holistic critic + multi-section auto-fix (keep good sections). */
export const runDocumentQualityCycle = async (projectId: string, maxPasses = 2) => {
const { data } = await apiClient.post(
`/api/projects/${projectId}/quality-cycle`,
null,
{ params: { max_passes: maxPasses } },
);
return data as {
status: string;
passes: Array<{ pass: number; overall_score?: number; is_approved?: boolean; fixed_sections?: string[] }>;
holistic?: Record<string, unknown>;
readiness?: { score: number; complete_sections: number; total: number };
message?: string;
};
};
export const prepareFullAutopilot = async (projectId: string, resume = false) => {
const { data } = await apiClient.post(`/api/generator/projects/${projectId}/generate-full`, {
resume,
});
return data as { status: string; ticket?: string; stream_url?: string };
};
export const mintAdminDiagnosticsTicket = async () => {
const { data } = await apiClient.post('/api/admin/diagnostics/stream-ticket');
return data as { ticket: string; expires_in: number };
};
export const getTrustSummary = async () => {
const { data } = await apiClient.get('/api/trust/summary');
return data;
};
/** Multi-registry company lookup (GUS/CEIDG/KRS/MF/SUDOP/Bizraport). */
export type LookupCompanyResult = {
nip: string;
name: string;
status: string;
sme_status?: string;
sme_confidence?: number;
sme_related_entities?: number;
regon?: string;
krs?: string;
address?: string;
voivodeship?: string;
pkd?: string[];
legal_form?: string;
sources_used?: string[];
registry_score?: number;
gaps?: string[];
risk_hints?: string[];
sudop?: Record<string, unknown>;
provenance?: Record<string, unknown>;
revenue?: number;
employment?: number;
};
export const lookupCompany = async (nip: string) => {
const { data } = await apiClient.get('/api/projects/lookup-company', { params: { nip } });
return data as LookupCompanyResult;
};
/** Full credibility pipeline: registries → match → regulation scoring. */
export const runCredibilityPipeline = async (payload: {
nip: string;
description?: string;
limit?: number;
user_answers?: { question: string; answer: string }[];
}) => {
const { data } = await apiClient.post('/api/projects/credibility-pipeline', payload);
return data;
};
/** Grant Pulse kinds: new | planned | closing | current | all */
export type PulseKind = 'new' | 'planned' | 'closing' | 'current' | 'all';
export type PulseVerification = {
deadline_verified?: boolean;
status_verified?: boolean;
regulation_verified?: boolean;
continuous?: boolean;
credibility_ok?: boolean;
alert_worthy?: boolean;
reasons?: string[];
};
export type PulseItem = {
id?: string;
name?: string;
status?: string;
deadline?: string;
continuous?: boolean;
operator?: string;
source?: string;
url?: string;
regulation_url?: string;
eurlex_url?: string;
celex_id?: string;
source_credibility_score?: number;
pulse_kinds?: string[];
primary_kind?: string;
first_seen_at?: string | null;
verification?: PulseVerification;
match_score?: number;
why_it_fits?: string;
alert_worthy?: boolean;
generation_blockers?: string[];
};
export type PulseResponse = {
status: string;
kind: string;
count: number;
counts?: Record<string, number>;
quality?: Record<string, number>;
items: PulseItem[];
generated_at?: string;
};
export const getGrantPulse = async (kind: PulseKind = 'current', limit = 40) => {
const { data } = await apiClient.get('/api/grants/pulse', { params: { kind, limit } });
return data as PulseResponse;
};
export const getFirmPulse = async (payload: {
nip?: string;
description?: string;
kind?: PulseKind;
limit?: number;
min_score?: number;
require_alert_worthy?: boolean;
company?: Record<string, unknown>;
}) => {
const { data } = await apiClient.post('/api/grants/pulse/firm', payload);
return data as PulseResponse & {
company?: Record<string, unknown>;
gaps?: string[];
alert_count?: number;
enrichment_status?: string;
};
};
export const getPulseMetrics = async () => {
const { data } = await apiClient.get('/api/grants/pulse/metrics');
return data;
};
export const postUserConsent = async (consent: Record<string, unknown>) => {
const { data } = await apiClient.post('/api/user/consent', { consent });
return data;
};
export const getMe = getSubscriptionStatus;
export const deleteUserAccount = async () => {
const { data } = await apiClient.delete('/api/account');
return data;
};
export const getAccountExport = async () => {
const { data } = await apiClient.get('/api/account/export');
return data;
};
export const updateAccountSettings = async (settings: { gdpr_consent_accepted?: boolean; ai_disclaimer_enabled?: boolean }) => {
const { data } = await apiClient.post('/api/account/settings', settings);
return data;
};
export const submitFeedback = async (text: string, type: string = "feedback") => {
const { data } = await apiClient.post('/api/feedback', { text, type });
return data;
};
export const getAdminStats = async () => {
const { data } = await apiClient.get('/api/admin/stats');
return data;
};
export const getCurrentSession = async () => {
const { data } = await apiClient.get('/api/session/current');
return data;
};
// Projects API
export const getProjects = async () => {
const { data } = await apiClient.get('/api/projects');
return data;
};
export const getProject = async (projectId: string) => {
const { data } = await apiClient.get(`/api/projects/${projectId}`);
return data;
};
export const updateProject = async (projectId: string, updateData: any) => {
const { data } = await apiClient.put(`/api/projects/${projectId}`, updateData);
return data;
};
export const deleteProject = async (projectId: string) => {
await apiClient.delete(`/api/projects/${projectId}`);
};
export const createProject = async (projectData: { title: string; program_type: string; description?: string; program_name?: string; estimated_value?: number; external_context?: Record<string, any> }) => {
const { data } = await apiClient.post('/api/projects', projectData);
return data;
};
export const resolveProjectRegulation = async (projectId: string, regulationUrl?: string) => {
const { data } = await apiClient.post(`/api/projects/${projectId}/regulation/resolve`, {
regulation_url: regulationUrl,
});
return data;
};
export const attachRegulationUrl = async (projectId: string, regulationUrl: string) => {
const { data } = await apiClient.post(`/api/projects/${projectId}/regulation/attach-url`, {
regulation_url: regulationUrl,
});
return data;
};
export type MatchProgramOptions = {
/** Canonical cascade path id (e.g. national_feng) — soft boost on match */
strategy_path?: string | null;
/** Optional pre-built eligibility report from spine / strategy recommend */
eligibility?: EligibilityReport | Record<string, unknown> | null;
};
export const matchProgram = async (
description: string,
nip?: string,
userAnswers?: { question: string; answer: string }[],
options?: MatchProgramOptions,
) => {
const { data } = await apiClient.post('/api/projects/match-program', {
description: description,
nip: nip,
user_answers: userAnswers?.length ? userAnswers : undefined,
...(options?.strategy_path
? { strategy_path: options.strategy_path }
: {}),
...(options?.eligibility
? { eligibility: options.eligibility }
: {}),
});
return data;
};
export const createWelcomeSeed = async () => {
const { data } = await apiClient.post('/api/projects/welcome-seed');
return data;
};
// --- SECTION ENDPOINTS ---
export const getProjectSections = async (projectId: string) => {
const { data } = await apiClient.get(`/api/projects/${projectId}/sections`);
return data;
};
export const generateProjectSection = async (projectId: string, sectionType: string, promptContext?: string) => {
const { data } = await apiClient.post(`/api/projects/${projectId}/generate-section`, {
section_type: sectionType,
prompt_context: promptContext
});
return data;
};
// --- Q&A / VERIFIER ENDPOINTS ---
export const getProjectQuestionsHistory = async (projectId: string) => {
const { data } = await apiClient.get(`/api/projects/${projectId}/ask/history`);
return data;
};
export const askProjectQuestion = async (projectId: string, question: string) => {
const { data } = await apiClient.post(`/api/projects/${projectId}/ask`, { question });
return data;
};
export const clearProjectQuestionsHistory = async (projectId: string) => {
const { data } = await apiClient.delete(`/api/projects/${projectId}/ask/history`);
return data;
};
export const updateProjectSection = async (projectId: string, sectionId: string, content: string) => {
const { data } = await apiClient.put(`/api/projects/${projectId}/sections/${sectionId}`, {
content: content
});
return data;
};
export const reviewProjectSection = async (projectId: string, sectionType: string, content: string) => {
const { data } = await apiClient.post(`/api/projects/${projectId}/review-section`, {
section_type: sectionType,
content: content
});
return data;
};
export interface SectionVersion {
id: string;
old_content: string;
author: string;
summary: string | null;
timestamp: string;
}
export const getSectionVersions = async (projectId: string, sectionId: string): Promise<SectionVersion[]> => {
const { data } = await apiClient.get(`/api/projects/${projectId}/sections/${sectionId}/versions`);
return data;
};
export const restoreSectionVersion = async (projectId: string, sectionId: string, versionId: string) => {
const { data } = await apiClient.post(`/api/projects/${projectId}/sections/${sectionId}/versions/${versionId}/restore`);
return data;
};
export const previewProject = async (projectId: string) => {
const { data } = await apiClient.get(`/api/projects/${projectId}/preview`);
return data;
};
export const compileFinalDocument = async (projectId: string, approvedOnly: boolean = false) => {
const { data } = await apiClient.post(`/api/projects/${projectId}/compile-final`, {
approved_only: approvedOnly
});
return data;
};
export const auditFinalDocument = async (projectId: string) => {
const { data } = await apiClient.post(`/api/projects/${projectId}/global-audit`);
return data;
};
export const clearGlobalAudit = async (projectId: string) => {
try {
const { data } = await apiClient.delete(`/api/projects/${projectId}/global-audit`);
return data;
} catch {
return { status: 'cleared' };
}
};
export const autofixProjectSection = async (projectId: string, sectionId: string) => {
const { data } = await apiClient.post(`/api/projects/${projectId}/sections/${sectionId}/autofix`);
return data;
};
export const syncRAGKnowledge = async () => {
const { data } = await apiClient.post('/api/rag/sync', { category: "ALL" });
return data;
};
// --- CHATBOT PROJECT ENDPOINTS ---
export const getProjectChatHistory = async (projectId: string) => {
const { data } = await apiClient.get(`/api/projects/${projectId}/chat`);
return data;
};
export const clearProjectChatHistory = async (projectId: string) => {
const { data } = await apiClient.delete(`/api/projects/${projectId}/chat`);
return data;
};
export const sendProjectChatMessage = async (projectId: string, content: string, activeSectionId?: string, activeSectionTitle?: string) => {
const payload: any = { content };
if (activeSectionId) {
payload.active_section = activeSectionId;
if (activeSectionTitle) payload.active_section_title = activeSectionTitle;
}
const { data } = await apiClient.post(`/api/projects/${projectId}/chat`, payload);
return data;
};
// --- EXPORT ENDPOINT ---
function parseFilenameFromDisposition(header?: string, fallback = 'Wniosek'): string {
if (!header) return fallback;
const match = header.match(/filename\*?=(?:UTF-8''|"?)([^";]+)"?/i);
return match?.[1] ? decodeURIComponent(match[1]) : fallback;
}
function triggerBlobDownload(blob: Blob, filename: string) {
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
}
export const exportProjectDocument = async (
projectId: string,
format: string,
template: string,
versionId?: string,
) => {
const payload: Record<string, string> = { format, template };
if (versionId) payload.version_id = versionId;
const response = await apiClient.post(
`/api/projects/${projectId}/export`,
payload,
{ responseType: 'blob' },
);
const contentType = (response.headers['content-type'] as string) || '';
if (contentType.includes('application/json')) {
const text = await (response.data as Blob).text();
try {
const parsed = JSON.parse(text);
const detail = parsed?.detail;
throw new Error(
typeof detail === 'string' ? detail : detail?.message || text || 'Błąd eksportu',
);
} catch (e) {
if (e instanceof Error && e.message !== 'Błąd eksportu') throw e;
throw new Error(text || 'Błąd eksportu');
}
}
const resolvedType =
contentType ||
(format === 'pdf'
? 'application/pdf'
: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
const blob =
response.data instanceof Blob
? response.data
: new Blob([response.data], { type: resolvedType });
if (blob.size < 128) {
throw new Error('Wygenerowany plik jest pusty — uzupełnij sekcje wniosku i spróbuj ponownie.');
}
return {
data: blob,
headers: {
'content-type': resolvedType,
'content-disposition': response.headers['content-disposition'] as string,
},
};
};
export const getProjectVersions = async (projectId: string) => {
const { data } = await apiClient.get(`/api/projects/${projectId}/versions`);
return data;
};
export const createProjectVersion = async (projectId: string, title?: string) => {
const { data } = await apiClient.post(`/api/projects/${projectId}/versions`, { title });
return data;
};
export const exportProjectPDF = async (projectId: string, versionId?: string) => {
const response = await exportProjectDocument(projectId, 'pdf', 'standard', versionId);
const fileName = parseFilenameFromDisposition(
response.headers['content-disposition'],
versionId ? `dotacja_v${versionId.substring(0, 6)}.pdf` : `dotacja_${projectId}.pdf`,
);
triggerBlobDownload(response.data, fileName);
};
export const exportProjectDOCX = async (projectId: string, approvedOnly: boolean = false, versionId?: string) => {
void approvedOnly;
const response = await exportProjectDocument(projectId, 'docx', 'standard', versionId);
const fileName = parseFilenameFromDisposition(
response.headers['content-disposition'],
versionId ? `dotacja_v${versionId.substring(0, 6)}.docx` : `dotacja_${projectId}.docx`,
);
triggerBlobDownload(response.data, fileName);
};
// --- EXTERNAL KNOWLEDGE / DOCUMENTS ENDPOINTS ---
export const uploadProjectDocument = async (projectId: string, file: File) => {
const formData = new FormData();
formData.append('file', file);
const { data } = await apiClient.post(`/api/projects/${projectId}/documents`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
return data;
};
export const getExportGateStatus = async (projectId: string) => {
const { data } = await apiClient.get(`/api/projects/${projectId}/export-gate`);
return data;
};
export const getGsdStatus = async (projectId: string) => {
const { data } = await apiClient.get(`/api/projects/${projectId}/gsd/status`);
return data;
};
export const resolveGsdHitl = async (projectId: string, hitlId: string, decision: string, comment = '') => {
const { data } = await apiClient.post(`/api/projects/${projectId}/gsd/resolve-hitl`, {
hitl_id: hitlId,
decision,
comment,
});
return data;
};
export const getProjectDocuments = async (projectId: string) => {
const { data } = await apiClient.get(`/api/projects/${projectId}/documents`);
if (Array.isArray(data)) return { documents: data, quota: null };
return data;
};
export const deleteProjectDocument = async (projectId: string, filename: string) => {
const { data } = await apiClient.delete(`/api/projects/${projectId}/documents/${filename}`);
return data;
};
// --- AUDIT ENDPOINTS ---
export type AuditMode = 'pro' | 'llm';
export interface GapReportResult {
program: string;
overall_score: number;
has_critical_gaps: boolean;
gaps: Array<{
category: string;
severity: string;
code?: string;
description: string;
recommendation?: string;
regulation_reference?: string;
}>;
summary?: string;
snapshot_backed?: boolean;
}
function mapGapReportToAuditUi(report: GapReportResult) {
return {
is_approved: !report.has_critical_gaps && report.overall_score >= 75,
overall_score: report.overall_score,
human_review_required: report.has_critical_gaps || report.overall_score < 70,
mode: 'pro' as const,
summary: report.summary,
issues: (report.gaps || []).map((g) => ({
category: g.category,
severity: g.severity,
perspective: 'deterministic',
message: g.description,
rule_citation: g.regulation_reference,
recommendation: g.recommendation,
code: g.code,
})),
};
}
export const requestGapReport = async (
programName: string,
documentText: string,
companyProfile?: Record<string, unknown>,
projectId?: string,
): Promise<GapReportResult> => {
const { data } = await apiClient.post('/api/audit/gap-report', {
program_name: programName,
document_text: documentText,
company_profile: companyProfile,
project_id: projectId,
export_format: 'json',
});
return data;
};
export const uploadGapReportDocument = async (
file: File,
programName: string,
): Promise<ReturnType<typeof mapGapReportToAuditUi>> => {
const formData = new FormData();
formData.append('file', file);
formData.append('program_name', programName);
formData.append('export_format', 'json');
const { data } = await apiClient.post('/api/audit/gap-report/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return mapGapReportToAuditUi(data);
};
export const exportGapReport = async (
programName: string,
documentText: string,
format: 'pdf' | 'docx',
projectId?: string,
) => {
const response = await apiClient.post(
'/api/audit/gap-report',
{
program_name: programName,
document_text: documentText,
project_id: projectId,
export_format: format,
},
{ responseType: 'blob' },
);
triggerBlobDownload(
response.data instanceof Blob ? response.data : new Blob([response.data]),
`gap_report_${programName.replace(/\s+/g, '_')}.${format}`,
);
};
export const exportGapReportFromFile = async (
file: File,
programName: string,
format: 'pdf' | 'docx',
) => {
const formData = new FormData();
formData.append('file', file);
formData.append('program_name', programName);
formData.append('export_format', format);
const response = await apiClient.post('/api/audit/gap-report/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
responseType: 'blob',
});
triggerBlobDownload(
response.data instanceof Blob ? response.data : new Blob([response.data]),
`gap_report_${programName.replace(/\s+/g, '_')}.${format}`,
);
};
export const uploadReverseAuditDocument = async (file: File, programName?: string) => {
const formData = new FormData();
formData.append('file', file);
if (programName) formData.append('program_name', programName);
const { data } = await apiClient.post(`/api/audit/reverse-audit`, formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
return data;
};
export const getProjectAudit = async (projectId: string) => {
const { data } = await apiClient.get(`/api/projects/${projectId}`);
return data.final_document_audit_result;
};
export const runProjectAudit = async (projectId: string) => {
const { data } = await apiClient.post(`/api/projects/${projectId}/global-audit`);
return data;
};
export const getHolisticReview = async (projectId: string) => {
const { data } = await apiClient.get(`/api/projects/${projectId}/holistic-review`);
return data;
};
export const runHolisticReview = async (projectId: string) => {
const { data } = await apiClient.post(`/api/projects/${projectId}/holistic-review`);
return data;
};
// --- GRANTS / NABORY ENDPOINTS ---
export interface NaboryFilters {
q?: string;
status?: string;
source?: string;
operator?: string;
verification_level?: string;
region?: string;
company_size?: string;
pkd?: string;
beneficiary?: string;
trusted_only?: boolean;
/** Default true on API — only current grants with verified links/sources */
require_certainty?: boolean;
hybrid?: boolean;
limit?: number;
}
export const getGrantNabory = async (forceRefresh = false, filters: NaboryFilters = {}) => {
const { require_certainty, ...rest } = filters;
const { data } = await apiClient.get('/api/grants/nabory', {
params: {
force_refresh: forceRefresh,
require_certainty: require_certainty ?? true,
...rest,
},
});
return data as {
status: string;
count: number;
nabory: any[];
catalog_stats?: {
total: number;
active: number;
planned: number;
unique_sources: number;
with_regulation_url?: number;
top_sources?: { source: string; count: number }[];
};
search_meta?: {
require_certainty?: boolean;
quality?: {
returned?: number;
with_regulation_url?: number;
avg_certainty?: number;
min_certainty?: number;
};
};
};
};
export const getGrantNaborySources = async () => {
const { data } = await apiClient.get('/api/grants/nabory/sources');
return data as {
status: string;
sources: { source: string; source_key?: string; count: number }[];
sources_by_key?: { source_key: string; source: string; count: number; labels?: string[] }[];
};
};
/** Komplet dokumentów + wymagania programu (dossier doradcy). */
export const getGrantDossier = async (grantId: string, refresh = false) => {
const { data } = await apiClient.get(`/api/grants/nabory/${encodeURIComponent(grantId)}/dossier`, {
params: { refresh },
});
return data as {
ok?: boolean;
name?: string;
official_page_url?: string;
precise_regulation_url?: string;
regulation_documents?: Array<{ url: string; title?: string; role?: string }>;
document_count?: number;
readiness?: {
level?: 'blind' | 'partial' | 'ready';
message?: string;
can_advise?: boolean;
is_blind?: boolean;
};
advisor_brief?: {
key_rules?: string[];
required_sections?: string[];
required_attachments?: string[];
attention_points?: string[];
};
firm_data_needed?: string[];
from_cache?: boolean;
};
};
export const ensureGrantDossier = async (grantId: string) => {
const { data } = await apiClient.post(`/api/grants/nabory/${encodeURIComponent(grantId)}/dossier/ensure`);
return data;
};
/** PLAN_ECOSYSTEM §10/§13 reportable metrics (instrument-stats.plan_12m). */
export interface Plan12mMetricsSnapshot {
version?: number;
plan_ref?: string;
targets?: Record<string, { d90?: number | null; d365?: number | null; direction?: string }>;
measured?: {
gold_family_count?: number;
gold_meets_dod_min_12?: boolean;
pct_projects_with_instrument_schema?: number | null;
pct_ready_to_generate?: number | null;
pct_structure_only?: number | null;
pct_instrument_mismatch?: number | null;
instrument_mismatch_count?: number | null;
pct_firms_with_eligibility_snapshot?: number | null;
pct_firms_with_de_minimis_source?: number | null;
pct_firms_msp_confidence_ge_0_7?: number | null;
pct_active_catalog_not_blind?: number | null;
meets_dod_70_not_blind?: boolean | null;
dossier_readiness?: Record<string, unknown> | null;
};
d90_target_hits?: Record<string, boolean | null>;
d365_target_hits?: Record<string, boolean | null>;
section_13_status?: Record<string, string>;
dod_all_pass?: boolean;
dod_product_pass?: boolean;
residual?: string[];
cascade?: Record<string, unknown>;
/** Always false until multi-month production SLAs are proven. */
sla_claim_allowed?: boolean;
note?: string;
}
/** Ops: instrument-first metrics (families, readiness, gold, plan_12m). */
export const getInstrumentStats = async (limit = 2000) => {
const { data } = await apiClient.get('/api/grants/nabory/instrument-stats', {
params: { limit },
});
return data as {
status?: string;
gold?: { families?: string[]; count?: number; meets_dod_min_12?: boolean };
projects?: Record<string, unknown>;
plan_12m?: Plan12mMetricsSnapshot;
};
};
/** F2: catalog dossier ready/partial/blind distribution. */
export const getDossierStats = async () => {
const { data } = await apiClient.get('/api/grants/nabory/dossier-stats');
return data as {
status?: string;
sample_size?: number;
pct_not_blind?: number | null;
meets_dod_70_not_blind?: boolean;
ready?: number;
partial?: number;
blind?: number;
levels?: Record<string, number>;
};
};
/** Instrument-first: pre-gen readiness + missing fields for HITL. */
export const getGenerationReadiness = async (projectId: string) => {
const { data } = await apiClient.get(`/api/projects/${projectId}/generation-readiness`);
return data as {
readiness?: {
status?: string;
ready_to_generate?: boolean;
full_autopilot_allowed?: boolean;
missing_fields?: Array<{ field: string; label: string; category: string; hint?: string }>;
warnings?: string[];
instrument_family?: string;
};
instrument_schema?: Record<string, unknown>;
consent_required?: boolean;
grounding_mode?: string;
};
};
export const postProjectFacts = async (
projectId: string,
facts: Record<string, unknown>,
) => {
const { data } = await apiClient.post(`/api/projects/${projectId}/project-facts`, { facts });
return data;
};
/** F1: explicit consent for structure-without-regulation generation. */
export const postGenerationConsent = async (
projectId: string,
body: { mode?: string; ack: boolean } = { ack: true, mode: 'structure_without_regulation' },
) => {
const { data } = await apiClient.post(`/api/projects/${projectId}/generation-consent`, {
mode: body.mode || 'structure_without_regulation',
ack: body.ack,
});
return data as {
status: string;
grounding_mode?: string;
dossier_readiness?: string;
consent_required?: boolean;
generation_allowed?: boolean;
generation_consent?: Record<string, unknown>;
};
};
export const deleteGenerationConsent = async (projectId: string) => {
const { data } = await apiClient.delete(`/api/projects/${projectId}/generation-consent`);
return data;
};
export const getGrantDossierStats = async () => {
const { data } = await apiClient.get('/api/grants/nabory/dossier-stats');
return data as {
status: string;
ready?: number;
partial?: number;
blind?: number;
pct_ready?: number;
pct_blind?: number;
pct_primary_pdf?: number;
avg_pack_size?: number;
flags?: Record<string, string>;
};
};
export interface UserAnswer {
question: string;
answer: string;
}
export const matchGrantsForProject = async (projectId: string, userAnswers: UserAnswer[] = []) => {
const { data } = await apiClient.post('/api/grants/match', {
project_id: projectId,
user_answers: userAnswers
});
return data as { status: string; project_id: string; needs_more_info: boolean; clarifying_questions: string[]; matches: any[]; detail?: string; fallback_mode?: string };
};
// --- HEALTH ---
export const getApiHealth = async () => {
const { data } = await apiClient.get('/api/health');
return data;
};
// --- COMPANY PROFILE ENDPOINTS ---
export interface CompanyProfileData {
nip: string;
name?: string | null;
pkd_codes: string[];
company_size?: string | null;
location?: string | null;
description?: string | null;
is_verified?: boolean;
}
export const getCompanyProfile = async (): Promise<CompanyProfileData> => {
const { data } = await apiClient.get('/api/company/profile');
return data;
};
export const updateCompanyProfile = async (profileData: Partial<CompanyProfileData>): Promise<CompanyProfileData> => {
const { data } = await apiClient.post('/api/company/profile', profileData);
return data;
};
// --- ELIGIBILITY SPINE (MŚP + de minimis 300k EUR) ---
export interface EligibilityMsp {
status: string;
confidence?: number;
source?: string;
message?: string;
is_sme?: boolean;
is_large?: boolean;
tree_summary?: unknown[];
}
export interface EligibilityDeMinimis {
used_eur: number | null;
limit_eur: number;
remaining_eur: number | null;
exhausted: boolean;
source: string;
configured?: boolean;
message?: string;
checked_at?: string;
}
export interface EligibilityReport {
nip?: string;
krs?: string;
msp: EligibilityMsp;
de_minimis: EligibilityDeMinimis;
eligible_instrument_kinds?: string[];
blockers: string[];
spine_enabled?: boolean;
ready_for_full_autopilot?: boolean;
version?: number;
}
export interface EligibilityResponse {
status: string;
eligibility: EligibilityReport;
persisted?: boolean;
persist_warning?: string;
}
export interface EligibilityRefreshPayload {
nip: string;
krs?: string;
company_data?: Record<string, unknown>;
fetch_sudop?: boolean;
de_minimis_manual_eur?: number | null;
msp_status?: string;
}
export const getCompanyEligibility = async (
nip: string,
krs?: string,
): Promise<EligibilityResponse> => {
const { data } = await apiClient.get('/api/company/eligibility', {
params: { nip, ...(krs ? { krs } : {}) },
});
return data as EligibilityResponse;
};
export const refreshCompanyEligibility = async (
payload: EligibilityRefreshPayload,
): Promise<EligibilityResponse> => {
const { data } = await apiClient.post('/api/company/eligibility/refresh', {
fetch_sudop: true,
...payload,
});
return data as EligibilityResponse;
};
// --- STRATEGY CASCADE 2026 ---
/** Canonical path ids from backend core/strategy/recommend.py */
export type StrategyPathId =
| 'tax_psi_ulgi'
| 'regional_fst'
| 'national_feng'
| 'debt_loan_guarantee'
| 'direct_eu_prep';
export interface StrategyPath {
id: StrategyPathId | string;
label: string;
score: number;
reasons: string[];
primary?: boolean;
de_minimis_blocked?: boolean;
/** Tax path only — ids from core/strategy/tax_paths.py */
checklist_id?: string | null;
checklist_ids?: string[];
/** PSI guardrail from tax_paths (true only on tax_psi_ulgi) */
never_say_submit_to_parp?: boolean;
/** Full tax sub-path checklists when attached (psi / ulga_br / ip_box) */
tax_paths?: Array<{
id: string;
label: string;
checklist?: string[];
advisor_notes?: string[];
never_say_submit_to_parp?: boolean;
}>;
}
export interface StrategyRecommendResult {
version?: number;
paths: StrategyPath[];
recommended_paths: StrategyPath[];
primary_path_id: StrategyPathId | string;
cascade_order: Array<StrategyPathId | string>;
/** Mirrors ENABLE_STRATEGY_CASCADE on backend (default true). */
cascade_enforced?: boolean;
eligibility_summary?: {
msp?: string;
de_minimis_exhausted?: boolean;
de_minimis_used_eur?: number | null;
};
notes?: string[];
}
export interface StrategyRecommendResponse {
status: string;
strategy: StrategyRecommendResult;
eligibility: EligibilityReport;
}
export interface StrategyRecommendParams {
nip?: string;
goal?: string;
region?: string;
}
export interface StrategyRecommendBody {
nip?: string | null;
goal?: string;
region?: string;
company_data?: Record<string, unknown> | null;
eligibility?: EligibilityReport | Record<string, unknown> | null;
}
/** GET /api/company/strategy/recommend — cascade from profile-backed eligibility. */
export const getStrategyRecommend = async (
params: StrategyRecommendParams = {},
): Promise<StrategyRecommendResponse> => {
const { data } = await apiClient.get('/api/company/strategy/recommend', {
params: {
...(params.nip ? { nip: params.nip } : {}),
...(params.goal != null && params.goal !== '' ? { goal: params.goal } : {}),
...(params.region != null && params.region !== '' ? { region: params.region } : {}),
},
});
return data as StrategyRecommendResponse;
};
/** POST /api/company/strategy/recommend — cascade with optional body eligibility/company_data. */
export const postStrategyRecommend = async (
body: StrategyRecommendBody,
): Promise<StrategyRecommendResponse> => {
const { data } = await apiClient.post('/api/company/strategy/recommend', body);
return data as StrategyRecommendResponse;
};
// --- TAX PATHS (F3) + BK2021 STUB (F8) ---
export interface TaxPathChecklist {
id: string;
label: string;
checklist: string[];
advisor_notes?: string[];
never_say_submit_to_parp?: boolean;
}
/** GET /api/tax/paths — plan-canonical tax checklists (PSI / B+R / IP Box). */
export const getTaxPaths = async (pathId?: string): Promise<{
status: string;
paths: TaxPathChecklist[];
checklist_ids?: string[];
parent_strategy_path_id?: string;
path?: TaxPathChecklist;
}> => {
const { data } = await apiClient.get('/api/tax/paths', {
params: pathId ? { path_id: pathId } : undefined,
});
return data;
};
/** GET /api/company/tax-paths — compact tax surface with enabled flag. */
export const getCompanyTaxPaths = async (): Promise<{
status: string;
enabled: boolean;
tax_paths: TaxPathChecklist[];
}> => {
const { data } = await apiClient.get('/api/company/tax-paths');
return data;
};
/** GET /api/company/bk2021/status — F8 stub (default flag off, no live BK). */
export const getBk2021Status = async (): Promise<{
status: string;
bk2021: Record<string, unknown>;
bk2021_stub_available?: boolean;
sample_alert?: Record<string, unknown>;
}> => {
const { data } = await apiClient.get('/api/company/bk2021/status');
return data;
};
// --- REGIONAL CATALOG (F4.4) ---
export interface RegionalProgram {
id: string;
name: string;
program: string;
operator: string;
voivodeship: string;
type?: string;
status?: string;
url?: string;
topics?: string[];
source?: string;
}
export interface RegionalProgramsResponse {
status: string;
count: number;
programs: RegionalProgram[];
voivodeships: string[];
bip_live_count?: number;
bip_parser_voivodeships?: string[];
}
export const getRegionalPrograms = async (voivodeship?: string) => {
const { data } = await apiClient.get('/api/grants/regional/programs', {
params: voivodeship ? { voivodeship } : {},
});
return data as RegionalProgramsResponse;
};
export const createPipelineEntry = async (projectId: string, entry: Record<string, unknown>) => {
const { data } = await apiClient.post(`/api/projects/${projectId}/pipeline`, entry);
return data;
};
// --- POST-AWARD ---
export interface ProjectMilestone {
id: string;
project_id: string;
title: string;
description?: string | null;
due_date: string;
status: string;
completed_at?: string | null;
}
export const getPostAwardMilestones = async (projectId: string) => {
const { data } = await apiClient.get(`/api/projects/${projectId}/post-award/milestones`);
return data as { milestones: ProjectMilestone[]; count: number };
};
export const createPostAwardMilestone = async (
projectId: string,
payload: { title: string; due_date: string; description?: string },
) => {
const { data } = await apiClient.post(`/api/projects/${projectId}/post-award/milestones`, payload);
return data as ProjectMilestone;
};
export const updatePostAwardMilestone = async (
projectId: string,
milestoneId: string,
payload: Partial<{ title: string; due_date: string; description: string; status: string }>,
) => {
const { data } = await apiClient.patch(
`/api/projects/${projectId}/post-award/milestones/${milestoneId}`,
payload,
);
return data as ProjectMilestone;
};
export const deletePostAwardMilestone = async (projectId: string, milestoneId: string) => {
const { data } = await apiClient.delete(
`/api/projects/${projectId}/post-award/milestones/${milestoneId}`,
);
return data;
};
// --- NOTIFICATIONS ---
export const getNotificationInbox = async (userId?: string, unreadOnly = false) => {
const { data } = await apiClient.get('/api/notifications/inbox', {
params: { user_id: userId, unread_only: unreadOnly },
});
return data;
};
export const markNotificationRead = async (notificationId: string) => {
const { data } = await apiClient.post(`/api/notifications/${notificationId}/read`);
return data;
};
// --- DEVELOPER API v2 ---
export const createDeveloperApiKey = async (label: string) => {
const { data } = await apiClient.post('/api/v2/developer/keys', { label });
return data as { api_key: string; key: { id: string; label: string; key_prefix: string } };
};
export const listDeveloperApiKeys = async () => {
const { data } = await apiClient.get('/api/v2/developer/keys');
return data as { keys: Array<{ id: string; label: string; key_prefix: string; created_at: string }> };
};
export const revokeDeveloperApiKey = async (keyId: string) => {
const { data } = await apiClient.delete(`/api/v2/developer/keys/${keyId}`);
return data;
};