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 |
|