docs: add JSDoc comments to API functions
This commit is contained in:
@@ -1,14 +1,21 @@
|
|||||||
const BASE = "/api";
|
const BASE = "/api";
|
||||||
|
|
||||||
// Safe fetch: always reads body as text first, then parses JSON.
|
/**
|
||||||
// If the server returns HTML (e.g. 404 from a stale process or proxy miss),
|
* Safely fetches JSON from the server, handling edge cases where the server
|
||||||
// this surfaces a clear error instead of "Unexpected token '<'".
|
* may return HTML/plain text (e.g., proxy errors, stale process 404s).
|
||||||
|
* @param {string} url - The endpoint URL.
|
||||||
|
* @param {RequestInit} [options={}] - Fetch options.
|
||||||
|
* @returns {Promise<Object>} Parsed JSON response.
|
||||||
|
* @throws {Error} On network failure or non-OK responses.
|
||||||
|
*/
|
||||||
async function safeFetch(url, options = {}) {
|
async function safeFetch(url, options = {}) {
|
||||||
let res;
|
let res;
|
||||||
try {
|
try {
|
||||||
res = await fetch(url, options);
|
res = await fetch(url, options);
|
||||||
} catch (networkErr) {
|
} catch (networkErr) {
|
||||||
throw new Error(`Network error — is the server running? (${networkErr.message})`);
|
const err = new Error(`Network error — is the server running? (${networkErr.message})`);
|
||||||
|
err.cause = networkErr;
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
@@ -33,23 +40,52 @@ async function safeFetch(url, options = {}) {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a paginated list of standards with optional filtering.
|
||||||
|
* @param {Object} [params={}] - Query parameters.
|
||||||
|
* @param {string} [params.q=""] - Search query.
|
||||||
|
* @param {string} [params.category=""] - Category filter.
|
||||||
|
* @param {number} [params.page=1] - Page number.
|
||||||
|
* @param {number} [params.limit=18] - Results per page.
|
||||||
|
* @returns {{data: Object[], meta: {total: number, page: number, limit: number, totalPages: number}}}
|
||||||
|
*/
|
||||||
export async function fetchStandards({ q = "", category = "", page = 1, limit = 18 } = {}) {
|
export async function fetchStandards({ q = "", category = "", page = 1, limit = 18 } = {}) {
|
||||||
const params = new URLSearchParams({ q, category, page, limit });
|
const params = new URLSearchParams({ q, category, page, limit });
|
||||||
return safeFetch(`${BASE}/standards?${params}`);
|
return safeFetch(`${BASE}/standards?${params}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a single standard by its IS identifier.
|
||||||
|
* @param {string} id - The standard IS ID (e.g., "IS 269").
|
||||||
|
* @returns {Promise<Object>} The standard object.
|
||||||
|
*/
|
||||||
export async function fetchStandard(id) {
|
export async function fetchStandard(id) {
|
||||||
return safeFetch(`${BASE}/standards/${encodeURIComponent(id)}`);
|
return safeFetch(`${BASE}/standards/${encodeURIComponent(id)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches all material categories.
|
||||||
|
* @returns {Promise<Array<{name: string, count: number}>>}
|
||||||
|
*/
|
||||||
export async function fetchCategories() {
|
export async function fetchCategories() {
|
||||||
return safeFetch(`${BASE}/categories`);
|
return safeFetch(`${BASE}/categories`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches portal statistics.
|
||||||
|
* @returns {{totalStandards: number, totalCategories: number, totalChunks: number}}
|
||||||
|
*/
|
||||||
export async function fetchStats() {
|
export async function fetchStats() {
|
||||||
return safeFetch(`${BASE}/stats`);
|
return safeFetch(`${BASE}/stats`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asks a conversational question about a specific standard.
|
||||||
|
* @param {Object} params - Parameters.
|
||||||
|
* @param {string} params.standard_id - The standard IS ID.
|
||||||
|
* @param {string} params.question - The question.
|
||||||
|
* @returns {{answer: string}}
|
||||||
|
*/
|
||||||
export async function askQuestion({ standard_id, question }) {
|
export async function askQuestion({ standard_id, question }) {
|
||||||
return safeFetch(`${BASE}/chat`, {
|
return safeFetch(`${BASE}/chat`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -58,7 +94,15 @@ export async function askQuestion({ standard_id, question }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /api/recommend — hybrid retrieval + LLM explanations
|
/**
|
||||||
|
* Hybrid retrieval with AI explanations.
|
||||||
|
* Uses FAISS + BM25 for retrieval, then Groq LLM for explanations.
|
||||||
|
* @param {Object} [params={}] - Query parameters.
|
||||||
|
* @param {string} params.query - Natural language query.
|
||||||
|
* @param {number} [params.top_n=5] - Number of results.
|
||||||
|
* @param {boolean} [params.rewrite=false] - Whether to rewrite the query with AI.
|
||||||
|
* @returns {{query: string, standards: Object[], latency: {retrieval_ms: number, llm_ms: number, total_ms: number}}}
|
||||||
|
*/
|
||||||
export async function recommend({ query, top_n = 5, rewrite = false } = {}) {
|
export async function recommend({ query, top_n = 5, rewrite = false } = {}) {
|
||||||
return safeFetch(`${BASE}/recommend`, {
|
return safeFetch(`${BASE}/recommend`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -67,7 +111,13 @@ export async function recommend({ query, top_n = 5, rewrite = false } = {}) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /api/ask — chunk-grounded QA for a specific standard
|
/**
|
||||||
|
* Chunk-grounded QA for a specific standard.
|
||||||
|
* @param {Object} [params={}] - Parameters.
|
||||||
|
* @param {string} params.standard_id - The standard IS ID.
|
||||||
|
* @param {string} params.question - The question.
|
||||||
|
* @returns {{answer: string, source: {standard_id: string, section: string, chunk_id: string}, latency: {llm_ms: number, total_ms: number}}}
|
||||||
|
*/
|
||||||
export async function askGrounded({ standard_id, question } = {}) {
|
export async function askGrounded({ standard_id, question } = {}) {
|
||||||
return safeFetch(`${BASE}/ask`, {
|
return safeFetch(`${BASE}/ask`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import { useEffect, useState, useCallback } from "react";
|
import { useEffect, useState, useCallback } from "react";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
import { fetchStandards, fetchCategories } from "../api/standards";
|
import { fetchStandards, fetchCategories } from "../api/standards";
|
||||||
import { useDebounce } from "../hooks/useDebounce";
|
|
||||||
import StandardCard from "../components/StandardCard";
|
import StandardCard from "../components/StandardCard";
|
||||||
import StandardModal from "../components/StandardModal";
|
import StandardModal from "../components/StandardModal";
|
||||||
import "./Standards.css";
|
import "./Standards.css";
|
||||||
|
|
||||||
const PAGE_SIZE = 18;
|
const PAGE_SIZE = 18;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standards search page with pagination.
|
||||||
|
* Uses URL search params for query/category state.
|
||||||
|
* Loads standards on mount and handles user interactions.
|
||||||
|
*/
|
||||||
export default function Standards() {
|
export default function Standards() {
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
@@ -22,12 +26,6 @@ export default function Standards() {
|
|||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [selected, setSelected] = useState(null);
|
const [selected, setSelected] = useState(null);
|
||||||
|
|
||||||
const debouncedQuery = useDebounce(query, 300);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchCategories().then(setCategories).catch(() => {});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const load = useCallback(async (q, cat, pg) => {
|
const load = useCallback(async (q, cat, pg) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -35,6 +33,7 @@ export default function Standards() {
|
|||||||
const data = await fetchStandards({ q, category: cat, page: pg, limit: PAGE_SIZE });
|
const data = await fetchStandards({ q, category: cat, page: pg, limit: PAGE_SIZE });
|
||||||
setResults(data.data);
|
setResults(data.data);
|
||||||
setMeta(data.meta);
|
setMeta(data.meta);
|
||||||
|
setPage(pg);
|
||||||
} catch {
|
} catch {
|
||||||
setError("Could not load standards. Is the server running?");
|
setError("Could not load standards. Is the server running?");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -43,20 +42,34 @@ export default function Standards() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPage(1);
|
fetchCategories().then(setCategories).catch(() => {});
|
||||||
|
load(query, category, 1);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
const params = {};
|
const params = {};
|
||||||
if (debouncedQuery) params.q = debouncedQuery;
|
if (query) params.q = query;
|
||||||
if (category) params.category = category;
|
if (category) params.category = category;
|
||||||
setSearchParams(params, { replace: true });
|
setSearchParams(params, { replace: true });
|
||||||
load(debouncedQuery, category, 1);
|
}, [query, category, setSearchParams]);
|
||||||
}, [debouncedQuery, category, load, setSearchParams]);
|
|
||||||
|
const handleCategoryChange = (value) => {
|
||||||
|
setCategory(value);
|
||||||
|
load(query, value, 1);
|
||||||
|
};
|
||||||
|
|
||||||
const handlePageChange = (pg) => {
|
const handlePageChange = (pg) => {
|
||||||
setPage(pg);
|
load(query, category, pg);
|
||||||
load(debouncedQuery, category, pg);
|
|
||||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleClearSearch = () => {
|
||||||
|
setQuery("");
|
||||||
|
setCategory("");
|
||||||
|
load("", "", 1);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="standards-page">
|
<main className="standards-page">
|
||||||
<section className="tile tile-parchment search-tile" aria-labelledby="search-heading">
|
<section className="tile tile-parchment search-tile" aria-labelledby="search-heading">
|
||||||
@@ -64,11 +77,7 @@ export default function Standards() {
|
|||||||
<h1 className="display-lg" id="search-heading">Find an IS Standard</h1>
|
<h1 className="display-lg" id="search-heading">Find an IS Standard</h1>
|
||||||
<p className="lead-sub">Search by standard number, title, material, or keyword.</p>
|
<p className="lead-sub">Search by standard number, title, material, or keyword.</p>
|
||||||
|
|
||||||
<form
|
<form className="search-form" role="search" onSubmit={(e) => e.preventDefault()}>
|
||||||
className="search-form"
|
|
||||||
role="search"
|
|
||||||
onSubmit={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
<div className="search-wrap">
|
<div className="search-wrap">
|
||||||
<SearchIcon />
|
<SearchIcon />
|
||||||
<input
|
<input
|
||||||
@@ -83,7 +92,7 @@ export default function Standards() {
|
|||||||
<button
|
<button
|
||||||
className="search-clear"
|
className="search-clear"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setQuery("")}
|
onClick={handleClearSearch}
|
||||||
aria-label="Clear search"
|
aria-label="Clear search"
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
@@ -94,7 +103,7 @@ export default function Standards() {
|
|||||||
<select
|
<select
|
||||||
className="category-filter"
|
className="category-filter"
|
||||||
value={category}
|
value={category}
|
||||||
onChange={(e) => setCategory(e.target.value)}
|
onChange={(e) => handleCategoryChange(e.target.value)}
|
||||||
aria-label="Filter by category"
|
aria-label="Filter by category"
|
||||||
>
|
>
|
||||||
<option value="">All Categories</option>
|
<option value="">All Categories</option>
|
||||||
@@ -213,4 +222,4 @@ function SearchIcon() {
|
|||||||
<path d="M14 14l4 4" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
|
<path d="M14 14l4 4" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user