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
2 changes: 2 additions & 0 deletions api/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@ const startServer = async () => {
app.use('/api/admin/skills', routes.adminSkills);
app.use('/api/admin/users', routes.adminUsers);
app.use('/api/admin/audit-log', routes.adminAuditLog);
app.use('/api/admin/questionnaires', routes.adminQuestionnaires);
app.use('/api/actions', routes.actions);
app.use('/api/keys', routes.keys);
app.use('/api/api-keys', routes.apiKeys);
Expand Down Expand Up @@ -424,6 +425,7 @@ const startServer = async () => {
app.use('/api/agents/chat', rejectChatStartsUntilReady);
app.use('/api/agents', routes.agents);
app.use('/api/banner', routes.banner);
app.use('/api/questionnaire', routes.questionnaire);
app.use('/api/memories', routes.memories);
app.use('/api/schedules', rejectScheduleWritesUntilReady, routes.schedules);
app.use('/api/permissions', routes.accessPermissions);
Expand Down
33 changes: 33 additions & 0 deletions api/server/routes/admin/questionnaires.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const express = require('express');
const { createAdminQuestionnairesHandlers } = require('@librechat/api');
const { SystemCapabilities } = require('@librechat/data-schemas');
const { requireCapability } = require('~/server/middleware/roles/capabilities');
const { requireJwtAuth } = require('~/server/middleware');
const db = require('~/models');

const router = express.Router();

const requireAdminAccess = requireCapability(SystemCapabilities.ACCESS_ADMIN);
const requireRead = requireCapability(SystemCapabilities.READ_QUESTIONNAIRES);
const requireManage = requireCapability(SystemCapabilities.MANAGE_QUESTIONNAIRES);

const handlers = createAdminQuestionnairesHandlers({
listQuestionnaires: db.listQuestionnaires,
getQuestionnaireById: db.getQuestionnaireById,
saveQuestionnaire: db.saveQuestionnaire,
setQuestionnaireStatus: db.setQuestionnaireStatus,
duplicateQuestionnaire: db.duplicateQuestionnaire,
deleteQuestionnaire: db.deleteQuestionnaire,
});

router.use(requireJwtAuth, requireAdminAccess);

router.get('/', requireRead, handlers.listQuestionnaires);
router.get('/:id', requireRead, handlers.getQuestionnaire);
router.post('/', requireManage, handlers.createQuestionnaire);
router.put('/:id', requireManage, handlers.updateQuestionnaire);
router.patch('/:id/status', requireManage, handlers.setStatus);
router.post('/:id/duplicate', requireManage, handlers.duplicateQuestionnaire);
router.delete('/:id', requireManage, handlers.deleteQuestionnaire);

module.exports = router;
4 changes: 4 additions & 0 deletions api/server/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const adminRoles = require('./admin/roles');
const adminSkills = require('./admin/skills');
const adminUsers = require('./admin/users');
const adminAuditLog = require('./admin/audit');
const adminQuestionnaires = require('./admin/questionnaires');
const endpoints = require('./endpoints');
const staticRoute = require('./static');
const messages = require('./messages');
Expand All @@ -25,6 +26,7 @@ const balance = require('./balance');
const actions = require('./actions');
const apiKeys = require('./apiKeys');
const banner = require('./banner');
const questionnaire = require('./questionnaire');
const search = require('./search');
const models = require('./models');
const convos = require('./convos');
Expand Down Expand Up @@ -58,6 +60,7 @@ module.exports = {
adminSkills,
adminUsers,
adminAuditLog,
adminQuestionnaires,
keys,
apiKeys,
user,
Expand All @@ -67,6 +70,7 @@ module.exports = {
files,
share,
banner,
questionnaire,
agents,
convos,
search,
Expand Down
79 changes: 79 additions & 0 deletions api/server/routes/questionnaire.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
const express = require('express');
const { logger } = require('@librechat/data-schemas');
const optionalJwtAuth = require('~/server/middleware/optionalJwtAuth');
const { requireJwtAuth } = require('~/server/middleware');
const {
getActiveQuestionnaire,
submitQuestionnaireResponse,
dismissQuestionnaire,
} = require('~/models');

const router = express.Router();

router.get('/', optionalJwtAuth, async (req, res) => {
try {
const { questionnaire, completed, dismissedAt } = await getActiveQuestionnaire(req.user);
res.status(200).send({ questionnaire, completed, dismissedAt });
} catch (error) {
logger.error('[getQuestionnaire] Error getting questionnaire', error);
res.status(500).json({ message: 'Error getting questionnaire' });
}
});

router.post('/dismiss', requireJwtAuth, async (req, res) => {
try {
const { questionnaireId } = req.body;
if (!questionnaireId) {
return res.status(400).json({ message: 'questionnaireId is required' });
}

const { questionnaire } = await getActiveQuestionnaire(req.user);
if (!questionnaire || questionnaire.questionnaireId !== questionnaireId) {
return res.status(409).json({ message: 'This questionnaire is no longer active' });
}
if (questionnaire.dismissible === false) {
return res.status(403).json({ message: 'This questionnaire cannot be dismissed' });
}

const dismissal = await dismissQuestionnaire(req.user.id, questionnaireId);
res.status(200).json({ success: true, dismissedAt: dismissal.dismissedAt });
} catch (error) {
logger.error('[dismissQuestionnaire] Error dismissing questionnaire', error);
res.status(500).json({ message: 'Error dismissing questionnaire' });
}
});

router.post('/response', requireJwtAuth, async (req, res) => {
try {
const { questionnaireId, answers } = req.body;

if (!questionnaireId || !Array.isArray(answers) || answers.length === 0) {
return res.status(400).json({ message: 'questionnaireId and answers are required' });
}

const { questionnaire } = await getActiveQuestionnaire(req.user);
if (!questionnaire || questionnaire.questionnaireId !== questionnaireId) {
return res.status(409).json({ message: 'This questionnaire is no longer active' });
}

const requiredIds = questionnaire.questions
.filter((question) => question.required)
.map((question) => question.id);
const answeredIds = new Set(answers.map((answer) => answer.questionId));
const missing = requiredIds.filter((id) => !answeredIds.has(id));
if (missing.length > 0) {
return res.status(400).json({ message: 'Missing required answers', missing });
}

await submitQuestionnaireResponse(req.user.id, questionnaireId, answers);
res.status(200).json({ success: true });
} catch (error) {
logger.error('[submitQuestionnaireResponse] Error submitting response', error);
if (error.message && error.message.includes('already responded')) {
return res.status(409).json({ message: error.message });
}
res.status(500).json({ message: 'Error submitting questionnaire response' });
}
});

module.exports = router;
2 changes: 2 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"@react-spring/web": "^9.7.5",
"@tanstack/react-query": "^4.28.0",
"@tanstack/react-table": "^8.11.7",
"canvas-confetti": "^1.9.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"copy-to-clipboard": "^3.3.3",
Expand Down Expand Up @@ -143,6 +144,7 @@
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
"@types/canvas-confetti": "^1.9.0",
"@types/jest": "^29.5.14",
"@types/js-cookie": "^3.0.6",
"@types/lodash": "^4.17.15",
Expand Down
Loading