Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
| 2026-08-15 | codex/calculators-mode | 1e3d9278fbddda7d36d12e5645429b33d2a374ce | Calculators first-class mode | Resolved the current-base documentation conflict; prior P2 local-search and options-menu corrections remain present; no further confirmed P0-P2 findings. | git diff --check; ledger inbox/outstanding-issues/branch-ledger/discipline guards; calculator static contracts; focused Vitest attempted but unavailable because the isolated worktree has no node_modules |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
| 2026-08-15 | codex/calculators-mode | 1938a6bc5f1845c67c85584ded78b3be1c8a961a | Calculators first-class mode | Merged the latest required base after the final freshness check; calculator local-search and action-menu P2 corrections remain intact. | git diff --check; ledger inbox/outstanding-issues/branch-ledger/discipline guards; reconciled search-chrome contract assertion; focused Vitest remains unavailable because this isolated worktree has no node_modules |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
| 2026-08-15 | codex/calculators-mode | bbf2207102a0b40b5ba048e8e9a04f37add9bf2c | required base sync through main 3824095 | Approved — required main update merged after calculator command follow-ups; no PR-path conflict | git diff --check; ci-change-scope self-test; ledger/inbox/outstanding/discipline guards passed |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
| 2026-08-15 | codex/calculators-mode | 3b896be3f5f6ea67df10c991257aeb05b41af619 | Fix calculator regression tests from exact-head CI and merge main d301d8f4 | fixed | git diff --check; static ledger guards; focused tests blocked without node_modules |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
| 2026-08-15 | codex/calculators-mode | 1ec9eb1028bf29ac633857846fe69c91249a64c7 | calculator suggestion regression test accessibility and formatting follow-up | Fixed the focused calculator suggestion test to match the full accessible option label; corrected formatter-required nested ternary indentation. Targeted Vitest unavailable because this isolated worktree has no node_modules/vitest. | git diff --check; ledger-inbox; outstanding-issues; branch-review-ledger; ledger-write-discipline; ci-change-scope --self-test |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
| 2026-08-14 | codex/calculators-mode | 8e7c9d65463ab37bc6e6cd658ec8091b4260db18 | calculators first-class mode | Clean after resolving local-only typeahead and legacy URL normalization | 20/20 focused unit/DOM; typecheck; lint; prior verify:ui 433/433; targeted browser request-interception queued then not run due coordinator contention |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
| 2026-08-15 | codex/calculators-mode | d8fde21e65ce010b72bc56f1e57334cdfac764c5 | required base sync through main 17402395 | Approved — required main update merged; prior calculator-mode review remains applicable with no PR-path conflict | git diff --check; ci-change-scope self-test; ledger/inbox/outstanding/discipline guards passed |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
| 2026-08-15 | codex/calculators-mode | ff3dfa4e32e63ccf288438a6371608b42847c18e | calculator search-chrome documentation formatter follow-up | Aligned the search-chrome ownership table to the new longest mode label after the changed-file formatter reported Markdown table drift. | git diff --check; ledger-inbox; outstanding-issues; branch-review-ledger; ledger-write-discipline; ci-change-scope --self-test |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
| 2026-08-15 | codex/calculators-mode | 39423d88bced6494ad2eed30f43fb859ab6abefb | Calculators mode: final current-base merge | Merged latest required base after prior focused review; no conflicts or new confirmed P0-P2 findings | git diff --check; ledger-inbox; outstanding-issues; branch-review-ledger; ledger-write-discipline; calculator registration assertion |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
| 2026-08-14 | codex/calculators-mode | 563ce4195512b9e623df6ba98762f4a3dc1b9e8e | calculators first-class mode | P1: calculator composer invokes universal-search API despite local-only boundary | source diff review; local verification evidence inspected; verify:pr-local dry-run; provider checks not run |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
| 2026-08-15 | codex/calculators-mode | 9dc3891ee8bc30a35fdb203607cdd984d3a24cc3 | calculator command-surface P1/P2 follow-up: suggestion submit and footer ownership docs | Fixed P1/P2 — selected calculator suggestions pass their exact text into navigation; chrome ownership docs no longer describe calculators as page-owned footers | manual control-flow review; focused DOM regression added; git diff --check; ci-change-scope self-test; ledger/inbox/outstanding/discipline guards passed; focused Vitest blocked: node_modules/vitest absent |
30 changes: 15 additions & 15 deletions docs/search-chrome-behaviour.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/site-map.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,6 +59,7 @@ This file is generated by `npm run docs:update` (or `npm run sitemap:update` dir
- `/formulation` - Formulation mode. Search kind: `formulation`. Query example: `/formulation?q=I+keep+going+over+it&focus=1&run=1`.
- `/medications` - Medication mode. Search kind: `documents`. Query example: `/?mode=prescribing&q=acamprosate+renal+dose&focus=1&run=1`.
- `/tools` - Tools mode. Search kind: `tools`. Query example: `/tools?q=medications&focus=1&run=1`.
- `/calculators` - Calculators mode. Search kind: `calculators`. Query example: `/calculators?q=PHQ-9&focus=1&run=1`.
- `/therapy-compass` - Therapy mode. Search kind: `tools`. Query example: `/therapy-compass?q=behavioural+activation&focus=1&run=1`.
- `/factsheets` - Factsheets mode. Search kind: `tools`. Query example: `/factsheets/search?q=sertraline&focus=1&run=1`.

Expand Down
1 change: 1 addition & 0 deletions scripts/generate-site-map.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -277,6 +277,7 @@ function renderModeRoutes() {
formulation: appModeHomeHref("formulation", { query: "I keep going over it", focus: true, run: true }),
prescribing: appModeHomeHref("prescribing", { query: "acamprosate renal dose", focus: true, run: true }),
tools: appModeHomeHref("tools", { query: "medications", focus: true, run: true }),
calculators: appModeHomeHref("calculators", { query: "PHQ-9", focus: true, run: true }),
"therapy-compass": appModeHomeHref("therapy-compass", { query: "behavioural activation", focus: true, run: true }),
factsheets: appModeHomeHref("factsheets", { query: "sertraline", focus: true, run: true }),
};
Expand Down
39 changes: 36 additions & 3 deletions src/app/(search-app)/calculators/page.tsx
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,45 @@
import type { Metadata } from "next";
import { redirect } from "next/navigation";

import { CalculatorsSearchPage } from "@/components/calculators";
import { CalculatorsHomePage, CalculatorsSearchPage } from "@/components/calculators";

export const metadata: Metadata = {
title: "Calculators - Clinical KB",
description: "Psychiatry clinical decision calculators and rating scales with source-cited scoring guidance.",
};

export default function CalculatorsRoute() {
return <CalculatorsSearchPage />;
type CalculatorsSearchParams = Promise<{ [key: string]: string | string[] | undefined }>;

function readFirstSearchParam(value: string | string[] | undefined) {
return Array.isArray(value) ? value[0] : value;
}

function toURLSearchParams(params: Awaited<CalculatorsSearchParams>) {
const normalized = new URLSearchParams();
for (const [key, value] of Object.entries(params)) {
if (Array.isArray(value)) value.forEach((item) => normalized.append(key, item));
else if (value !== undefined) normalized.set(key, value);
}
return normalized;
}

export default async function CalculatorsRoute({ searchParams }: { searchParams: CalculatorsSearchParams }) {
const resolvedSearchParams = await searchParams;
const hasSubmittedSearch = readFirstSearchParam(resolvedSearchParams.run) === "1";
const primaryQuery = readFirstSearchParam(resolvedSearchParams.q)?.trim();
const legacyQuery = readFirstSearchParam(resolvedSearchParams.query)?.trim();
const query = primaryQuery || legacyQuery;

if (resolvedSearchParams.query !== undefined) {
const canonicalSearchParams = toURLSearchParams(resolvedSearchParams);
if (query) canonicalSearchParams.set("q", query);
else canonicalSearchParams.delete("q");
canonicalSearchParams.delete("query");
const suffix = canonicalSearchParams.toString();
redirect(suffix ? `/calculators?${suffix}` : "/calculators");
}

if (!hasSubmittedSearch || !query) return <CalculatorsHomePage />;

return <CalculatorsSearchPage initialQuery={query} />;
}
94 changes: 94 additions & 0 deletions src/components/calculators/calculator-filters.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
import { domainLabels, type CalculatorDomain, type CalculatorFixture } from "./calculator-fixtures";
import type { DerivedCalculator } from "./calculator-ui";

export type CalculatorProgressFilter = "all" | "not-started" | "in-progress" | "completed";
export type CalculatorTimeFilter = "all" | "quick" | "standard" | "extended";

export type CalculatorFilterState = {
domains: ReadonlySet<CalculatorDomain>;
progress: CalculatorProgressFilter;
time: CalculatorTimeFilter;
};

export type CalculatorFilterRecord = {
calc: CalculatorFixture;
derived: DerivedCalculator;
};

export function normalizeCalculatorQuery(query: string) {
return query.trim().toLowerCase();
}

export function calculatorMatchesQuery(calc: CalculatorFixture, query: string) {
const normalized = normalizeCalculatorQuery(query);
if (!normalized) return true;
const haystack = [calc.abbrev, calc.name, calc.indication, calc.summary, domainLabels[calc.domain]]
.join(" ")
.toLowerCase();
return haystack.includes(normalized) || calc.items.some((item) => item.text.toLowerCase().includes(normalized));
}

export function calculatorMatchesProgress(derived: DerivedCalculator, progress: CalculatorProgressFilter) {
if (progress === "all") return true;
if (progress === "not-started") return !derived.started;
if (progress === "completed") return derived.complete;
return derived.started && !derived.complete;
}

export function calculatorMatchesTime(calc: CalculatorFixture, time: CalculatorTimeFilter) {
if (time === "all") return true;
if (time === "quick") return calc.timeEstimateMinutes.max <= 2;
if (time === "standard") return calc.timeEstimateMinutes.max >= 3 && calc.timeEstimateMinutes.max <= 4;
return calc.timeEstimateMinutes.max >= 5;
}

export function calculatorMatchesFilters(
record: CalculatorFilterRecord,
query: string,
filters: CalculatorFilterState,
) {
return (
calculatorMatchesQuery(record.calc, query) &&
(filters.domains.size === 0 || filters.domains.has(record.calc.domain)) &&
calculatorMatchesProgress(record.derived, filters.progress) &&
calculatorMatchesTime(record.calc, filters.time)
);
}

export function filterCalculatorRecords(
records: readonly CalculatorFilterRecord[],
query: string,
filters: CalculatorFilterState,
) {
return records.filter((record) => calculatorMatchesFilters(record, query, filters));
}

export function calculatorDomainCandidateCount(
records: readonly CalculatorFilterRecord[],
query: string,
filters: CalculatorFilterState,
candidate: CalculatorDomain,
) {
const domains = new Set(filters.domains);
if (domains.has(candidate)) domains.delete(candidate);
else domains.add(candidate);
return filterCalculatorRecords(records, query, { ...filters, domains }).length;
}

export function calculatorProgressCandidateCount(
records: readonly CalculatorFilterRecord[],
query: string,
filters: CalculatorFilterState,
candidate: CalculatorProgressFilter,
) {
return filterCalculatorRecords(records, query, { ...filters, progress: candidate }).length;
}

export function calculatorTimeCandidateCount(
records: readonly CalculatorFilterRecord[],
query: string,
filters: CalculatorFilterState,
candidate: CalculatorTimeFilter,
) {
return filterCalculatorRecords(records, query, { ...filters, time: candidate }).length;
}
10 changes: 10 additions & 0 deletions src/components/calculators/calculator-fixtures.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,6 +61,8 @@ export type CalculatorFixture = {
/** Question stem shown above the items. */
stem?: string;
timeEstimate: string;
/** Numeric range used by the local completion-time filter. */
timeEstimateMinutes: { min: number; max: number };
minScore: number;
maxScore: number;
items: CalculatorItem[];
Expand DownExpand Up@@ -121,6 +123,7 @@ export const calculators: CalculatorFixture[] = [
summary: "9-item depression severity score with treatment-action bands.",
stem: "Over the last 2 weeks, how often have you been bothered by:",
timeEstimate: "2–3 min",
timeEstimateMinutes: { min: 2, max: 3 },
minScore: 0,
maxScore: 27,
scoringNote: "Sum of 9 items (0–3 each). Severity bands map to stepped treatment actions.",
Expand DownExpand Up@@ -199,6 +202,7 @@ export const calculators: CalculatorFixture[] = [
summary: "7-item anxiety severity score; also performs well for panic and social anxiety.",
stem: "Over the last 2 weeks, how often have you been bothered by:",
timeEstimate: "1–2 min",
timeEstimateMinutes: { min: 1, max: 2 },
minScore: 0,
maxScore: 21,
scoringNote: "Sum of 7 items (0–3 each). ≥10 is the usual cut-point for probable GAD.",
Expand DownExpand Up@@ -242,6 +246,7 @@ export const calculators: CalculatorFixture[] = [
summary: "10-item distress measure scored 10–50, widely used across Australian primary care.",
stem: "In the past 4 weeks, about how often did you feel:",
timeEstimate: "2–3 min",
timeEstimateMinutes: { min: 2, max: 3 },
minScore: 10,
maxScore: 50,
scoringNote: "Sum of 10 items (1–5 each); range 10–50. Higher scores indicate greater distress.",
Expand DownExpand Up@@ -293,6 +298,7 @@ export const calculators: CalculatorFixture[] = [
summary: "13 lifetime hypomanic symptoms plus co-occurrence and impairment criteria.",
stem: "Has there ever been a period of time when you were not your usual self and you…",
timeEstimate: "3–4 min",
timeEstimateMinutes: { min: 3, max: 4 },
minScore: 0,
maxScore: 13,
scoringNote:
Expand DownExpand Up@@ -399,6 +405,7 @@ export const calculators: CalculatorFixture[] = [
indication: "Rapid 4-question lifetime screen for problem drinking in adults.",
summary: "Four yes/no questions; two or more positives is a clinically significant screen.",
timeEstimate: "under 1 min",
timeEstimateMinutes: { min: 0, max: 1 },
minScore: 0,
maxScore: 4,
scoringNote: "1 point per “yes”. ≥2 is a positive screen; follow with AUDIT and drinking history.",
Expand DownExpand Up@@ -440,6 +447,7 @@ export const calculators: CalculatorFixture[] = [
indication: "Brief consumption screen for hazardous drinking — the first three AUDIT items.",
summary: "3 consumption questions scored 0–4 each; sex-specific positive thresholds.",
timeEstimate: "1 min",
timeEstimateMinutes: { min: 1, max: 1 },
minScore: 0,
maxScore: 12,
scoringNote: "Positive screen at ≥3 for women and ≥4 for men. Higher totals track hazard severity.",
Expand DownExpand Up@@ -509,6 +517,7 @@ export const calculators: CalculatorFixture[] = [
indication: "Structure a rapid inventory of static suicide-risk factors as an adjunct to clinical assessment.",
summary: "10 yes/no risk factors, one point each; supports — never replaces — clinical judgement.",
timeEstimate: "1–2 min",
timeEstimateMinutes: { min: 1, max: 2 },
minScore: 0,
maxScore: 10,
scoringNote: "1 point per factor present. Bands are indicative only; act on clinical concern at any score.",
Expand DownExpand Up@@ -566,6 +575,7 @@ export const calculators: CalculatorFixture[] = [
indication: "Grade the severity of obsessive-compulsive symptoms and track treatment response.",
summary: "10 severity items (0–4) across obsessions and compulsions; total 0–40.",
timeEstimate: "5–8 min",
timeEstimateMinutes: { min: 5, max: 8 },
minScore: 0,
maxScore: 40,
scoringNote: "Sum of 10 items. Items 1–5 grade obsessions, 6–10 compulsions; subscales can be reported separately.",
Expand Down
58 changes: 58 additions & 0 deletions src/components/calculators/home-page.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
import { Calculator, CloudRain, GlassWater, Search, Zap } from "lucide-react";

import { ModeHomeMain, ModeHomeTemplate, ModeHomeVerificationFooter } from "@/components/mode-home-template";
import { appModeHomeHref } from "@/lib/app-modes";
import { modeHomeDesktopComposerSlotId } from "@/lib/mode-home-composer";

const calculatorQuickLinks = ["PHQ-9", "GAD-7", "K10", "MDQ", "AUDIT-C"] as const;

function calculatorSearchHref(query: string) {
return appModeHomeHref("calculators", { query, run: true });
}

export function CalculatorsHomePage() {
return (
<ModeHomeMain testId="calculators-home-main" contentAlign="startOnPhone">
<ModeHomeTemplate
testId="calculators-home"
title="Clinical Calculators"
subtitle="Validated psychiatry scores with the indication, items, and next actions in one place."
icon={Calculator}
actionsLabel="Starter calculator searches"
desktopComposerSlotId={modeHomeDesktopComposerSlotId}
actions={[
{
title: "Depression severity",
description: "Find screening and symptom-severity scales.",
icon: CloudRain,
href: calculatorSearchHref("depression"),
},
{
title: "Anxiety & OCD",
description: "Find anxiety and obsessive-compulsive measures.",
icon: Zap,
href: calculatorSearchHref("anxiety"),
},
{
title: "Alcohol use",
description: "Find brief alcohol-use screening tools.",
icon: GlassWater,
href: calculatorSearchHref("alcohol use"),
},
]}
pillsTitle="Open a calculator"
pills={calculatorQuickLinks.map((label) => ({
label,
href: calculatorSearchHref(label),
icon: Search,
}))}
footer={
<ModeHomeVerificationFooter
label="Source-cited scoring"
body="Scores support clinical judgement and never replace a full assessment. Nothing entered here is stored."
/>
}
/>
</ModeHomeMain>
);
}
1 change: 1 addition & 0 deletions src/components/calculators/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,7 @@ export { CalculatorsBedsideSheetMockup } from "./bedside-sheet";
export { CalculatorsClinicalConsoleMockup } from "./clinical-console";
export { CalculatorsDirectoryGridMockup } from "./directory-grid";
export { CalculatorsGuidedFlowMockup } from "./guided-flow";
export { CalculatorsHomePage } from "./home-page";
export { CalculatorsPopupSheetMockup } from "./calculator-sheet";
export { CalculatorsSearchPage } from "./search-page";
export { CalculatorsSearchDetailMockup } from "./search-detail";
Loading
Loading