Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .node-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22
19 changes: 13 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ questions with source citations that link back to the original PDF/document.

## Setup

1. Copy `.env.example` to `.env.local` and fill in Supabase and OpenAI values.
2. Confirm the Supabase target:
1. Use Node.js 22.x. CI runs on Node 22, and `.nvmrc` / `.node-version`
pin the same runtime for local version managers.
2. Copy `.env.example` to `.env.local` and fill in Supabase and OpenAI values.
3. Confirm the Supabase target:

```bash
npm run check:supabase-project
Expand All @@ -26,15 +28,15 @@ Do not use the older unused Supabase project `Database`
(`qjgitjyhxrwxsrydablr`). Local checks and runtime guards warn or fall back to
demo mode if that stale ref appears in `.env.local`.

3. Run `supabase/schema.sql` in the `Clinical KB Database` Supabase project SQL
4. Run `supabase/schema.sql` in the `Clinical KB Database` Supabase project SQL
editor.
4. Install optional PDF/OCR worker dependencies:
5. Install optional PDF/OCR worker dependencies:

```bash
python -m pip install -r worker/python/requirements.txt
```

5. Start the app:
6. Start the app:

```bash
npm run dev
Expand All @@ -57,7 +59,7 @@ belongs to this project, and starts the dev server in the background if needed.
When you say `run` in this chat, Codex should use this command and return the
printed URL.

6. In a second terminal, start the local ingestion worker:
7. In a second terminal, start the local ingestion worker:

```bash
npm run worker
Expand Down Expand Up @@ -141,3 +143,8 @@ Run `npm run samples` to generate synthetic documents under
a scanned-style PDF for OCR fallback testing. Upload those files through the UI
and start `npm run worker` to index them. The sample content is deliberately
synthetic and must not be used as clinical guidance.

`sample-documents/` is generated local test output and is intentionally ignored
by Git. The smaller `public/demo-documents/` set is tracked because the app uses
it for demo-mode source and image rendering when live Supabase setup is
unavailable.
6 changes: 6 additions & 0 deletions docs/project-alignment-cleanup.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
- Added transitive overrides for `postcss`, `tmp`, and `uuid` to remove audit findings from Next and ExcelJS dependency paths while preserving the existing spreadsheet import/export code.
- Verified ExcelJS still writes and reads an XLSX buffer with the `uuid` override in place.

## Runtime policy

- CI verifies the project on Node.js 22, so local development should also use Node.js 22.x.
- `.nvmrc`, `.node-version`, and `package.json` `engines` all declare the Node 22 runtime expectation.
- New cleanup or dependency work should be verified on Node 22 before release, even when local shells happen to use newer Node versions.

## Stale branch audit

- `temporary` and `codex/spark` contain the same four unique commits covering upload workflow notes, Supabase project checks, and database cleanup hardening.
Expand Down
4 changes: 4 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
"name": "prompt-for-codex-medical-knowledge-base",
"version": "0.1.0",
"private": true,
"engines": {
"node": "22.x",
"npm": ">=10"
},
"scripts": {
"dev": "node scripts/dev-free-port.mjs",
"ensure": "node scripts/ensure-local-server.mjs",
Expand Down
1 change: 0 additions & 1 deletion public/file.svg

This file was deleted.

1 change: 0 additions & 1 deletion public/globe.svg

This file was deleted.

1 change: 0 additions & 1 deletion public/next.svg

This file was deleted.

1 change: 0 additions & 1 deletion public/vercel.svg

This file was deleted.

1 change: 0 additions & 1 deletion public/window.svg

This file was deleted.

170 changes: 14 additions & 156 deletions src/components/ClinicalDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,20 @@ import {
import { AUTH_EMAIL_STORAGE_KEY, useAuthSession } from "@/lib/supabase/client";
import { nextTheme, resolveThemePreference, type ResolvedTheme } from "@/lib/theme";
import { SafeBoldText } from "@/components/SafeBoldText";
import {
answerPayloadIsUsable,
isRetryableError,
isRetryableMessage,
isRetryableStatus,
keywordQueryFromNaturalLanguage,
makeSearchError,
progressForRetry,
searchRetryCount,
searchRetryDelaysMs,
sleep,
type AnswerPayload,
type SearchError,
} from "@/components/clinical-dashboard/search-utils";
import {
parseAnswerDisplayContent,
type AnswerDisplayLine,
Expand Down Expand Up @@ -196,7 +210,6 @@ type BatchesPayload = {
pollAfterMs?: number | null;
};

type AnswerPayload = RagAnswer & { demoMode?: boolean };
type SearchResultModePayload =
| {
kind: "documents";
Expand All @@ -212,161 +225,6 @@ type SearchResultModePayload =
payload: AnswerPayload;
};

type SearchError = Error & {
status?: number;
retryable?: boolean;
};

const searchRetryDelaysMs = [500, 1000, 2000] as const;
const searchRetryCount = 2;
const keywordStopWords = new Set([
"a",
"about",
"all",
"an",
"and",
"are",
"as",
"at",
"be",
"before",
"both",
"by",
"can",
"could",
"did",
"do",
"does",
"for",
"from",
"get",
"had",
"has",
"have",
"her",
"his",
"how",
"if",
"in",
"is",
"it",
"its",
"into",
"me",
"may",
"more",
"my",
"no",
"not",
"of",
"on",
"or",
"our",
"out",
"should",
"so",
"such",
"that",
"the",
"their",
"them",
"there",
"these",
"they",
"this",
"those",
"to",
"when",
"where",
"which",
"who",
"why",
"with",
"would",
"you",
]);

function makeSearchError(message: string, status?: number, retryable = false): SearchError {
const error = new Error(message) as SearchError;
error.status = status;
error.retryable = retryable;
return error;
}

function isRetryableStatus(status: number) {
return status === 408 || status === 429 || (status >= 500 && status <= 599);
}

function isRetryableMessage(message: string) {
const normalized = message.toLowerCase();
return (
normalized.includes("failed to fetch") ||
normalized.includes("network") ||
normalized.includes("timeout") ||
normalized.includes("timed out") ||
normalized.includes("rate limit") ||
normalized.includes("rate-limited") ||
normalized.includes("temporar") ||
normalized.includes("overload") ||
normalized.includes("retry") ||
normalized.includes("unavailable") ||
normalized.includes("upstream") ||
normalized.includes("service is currently")
);
}

function isRetryableError(error: unknown) {
if (!(error instanceof Error)) return false;

const searchError = error as SearchError;
if (searchError.name === "TypeError") return true;
if (searchError.retryable !== undefined) return searchError.retryable;
if (searchError.status !== undefined) return isRetryableStatus(searchError.status);
return isRetryableMessage(searchError.message);
}

function sleep(ms: number) {
return new Promise((resolve) => window.setTimeout(resolve, ms));
}

function keywordQueryFromNaturalLanguage(query: string) {
const normalized = query
.normalize("NFKD")
.toLowerCase()
.replace(/[^\w\s]+/g, " ")
.replace(/_/g, " ")
.trim();
const tokens = normalized.split(/\s+/).filter((token) => token.length >= 3 && !keywordStopWords.has(token));
const terms: string[] = [];
const seen = new Set<string>();

for (const token of tokens) {
if (seen.has(token)) continue;
seen.add(token);
terms.push(token);
}

return terms.slice(0, 7).join(" ");
}

function answerPayloadIsUsable(payload: AnswerPayload) {
const answerText = payload.answer.trim();
if (!answerText) return false;
if (payload.confidence === "unsupported") {
const hasGapContext = Boolean(
payload.relevance || payload.smartPanel?.relevance || payload.sources?.length || payload.relatedDocuments?.length,
);
return hasGapContext;
}

return true;
}

function progressForRetry(attempt: number) {
if (attempt <= 1) return "Retrying...";
return `Retrying... (${Math.min(attempt, searchRetryCount)}/${searchRetryCount})`;
}

async function readLocalProjectIdentity() {
const response = await fetch("/api/local-project-id", { cache: "no-store" });
if (!response.ok) return null;
Expand Down
Loading