Skip to content
Open
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 .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ jobs:
npm ci --prefix backend --ignore-scripts
npm ci --prefix utils/modules/editor-delta-conversion --ignore-scripts
npm ci --prefix utils/modules/assessment-score --ignore-scripts
npm ci --prefix utils/modules/placeholder-tokens --ignore-scripts
- name: Run npm audit
run: make audit

Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ utils/modules/editor-delta-conversion/node_modules
utils/modules/editor-delta-conversion/coverage
utils/modules/assessment-score/node_modules
utils/modules/assessment-score/coverage
utils/modules/placeholder-tokens/node_modules
utils/modules/placeholder-tokens/coverage

yarn-error.log
.tox
Expand Down
1 change: 1 addition & 0 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,5 +110,6 @@ audit:
- npm ci --prefix backend --ignore-scripts
- npm ci --prefix utils/modules/editor-delta-conversion --ignore-scripts
- npm ci --prefix utils/modules/assessment-score --ignore-scripts
- npm ci --prefix utils/modules/placeholder-tokens --ignore-scripts
script:
- make audit
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ test-rpc: backend/node_modules/.uptodate
test-modules:
cd utils/modules/editor-delta-conversion && npm run test:module -- tests/editor-delta-conversion.test.js
cd utils/modules/assessment-score && npm run test:module -- tests/assessment-score.test.js
cd utils/modules/placeholder-tokens && npm run test:module -- tests/placeholder-tokens.test.js

.PHONY: lint
lint: frontend/node_modules/.uptodate
Expand Down Expand Up @@ -240,13 +241,15 @@ ifeq ($(OS),Windows_NT)
@if exist "backend\node_modules" rmdir /S /Q "backend\node_modules"
@if exist "utils\modules\editor-delta-conversion\node_modules" rmdir /S /Q "utils\modules\editor-delta-conversion\node_modules"
@if exist "utils\modules\assessment-score\node_modules" rmdir /S /Q "utils\modules\assessment-score\node_modules"
@if exist "utils\modules\placeholder-tokens\node_modules" rmdir /S /Q "utils\modules\placeholder-tokens\node_modules"
@if exist "dist" rmdir /S /Q "dist"
@for %%F in (files*) do if "%%~nxF" neq "8852a746-360e-4c31-add2-4d1c75bfb96d.pdf" del "%%F"
else
rm -rf frontend/node_modules
rm -rf backend/node_modules
rm -rf care/utils/modules/editor-delta-conversion/node_modules
rm -rf care/utils/modules/assessment-score/node_modules
rm -rf care/utils/modules/placeholder-tokens/node_modules
rm -rf dist
find files -maxdepth 1 -type f ! -name "8852a746-360e-4c31-add2-4d1c75bfb96d.pdf" -exec rm {} \;
endif
Expand Down Expand Up @@ -304,10 +307,10 @@ modules: install-utils-modules
# All three audits run even if one fails; exit 1 if any failed (npm.cmd avoids PowerShell execution policy on npm.ps1).
ifeq ($(OS),Windows_NT)
audit:
@powershell -NoProfile -Command "$$e=0; foreach ($$p in @('frontend','backend','utils/modules/editor-delta-conversion','utils/modules/assessment-score')) { Write-Host ''; Write-Host ('=== npm audit: ' + $$p + ' ==='); npm.cmd audit --prefix (Join-Path '$(CURDIR)' $$p); if ($$LASTEXITCODE -ne 0) { $$e=1 } }; exit $$e"
@powershell -NoProfile -Command "$$e=0; foreach ($$p in @('frontend','backend','utils/modules/editor-delta-conversion','utils/modules/assessment-score','utils/modules/placeholder-tokens')) { Write-Host ''; Write-Host ('=== npm audit: ' + $$p + ' ==='); npm.cmd audit --prefix (Join-Path '$(CURDIR)' $$p); if ($$LASTEXITCODE -ne 0) { $$e=1 } }; exit $$e"
else
audit:
@st=0; for d in frontend backend utils/modules/editor-delta-conversion utils/modules/assessment-score; do \
@st=0; for d in frontend backend utils/modules/editor-delta-conversion utils/modules/assessment-score utils/modules/placeholder-tokens; do \
echo ""; echo "=== npm audit: $$d ==="; \
npm audit --prefix $$d || st=1; \
done; exit $$st
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,72 +7,89 @@ const promptPlaceholders = [
placeholderKey: "pdfText",
placeholderLabel: "PDF text",
placeholderType: "text",
placeholderDescription: "Text from the PDF in the current context.",
placeholderExample: "Document text content from the current PDF context. ... [truncated at 15000 chars if needed]",
placeholderDescription: "Plain text extracted from a PDF.",
placeholderExample:
"PDF Text: We compared three annotation tools in a graduate seminar (n=24). " +
"Reviewers highlighted claims on pages 2–4 and left margin notes about study design.",
},
{
type: 8,
placeholderKey: "editorText",
placeholderLabel: "Editor text",
placeholderType: "text",
placeholderDescription: "Text from the current editor document in the current context.",
placeholderExample: "Document text content from the current editor context. ... [truncated at 15000 chars if needed]",
placeholderDescription: "Plain text from an HTML or modal editor document.",
placeholderExample:
"Editor Text: The introduction should state the research question clearly. " +
"Draft paragraph: \"This study examines how peer feedback changes revision quality.\"",
},
{
type: 8,
placeholderKey: "assessmentResult",
placeholderLabel: "Assessment result",
placeholderType: "text",
placeholderDescription: "Saved rubric from the assessment sidebar for this document and step.",
placeholderExample: "{\"Clarity\":{\"currentScore\":3,\"assessment\":\"Good structure but weak conclusion\"},\"Sources\":{\"currentScore\":2,\"assessment\":\"Missing one reference\"}}",
placeholderDescription: "Saved assessment scores and comments from the Assessment sidebar.",
placeholderExample:
"Assessment Result: {\"Clarity\":{\"currentScore\":3,\"assessment\":\"Clear structure; conclusion could be stronger.\"}," +
"\"Sources\":{\"currentScore\":2,\"assessment\":\"Two claims need citations.\"}}",
},
{
type: 8,
placeholderKey: "inlineComments",
placeholderLabel: "Inline comments",
placeholderType: "text",
placeholderDescription: "Structured comments and annotations for this document and step.",
placeholderExample: "[{\"page\":2,\"quote\":\"Baseline is unclear\",\"comment\":\"Please define baseline.\",\"tag\":\"MajorIssue\"},{\"page\":4,\"quote\":\"Table 2\",\"comment\":\"Nice comparison.\",\"tag\":\"Strength\"}]",
placeholderDescription: "PDF inline comments and annotations.",
placeholderExample:
"Inline Comments: [{\"page\":2,\"quote\":\"The baseline is unclear\",\"comment\":\"Define the baseline in the methods section.\"," +
"\"tag\":\"MajorIssue\"},{\"page\":4,\"quote\":\"Table 2 summarizes the main findings\",\"comment\":\"Good comparison of conditions.\"," +
"\"tag\":\"Strength\"}]",
},
{
type: 8,
placeholderKey: "nlpAssessmentSuggestion",
placeholderLabel: "NLP assessment suggestion",
placeholderType: "text",
placeholderDescription: "Model draft assessment for this step if available.",
placeholderExample: "[{\"name\":\"Clarity\",\"score\":3,\"justification\":\"Clear flow with minor issues\"},{\"name\":\"Sources\",\"score\":2,\"justification\":\"Some claims lack citations\"}]",
placeholderDescription: "AI-generated draft assessment, when available.",
placeholderExample:
"NLP Assessment Suggestion: [{\"name\":\"Clarity\",\"score\":3,\"justification\":\"Logical flow with minor gaps in the conclusion.\"}," +
"{\"name\":\"Sources\",\"score\":2,\"justification\":\"Several statements lack supporting references.\"}]",
},
{
type: 8,
placeholderKey: "previousAssessmentResult",
placeholderLabel: "Previous assessment result",
placeholderType: "text",
placeholderDescription: "Saved rubric from the previous step when carry-over is configured.",
placeholderExample: "{\"Clarity\":{\"currentScore\":2,\"assessment\":\"Argumentation was fragmented\"},\"Sources\":{\"currentScore\":2,\"assessment\":\"References were incomplete\"}}",
placeholderDescription: "Saved assessment from a prior step when carry-over is enabled.",
placeholderExample:
"Previous Assessment Result: {\"Clarity\":{\"currentScore\":2,\"assessment\":\"Argumentation was fragmented in the prior draft.\"}," +
"\"Sources\":{\"currentScore\":2,\"assessment\":\"Reference list was incomplete last step.\"}}",
},
{
type: 8,
placeholderKey: "assessmentConfiguration",
placeholderLabel: "Assessment configuration",
placeholderType: "text",
placeholderDescription: "Assessment rubric configuration used in this step.",
placeholderExample: "{\"type\":\"assessment\",\"rubrics\":[{\"name\":\"Overall\",\"criteria\":[{\"name\":\"Clarity\",\"maxPoints\":5},{\"name\":\"Sources\",\"maxPoints\":5}]}]}",
placeholderDescription: "Rubric structure and criteria from the assessment configuration.",
placeholderExample:
"Assessment Configuration: {\"type\":\"assessment\",\"rubrics\":[{\"name\":\"Overall\",\"criteria\":[{\"name\":\"Clarity\",\"maxPoints\":5},{\"name\":\"Sources\",\"maxPoints\":5}]}]}",
},
{
type: 8,
placeholderKey: "submissionFiles",
placeholderLabel: "Submission file",
placeholderType: "text",
placeholderDescription: "Text from submission files mapped per slot in the hook step.",
placeholderExample: "Extracted text from the file mapped to this instance (e.g. main PDF body)…",
placeholderDescription: "Text extracted from a mapped submission file (PDF, TeX, etc.).",
placeholderExample:
"Submission File: Chapter 1 — Introduction\n\nThis thesis presents a user study on collaborative reading platforms. " +
"Participants annotated shared PDFs over three weekly sessions.",
},
{
type: 8,
placeholderKey: "studyContext",
placeholderLabel: "Study context",
placeholderType: "text",
placeholderDescription: "Basic metadata from current study, step, and document context.",
placeholderExample: "{\"studyName\":\"Current study name\",\"stepName\":\"Current step name\",\"documentTitle\":\"Current document title\"}",
placeholderDescription: "Study, step, and document metadata.",
placeholderExample:
"Study Context: {\"studyName\":\"Peer Review Pilot\",\"stepName\":\"Essay feedback\",\"documentTitle\":\"Draft essay v2.pdf\"}",
},
];

Expand Down
51 changes: 51 additions & 0 deletions backend/db/migrations/20260720121142-extend-placeholder-options.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
'use strict';

/** @type {import('sequelize-cli').Migration} */

const pdfTextPlaceholderOptions = [
{
name: 'characterLimit',
label: 'Character limit',
valueType: 'positiveInteger',
},
];

module.exports = {
async up(queryInterface, Sequelize) {
const table = await queryInterface.describeTable('placeholder');
if (!table.placeholderOptions) {
await queryInterface.addColumn('placeholder', 'placeholderOptions', {
type: Sequelize.JSONB,
allowNull: true,
defaultValue: null,
});
}

const placeholderOptionsJson = Sequelize.literal(
`'${JSON.stringify(pdfTextPlaceholderOptions).replace(/'/g, "''")}'::jsonb`
);

await queryInterface.bulkUpdate(
'placeholder',
{ placeholderOptions: placeholderOptionsJson },
{
type: 8,
placeholderKey: 'pdfText',
deleted: false,
}
);
},

async down(queryInterface, Sequelize) {
await queryInterface.bulkUpdate(
'placeholder',
{ placeholderOptions: null },
{
type: 8,
placeholderKey: 'pdfText',
}
);

await queryInterface.removeColumn('placeholder', 'placeholderOptions');
},
};
1 change: 1 addition & 0 deletions backend/db/models/placeholder.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ module.exports = (sequelize, DataTypes) => {
placeholderDescription: DataTypes.TEXT,
placeholderExample: DataTypes.TEXT,
placeholderType: DataTypes.STRING,
placeholderOptions: DataTypes.JSONB,
required: DataTypes.BOOLEAN,
deleted: DataTypes.BOOLEAN,
createdAt: DataTypes.DATE,
Expand Down
13 changes: 13 additions & 0 deletions backend/package-lock.json

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

1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"passport-orcid": "^0.0.4",
"pdf-parse": "^2.4.5",
"pg-promise": "^12.1.3",
"placeholder-tokens": "file:../utils/modules/placeholder-tokens",
"quill-delta": "^5.1.0",
"sequelize": "^6.37.8",
"sequelize-cli": "^6.6.5",
Expand Down
61 changes: 54 additions & 7 deletions backend/utils/templateResolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const {
getUsedIndexes,
hasPlaceholderForKey,
tokenInnerText,
} = require("./placeholderTokens");
} = require("placeholder-tokens");
const UPLOAD_PATH = `${__dirname}/../../files`;
const TEXT_PLACEHOLDER_CHAR_CAP = 2000;

Expand Down Expand Up @@ -66,6 +66,48 @@ function capText(text, cap = TEXT_PLACEHOLDER_CHAR_CAP) {
return text.length > cap ? text.slice(0, cap) : text;
}

/**
* Apply optional characterLimit from a pdfText token instance.
*
* @param {string} text - Resolved pdfText value
* @param {Object} [tokenOptions] - Parsed token options
* @returns {string}
*/
function applyPdfTextCharacterLimit(text, tokenOptions = {}) {
if (typeof text !== "string" || !text) {
return "";
}
const limitRaw = tokenOptions.characterLimit;
if (limitRaw === undefined || limitRaw === null || String(limitRaw).trim() === "") {
return text;
}
const limit = parseInt(String(limitRaw), 10);
if (!Number.isInteger(limit) || limit <= 0) {
return text;
}
return capText(text, limit);
}

/**
* Resolve a placeholder token and apply per-instance options where supported.
*
* @param {string} baseKey - Placeholder key
* @param {number|null} index - Placeholder index
* @param {Object} tokenOptions - Parsed token options
* @param {Object} replacements - Replacement map
* @returns {string|undefined}
*/
function resolveTokenWithOptions(baseKey, index, tokenOptions, replacements) {
const value = resolveReplacementForToken(baseKey, index, replacements);
if (value === undefined) {
return undefined;
}
if (baseKey === "pdfText") {
return applyPdfTextCharacterLimit(value, tokenOptions);
}
return value;
}

/**
* Convert a placeholder value to a string for template replacement.
* Objects/arrays are serialized to JSON text.
Expand Down Expand Up @@ -359,7 +401,7 @@ async function buildPromptPlaceholderValues(context, models, allow, options = {}
if (!pdfText && context.documentId) {
pdfText = await models["document"].loadPlainText(context.documentId);
}
promptValues["~pdfText~"] = pdfText ? capText(pdfText) : "";
promptValues["~pdfText~"] = pdfText || "";
}

if (allow("editorText")) {
Expand Down Expand Up @@ -697,8 +739,8 @@ async function resolveTemplate(templateId, context, models, options = {}) {
if (template.type === 8) {
await addIndexedSubmissionFileReplacements(text, replacements, context, models, options);
}
let resolvedText = applyPlaceholderReplacements(text, (baseKey, index) => {
return resolveReplacementForToken(baseKey, index, replacements);
let resolvedText = applyPlaceholderReplacements(text, (baseKey, index, tokenOptions) => {
return resolveTokenWithOptions(baseKey, index, tokenOptions, replacements);
});

// Make URLs clickable: split by URL pattern, escape non-URL parts, wrap URLs in <a>
Expand Down Expand Up @@ -771,7 +813,8 @@ async function resolveTemplateToDelta(templateId, context, models, options = {})
if (template.type === 8) {
await addIndexedSubmissionFileReplacements(text, replacements, context, models, options);
}
const resolveToken = (baseKey, index) => resolveReplacementForToken(baseKey, index, replacements);
const resolveToken = (baseKey, index, tokenOptions) =>
resolveTokenWithOptions(baseKey, index, tokenOptions, replacements);

const resolvedDelta = new Delta();

Expand Down Expand Up @@ -881,11 +924,15 @@ async function resolveTemplateWithValues(templateId, values, models, options = {
return typeof value === "string" ? value : JSON.stringify(value);
};
const valueMap = values || {};
resolvedText = applyPlaceholderReplacements(resolvedText, (baseKey, index) => {
resolvedText = applyPlaceholderReplacements(resolvedText, (baseKey, index, tokenOptions) => {
if (index != null) {
const inner = tokenInnerText(baseKey, index);
if (Object.prototype.hasOwnProperty.call(valueMap, inner)) {
return toText(valueMap[inner]);
let text = toText(valueMap[inner]);
if (baseKey === "pdfText") {
text = applyPdfTextCharacterLimit(text, tokenOptions);
}
return text;
}
return undefined;
}
Expand Down
2 changes: 1 addition & 1 deletion docs/source/for_developers/before_you_start.rst
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ More Commands
* - ``make test-rpc``
- Run backend RPC tests.
* - ``make test-modules``
- Run unit tests for the ``editor-delta-conversion`` module.
- Run unit tests for the ``editor-delta-conversion`` and ``placeholder-tokens`` modules.
* - ``make lint``
- Run frontend linter.
* - ``make modules``
Expand Down
Loading
Loading