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
175 changes: 175 additions & 0 deletions packages/mukti-web/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,22 @@

/* RGB values for animations */
--primary-rgb: 53, 53, 54;

/* Assistant response semantic tokens (light mode) */
--assistant-text: #1f2937;
--assistant-muted: #5c6676;
--assistant-border: #d7dde7;
--assistant-heading: #3a4454;
--assistant-accent-info: #2f5f8f;
--assistant-accent-success: #2e6a54;
--assistant-accent-warning: #8a642f;
--assistant-accent-question: #355c88;
--assistant-code-bg: #f4f7fb;
--assistant-code-border: #d7e0eb;
--assistant-callout-question-bg: #eef4fb;
--assistant-callout-question-border: #6f8bab;
--assistant-callout-default-bg: #f8fafc;
--assistant-callout-default-border: #d7dde7;
}

.dark {
Expand Down Expand Up @@ -184,6 +200,22 @@

/* RGB values for animations */
--primary-rgb: 234, 234, 235;

/* Assistant response semantic tokens (dark mode) */
--assistant-text: #e6ecf4;
--assistant-muted: #a1adbd;
--assistant-border: #2a3544;
--assistant-heading: #c7d2e2;
--assistant-accent-info: #8eb0d3;
--assistant-accent-success: #86b8a1;
--assistant-accent-warning: #c9a673;
--assistant-accent-question: #a6bedb;
--assistant-code-bg: #151c27;
--assistant-code-border: #2f3c4d;
--assistant-callout-question-bg: #182231;
--assistant-callout-question-border: #5d7b9b;
--assistant-callout-default-bg: #141b26;
--assistant-callout-default-border: #2a3544;
}

@layer base {
Expand All @@ -196,6 +228,149 @@
}
}

/* Assistant response typography and semantics */
.assistant-markdown {
color: var(--assistant-text);
line-height: 1.7;
max-width: 72ch;
overflow-wrap: anywhere;
text-wrap: pretty;
}

.assistant-markdown .assistant-paragraph {
margin: 0 0 0.85rem;
}

.assistant-markdown .assistant-paragraph:last-child {
margin-bottom: 0;
}

.assistant-markdown .assistant-section-heading {
border-bottom: 1px solid var(--assistant-border);
color: var(--assistant-heading);
font-size: 0.82rem;
font-weight: 600;
letter-spacing: 0.04em;
margin: 1.35rem 0 0.65rem;
padding-bottom: 0.3rem;
text-transform: uppercase;
}

.assistant-markdown .assistant-section-heading:first-child {
margin-top: 0;
}

.assistant-markdown .assistant-list {
margin: 0.4rem 0 1rem;
padding-left: 1.35rem;
}

.assistant-markdown .assistant-list-ordered {
list-style: decimal;
}

.assistant-markdown .assistant-list-unordered {
list-style: disc;
}

.assistant-markdown .assistant-list-item {
margin-bottom: 0.5rem;
padding-left: 0.15rem;
}

.assistant-markdown .assistant-list-item:last-child {
margin-bottom: 0;
}

.assistant-markdown .assistant-link {
color: var(--assistant-accent-info);
text-decoration: underline;
text-decoration-thickness: 1px;
text-underline-offset: 0.18em;
}

.assistant-markdown .assistant-inline-code {
background: var(--assistant-code-bg);
border: 1px solid var(--assistant-code-border);
border-radius: 0.375rem;
font-family: var(--font-mono);
font-size: 0.86em;
padding: 0.1rem 0.35rem;
}

.assistant-markdown .assistant-pre {
background: var(--assistant-code-bg);
border: 1px solid var(--assistant-code-border);
border-radius: 0.625rem;
margin: 0.55rem 0 1rem;
overflow-x: auto;
padding: 0.8rem 0.95rem;
}

.assistant-markdown .assistant-code-block-content {
background: transparent;
border: 0;
color: var(--assistant-text);
display: block;
font-family: var(--font-mono);
font-size: 0.85rem;
line-height: 1.6;
}

.assistant-markdown .assistant-rule {
border-color: var(--assistant-border);
margin: 1rem 0;
}

.assistant-markdown .assistant-blockquote {
border-left: 3px solid var(--assistant-callout-default-border);
border-radius: 0.25rem;
margin: 0.55rem 0 1rem;
padding: 0.65rem 0.85rem;
}

.assistant-markdown .assistant-default-callout {
background: var(--assistant-callout-default-bg);
}

.assistant-markdown .assistant-question-callout {
background: var(--assistant-callout-question-bg);
border-left-color: var(--assistant-callout-question-border);
}

.assistant-markdown .assistant-question-callout .assistant-paragraph:first-child {
color: var(--assistant-accent-question);
font-weight: 700;
margin-bottom: 0.45rem;
}

.assistant-markdown .assistant-question-callout .assistant-list {
margin-bottom: 0;
margin-top: 0.35rem;
}

.assistant-markdown .assistant-table-wrap {
margin: 0.6rem 0 1rem;
overflow-x: auto;
}

.assistant-markdown .assistant-table {
border-collapse: collapse;
font-size: 0.9rem;
min-width: 100%;
}

.assistant-markdown .assistant-table-cell,
.assistant-markdown .assistant-table-head {
border: 1px solid var(--assistant-border);
padding: 0.45rem 0.6rem;
text-align: left;
}

.assistant-markdown .assistant-table-header-row {
background: var(--assistant-code-bg);
}

/* Custom animations */
@keyframes fadeIn {
from {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export function ChatMessage({ message }: ChatMessageProps) {
{isUser ? (
<p className="whitespace-pre-wrap break-words text-sm">{message.content}</p>
) : (
<Markdown className="text-sm">{message.content}</Markdown>
<Markdown className="text-[0.94rem] leading-6">{message.content}</Markdown>
)}
</div>

Expand Down
8 changes: 4 additions & 4 deletions packages/mukti-web/src/components/conversations/message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ export function Message({ message }: MessageProps) {
>
<div
className={cn(
'max-w-[85%] rounded-2xl px-6 py-4 transition-all duration-300',
'w-full max-w-[85%] rounded-2xl px-6 py-4 transition-all duration-300',
isUser
? 'bg-white/10 text-foreground backdrop-blur-sm'
: 'bg-transparent text-foreground px-0 pl-2'
: 'bg-transparent px-0 pl-2 text-[var(--assistant-text)]'
)}
>
<div className="flex items-start gap-2">
Expand All @@ -41,7 +41,7 @@ export function Message({ message }: MessageProps) {
{message.content}
</p>
) : (
<Markdown className="text-base leading-relaxed prose-invert prose-p:leading-relaxed prose-pre:bg-white/5 prose-pre:border prose-pre:border-white/10">
<Markdown className="text-[0.99rem] leading-7 md:max-w-[76ch]">
{message.content}
</Markdown>
)}
Expand All @@ -51,7 +51,7 @@ export function Message({ message }: MessageProps) {
<div
className={cn(
'flex items-center gap-2 text-[10px] opacity-40 uppercase tracking-widest mt-2',
isUser ? 'justify-end' : 'justify-start'
isUser ? 'justify-end' : 'justify-start text-[var(--assistant-muted)]'
)}
>
<time dateTime={message.timestamp}>
Expand Down
101 changes: 101 additions & 0 deletions packages/mukti-web/src/components/ui/__tests__/markdown.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import type { AssistantMarkdownNode } from '@/components/ui/markdown';

import { transformAssistantMarkdownNodes } from '@/components/ui/markdown';

function createTextParagraph(text: string): AssistantMarkdownNode {
return {
children: [{ type: 'text', value: text }],
type: 'paragraph',
};
}

function textFromNode(node: AssistantMarkdownNode | undefined): string {
if (!node) {
return '';
}

if (typeof node.value === 'string') {
return node.value;
}

if (!node.children || node.children.length === 0) {
return '';
}

return node.children.map((child) => textFromNode(child)).join('');
}

describe('transformAssistantMarkdownNodes', () => {
it('converts label-style summary text to a section heading and paragraph', () => {
const nodes: AssistantMarkdownNode[] = [createTextParagraph('Summary: Keep it concise.')];

const transformed = transformAssistantMarkdownNodes(nodes);

expect(transformed).toHaveLength(2);
expect(transformed[0].type).toBe('heading');
expect(transformed[0].depth).toBe(3);
expect(textFromNode(transformed[0])).toBe('Summary');
expect(transformed[1].type).toBe('paragraph');
expect(textFromNode(transformed[1])).toBe('Keep it concise.');
});

it('wraps question sections in a semantic question callout and converts lists to ordered', () => {
const nodes: AssistantMarkdownNode[] = [
{
children: [{ type: 'text', value: 'Questions to consider' }],
depth: 2,
type: 'heading',
},
{
children: [
{
children: [createTextParagraph('What changed in this branch?')],
spread: false,
type: 'listItem',
},
{
children: [createTextParagraph('How should we verify it?')],
spread: false,
type: 'listItem',
},
],
ordered: false,
spread: false,
type: 'list',
},
createTextParagraph('Summary: Ready to ship'),
];

const transformed = transformAssistantMarkdownNodes(nodes);
const questionCallout = transformed[0];

expect(questionCallout.type).toBe('blockquote');
expect(questionCallout.data?.hProperties?.['data-callout']).toBe('question');
expect(textFromNode(questionCallout.children?.[0])).toBe('Question:');
expect(questionCallout.children?.[1].type).toBe('list');
expect(questionCallout.children?.[1].ordered).toBe(true);
expect(transformed[1].type).toBe('heading');
expect(textFromNode(transformed[1])).toBe('Summary');
expect(textFromNode(transformed[2])).toBe('Ready to ship');
});

it('renders multiple question paragraphs as a numbered list in the callout', () => {
const nodes: AssistantMarkdownNode[] = [
createTextParagraph('Question: What changed?'),
createTextParagraph('Which file should be updated?'),
createTextParagraph('Details: Focus only on message rendering.'),
];

const transformed = transformAssistantMarkdownNodes(nodes);
const questionCallout = transformed[0];
const orderedList = questionCallout.children?.[1];

expect(questionCallout.type).toBe('blockquote');
expect(textFromNode(questionCallout.children?.[0])).toBe('Question:');
expect(orderedList?.type).toBe('list');
expect(orderedList?.ordered).toBe(true);
expect(orderedList?.children).toHaveLength(2);
expect(textFromNode(transformed[1])).toBe('Details');
expect(textFromNode(transformed[2])).toBe('Focus only on message rendering.');
});
});
Loading
Loading