Spaces:
Running
Running
| 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; | |
| }; | |