Soften the tone of the docs and comments written today

This commit is contained in:
2026-08-11 10:12:25 +07:00
parent 97cb6d16f4
commit 6b8f7584ed
18 changed files with 921 additions and 64 deletions
+33 -2
View File
@@ -1,4 +1,35 @@
# web
Next.js frontend. Chat UI, auth pages, citation/disclaimer rendering, session
history. Talks only to `api-gateway` — never calls internal services directly.
Next.js frontend: chat UI, citation/evidence panel, drug autocomplete,
persistent clinical disclaimer banner.
**Current call path: browser → this app's own route handlers → `ai-service`.**
`api-gateway` is not built yet (its directory holds a `README.md` and a
`package.json`), so requests go through `app/api/` and on to `ai-service`
(`API_GATEWAY_URL ?? AI_SERVICE_URL`, set to `http://ai-service:8000` in
production). There is no authentication layer in this path today. When the
gateway and auth services are built, this is the seam that changes.
`app/api/chat/route.ts` is more than a proxy: it maps `ai-service`'s abstain
`reason` codes to the Vietnamese text the user reads. A reason code with no
entry there falls back to wording that reads as "no data in the formulary",
which would misrepresent an outage or a timeout — so a new backend reason
code needs an entry here in the same change.
Timeouts: `_components/ChatPanel.tsx` aborts a request at 65s, derived from
the backend's ceiling (a 40s request budget that can overrun by one in-flight
model call). A lower value can cut off answers the server has successfully
produced, so keep it above that ceiling if it is ever revisited.
## Running
```powershell
pnpm install
pnpm --filter web dev # http://localhost:3000, expects ai-service on :8079
```
## Tests
There are none, and no test runner is configured. Verify changes by driving
the real UI in a browser; `pytest` passing in `apps/ai-service` says nothing
about this app.
+45 -4
View File
@@ -29,6 +29,28 @@ interface ChatPanelProps {
className?: string;
}
// The client must never be the thing that gives up first.
//
// The backend's own per-request budget is 40s (`max_wall_clock_ms` in
// `apps/ai-service/config.py`), and that budget is only checked *between*
// model calls — `rag/budget.py` says so explicitly: it cannot cancel a boto3
// call already in flight, which is separately bounded by `read_timeout=20`
// in `adapters/bedrock_converse.py`. So the backend's real worst case is
// ~40s + one in-flight call ≈ 60s, and anything below that truncates work
// the server was still legitimately doing.
//
// Measured against production 2026-08-11 (n=8, sequential, one user):
// 6.2 / 6.4 / 8.4 / 10.9 / 12.4 / 21.7 / 25.1 / 40.3 seconds. A 25s limit
// cuts off 2 of those 8, including the 25.1s case, which had returned a
// correct grounded answer with 2 citations.
const REQUEST_TIMEOUT_MS = 65_000;
// A single spinner for a minute reads as a hang, so the wait is made
// legible rather than merely longer. This is a stopgap for the real fix
// (streaming verified claims as they land); it does not make the request
// faster, it only stops it looking broken.
const SLOW_REQUEST_NOTICE_MS = 15_000;
const STARTER_QUESTIONS = [
{
category: "Chỉ Định",
@@ -65,6 +87,7 @@ export function ChatPanel({
}: ChatPanelProps) {
const { resolvedTheme } = useTheme();
const [isLoading, setIsLoading] = useState(false);
const [elapsedMs, setElapsedMs] = useState(0);
const [error, setError] = useState<string | null>(null);
const messagesEndRef = useRef<HTMLDivElement>(null);
const abortControllerRef = useRef<AbortController | null>(null);
@@ -98,7 +121,12 @@ export function ChatPanel({
abortControllerRef.current = new AbortController();
const timeoutId = window.setTimeout(() => {
abortControllerRef.current?.abort();
}, 25_000);
}, REQUEST_TIMEOUT_MS);
setElapsedMs(0);
const startedAt = Date.now();
const tickId = window.setInterval(() => {
setElapsedMs(Date.now() - startedAt);
}, 1000);
try {
const res = await fetch("/api/chat", {
@@ -128,13 +156,19 @@ export function ChatPanel({
setError(
stopRequestedRef.current
? "Đã dừng chờ trên giao diện. Tác vụ đang chạy có thể cần vài giây để kết thúc an toàn."
: "Yêu cầu vượt quá 25 giây và đã được dừng. Vui lòng thử lại với câu hỏi cụ thể hơn."
// Describes the timing cause rather than suggesting the
// question needs rewording, which rephrasing would not fix.
: `Hệ thống xử lý quá ${Math.round(
REQUEST_TIMEOUT_MS / 1000
)} giây nên đã dừng yêu cầu này. Vui lòng bấm gửi lại.`
);
return;
}
setError("Không thể kết nối đến máy chủ AI Service. Vui lòng kiểm tra lại dịch vụ backend.");
} finally {
window.clearTimeout(timeoutId);
window.clearInterval(tickId);
setElapsedMs(0);
setIsLoading(false);
abortControllerRef.current = null;
}
@@ -363,8 +397,15 @@ export function ChatPanel({
<Pill className="h-4 w-4 animate-spin" />
</div>
<div>
<p className="text-xs font-bold text-txt-primary">Đang truy xuất Dược thư QGVN 2018...</p>
<p className="text-[0.68rem] text-txt-muted">Đang phân tích chuyên luận & xác thực Entailment</p>
<p className="text-xs font-bold text-txt-primary">
Đang truy xuất Dược thư QGVN 2018...
{elapsedMs >= 1000 && ` ${Math.floor(elapsedMs / 1000)}s`}
</p>
<p className="text-[0.68rem] text-txt-muted">
{elapsedMs >= SLOW_REQUEST_NOTICE_MS
? "Câu hỏi tra cả mục nên cần thêm thời gian đối chiếu nguồn — vẫn đang xử lý."
: "Đang phân tích chuyên luận & xác thực Entailment"}
</p>
</div>
</div>
)}
+1 -1
View File
@@ -186,7 +186,7 @@ export function Composer({
value={value}
onChange={(e) => setValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Nhập tên thuốc hoặc thuộc tính cần tra (Ví dụ: Liều dùng Paracetamol, Chống chỉ định Amoxicillin...)"
placeholder="Nhập câu hỏi của bạn..."
rows={2}
className="w-full resize-none bg-transparent px-3 py-2 text-sm text-txt-primary placeholder:text-txt-muted focus:outline-none"
/>