From 2b6b7186d3748de5c2a971dc222ad9279a60416f Mon Sep 17 00:00:00 2001 From: Manmohan Sharma Date: Wed, 22 Apr 2026 15:31:00 -0700 Subject: [PATCH] feat(ui): cleaner input layout + sanitize model-output artifacts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ChatInput: textarea on top, inline tool pills (Think, Search) on the left and send button on the right — single rounded pod, no more bolted-on feel. Smaller pill buttons with subtle ring instead of heavy borders. MessageBubble: add sanitizeModelOutput() that strips training-artifact leaks: /// HTML tags, stray standalone '<' markers, leading 'Answer:/Response:' labels, placeholder image markdown. Applied before tool-marker parsing so cleaned text also feeds the card renderer. --- .../frontend/components/chat/ChatInput.tsx | 93 +++++++++---------- .../components/chat/MessageBubble.tsx | 26 +++++- 2 files changed, 67 insertions(+), 52 deletions(-) diff --git a/services/frontend/components/chat/ChatInput.tsx b/services/frontend/components/chat/ChatInput.tsx index 90639708..b0be5a56 100644 --- a/services/frontend/components/chat/ChatInput.tsx +++ b/services/frontend/components/chat/ChatInput.tsx @@ -43,10 +43,10 @@ export default function ChatInput({ value, onChange, onSubmit, onStop, isStreami return (
- {/* Input pod — single rounded container with the button inside */} + {/* Input pod — textarea on top, tool row + send below, single rounded container */}
onChange(e.target.value)} onKeyDown={handleKeyDown} disabled={disabled} - className="flex-1 resize-none bg-transparent px-5 py-4 pr-2 text-[0.95rem] leading-relaxed text-gray-900 dark:text-ink-text placeholder-gray-400 dark:placeholder-ink-text-soft focus:outline-none min-h-[52px] max-h-[200px]" + className="resize-none bg-transparent px-5 pt-4 pb-1 text-[0.95rem] leading-relaxed text-gray-900 dark:text-ink-text placeholder-gray-400 dark:placeholder-ink-text-soft focus:outline-none min-h-[48px] max-h-[200px]" /> - {/* Think toggle */} - {onToggleThinking && ( -
- + {/* Tool row: subtle inline pills on the left, send on the right */} +
+
+ {onToggleThinking && ( + + )} + {onToggleWebSearch && ( + + )}
- )} - {/* Force web-search toggle */} - {onToggleWebSearch && ( -
- -
- )} - - {/* Send / stop button — vertically centered with the textarea baseline */} -
{isStreaming && onStop ? ( ) : ( )}
diff --git a/services/frontend/components/chat/MessageBubble.tsx b/services/frontend/components/chat/MessageBubble.tsx index 7d7f9a59..034b7475 100644 --- a/services/frontend/components/chat/MessageBubble.tsx +++ b/services/frontend/components/chat/MessageBubble.tsx @@ -17,10 +17,30 @@ type Segment = | { kind: 'tool_call'; content: string; closed: boolean } | { kind: 'tool_result'; content: string; closed: boolean }; +function sanitizeModelOutput(s: string): string { + // Strip training-artifact leaks the small model sometimes emits: + // - HTML bold/italic tags from /// that were never meant to render + // - Stray leading "<" (a dangling tool marker that was never closed — cosmetic only) + // - Leading "Answer:" / "Response:" labels that are training-data prefixes + // - Markdown image references whose payload is clearly placeholder text (e.g. [something image]) + // - Duplicate paragraphs the model sometimes emits verbatim after <|output_end|> + s = s.replace(/<\/?(?:b|i|strong|em|u|small|big|code)\s*>/gi, ''); + // stray standalone "<" on its own line + s = s.replace(/^\s*<\s*$/gm, ''); + // leading "Answer:" / "Response:" labels at start of a line + s = s.replace(/^\s*(?:Answer|Response|Final answer|Reply)\s*:\s*/gim, ''); + // placeholder-image markdown e.g. ![Diary entry for samosa history] + s = s.replace(/!\[[^\]]*?\](?!\()/g, ''); + // normalize double newlines + s = s.replace(/\n{3,}/g, '\n\n'); + return s.trim(); +} + function parseSegments(raw: string): Segment[] { - // First pass: strip orphan tool markers (end-tag without open-tag, or any - // stray marker outside a pair) that the model sometimes emits as loop - // artifacts — otherwise they leak into the message body as raw text. + // Sanitize first — remove HTML tag leaks / stray Answer: / orphan "<" + raw = sanitizeModelOutput(raw); + // Then strip orphan tool markers (end without open) that the model sometimes + // emits as loop artifacts. raw = stripOrphanMarkers(raw); const segs: Segment[] = [];