feat: add web client frontend with monorepo config.

This commit is contained in:
K
2026-04-28 23:56:23 +05:30
parent 3a0c32ea8f
commit a5cf7bbfda
37 changed files with 5505 additions and 0 deletions
+52
View File
@@ -0,0 +1,52 @@
.footer {
background: var(--parchment);
border-top: 1px solid var(--divider);
padding: 56px 0 28px;
}
.footer-inner {
max-width: 1100px;
margin: 0 auto;
padding: 0 40px;
}
.footer-cols {
display: grid;
grid-template-columns: 1.5fr 1fr 1fr 1fr;
gap: 40px;
margin-bottom: 40px;
}
.footer-brand {
font-family: var(--font-display);
font-size: 17px;
font-weight: 600;
color: var(--ink);
margin-bottom: 6px;
}
.footer-tagline { font-size: 13px; color: var(--ink-48); line-height: 1.5; }
.footer-heading { font-size: 13px; font-weight: 600; color: var(--ink-80); margin-bottom: 10px; }
.footer-link {
display: block;
font-size: 13px;
color: var(--ink-48);
text-decoration: none;
line-height: 2.2;
transition: color .12s;
}
.footer-link:hover { color: var(--accent); }
.footer-legal {
border-top: 1px solid var(--divider);
padding-top: 20px;
font-size: 11px;
color: var(--ink-48);
line-height: 1.7;
}
.footer-legal p + p { margin-top: 4px; }
.legal-link { color: var(--ink-48); text-decoration: underline; text-underline-offset: 2px; }
.legal-link:hover { color: var(--accent); }
@media (max-width: 900px) {
.footer-cols { grid-template-columns: 1fr 1fr; gap: 28px; }
}
@media (max-width: 500px) {
.footer-cols { grid-template-columns: 1fr; }
.footer-inner { padding: 0 20px; }
}
+43
View File
@@ -0,0 +1,43 @@
import { Link } from "react-router-dom";
import "./Footer.css";
export default function Footer() {
return (
<footer className="footer" role="contentinfo">
<div className="footer-inner">
<div className="footer-cols">
<div className="footer-col">
<p className="footer-brand">BIS SP21</p>
<p className="footer-tagline">
Handbook on Building Materials<br />
Special Publication 21 : 2005
</p>
</div>
<div className="footer-col">
<p className="footer-heading">Portal</p>
<Link className="footer-link" to="/standards">Search Standards</Link>
<Link className="footer-link" to="/categories">Browse Categories</Link>
<Link className="footer-link" to="/about">About</Link>
</div>
<div className="footer-col">
<p className="footer-heading">Bureau of Indian Standards</p>
<a className="footer-link" href="https://www.bis.gov.in" target="_blank" rel="noopener noreferrer">BIS Official Website</a>
<a className="footer-link" href="https://www.manakonline.in" target="_blank" rel="noopener noreferrer">Manak Online</a>
<a className="footer-link" href="https://standardsbis.bsbedge.com" target="_blank" rel="noopener noreferrer">Standards Portal</a>
</div>
<div className="footer-col">
<p className="footer-heading">Ministry</p>
<a className="footer-link" href="https://dpiit.gov.in" target="_blank" rel="noopener noreferrer">DPIIT</a>
<a className="footer-link" href="https://www.india.gov.in" target="_blank" rel="noopener noreferrer">National Portal</a>
</div>
</div>
<div className="footer-legal">
<p>© Bureau of Indian Standards, Ministry of Commerce & Industry, Government of India. All rights reserved.</p>
<p>Content sourced from BIS Special Publication 21 : 2005. For official standards, refer to{" "}
<a href="https://www.bis.gov.in" target="_blank" rel="noopener noreferrer" className="legal-link">bis.gov.in</a>.
</p>
</div>
</div>
</footer>
);
}
+99
View File
@@ -0,0 +1,99 @@
.global-nav {
position: sticky;
top: 0;
z-index: 200;
background: var(--surface-black);
height: var(--nav-h);
display: flex;
align-items: center;
border-bottom: 1px solid rgba(255,255,255,0.07);
}
.nav-inner {
width: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 0 32px;
display: flex;
align-items: center;
gap: var(--sp-lg);
}
.nav-emblem {
display: flex;
align-items: center;
gap: 10px;
text-decoration: none;
flex-shrink: 0;
}
.emblem-icon { width: 28px; height: 28px; }
.nav-brand {
font-family: var(--font-display);
font-size: 15px;
font-weight: 600;
letter-spacing: 0.2px;
color: var(--on-dark);
white-space: nowrap;
}
.nav-links {
display: flex;
align-items: center;
gap: 4px;
margin-left: auto;
}
.nav-link {
font-family: var(--font-text);
font-size: 12px;
font-weight: 400;
letter-spacing: -0.12px;
color: rgba(255,255,255,0.75);
text-decoration: none;
padding: 6px 10px;
border-radius: var(--r-pill);
transition: color .15s, background .15s;
}
.nav-link:hover { color: #fff; background: rgba(255,255,255,0.08); }
.nav-link.active { color: #fff; }
.nav-hamburger {
display: none;
flex-direction: column;
gap: 5px;
background: none;
border: none;
cursor: pointer;
padding: 6px;
margin-left: auto;
}
.nav-hamburger span {
display: block;
width: 22px;
height: 2px;
background: var(--on-dark);
border-radius: 2px;
}
.mobile-menu {
position: sticky;
top: var(--nav-h);
z-index: 190;
background: var(--tile-dark-3);
display: flex;
flex-direction: column;
border-bottom: 1px solid rgba(255,255,255,0.08);
}
.mobile-link {
display: block;
padding: 14px 32px;
color: rgba(255,255,255,0.85);
text-decoration: none;
font-size: 15px;
border-bottom: 1px solid rgba(255,255,255,0.06);
transition: background .15s;
}
.mobile-link:hover { background: rgba(255,255,255,0.05); }
@media (max-width: 833px) {
.nav-links { display: none; }
.nav-hamburger { display: flex; }
}
@media (max-width: 640px) {
.nav-inner { padding: 0 20px; }
}
+98
View File
@@ -0,0 +1,98 @@
import { useState } from "react";
import { Link, useLocation } from "react-router-dom";
import "./Navbar.css";
const NAV_LINKS = [
{ label: "Standards", to: "/standards" },
{ label: "Categories", to: "/categories" },
{ label: "✦ AI Recommend", to: "/recommend" },
{ label: "About", to: "/about" },
];
export default function Navbar() {
const [open, setOpen] = useState(false);
const { pathname } = useLocation();
return (
<>
<nav className="global-nav" role="navigation" aria-label="Primary navigation">
<div className="nav-inner">
<Link className="nav-emblem" to="/" aria-label="BIS SP-21 home" onClick={() => setOpen(false)}>
<BISIcon />
<span className="nav-brand">BIS SP21</span>
</Link>
<div className="nav-links" role="list">
{NAV_LINKS.map(({ label, to }) => (
<Link
key={to}
className={`nav-link${pathname === to ? " active" : ""}`}
to={to}
role="listitem"
>
{label}
</Link>
))}
<a
className="nav-link"
href="https://www.bis.gov.in"
target="_blank"
rel="noopener noreferrer"
role="listitem"
>
BIS Portal
</a>
</div>
<button
className="nav-hamburger"
aria-label={open ? "Close menu" : "Open menu"}
aria-expanded={open}
aria-controls="mobile-menu"
onClick={() => setOpen((o) => !o)}
>
<span /><span /><span />
</button>
</div>
</nav>
{open && (
<div className="mobile-menu" id="mobile-menu" role="menu">
{NAV_LINKS.map(({ label, to }) => (
<Link
key={to}
className="mobile-link"
to={to}
role="menuitem"
onClick={() => setOpen(false)}
>
{label}
</Link>
))}
<a
className="mobile-link"
href="https://www.bis.gov.in"
target="_blank"
rel="noopener noreferrer"
role="menuitem"
onClick={() => setOpen(false)}
>
BIS Portal
</a>
</div>
)}
</>
);
}
function BISIcon() {
return (
<svg className="emblem-icon" viewBox="0 0 36 36" fill="none" aria-hidden="true">
<circle cx="18" cy="18" r="16" stroke="#FF9933" strokeWidth="2.5" />
<circle cx="18" cy="18" r="6" fill="#FF9933" />
<path d="M18 4v4M18 28v4M4 18h4M28 18h4" stroke="#FF9933" strokeWidth="2" strokeLinecap="round" />
<path d="M8.7 8.7l2.8 2.8M24.5 24.5l2.8 2.8M8.7 27.3l2.8-2.8M24.5 11.5l2.8-2.8"
stroke="#FF9933" strokeWidth="1.5" strokeLinecap="round" />
</svg>
);
}
@@ -0,0 +1,82 @@
.result-card {
background: var(--canvas);
border: 1px solid var(--hairline);
border-radius: var(--r-lg);
padding: var(--sp-lg);
cursor: pointer;
transition: border-color .15s, box-shadow .15s, transform .1s;
text-align: left;
display: flex;
flex-direction: column;
}
.result-card:hover {
border-color: var(--accent);
box-shadow: 0 4px 24px rgba(212,83,10,0.08);
transform: translateY(-1px);
}
.result-card:active { transform: scale(.98); }
.result-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.card-cat {
display: inline-block;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;
color: var(--chip-text);
background: var(--chip-bg);
border-radius: var(--r-pill);
padding: 3px 10px;
margin-bottom: 8px;
width: fit-content;
}
.card-id {
font-size: 12px;
font-weight: 600;
color: var(--ink-48);
margin-bottom: 4px;
letter-spacing: 0;
}
.card-title {
font-family: var(--font-display);
font-size: 16px;
font-weight: 600;
line-height: 1.25;
letter-spacing: -0.2px;
color: var(--ink);
margin-bottom: 8px;
}
.card-summary {
font-size: 13px;
line-height: 1.5;
color: var(--ink-48);
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
flex: 1;
}
.card-keywords {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 10px;
}
.keyword-chip {
font-size: 11px;
color: var(--ink-80);
background: var(--parchment);
border: 1px solid var(--divider);
border-radius: var(--r-pill);
padding: 2px 8px;
}
.card-footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 14px;
padding-top: 12px;
border-top: 1px solid var(--divider);
}
.card-sections-count { font-size: 12px; color: var(--ink-48); }
.card-arrow { color: var(--accent); font-size: 14px; font-weight: 600; }
@@ -0,0 +1,36 @@
import "./StandardCard.css";
export default function StandardCard({ standard, onClick }) {
const sectionCount = Object.keys(standard.key_sections || {}).length;
return (
<article
className="result-card"
onClick={onClick}
onKeyDown={(e) => e.key === "Enter" && onClick()}
tabIndex={0}
role="button"
aria-label={`View details for ${standard.standard_id}`}
>
<span className="card-cat">{standard.category}</span>
<p className="card-id">{standard.standard_id}</p>
<h3 className="card-title">{standard.title}</h3>
{standard.summary && (
<p className="card-summary">{standard.summary}</p>
)}
{standard.keywords?.length > 0 && (
<div className="card-keywords" aria-label="Keywords">
{standard.keywords.slice(0, 5).map((kw) => (
<span className="keyword-chip" key={kw}>{kw}</span>
))}
</div>
)}
<div className="card-footer">
<span className="card-sections-count">
{sectionCount} section{sectionCount !== 1 ? "s" : ""}
</span>
<span className="card-arrow" aria-hidden="true"></span>
</div>
</article>
);
}
+297
View File
@@ -0,0 +1,297 @@
.modal-backdrop {
position: fixed;
inset: 0;
z-index: 400;
background: rgba(13,17,23,0.72);
backdrop-filter: blur(8px);
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
animation: fadeIn .18s ease;
}
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
.modal {
background: var(--canvas);
border-radius: var(--r-lg);
width: 100%;
max-width: 680px;
max-height: 85vh;
overflow-y: auto;
animation: slideUp .2s ease;
box-shadow: 0 20px 60px rgba(0,0,0,0.35);
}
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.modal-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
padding: 28px 28px 20px;
border-bottom: 1px solid var(--divider);
position: sticky;
top: 0;
background: var(--canvas);
z-index: 1;
}
.modal-eyebrow {
display: inline-block;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;
color: var(--chip-text);
background: var(--chip-bg);
border-radius: var(--r-pill);
padding: 3px 10px;
margin-bottom: 8px;
}
.modal-title {
font-family: var(--font-display);
font-size: 22px;
font-weight: 600;
line-height: 1.2;
letter-spacing: -0.3px;
color: var(--ink);
margin-bottom: 4px;
}
.modal-id {
font-size: 13px;
font-weight: 600;
color: var(--ink-48);
}
.modal-close {
flex-shrink: 0;
background: var(--parchment);
border: none;
border-radius: 50%;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--ink-48);
font-size: 14px;
transition: background .15s, color .15s;
}
.modal-close:hover { background: var(--divider); color: var(--ink); }
.modal-body { padding: 24px 28px 32px; }
.modal-section { margin-bottom: 24px; }
.modal-section:last-child { margin-bottom: 0; }
.modal-section-title {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.6px;
text-transform: uppercase;
color: var(--ink-48);
margin-bottom: 8px;
}
.modal-section-body {
font-size: 15px;
line-height: 1.6;
letter-spacing: -0.2px;
color: var(--ink-80);
}
.modal-keywords { display: flex; flex-wrap: wrap; gap: 6px; }
.modal-keyword {
font-size: 12px;
color: var(--ink-80);
background: var(--parchment);
border: 1px solid var(--divider);
border-radius: var(--r-pill);
padding: 4px 12px;
}
.modal-key-sections { display: flex; flex-direction: column; gap: 16px; }
.key-section-item {
background: var(--parchment);
border-radius: var(--r-md);
padding: 16px;
border-left: 3px solid var(--accent);
}
.key-section-name {
font-size: 12px;
font-weight: 600;
letter-spacing: 0.3px;
color: var(--accent);
text-transform: uppercase;
margin-bottom: 6px;
}
.key-section-text {
font-size: 14px;
line-height: 1.6;
color: var(--ink-80);
}
/* ── AI Chat Panel ── */
.ai-panel {
background: linear-gradient(135deg, rgba(212,83,10,0.04) 0%, rgba(26,34,48,0.04) 100%);
border: 1px solid rgba(212,83,10,0.15);
border-radius: var(--r-lg);
padding: 20px;
margin-top: 8px;
}
.ai-label-icon {
color: var(--accent);
margin-right: 6px;
font-size: 13px;
}
/* Suggestion chips */
.chat-suggestions {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 16px;
}
.suggestion-chip {
background: var(--canvas);
border: 1px solid var(--hairline);
border-radius: var(--r-md);
padding: 10px 14px;
font-family: var(--font-text);
font-size: 13px;
color: var(--ink-80);
text-align: left;
cursor: pointer;
transition: border-color .15s, background .15s;
}
.suggestion-chip:hover {
border-color: var(--accent);
background: rgba(212,83,10,0.04);
color: var(--ink);
}
/* Chat log */
.chat-log {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 16px;
max-height: 280px;
overflow-y: auto;
padding-right: 4px;
}
.chat-bubble {
display: flex;
align-items: flex-start;
gap: 10px;
max-width: 92%;
}
.chat-bubble--user {
align-self: flex-end;
flex-direction: row-reverse;
}
.chat-bubble--user .bubble-text {
background: var(--accent);
color: #fff;
border-radius: var(--r-md) var(--r-md) 4px var(--r-md);
}
.chat-bubble--ai .bubble-text {
background: var(--canvas);
border: 1px solid var(--divider);
color: var(--ink-80);
border-radius: var(--r-md) var(--r-md) var(--r-md) 4px;
}
.bubble-text {
font-size: 14px;
line-height: 1.6;
padding: 10px 14px;
margin: 0;
}
.bubble-label {
flex-shrink: 0;
font-size: 14px;
color: var(--accent);
margin-top: 10px;
}
/* Typing dots */
.chat-bubble--loading .bubble-text { display: none; }
.typing-dots {
display: inline-flex;
align-items: center;
gap: 5px;
background: var(--canvas);
border: 1px solid var(--divider);
padding: 12px 16px;
border-radius: var(--r-md) var(--r-md) var(--r-md) 4px;
}
.typing-dots span {
width: 7px;
height: 7px;
background: var(--accent);
border-radius: 50%;
animation: bounce 1.2s infinite ease-in-out;
}
.typing-dots span:nth-child(2) { animation-delay: .2s; }
.typing-dots span:nth-child(3) { animation-delay: .4s; }
@keyframes bounce {
0%, 80%, 100% { transform: scale(0.7); opacity: .5; }
40% { transform: scale(1); opacity: 1; }
}
.chat-error {
font-size: 13px;
color: #b91c1c;
background: #fff5f5;
border: 1px solid #fca5a5;
border-radius: var(--r-sm);
padding: 10px 14px;
}
/* Chat input */
.chat-form {
display: flex;
gap: 8px;
align-items: center;
}
.chat-input {
flex: 1;
font-family: var(--font-text);
font-size: 14px;
color: var(--ink);
background: var(--canvas);
border: 1.5px solid var(--hairline);
border-radius: var(--r-pill);
padding: 10px 16px;
outline: none;
transition: border-color .15s, box-shadow .15s;
}
.chat-input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(212,83,10,0.1);
}
.chat-input:disabled { opacity: 0.6; }
.chat-input::placeholder { color: var(--body-muted); }
.chat-send {
background: var(--accent);
color: #fff;
font-family: var(--font-text);
font-size: 14px;
font-weight: 600;
border: none;
border-radius: var(--r-pill);
padding: 10px 20px;
cursor: pointer;
transition: background .15s, transform .1s;
white-space: nowrap;
flex-shrink: 0;
}
.chat-send:hover:not(:disabled) { background: var(--accent-hover); }
.chat-send:active:not(:disabled) { transform: scale(.95); }
.chat-send:disabled { opacity: 0.4; cursor: not-allowed; }
@media (max-width: 640px) {
.modal-backdrop { align-items: flex-end; padding: 0; }
.modal { border-radius: var(--r-md) var(--r-md) 0 0; max-height: 90vh; }
.chat-form { flex-direction: column; align-items: stretch; }
.chat-send { text-align: center; }
}
+215
View File
@@ -0,0 +1,215 @@
import { useEffect, useRef, useState } from "react";
import { askQuestion } from "../api/standards";
import "./StandardModal.css";
export default function StandardModal({ standard, onClose }) {
const modalRef = useRef(null);
const closeBtnRef = useRef(null);
const inputRef = useRef(null);
const [question, setQuestion] = useState("");
const [messages, setMessages] = useState([]); // [{role, text}]
const [asking, setAsking] = useState(false);
const [aiError, setAiError] = useState(null);
const chatEndRef = useRef(null);
useEffect(() => {
closeBtnRef.current?.focus();
const onKey = (e) => e.key === "Escape" && onClose();
document.addEventListener("keydown", onKey);
document.body.style.overflow = "hidden";
return () => {
document.removeEventListener("keydown", onKey);
document.body.style.overflow = "";
};
}, [onClose]);
useEffect(() => {
chatEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages, asking]);
const handleBackdrop = (e) => {
if (e.target === e.currentTarget) onClose();
};
const handleAsk = async (e) => {
e.preventDefault();
const q = question.trim();
if (!q || asking) return;
setMessages((prev) => [...prev, { role: "user", text: q }]);
setQuestion("");
setAsking(true);
setAiError(null);
try {
const { answer } = await askQuestion({ standard_id: standard.standard_id, question: q });
setMessages((prev) => [...prev, { role: "ai", text: answer }]);
} catch (err) {
setAiError(err.message || "Something went wrong. Please try again.");
} finally {
setAsking(false);
setTimeout(() => inputRef.current?.focus(), 50);
}
};
if (!standard) return null;
const sections = Object.entries(standard.key_sections || {});
return (
<div
className="modal-backdrop"
role="presentation"
onClick={handleBackdrop}
>
<div
className="modal"
ref={modalRef}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
tabIndex={-1}
>
{/* Header */}
<div className="modal-header">
<div>
<span className="modal-eyebrow">{standard.category}</span>
<h2 className="modal-title" id="modal-title">{standard.title}</h2>
<span className="modal-id">{standard.standard_id}</span>
</div>
<button
className="modal-close"
ref={closeBtnRef}
onClick={onClose}
aria-label="Close standard detail"
>
</button>
</div>
{/* Standard detail */}
<div className="modal-body">
{standard.summary && (
<div className="modal-section">
<p className="modal-section-title">Summary</p>
<p className="modal-section-body">{standard.summary}</p>
</div>
)}
{standard.keywords?.length > 0 && (
<div className="modal-section">
<p className="modal-section-title">Keywords</p>
<div className="modal-keywords">
{standard.keywords.map((kw) => (
<span className="modal-keyword" key={kw}>{kw}</span>
))}
</div>
</div>
)}
{sections.length > 0 && (
<div className="modal-section">
<p className="modal-section-title">Key Sections</p>
<div className="modal-key-sections">
{sections.map(([name, text]) => (
<div className="key-section-item" key={name}>
<p className="key-section-name">{name}</p>
<p className="key-section-text">{text}</p>
</div>
))}
</div>
</div>
)}
{/* AI chat panel */}
<div className="modal-section ai-panel" aria-label="Ask AI about this standard">
<p className="modal-section-title">
<span className="ai-label-icon" aria-hidden="true"></span>
Ask AI about this standard
</p>
{messages.length > 0 && (
<div className="chat-log" aria-live="polite" aria-label="Conversation">
{messages.map((m, i) => (
<div key={i} className={`chat-bubble chat-bubble--${m.role}`}>
{m.role === "ai" && (
<span className="bubble-label" aria-label="AI response"></span>
)}
<p className="bubble-text">{m.text}</p>
</div>
))}
{asking && (
<div className="chat-bubble chat-bubble--ai chat-bubble--loading" aria-label="AI is thinking">
<span className="bubble-label" aria-hidden="true"></span>
<span className="typing-dots">
<span /><span /><span />
</span>
</div>
)}
{aiError && (
<p className="chat-error" role="alert">{aiError}</p>
)}
<div ref={chatEndRef} />
</div>
)}
{messages.length === 0 && !asking && (
<div className="chat-suggestions" aria-label="Suggested questions">
{getSuggestions(standard).map((s) => (
<button
key={s}
className="suggestion-chip"
onClick={() => {
setQuestion(s);
setTimeout(() => inputRef.current?.focus(), 50);
}}
>
{s}
</button>
))}
</div>
)}
<form className="chat-form" onSubmit={handleAsk} aria-label="Ask a question">
<input
ref={inputRef}
className="chat-input"
type="text"
value={question}
onChange={(e) => setQuestion(e.target.value)}
placeholder="Ask a question about this standard…"
maxLength={500}
disabled={asking}
aria-label="Your question"
/>
<button
className="chat-send"
type="submit"
disabled={!question.trim() || asking}
aria-label="Send question"
>
{asking ? "…" : "Ask"}
</button>
</form>
</div>
</div>
</div>
</div>
);
}
function getSuggestions(standard) {
const base = [
`What are the key requirements of ${standard.standard_id}?`,
"What materials or tests are specified?",
"What are the delivery or packaging specifications?",
];
if (standard.key_sections?.["Chemical Requirements"]) {
base.splice(1, 0, "Summarise the chemical requirements.");
}
if (standard.key_sections?.["Physical Requirements"] || standard.key_sections?.["Physical Requirement"]) {
base.splice(1, 0, "What are the physical requirements?");
}
return base.slice(0, 3);
}