Two small ergonomic fixes after the impeccable pass: - TaskLane keyboard hints (⏎ submit · ⇧⏎ newline) under each open input were rendered at text-muted-foreground/70, just shy of legible at a glance. Drop the /70 opacity modifier so they read at full muted weight on first look without becoming visually loud. - 12 sites across the session screen had explicit font-sans utilities, but the body default is already IBM Plex Sans (via --font-sans in index.css and Tailwind v4's default-sans binding). None of the call sites sit inside a font-heading or font-mono cascade, so every font-sans there was a no-op. Drop them. ConcludeSessionModal also had three "text-xs font-sans text-xs" triplets — drop both the redundant font-sans and the doubled text-xs in one pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
86 lines
3.3 KiB
TypeScript
86 lines
3.3 KiB
TypeScript
import { Sparkles, User, ListChecks } from 'lucide-react'
|
|
import { MarkdownContent } from '@/components/ui/MarkdownContent'
|
|
import { SuggestedFlowCard } from './SuggestedFlowCard'
|
|
import type { SuggestedFlow } from '@/types/copilot'
|
|
|
|
interface ChatMessageProps {
|
|
role: 'user' | 'assistant'
|
|
content: string
|
|
suggestedFlows?: SuggestedFlow[]
|
|
imageUrls?: string[]
|
|
/** When set on an assistant message, renders a leading "Next steps · N pending"
|
|
* emphasis above the bubble. Used on the current turn only — the canonical
|
|
* list of items lives in the TaskLane. */
|
|
actionCount?: number
|
|
}
|
|
|
|
export function ChatMessage({ role, content, suggestedFlows, imageUrls, actionCount }: ChatMessageProps) {
|
|
const hasActionEmphasis = role === 'assistant' && actionCount !== undefined && actionCount > 0
|
|
return (
|
|
<div className={`flex gap-3 ${role === 'user' ? 'flex-row-reverse' : ''}`}>
|
|
{/* Avatar */}
|
|
<div
|
|
className={`shrink-0 w-8 h-8 rounded-full flex items-center justify-center ${
|
|
role === 'assistant'
|
|
? 'bg-primary/15 text-primary'
|
|
: 'bg-elevated text-muted-foreground'
|
|
}`}
|
|
>
|
|
{role === 'assistant' ? <Sparkles size={14} /> : <User size={14} />}
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className={`max-w-[80%] space-y-2 ${role === 'user' ? 'text-right' : ''}`}>
|
|
{/* Image attachments (user messages only) */}
|
|
{role === 'user' && imageUrls && imageUrls.length > 0 && (
|
|
<div className={`flex flex-wrap gap-2 ${role === 'user' ? 'justify-end' : ''}`}>
|
|
{imageUrls.map((url, i) => (
|
|
<a key={i} href={url} target="_blank" rel="noopener noreferrer">
|
|
<img
|
|
src={url}
|
|
alt={`Attachment ${i + 1}`}
|
|
className="h-24 w-auto max-w-[200px] rounded-xl object-cover border border-border cursor-pointer hover:opacity-90 transition-opacity"
|
|
/>
|
|
</a>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{hasActionEmphasis && (
|
|
<div className="flex items-center gap-1.5 text-xs font-medium text-heading">
|
|
<ListChecks size={12} className="text-primary" />
|
|
Next steps
|
|
<span className="text-muted-foreground font-normal">
|
|
· {actionCount} pending in Tasks
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
<div
|
|
className={`rounded-xl px-4 py-3 text-sm leading-relaxed ${
|
|
role === 'user'
|
|
? 'bg-primary/15 text-foreground'
|
|
: hasActionEmphasis
|
|
? 'bg-input text-foreground border border-hover'
|
|
: 'bg-input text-foreground border border-border'
|
|
}`}
|
|
>
|
|
<MarkdownContent content={content} className="text-sm leading-relaxed" />
|
|
</div>
|
|
|
|
{/* Suggested flows (assistant only) */}
|
|
{role === 'assistant' && suggestedFlows && suggestedFlows.length > 0 && (
|
|
<div className="space-y-1.5">
|
|
<span className="text-[0.625rem] uppercase tracking-widest text-muted-foreground">
|
|
Related Flows
|
|
</span>
|
|
{suggestedFlows.map(flow => (
|
|
<SuggestedFlowCard key={flow.tree_id} flow={flow} />
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|