Files
gitmost/apps/client/src/features/ai-chat/components/ai-chat.module.css
claude code agent 227 d14ab4a012 feat(ai-chat): compact reasoning rendering — collapse blank lines (#181)
The "Thinking" (reasoning) block rendered with large vertical gaps: models
emit reasoning with a blank line (\n\n) between every list item and
paragraph, which `marked` turns into loose lists (each <li> wrapped in a
<p>) and separate <p> paragraphs, each carrying a margin.

- Add `collapseBlankLines(text)`: collapse 2+ newlines to one, EXCEPT inside
  fenced code blocks (``` / ~~~) where blank lines are significant. Applied
  in reasoning-block.tsx before renderChatMarkdown, so loose lists become
  tight (no <li><p>) and paragraphs join; `breaks: true` keeps single \n as
  <br>, preserving line breaks. Reasoning-only — the normal answer is
  untouched.
- Drop `white-space: pre-wrap` from `.reasoningText`: on the rendered
  markdown <div> it turned the newlines between block tags into visible
  blank lines on top of the margins. The plain-text fallback <Text> that
  needs pre-wrap already sets it inline.

Tests: collapseBlankLines unit (collapse, fence preservation incl. tilde and
unclosed fences) + rendered-HTML assertions that a blank-line-separated list
becomes a tight list and still parses as a list after a paragraph.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 05:17:56 +03:00

179 lines
4.3 KiB
CSS

.panel {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
}
.messages {
flex: 1 1 auto;
min-height: 0;
/* Smaller, denser chat text (cascades into user bubble + assistant markdown
+ tool cards; the explicit-size Mantine labels keep their own size). */
font-size: var(--mantine-font-size-xs);
}
.messageRow {
margin-bottom: var(--mantine-spacing-md);
}
.userBubble {
background: var(--mantine-color-gray-light);
border-radius: var(--mantine-radius-md);
padding: 8px 12px;
white-space: pre-wrap;
overflow-wrap: break-word;
word-break: break-word;
}
/* Rendered markdown for assistant messages. Keep block margins compact. */
.markdown {
overflow-wrap: break-word;
word-break: break-word;
}
.markdown p {
margin-block-start: 0;
margin-block-end: 0.5em;
}
.markdown pre {
background: var(--mantine-color-gray-light);
border-radius: var(--mantine-radius-sm);
padding: 8px;
overflow-x: auto;
}
.markdown code {
font-size: 0.85em;
}
.markdown ul,
.markdown ol {
margin-block-start: 0;
margin-block-end: 0.5em;
padding-inline-start: 1.4em;
}
/* Animated three-dot "typing" indicator shown while the agent is thinking but
has not yet produced any visible text/tool parts. */
.typingDots {
display: inline-flex;
align-items: center;
gap: 4px;
}
.typingDots span {
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--mantine-color-dimmed, var(--mantine-color-gray-5));
opacity: 0.4;
animation: aiTypingBounce 1.2s infinite ease-in-out;
}
.typingDots span:nth-child(2) {
animation-delay: 0.2s;
}
.typingDots span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes aiTypingBounce {
0%,
80%,
100% {
transform: translateY(0);
opacity: 0.4;
}
40% {
/* Bounce height is driven by --bounce so reduced-motion can dampen it
(below) without disabling the animation outright. */
transform: translateY(var(--bounce, -6px));
opacity: 1;
}
}
/* Respect reduced-motion preferences: keep a smaller bounce instead of a full
stop, so the "thinking" indicator still reads as active rather than frozen. */
@media (prefers-reduced-motion: reduce) {
.typingDots span {
--bounce: -3px;
}
}
.toolCard {
border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
border-radius: var(--mantine-radius-sm);
padding: 6px 10px;
margin-bottom: 6px;
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
}
/* Collapsible "Thinking" (reasoning) block: a subtle left rule, dimmer than the
answer so it reads as secondary thinking context above the real answer. */
.reasoningBlock {
border-left: 2px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
padding-left: 8px;
}
.reasoningText {
margin-top: 4px;
font-size: var(--mantine-font-size-xs);
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
/* NOTE: `white-space: pre-wrap` is intentionally NOT set here. On the
rendered markdown <div> it would turn the newlines between block tags
(</li>\n<li>, </p>\n<ol>) into visible blank lines/indents on top of the
margins. The plain-text fallback <Text> that needs pre-wrap sets it
inline itself (see reasoning-block.tsx). */
}
.reasoningText p {
margin: 0 0 4px;
}
.inputWrapper {
flex: 0 0 auto;
padding-top: var(--mantine-spacing-xs);
}
.conversationItem {
cursor: pointer;
border-radius: var(--mantine-radius-sm);
}
.conversationItem:hover {
background: var(--mantine-color-gray-light);
}
.conversationItemActive {
background: var(--mantine-color-gray-light);
}
/* Pending messages queued by the user while a turn is still streaming. They
are sent automatically, FIFO, once the current turn finishes. */
.queuedList {
padding-bottom: var(--mantine-spacing-xs);
}
.queuedItem {
background: var(--mantine-color-gray-light);
border-radius: var(--mantine-radius-sm);
padding: 4px 8px;
}
.queuedIcon {
flex: none;
color: var(--mantine-color-dimmed);
}
.queuedText {
flex: 1;
min-width: 0;
color: var(--mantine-color-dimmed);
white-space: pre-wrap;
overflow-wrap: break-word;
word-break: break-word;
}