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
29 changes: 28 additions & 1 deletion backend/app/services/llms_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from app.core.singleton import get_openai_client
from app.core.config import settings
import json
import re
from app.core.logger import get_logger

logger = get_logger("llm_service")
Expand Down Expand Up @@ -107,7 +108,33 @@ async def get_response_with_tools(conversation_history: list[dict]):
},
)

final_response = completion.choices[0].message.content
raw_response = completion.choices[0].message.content

# ---------------------------------------------------------------------------
# Output normalisation
#
# The LLM sometimes returns responses with inconsistent whitespace: leading/
# trailing blank lines, or runs of three or more consecutive newlines between
# paragraphs. This is especially common when tool results are pasted verbatim
# into the context, causing the model to mirror that extra spacing.
#
# We apply two lightweight fixes here rather than in the frontend so that
# every consumer of this function (REST API, tests, future streaming) gets
# the same clean text:
#
# 1. Strip leading and trailing whitespace from the entire response.
# 2. Collapse any run of 3+ consecutive newlines down to exactly two
# newlines (one blank line), which is the standard Markdown paragraph
# separator. Two-newline sequences (intentional paragraph breaks) are
# left untouched.
# ---------------------------------------------------------------------------
final_response = raw_response.strip() if raw_response else raw_response
if final_response:
# Replace 3 or more consecutive newlines with exactly 2 newlines.
# The `\n{3,}` pattern matches any run of 3+ newlines (including
# Windows-style \r\n sequences that were already normalised to \n by
# the OpenAI SDK).
final_response = re.sub(r"\n{3,}", "\n\n", final_response)

logger.info(f"LLM Response: {final_response}")

Expand Down
58 changes: 44 additions & 14 deletions frontend/components/ChatBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -266,20 +266,50 @@ export const ChatBox: React.FC<ChatBoxProps> = ({
<Bot className="h-4 w-4" />
)}
</div>
<div
className={`rounded-lg p-3 ${
message.role === "user"
? "bg-blue-600 text-white"
: "bg-white border border-slate-200"
}`}
>
{message.role === "user" ? (
<div className="whitespace-pre-wrap text-sm leading-relaxed">
{message.content}
</div>
) : (
<MarkdownRenderer content={message.content} />
)}
{/*
* Message bubble wrapper: combines the chat bubble with a
* timestamp displayed beneath it. Previously the timestamp
* field was stored in every Message object but never
* rendered, so users had no sense of when each message was
* sent. Wrapping in a flex column lets us add the timestamp
* without touching the bubble's own layout.
*/}
<div className="flex flex-col gap-1">
<div
className={`rounded-lg p-3 ${
message.role === "user"
? "bg-blue-600 text-white"
: "bg-white border border-slate-200"
}`}
>
{message.role === "user" ? (
<div className="whitespace-pre-wrap text-sm leading-relaxed">
{message.content}
</div>
) : (
<MarkdownRenderer content={message.content} />
)}
</div>

{/*
* Timestamp: shown in muted text below the bubble,
* aligned to match the bubble's side (right for user,
* left for assistant). Uses toLocaleTimeString so the
* format respects the user's locale (e.g. 2:34 PM vs
* 14:34). The "opacity-0 group-hover:opacity-100"
* approach was considered but a persistent low-opacity
* display is friendlier for accessibility.
*/}
<span
className={`text-[10px] text-slate-400 px-1 ${
message.role === "user" ? "text-right" : "text-left"
}`}
>
{message.timestamp.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
</span>
</div>
</div>
</div>
Expand Down