mirror of
https://github.com/karpathy/nanochat.git
synced 2026-05-15 20:27:36 +00:00
Merge pull request #58 from manmohan659/feat/ui-input-redesign-and-sanitize
feat(ui): cleaner input + sanitize model-output artifacts
This commit is contained in:
commit
56988b8e06
|
|
@ -43,10 +43,10 @@ export default function ChatInput({ value, onChange, onSubmit, onStop, isStreami
|
|||
return (
|
||||
<div className="sticky bottom-0 bg-white/85 dark:bg-ink/85 backdrop-blur pt-3 pb-[calc(1rem+env(safe-area-inset-bottom))] px-4">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
{/* Input pod — single rounded container with the button inside */}
|
||||
{/* Input pod — textarea on top, tool row + send below, single rounded container */}
|
||||
<div
|
||||
className={clsx(
|
||||
'relative flex items-end gap-2 rounded-[26px] border bg-white dark:bg-ink-soft transition-shadow',
|
||||
'flex flex-col rounded-[24px] border bg-white dark:bg-ink-soft transition-shadow',
|
||||
'border-cream-border dark:border-ink-border',
|
||||
'focus-within:border-saffron/60 dark:focus-within:border-saffron/50 focus-within:shadow-[0_8px_30px_rgba(255,153,51,0.12)]',
|
||||
)}
|
||||
|
|
@ -59,61 +59,56 @@ export default function ChatInput({ value, onChange, onSubmit, onStop, isStreami
|
|||
onChange={(e) => 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 && (
|
||||
<div className="self-end p-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggleThinking}
|
||||
aria-pressed={!!thinkingMode}
|
||||
title={thinkingMode ? 'Reasoning mode ON — model will think step-by-step' : 'Enable reasoning mode'}
|
||||
className={clsx(
|
||||
'h-10 px-3 rounded-full flex items-center gap-1.5 text-xs font-medium transition-all border',
|
||||
thinkingMode
|
||||
? 'bg-saffron/15 dark:bg-saffron/20 border-saffron/40 dark:border-saffron/50 text-saffron dark:text-saffron-soft shadow-[0_4px_14px_rgba(255,153,51,0.15)]'
|
||||
: 'bg-transparent border-cream-border dark:border-ink-border text-gray-500 dark:text-ink-text-soft hover:bg-gray-50 dark:hover:bg-ink-elev',
|
||||
)}
|
||||
>
|
||||
<Brain size={14} />
|
||||
<span>Think</span>
|
||||
</button>
|
||||
{/* Tool row: subtle inline pills on the left, send on the right */}
|
||||
<div className="flex items-center justify-between gap-2 px-2 pb-2">
|
||||
<div className="flex items-center gap-1">
|
||||
{onToggleThinking && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggleThinking}
|
||||
aria-pressed={!!thinkingMode}
|
||||
title={thinkingMode ? 'Thinking on — step-by-step reasoning' : 'Think step-by-step before answering'}
|
||||
className={clsx(
|
||||
'h-8 px-2.5 rounded-full inline-flex items-center gap-1.5 text-[12px] font-medium transition-all',
|
||||
thinkingMode
|
||||
? 'bg-saffron/10 text-saffron dark:text-saffron-soft ring-1 ring-saffron/40'
|
||||
: 'text-gray-500 dark:text-ink-text-soft hover:bg-gray-100 dark:hover:bg-ink-elev',
|
||||
)}
|
||||
>
|
||||
<Brain size={14} />
|
||||
<span>Think</span>
|
||||
</button>
|
||||
)}
|
||||
{onToggleWebSearch && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggleWebSearch}
|
||||
aria-pressed={!!webSearchMode}
|
||||
title={webSearchMode ? 'Web search on — every message is searched online' : 'Force a web search'}
|
||||
className={clsx(
|
||||
'h-8 px-2.5 rounded-full inline-flex items-center gap-1.5 text-[12px] font-medium transition-all',
|
||||
webSearchMode
|
||||
? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 ring-1 ring-emerald-500/40'
|
||||
: 'text-gray-500 dark:text-ink-text-soft hover:bg-gray-100 dark:hover:bg-ink-elev',
|
||||
)}
|
||||
>
|
||||
<Globe size={14} />
|
||||
<span>Search</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Force web-search toggle */}
|
||||
{onToggleWebSearch && (
|
||||
<div className="self-end p-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggleWebSearch}
|
||||
aria-pressed={!!webSearchMode}
|
||||
title={webSearchMode ? 'Web search ON — every message will be searched online' : 'Force a web search for your next message'}
|
||||
className={clsx(
|
||||
'h-10 px-3 rounded-full flex items-center gap-1.5 text-xs font-medium transition-all border',
|
||||
webSearchMode
|
||||
? 'bg-emerald-500/15 dark:bg-emerald-500/20 border-emerald-500/50 text-emerald-600 dark:text-emerald-400 shadow-[0_4px_14px_rgba(16,185,129,0.15)]'
|
||||
: 'bg-transparent border-cream-border dark:border-ink-border text-gray-500 dark:text-ink-text-soft hover:bg-gray-50 dark:hover:bg-ink-elev',
|
||||
)}
|
||||
>
|
||||
<Globe size={14} />
|
||||
<span>Search</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Send / stop button — vertically centered with the textarea baseline */}
|
||||
<div className="self-end p-2">
|
||||
{isStreaming && onStop ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onStop}
|
||||
className="w-10 h-10 rounded-full bg-chutney-red text-white flex items-center justify-center hover:brightness-110 transition shadow-md"
|
||||
className="w-9 h-9 rounded-full bg-chutney-red text-white flex items-center justify-center hover:brightness-110 transition shadow-md"
|
||||
aria-label="Stop generating"
|
||||
>
|
||||
<Square size={14} fill="currentColor" />
|
||||
<Square size={12} fill="currentColor" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
|
|
@ -121,14 +116,14 @@ export default function ChatInput({ value, onChange, onSubmit, onStop, isStreami
|
|||
onClick={onSubmit}
|
||||
disabled={!canSend}
|
||||
className={clsx(
|
||||
'w-10 h-10 rounded-full flex items-center justify-center transition-all',
|
||||
'w-9 h-9 rounded-full flex items-center justify-center transition-all',
|
||||
canSend
|
||||
? 'bg-gray-900 dark:bg-ink-text text-white dark:text-ink shadow-[0_6px_18px_rgba(0,0,0,0.2)] hover:-translate-y-px'
|
||||
: 'bg-gray-200 dark:bg-ink-elev text-gray-400 dark:text-ink-text-soft cursor-not-allowed',
|
||||
)}
|
||||
aria-label="Send message"
|
||||
>
|
||||
<ArrowUp size={18} strokeWidth={2.4} />
|
||||
<ArrowUp size={16} strokeWidth={2.4} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 <b>/<i>/<strong>/<em> 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[] = [];
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user