feat: implement QuestionBank CRUD with pagination and template query

- Add pagination support to findAll (page, limit query params)
- Add findByTemplateId method to service
- Add GET /by-template/:templateId endpoint to controller
- Service already includes CRUD for QuestionBank and QuestionBankItem
This commit is contained in:
Developer
2026-04-23 17:19:11 +08:00
commit 0a9588abb7
492 changed files with 112453 additions and 0 deletions
@@ -0,0 +1,99 @@
import { AIMessage } from '@langchain/core/messages';
import { RunnableConfig } from '@langchain/core/runnables';
import { EvaluationState } from '../state';
/**
* Node responsible for presenting the current question or follow-up to the user.
*/
export const interviewerNode = async (
state: EvaluationState,
config?: RunnableConfig,
): Promise<Partial<EvaluationState>> => {
const { questions, currentQuestionIndex, shouldFollowUp, messages } = state;
console.log('[InterviewerNode] Entering node...', {
numQuestions: questions?.length,
currentIndex: currentQuestionIndex,
shouldFollowUp,
numMessages: messages?.length,
});
if (!questions || questions.length === 0) {
const isZh = state.language === 'zh';
const isJa = state.language === 'ja';
const msg = isZh
? '很抱歉,我无法为此会话生成任何问题。'
: isJa
? '申し訳ありませんが、このセッションの問題を生成できませんでした。'
: "I'm sorry, I couldn't generate any questions for this session.";
return {
messages: [new AIMessage(msg)],
};
}
const currentQuestion = questions[currentQuestionIndex];
// If it's a follow-up, we add a prefix to the label later.
// If we've run out of questions and no follow-up requested, we shouldn't be here, but let's be safe.
if (currentQuestionIndex >= questions.length) {
return { shouldFollowUp: false };
}
const isZh = state.language === 'zh';
const isJa = state.language === 'ja';
let prompt = '';
if (
shouldFollowUp &&
state.feedbackHistory &&
state.feedbackHistory.length > 0
) {
// Construct a follow-up prompt based on last feedback
const lastFeedbackMsg =
state.feedbackHistory[state.feedbackHistory.length - 1];
const feedbackText = lastFeedbackMsg.content.toString();
// Extract the "Feedback: ..." part if possible, otherwise use whole text
const feedbackMatch = feedbackText.match(
/(?:Feedback|反馈|フィードバック): ([\s\S]*)/i,
);
const specificFeedback = feedbackMatch
? feedbackMatch[1].trim()
: feedbackText;
const followUpLabel = isZh
? '补充追问'
: isJa
? '追加の質問'
: 'Follow-up Clarification';
const followUpInstruction = isZh
? '根据以上反馈,请补充更具体的信息:'
: isJa
? '上記のフィードバックに基づき、より具体的な情報を追加してください:'
: 'Based on the feedback above, please provide more specific details:';
prompt = `${followUpLabel}\n\n${specificFeedback}\n\n${followUpInstruction}`;
} else {
// Standard question presentation
const label = isZh
? `问题 ${currentQuestionIndex + 1}`
: isJa
? `質問 ${currentQuestionIndex + 1}`
: `Question ${currentQuestionIndex + 1}`;
const instruction = isZh
? '请提供您的回答。'
: isJa
? '回答を入力してください。'
: 'Please provide your answer.';
prompt = `${label}: ${currentQuestion.questionText}\n\n${instruction}`;
}
console.log('[InterviewerNode] Returning question:', { currentQuestionIndex, questionText: currentQuestion?.questionText });
return {
messages: [new AIMessage(prompt)],
shouldFollowUp: false,
};
};