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
2 changes: 1 addition & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Do not worry about migrations either client side or backend unless specifically

## Libraries

Use Zod for schema and input validation.
Use Zod for schema and input validation (backend).
Use Drizzle ORM for database interactions and migrations.
Use Better-Auth for authentication and user management.
Use Zag.js for UI components and design system.
Expand Down
8 changes: 4 additions & 4 deletions .vscode/mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
"type": "stdio",
"command": "node",
"args": ["${workspaceFolder}/packages/mcp/server.js"]
},
"stripe": {
"type": "http",
"url": "https://mcp.stripe.com"
}
// "stripe": {
// "type": "http",
// "url": "https://mcp.stripe.com"
// }
}
}
2 changes: 1 addition & 1 deletion packages/mcp/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ registerCodeReviewTools(server, repoRoot);
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Corates MCP Server started');
console.log('Corates MCP Server started');
}

main().catch(err => {
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp/tools/lint.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function registerLintTools(server, repoRoot) {
fix: z.boolean().optional().default(false).describe('Whether to run lint with --fix'),
},
async ({ fix = false }) => {
const command = `pnpm run lint${fix ? ' -- --fix' : ''}`;
const command = `pnpm run lint${fix ? ' --fix' : ''}`;

try {
const { stdout, stderr } = await exec(command, {
Expand Down
28 changes: 11 additions & 17 deletions packages/ui/src/zag/Collapsible.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,15 @@ import { createMemo, createUniqueId } from 'solid-js';
* @param {JSX.Element} props.children - The content to show/hide
*/
export default function Collapsible(props) {
const open = () => props.open;
const defaultOpen = () => props.defaultOpen;
const disabled = () => props.disabled;
const trigger = () => props.trigger;
const children = () => props.children;

const service = useMachine(collapsible.machine, () => ({
id: createUniqueId(),
open: open(),
defaultOpen: defaultOpen(),
disabled: disabled(),
get open() {
return props.open;
},
defaultOpen: props.defaultOpen,
get disabled() {
return props.disabled;
},
onOpenChange(details) {
props.onOpenChange?.(details.open);
},
Expand All @@ -33,34 +31,30 @@ export default function Collapsible(props) {

return (
<div {...api().getRootProps()}>
{trigger()?.(api())}
{props.trigger?.(api())}
<div {...api().getContentProps()} class='collapsible-content overflow-hidden'>
{children()}
{props.children}
</div>
<style>{`
.collapsible-content[data-state="open"] {
animation: collapsible-slideDown 200ms ease-out;
animation: collapsible-slideDown 150ms ease-out;
}
.collapsible-content[data-state="closed"] {
animation: collapsible-slideUp 200ms ease-out;
animation: collapsible-slideUp 150ms ease-out;
}
@keyframes collapsible-slideDown {
from {
opacity: 0;
height: 0;
}
to {
opacity: 1;
height: var(--height);
}
}
@keyframes collapsible-slideUp {
from {
opacity: 1;
height: var(--height);
}
to {
opacity: 0;
height: 0;
}
}
Expand Down
70 changes: 69 additions & 1 deletion packages/web/src/components/checklist-ui/AMSTAR2Checklist.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { AMSTAR_CHECKLIST } from '@/AMSTAR2/checklist-map.js';
import { createChecklist as createAMSTAR2Checklist } from '@/AMSTAR2/checklist.js';
import { FaSolidCircleInfo } from 'solid-icons/fa';
import { Tooltip, FloatingPanel } from '@corates/ui';
import NoteEditor from '@checklist-ui/common/NoteEditor.jsx';

export function Question1(props) {
const state = () => props.checklistState().q1;
Expand Down Expand Up @@ -329,6 +330,12 @@ export function Question9(props) {
}, 10);
}

// Get Y.Text for the note (q9 is parent key for q9a/q9b)
const noteYText = () => {
if (!props.getQuestionNote) return null;
return props.getQuestionNote('q9');
};

let containerRef;

return (
Expand All @@ -352,6 +359,9 @@ export function Question9(props) {
columns={question.columns2}
handleChange={handleChangeB}
/>
<Show when={props.getQuestionNote}>
<NoteEditor yText={noteYText()} readOnly={props.readOnly} collapsed={true} />
</Show>
</div>
);
}
Expand Down Expand Up @@ -447,6 +457,12 @@ export function Question11(props) {
}, 10);
}

// Get Y.Text for the note (q11 is parent key for q11a/q11b)
const noteYText = () => {
if (!props.getQuestionNote) return null;
return props.getQuestionNote('q11');
};

let containerRef;

return (
Expand All @@ -473,6 +489,9 @@ export function Question11(props) {
handleChange={handleChangeB}
width='w-48'
/>
<Show when={props.getQuestionNote}>
<NoteEditor yText={noteYText()} readOnly={props.readOnly} collapsed={true} />
</Show>
</div>
);
}
Expand Down Expand Up @@ -646,14 +665,31 @@ export function Question16(props) {
function StandardQuestion(props) {
let containerRef;

// Get the question key from the question text (e.g., "1. Did..." -> "q1")
const questionKey = () => {
const text = props.question?.text || '';
const match = text.match(/^(\d+[a-z]?)\./);
return match ? `q${match[1]}` : null;
};
Comment on lines +668 to +673
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Regex-based questionKey extraction may be fragile.

The regex pattern /^(\d+[a-z]?)\./ extracts the question key from the question text (e.g., "1. Did..." → "q1"). This creates a tight coupling between UI text and data keys. If question text formatting changes, note retrieval will break silently.

Consider:

  1. Passing an explicit questionKey prop to StandardQuestion instead of deriving it
  2. Maintaining a constant mapping of question objects to their keys
  3. Adding validation that the extracted key matches expected AMSTAR2 question keys (q1-q16)


// Get Y.Text for the note if getQuestionNote is available
const noteYText = () => {
const key = questionKey();
if (!key || !props.getQuestionNote) return null;
return props.getQuestionNote(key);
};

return (
<div class='bg-white rounded-lg shadow-md p-7 relative' ref={el => (containerRef = el)}>
<div class='bg-white rounded-lg shadow-md p-7 pb-3 relative' ref={el => (containerRef = el)}>
<QuestionInfo question={props.question} containerRef={containerRef} />
<div class='flex'>
<h3 class='font-semibold text-sm text-gray-900 mb-1'>{props.question.text}</h3>
<CriticalButton state={props.state} onUpdate={props.onUpdate} />
</div>
<StandardQuestionInternal columns={props.question.columns} {...props} />
<Show when={props.getQuestionNote}>
<NoteEditor yText={noteYText()} readOnly={props.readOnly} collapsed={true} />
</Show>
</div>
);
}
Expand Down Expand Up @@ -828,68 +864,100 @@ export default function AMSTAR2Checklist(props = {}) {
<Question1
onUpdate={newQ1 => handleChecklistChange({ q1: newQ1 })}
checklistState={currentChecklist}
getQuestionNote={props.getQuestionNote}
readOnly={props.readOnly}
/>
<Question2
onUpdate={newQ2 => handleChecklistChange({ q2: newQ2 })}
checklistState={currentChecklist}
getQuestionNote={props.getQuestionNote}
readOnly={props.readOnly}
/>
<Question3
onUpdate={newQ3 => handleChecklistChange({ q3: newQ3 })}
checklistState={currentChecklist}
getQuestionNote={props.getQuestionNote}
readOnly={props.readOnly}
/>
<Question4
onUpdate={newQ4 => handleChecklistChange({ q4: newQ4 })}
checklistState={currentChecklist}
getQuestionNote={props.getQuestionNote}
readOnly={props.readOnly}
/>
<Question5
onUpdate={newQ5 => handleChecklistChange({ q5: newQ5 })}
checklistState={currentChecklist}
getQuestionNote={props.getQuestionNote}
readOnly={props.readOnly}
/>
<Question6
onUpdate={newQ6 => handleChecklistChange({ q6: newQ6 })}
checklistState={currentChecklist}
getQuestionNote={props.getQuestionNote}
readOnly={props.readOnly}
/>
<Question7
onUpdate={newQ7 => handleChecklistChange({ q7: newQ7 })}
checklistState={currentChecklist}
getQuestionNote={props.getQuestionNote}
readOnly={props.readOnly}
/>
<Question8
onUpdate={newQ8 => handleChecklistChange({ q8: newQ8 })}
checklistState={currentChecklist}
getQuestionNote={props.getQuestionNote}
readOnly={props.readOnly}
/>
<Question9
onUpdatea={newQ9a => handleChecklistChange({ q9a: newQ9a })}
onUpdateb={newQ9b => handleChecklistChange({ q9b: newQ9b })}
checklistState={currentChecklist}
getQuestionNote={props.getQuestionNote}
readOnly={props.readOnly}
/>
<Question10
onUpdate={newQ10 => handleChecklistChange({ q10: newQ10 })}
checklistState={currentChecklist}
getQuestionNote={props.getQuestionNote}
readOnly={props.readOnly}
/>
<Question11
onUpdatea={newQ11a => handleChecklistChange({ q11a: newQ11a })}
onUpdateb={newQ11b => handleChecklistChange({ q11b: newQ11b })}
checklistState={currentChecklist}
getQuestionNote={props.getQuestionNote}
readOnly={props.readOnly}
/>
<Question12
onUpdate={newQ12 => handleChecklistChange({ q12: newQ12 })}
checklistState={currentChecklist}
getQuestionNote={props.getQuestionNote}
readOnly={props.readOnly}
/>
<Question13
onUpdate={newQ13 => handleChecklistChange({ q13: newQ13 })}
checklistState={currentChecklist}
getQuestionNote={props.getQuestionNote}
readOnly={props.readOnly}
/>
<Question14
onUpdate={newQ14 => handleChecklistChange({ q14: newQ14 })}
checklistState={currentChecklist}
getQuestionNote={props.getQuestionNote}
readOnly={props.readOnly}
/>
<Question15
onUpdate={newQ15 => handleChecklistChange({ q15: newQ15 })}
checklistState={currentChecklist}
getQuestionNote={props.getQuestionNote}
readOnly={props.readOnly}
/>
<Question16
onUpdate={newQ16 => handleChecklistChange({ q16: newQ16 })}
checklistState={currentChecklist}
getQuestionNote={props.getQuestionNote}
readOnly={props.readOnly}
/>
</div>
</fieldset>
Expand Down
2 changes: 2 additions & 0 deletions packages/web/src/components/checklist-ui/ChecklistWithPdf.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export default function ChecklistWithPdf(props) {
// props.pdfs - array of PDFs for multi-PDF selection
// props.selectedPdfId - currently selected PDF ID
// props.onPdfSelect - handler for PDF selection change
// props.getQuestionNote - function to get Y.Text for a question note

return (
<div class='h-full flex flex-col bg-blue-50'>
Expand All @@ -39,6 +40,7 @@ export default function ChecklistWithPdf(props) {
checklist={props.checklist}
onUpdate={props.onUpdate}
readOnly={props.readOnly}
getQuestionNote={props.getQuestionNote}
/>

{/* Second panel: PDF Viewer */}
Expand Down
13 changes: 10 additions & 3 deletions packages/web/src/components/checklist-ui/ChecklistYjsWrapper.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,13 @@ export default function ChecklistYjsWrapper() {
const [selectedPdfId, setSelectedPdfId] = createSignal(null);

// Use full hook for write operations
const { updateChecklistAnswer, updateChecklist, getChecklistData, addPdfToStudy } = useProject(
params.projectId,
);
const {
updateChecklistAnswer,
updateChecklist,
getChecklistData,
addPdfToStudy,
getQuestionNote,
} = useProject(params.projectId);

// Read data directly from store for faster reactivity
const connectionState = () => projectStore.getConnectionState(params.projectId);
Expand Down Expand Up @@ -362,6 +366,9 @@ export default function ChecklistYjsWrapper() {
pdfs={studyPdfs()}
selectedPdfId={selectedPdfId()}
onPdfSelect={handlePdfSelect}
getQuestionNote={questionKey =>
getQuestionNote(params.studyId, params.checklistId, questionKey)
}
/>
</Show>
</>
Expand Down
2 changes: 2 additions & 0 deletions packages/web/src/components/checklist-ui/GenericChecklist.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { ROBINSIChecklist } from '@checklist-ui/ROBINSIChecklist/index.js';
* @param {Object} props.checklist - The checklist state data
* @param {Function} props.onUpdate - Callback for checklist updates
* @param {boolean} [props.readOnly] - Whether the checklist is read-only
* @param {Function} [props.getQuestionNote] - Function to get Y.Text for a question note
*/
export default function GenericChecklist(props) {
// Determine the checklist type from props or state
Expand All @@ -44,6 +45,7 @@ export default function GenericChecklist(props) {
externalChecklist={props.checklist}
onExternalUpdate={props.onUpdate}
readOnly={props.readOnly}
getQuestionNote={props.getQuestionNote}
/>
</Show>
<Show when={checklistType() === CHECKLIST_TYPES.ROBINS_I}>
Expand Down
Loading