Dirbol's picture
feat(frontend): v3.0 — project-first 3-step wizard
4308ec9
Raw
History Blame Contribute Delete
11.7 kB
/** Typed API client for the Virtual Foreman backend. */
import { API_URL } from "./config";
export interface TechStep {
order: number;
title: string;
description: string;
duration_hours?: number | null;
required_acts: string[];
dependencies: number[];
weather_sensitive: boolean;
}
export interface TechStack {
source_input: string;
inferred_summary: string;
steps: TechStep[];
required_documents: string[];
missing_info_prompts: string[];
model_used: string;
generated_at: string;
}
export interface InferredWorkResponse {
tech_stack: TechStack;
request_id: string;
}
// ----- Blueprint extraction -----
export interface GridAxis {
label: string;
order: number;
}
export interface BlueprintDimensions {
length_m?: number | null;
width_m?: number | null;
height_m?: number | null;
total_area_m2?: number | null;
}
export interface BlueprintSheet {
sheet_number: string;
title: string;
scale?: string | null;
dimensions?: BlueprintDimensions | null;
room_labels: string[];
grid_axes: GridAxis[];
notes_ru: string[];
}
export interface BlueprintExtraction {
project_hint: string;
sheets: BlueprintSheet[];
inferred_disciplines: string[];
missing_info_prompts: string[];
file_name: string;
file_size_bytes: number;
mime_type: string;
model_used: string;
generated_at: string;
}
export interface BlueprintInferenceResponse {
extraction: BlueprintExtraction;
request_id: string;
}
// ----- Projects -----
export interface ProjectPayload {
contract_no: string;
object_name: string;
client: string;
technical_supervisor: string;
contractor: string;
work_producer: string;
chief_engineer: string;
start_date?: string;
end_date_estimated?: string;
}
export interface Project extends ProjectPayload {
id: string;
created_at: string;
persisted?: boolean;
warning?: string;
}
// ----- Inference history -----
export interface InferenceHistoryRow {
id: string;
project_id: string | null;
request_id: string;
source_text: string | null;
payload: unknown;
model_used: string | null;
created_at: string;
}
// ----- Document templates -----
export interface DocumentContext {
[key: string]: unknown;
}
async function postJSON<T>(path: string, body: unknown): Promise<T> {
const res = await fetch(`${API_URL}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) {
const detail = await res.text();
throw new Error(`API ${path} failed: ${res.status} ${detail}`);
}
return (await res.json()) as T;
}
async function getJSON<T>(path: string): Promise<T> {
const res = await fetch(`${API_URL}${path}`, {
method: "GET",
headers: { Accept: "application/json" },
});
if (!res.ok) {
const detail = await res.text();
throw new Error(`API ${path} failed: ${res.status} ${detail}`);
}
return (await res.json()) as T;
}
async function delJSON<T>(path: string): Promise<T> {
const res = await fetch(`${API_URL}${path}`, {
method: "DELETE",
headers: { Accept: "application/json" },
});
if (!res.ok) {
const detail = await res.text();
throw new Error(`API ${path} failed: ${res.status} ${detail}`);
}
return (await res.json()) as T;
}
async function postForm<T>(
path: string,
form: FormData,
opts: { signal?: AbortSignal } = {}
): Promise<T> {
const res = await fetch(`${API_URL}${path}`, {
method: "POST",
body: form,
signal: opts.signal,
});
if (!res.ok) {
const detail = await res.text();
throw new Error(`API ${path} failed: ${res.status} ${detail}`);
}
return (await res.json()) as T;
}
export async function inferWork(payload: {
raw_text: string;
date_from: string;
date_to: string;
axes?: string;
project_id?: string | null;
}): Promise<InferredWorkResponse> {
return postJSON<InferredWorkResponse>("/work/infer", payload);
}
export async function inferBlueprint(
file: File,
project_id?: string | null,
opts: { signal?: AbortSignal } = {}
): Promise<BlueprintInferenceResponse> {
const form = new FormData();
form.append("file", file);
if (project_id) form.append("project_id", project_id);
return postForm<BlueprintInferenceResponse>(
"/work/blueprint",
form,
opts
);
}
export async function createProject(payload: ProjectPayload): Promise<Project> {
return postJSON<Project>("/projects", payload);
}
export async function listProjects(): Promise<Project[]> {
return getJSON<Project[]>("/projects");
}
export async function getProject(project_id: string): Promise<Project> {
return getJSON<Project>(`/projects/${project_id}`);
}
export async function deleteProject(project_id: string): Promise<{ deleted: boolean; id: string }> {
return delJSON<{ deleted: boolean; id: string }>(`/projects/${project_id}`);
}
export async function listInferenceHistory(project_id?: string): Promise<InferenceHistoryRow[]> {
const q = project_id ? `?project_id=${encodeURIComponent(project_id)}` : "";
return getJSON<InferenceHistoryRow[]>(`/inference/history${q}`);
}
/** POST /documents/render — get back a .docx file as Blob. */
export async function renderDocument(
template_name: string,
project_id: string,
context: DocumentContext = {}
): Promise<Blob> {
const res = await fetch(`${API_URL}/documents/render`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ template_name, project_id, context }),
});
if (!res.ok) {
const detail = await res.text();
throw new Error(`API /documents/render failed: ${res.status} ${detail}`);
}
return res.blob();
}
export async function listDocumentTemplates(): Promise<{ supported: string[] }> {
return getJSON<{ supported: string[] }>("/documents/templates");
}
// ----- Project documentation (D-light) -----
export interface PageExcerpt {
page_no: number;
stamp: string | null;
title_guess: string | null;
char_count: number;
text_preview: string;
}
export interface UploadBlueprintStatus {
upload_id: string | null;
file_name: string;
mime_type: string;
page_count: number;
has_native_text: boolean;
stored_in_supabase: boolean;
preview_cached: boolean;
pages: PageExcerpt[];
}
export interface SheetIndexEntry {
page_no: number;
stamp: string | null;
title_guess: string | null;
}
export interface ProjectSummaryTotals {
sheet_count: number;
text_pages: number;
sheets_with_stamp: number;
}
export interface ProjectSummary {
object_name: string;
object_address: string | null;
project_section: string | null;
totals: ProjectSummaryTotals;
sheets_index: SheetIndexEntry[];
spec_summary: string;
missing_info_prompts: string[];
}
export interface ProjectSummaryResponse {
upload_id: string;
model_used: string;
page_count: number;
generated_at: string;
summary: ProjectSummary;
}
export interface PagePreview {
upload_id: string;
page_no: number;
mime: string;
width: number;
height: number;
data_b64: string;
}
/** Phase-1 — ingest a blueprint PDF (or image) and persist all pages. */
export async function uploadBlueprint(
file: File,
project_id?: string | null,
opts: { signal?: AbortSignal } = {}
): Promise<UploadBlueprintStatus> {
const form = new FormData();
form.append("file", file);
if (project_id) form.append("project_id", project_id);
return postForm<UploadBlueprintStatus>(
"/uploads/blueprint",
form,
opts
);
}
/** Read the ingest status + per-page record list. */
export async function getBlueprintStatus(
upload_id: string
): Promise<UploadBlueprintStatus> {
return getJSON<UploadBlueprintStatus>(
`/uploads/blueprint/${upload_id}`
);
}
/** Render page_no of the cached PDF to a PNG (base64). */
export async function getPagePreview(
upload_id: string,
page_no: number
): Promise<PagePreview> {
return getJSON<PagePreview>(
`/uploads/blueprint/${upload_id}/pages/${page_no}/preview`
);
}
/** Phase-2 text-only — run the LLM over all extracted pages. */
export async function extractProjectText(
upload_id: string
): Promise<ProjectSummaryResponse> {
return postJSON<ProjectSummaryResponse>(
`/uploads/blueprint/${upload_id}/extract-text`,
{}
);
}
/** Phase-2 vision — VLM only on the selected pages. */
export async function extractProjectSheets(
upload_id: string,
pages: number[]
): Promise<ProjectSummaryResponse> {
return postJSON<ProjectSummaryResponse>(
`/uploads/blueprint/${upload_id}/extract-sheets`,
{ pages }
);
}
// ===== v2.0 — Project-aware blueprint aggregation =====
export interface UploadRow {
upload_id: string;
file_name: string;
mime_type: string;
sha256: string;
page_count: number;
has_native_text: boolean;
status: string;
created_at: string | null;
}
export interface UploadsListResponse {
project_id: string;
uploads_count: number;
uploads: UploadRow[];
generated_at: string;
}
export interface AggregatedPage {
upload_id: string;
upload_file_name: string;
page_no: number;
char_count: number;
stamp: string | null;
title_guess: string | null;
is_empty: boolean;
section_guess: string | null;
text_preview: string;
}
export interface UploadSummary {
upload_id: string;
file_name: string;
mime_type: string;
sha256: string;
page_count: number;
total_chars: number;
section_floor: string | null;
sections: string[];
}
export interface DokumentatsiyaResponse {
project_id: string;
aggregate_sha256: string;
total_uploads: number;
total_pages: number;
total_chars: number;
overall_project_section: string | null;
upload_summaries: UploadSummary[];
pages: AggregatedPage[];
cached_summary: ProjectSummary | null;
cached_summary_model: string | null;
generated_at: string;
}
export interface ExtractAllResponse {
upload_id: string; // field reuse — UI sees project_id here
model_used: string;
page_count: number;
generated_at: string;
summary: ProjectSummary;
}
export interface SheetExtractPair {
upload_id: string | null;
page_no: number;
}
/** GET /projects/{id}/uploads. */
export async function listProjectUploads(project_id: string): Promise<UploadsListResponse> {
return getJSON<UploadsListResponse>(`/projects/${project_id}/uploads`);
}
/** GET /projects/{id}/dokumentatsiya. */
export async function projectDokumentatsiya(project_id: string): Promise<DokumentatsiyaResponse> {
return getJSON<DokumentatsiyaResponse>(`/projects/${project_id}/dokumentatsiya`);
}
/** POST /projects/{id}/extract-all. */
export async function extractProjectAll(
project_id: string,
opts: { force?: boolean; max_chars?: number } = {}
): Promise<ExtractAllResponse> {
const qs = new URLSearchParams();
if (opts.force) qs.set("force", "true");
if (typeof opts.max_chars === "number") qs.set("max_chars", String(opts.max_chars));
const path = `/projects/${project_id}/extract-all${qs.toString() ? `?${qs}` : ""}`;
return postJSON<ExtractAllResponse>(path, {});
}
/** POST /projects/{id}/extract-sheets. */
export async function extractProjectSheetsV2(
project_id: string,
pairs: SheetExtractPair[]
): Promise<ExtractAllResponse> {
return postJSON<ExtractAllResponse>(
`/projects/${project_id}/extract-sheets`,
{ pages: pairs }
);
}
/** POST /uploads/blueprint (project-scoped upload). */
export async function uploadBlueprintV2(
file: File,
project_id: string,
opts: { signal?: AbortSignal } = {}
): Promise<UploadBlueprintStatus> {
const form = new FormData();
form.append("file", file);
form.append("project_id", project_id);
return postForm<UploadBlueprintStatus>("/uploads/blueprint", form, opts);
}