Spaces:
Paused
Paused
Commit
·
6720344
1
Parent(s):
32acb92
updated
Browse files
backend/models/database.py
CHANGED
|
@@ -61,6 +61,7 @@ class Application(db.Model):
|
|
| 61 |
extracted_features = db.Column(db.Text, nullable=True)
|
| 62 |
status = db.Column(db.String(50), default='applied')
|
| 63 |
date_applied = db.Column(db.DateTime, default=datetime.utcnow)
|
|
|
|
| 64 |
|
| 65 |
user = db.relationship('User', backref='applications')
|
| 66 |
|
|
|
|
| 61 |
extracted_features = db.Column(db.Text, nullable=True)
|
| 62 |
status = db.Column(db.String(50), default='applied')
|
| 63 |
date_applied = db.Column(db.DateTime, default=datetime.utcnow)
|
| 64 |
+
interview_log = Column(Text)
|
| 65 |
|
| 66 |
user = db.relationship('User', backref='applications')
|
| 67 |
|
backend/routes/interview_api.py
CHANGED
|
@@ -177,7 +177,7 @@ def download_report(application_id: int):
|
|
| 177 |
pdf_buffer = create_pdf_report(report_text)
|
| 178 |
pdf_buffer.seek(0)
|
| 179 |
|
| 180 |
-
filename = f"
|
| 181 |
return send_file(
|
| 182 |
pdf_buffer,
|
| 183 |
download_name=filename,
|
|
@@ -215,6 +215,32 @@ def process_answer():
|
|
| 215 |
# Evaluate the answer
|
| 216 |
evaluation_result = evaluate_answer(current_question, answer)
|
| 217 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
# Determine the number of questions configured for this job
|
| 219 |
total_questions = 3
|
| 220 |
if job_id is not None:
|
|
|
|
| 177 |
pdf_buffer = create_pdf_report(report_text)
|
| 178 |
pdf_buffer.seek(0)
|
| 179 |
|
| 180 |
+
filename = f"{application.name.replace(' ', '_')}_interview_report.pdf"
|
| 181 |
return send_file(
|
| 182 |
pdf_buffer,
|
| 183 |
download_name=filename,
|
|
|
|
| 215 |
# Evaluate the answer
|
| 216 |
evaluation_result = evaluate_answer(current_question, answer)
|
| 217 |
|
| 218 |
+
# 🔥 Save Q&A in interview_log for report
|
| 219 |
+
try:
|
| 220 |
+
application = Application.query.filter_by(
|
| 221 |
+
user_id=current_user.id,
|
| 222 |
+
job_id=job_id
|
| 223 |
+
).first()
|
| 224 |
+
|
| 225 |
+
if application:
|
| 226 |
+
log_data = []
|
| 227 |
+
if application.interview_log:
|
| 228 |
+
try:
|
| 229 |
+
log_data = json.loads(application.interview_log)
|
| 230 |
+
except Exception:
|
| 231 |
+
log_data = []
|
| 232 |
+
|
| 233 |
+
log_data.append({
|
| 234 |
+
"question": current_question,
|
| 235 |
+
"answer": answer,
|
| 236 |
+
"evaluation": evaluation_result
|
| 237 |
+
})
|
| 238 |
+
|
| 239 |
+
application.interview_log = json.dumps(log_data, ensure_ascii=False)
|
| 240 |
+
db.session.commit()
|
| 241 |
+
except Exception as log_err:
|
| 242 |
+
logging.error(f"Error saving interview log: {log_err}")
|
| 243 |
+
|
| 244 |
# Determine the number of questions configured for this job
|
| 245 |
total_questions = 3
|
| 246 |
if job_id is not None:
|
backend/services/report_generator.py
CHANGED
|
@@ -118,66 +118,72 @@ def generate_llm_interview_report(application) -> str:
|
|
| 118 |
lines.append(f' Match Ratio: {ratio * 100:.0f}%')
|
| 119 |
lines.append(f' Score: {score_label}')
|
| 120 |
lines.append('')
|
| 121 |
-
lines.append('
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
return '\n'.join(lines)
|
| 127 |
|
| 128 |
|
| 129 |
def create_pdf_report(report_text: str) -> BytesIO:
|
| 130 |
-
"""Convert a
|
| 131 |
-
|
| 132 |
-
This helper uses Matplotlib's ``PdfPages`` backend to compose a PDF
|
| 133 |
-
containing the supplied text. Lines are wrapped to a reasonable
|
| 134 |
-
width and paginated as needed. The returned ``BytesIO`` object can
|
| 135 |
-
be passed directly to Flask's ``send_file`` function.
|
| 136 |
-
|
| 137 |
-
Parameters
|
| 138 |
-
----------
|
| 139 |
-
report_text : str
|
| 140 |
-
The full contents of the report to be included in the PDF.
|
| 141 |
-
|
| 142 |
-
Returns
|
| 143 |
-
-------
|
| 144 |
-
BytesIO
|
| 145 |
-
A file‑like buffer containing the PDF data. The caller is
|
| 146 |
-
responsible for rewinding the buffer (via ``seek(0)``) before
|
| 147 |
-
sending it over HTTP.
|
| 148 |
-
"""
|
| 149 |
buffer = BytesIO()
|
| 150 |
|
| 151 |
-
#
|
| 152 |
-
raw_lines = report_text.split(
|
| 153 |
-
wrapper = textwrap.TextWrapper(width=
|
| 154 |
-
|
|
|
|
| 155 |
for line in raw_lines:
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
else:
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
# of 40 lines per page fits comfortably on an A4 sheet using the
|
| 168 |
-
# selected font size and margins.
|
| 169 |
-
lines_per_page = 40
|
| 170 |
|
|
|
|
|
|
|
| 171 |
with PdfPages(buffer) as pdf:
|
| 172 |
-
for i in range(0, len(
|
| 173 |
-
fig = plt.figure(figsize=(8.27, 11.69)) # A4
|
| 174 |
fig.patch.set_facecolor('white')
|
| 175 |
-
|
| 176 |
-
page_text =
|
| 177 |
-
# Place the text near the top-left corner. ``va='top'``
|
| 178 |
-
# ensures the first line starts at the specified y
|
| 179 |
-
fig.text(0.1, 0.95, page_text, va='top', ha='left', fontsize=10, family='monospace')
|
| 180 |
-
# Save and close the figure to free memory
|
| 181 |
pdf.savefig(fig)
|
| 182 |
plt.close(fig)
|
| 183 |
|
|
|
|
| 118 |
lines.append(f' Match Ratio: {ratio * 100:.0f}%')
|
| 119 |
lines.append(f' Score: {score_label}')
|
| 120 |
lines.append('')
|
| 121 |
+
lines.append('Interview Transcript & Evaluation:')
|
| 122 |
+
try:
|
| 123 |
+
if application.interview_log:
|
| 124 |
+
try:
|
| 125 |
+
qa_log = json.loads(application.interview_log)
|
| 126 |
+
except Exception:
|
| 127 |
+
qa_log = []
|
| 128 |
+
|
| 129 |
+
if qa_log:
|
| 130 |
+
for idx, entry in enumerate(qa_log, 1):
|
| 131 |
+
q = entry.get("question", "N/A")
|
| 132 |
+
a = entry.get("answer", "N/A")
|
| 133 |
+
eval_score = entry.get("evaluation", {}).get("score", "N/A")
|
| 134 |
+
eval_feedback = entry.get("evaluation", {}).get("feedback", "N/A")
|
| 135 |
+
|
| 136 |
+
lines.append(f"\nQuestion {idx}: {q}")
|
| 137 |
+
lines.append(f"Answer: {a}")
|
| 138 |
+
lines.append(f"Score: {eval_score}")
|
| 139 |
+
lines.append(f"Feedback: {eval_feedback}")
|
| 140 |
+
else:
|
| 141 |
+
lines.append("No interview log data recorded.")
|
| 142 |
+
else:
|
| 143 |
+
lines.append("No interview log data recorded.")
|
| 144 |
+
except Exception as e:
|
| 145 |
+
lines.append(f"Error loading interview log: {e}")
|
| 146 |
|
| 147 |
return '\n'.join(lines)
|
| 148 |
|
| 149 |
|
| 150 |
def create_pdf_report(report_text: str) -> BytesIO:
|
| 151 |
+
"""Convert a formatted report into a visually clear PDF with bold Qs and indented As."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
buffer = BytesIO()
|
| 153 |
|
| 154 |
+
# Prepare wrapped lines
|
| 155 |
+
raw_lines = report_text.split("\n")
|
| 156 |
+
wrapper = textwrap.TextWrapper(width=85, break_long_words=True, replace_whitespace=False)
|
| 157 |
+
formatted_lines: List[str] = []
|
| 158 |
+
|
| 159 |
for line in raw_lines:
|
| 160 |
+
# Highlight Questions
|
| 161 |
+
if line.strip().startswith("Question"):
|
| 162 |
+
formatted_lines.append("") # Extra spacing before new question
|
| 163 |
+
formatted_lines.append(f"**{line.strip()}**") # Bold style marker
|
| 164 |
+
elif line.strip().startswith("Answer:"):
|
| 165 |
+
# Indent answers for clarity
|
| 166 |
+
answer_text = line.replace("Answer:", "").strip()
|
| 167 |
+
wrapped_answer = wrapper.wrap(answer_text)
|
| 168 |
+
formatted_lines.append(f" Answer: {wrapped_answer[0]}")
|
| 169 |
+
for extra in wrapped_answer[1:]:
|
| 170 |
+
formatted_lines.append(f" {extra}")
|
| 171 |
+
elif line.strip().startswith("Score:") or line.strip().startswith("Feedback:"):
|
| 172 |
+
# Keep score & feedback on separate lines
|
| 173 |
+
formatted_lines.append(f" {line.strip()}")
|
| 174 |
else:
|
| 175 |
+
# Wrap other lines normally
|
| 176 |
+
wrapped = wrapper.wrap(line)
|
| 177 |
+
formatted_lines.extend(wrapped if wrapped else [""])
|
|
|
|
|
|
|
|
|
|
| 178 |
|
| 179 |
+
# PDF creation
|
| 180 |
+
lines_per_page = 38
|
| 181 |
with PdfPages(buffer) as pdf:
|
| 182 |
+
for i in range(0, len(formatted_lines), lines_per_page):
|
| 183 |
+
fig = plt.figure(figsize=(8.27, 11.69)) # A4
|
| 184 |
fig.patch.set_facecolor('white')
|
| 185 |
+
page_text = "\n".join(formatted_lines[i:i + lines_per_page])
|
| 186 |
+
fig.text(0.1, 0.95, page_text, va="top", ha="left", fontsize=10, family="monospace")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
pdf.savefig(fig)
|
| 188 |
plt.close(fig)
|
| 189 |
|