Soften the tone of the docs and comments written today
This commit is contained in:
+33
-2
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user