style: refine chat UI consistency and enhance dark mode (#393)

This commit is contained in:
DigHuang
2026-03-10 18:09:46 +08:00
committed by GitHub
Unverified
parent 99681777a0
commit 9502d9b1c5
4 changed files with 26 additions and 42 deletions

View File

@@ -353,7 +353,7 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false, i
)}
{/* Input Row */}
<div className={`flex items-end gap-1.5 bg-white dark:bg-accent/50 rounded-[28px] shadow-sm border border-black/5 dark:border-white/10 p-1.5 transition-shadow ${dragOver ? 'ring-2 ring-primary' : 'focus-within:ring-1 focus-within:ring-black/5 dark:focus-within:ring-white/10'}`}>
<div className={`flex items-end gap-1.5 bg-white dark:bg-[#1a1a19] rounded-[28px] shadow-sm border p-1.5 transition-all ${dragOver ? 'border-primary ring-1 ring-primary' : 'border-black/10 dark:border-white/10'}`}>
{/* Attach Button */}
<Button
@@ -383,7 +383,7 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false, i
onPaste={handlePaste}
placeholder={disabled ? 'Gateway not connected...' : ''}
disabled={disabled}
className="min-h-[40px] max-h-[200px] resize-none border-0 focus-visible:ring-0 shadow-none bg-transparent py-2.5 px-2 text-[15px] placeholder:text-muted-foreground/60 leading-relaxed"
className="min-h-[40px] max-h-[200px] resize-none border-0 focus-visible:ring-0 focus-visible:ring-offset-0 shadow-none bg-transparent py-2.5 px-2 text-[15px] placeholder:text-muted-foreground/60 leading-relaxed"
rows={1}
/>
</div>

View File

@@ -4,7 +4,7 @@
* with markdown, thinking sections, images, and tool cards.
*/
import { useState, useCallback, useEffect, memo } from 'react';
import { User, Sparkles, Copy, Check, ChevronDown, ChevronRight, Wrench, FileText, Film, Music, FileArchive, File, X, FolderOpen, ZoomIn, Loader2, CheckCircle2, AlertCircle } from 'lucide-react';
import { Sparkles, Copy, Check, ChevronDown, ChevronRight, Wrench, FileText, Film, Music, FileArchive, File, X, FolderOpen, ZoomIn, Loader2, CheckCircle2, AlertCircle } from 'lucide-react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { createPortal } from 'react-dom';
@@ -71,16 +71,11 @@ export const ChatMessage = memo(function ChatMessage({
)}
>
{/* Avatar */}
<div
className={cn(
'flex h-8 w-8 shrink-0 items-center justify-center rounded-full mt-1',
isUser
? 'bg-primary text-primary-foreground'
: 'bg-gradient-to-br from-indigo-500 to-purple-600 text-white',
)}
>
{isUser ? <User className="h-4 w-4" /> : <Sparkles className="h-4 w-4" />}
</div>
{!isUser && (
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full mt-1 bg-black/5 dark:bg-white/5 text-foreground">
<Sparkles className="h-4 w-4" />
</div>
)}
{/* Content */}
<div
@@ -148,7 +143,7 @@ export const ChatMessage = memo(function ChatMessage({
) : (
<div
key={`local-${i}`}
className="w-36 h-36 rounded-xl border overflow-hidden bg-muted flex items-center justify-center text-muted-foreground"
className="w-36 h-36 rounded-xl border border-black/10 dark:border-white/10 bg-black/5 dark:bg-white/5 flex items-center justify-center text-muted-foreground"
>
<File className="h-8 w-8" />
</div>
@@ -209,7 +204,7 @@ export const ChatMessage = memo(function ChatMessage({
}
if (isImage && !file.preview) {
return (
<div key={`local-${i}`} className="w-36 h-36 rounded-xl border overflow-hidden bg-muted flex items-center justify-center text-muted-foreground">
<div key={`local-${i}`} className="w-36 h-36 rounded-xl border border-black/10 dark:border-white/10 bg-black/5 dark:bg-white/5 flex items-center justify-center text-muted-foreground">
<File className="h-8 w-8" />
</div>
);
@@ -286,7 +281,7 @@ function ToolStatusBar({
{isError && <AlertCircle className="h-3.5 w-3.5 text-destructive shrink-0" />}
<Wrench className="h-3 w-3 shrink-0 opacity-60" />
<span className="font-mono text-[12px] font-medium">{tool.name}</span>
{duration && <span className="text-[11px] opacity-60">{duration}</span>}
{duration && <span className="text-[11px] opacity-60">{tool.summary ? `(${duration})` : duration}</span>}
{tool.summary && (
<span className="truncate text-[11px] opacity-70">{tool.summary}</span>
)}
@@ -342,8 +337,8 @@ function MessageBubble({
'relative rounded-2xl px-4 py-3',
!isUser && 'w-full',
isUser
? 'bg-primary text-primary-foreground'
: 'bg-muted',
? 'bg-[#0a84ff] text-white shadow-sm'
: 'bg-black/5 dark:bg-white/5 text-foreground',
)}
>
{isUser ? (
@@ -398,7 +393,7 @@ function ThinkingBlock({ content }: { content: string }) {
const [expanded, setExpanded] = useState(false);
return (
<div className="w-full rounded-lg border border-border/50 bg-muted/30 text-sm">
<div className="w-full rounded-xl border border-black/10 dark:border-white/10 bg-black/5 dark:bg-white/5 text-[14px]">
<button
className="flex items-center gap-2 w-full px-3 py-2 text-muted-foreground hover:text-foreground transition-colors"
onClick={() => setExpanded(!expanded)}
@@ -437,7 +432,7 @@ function FileIcon({ mimeType, className }: { mimeType: string; className?: strin
function FileCard({ file }: { file: AttachedFileMeta }) {
return (
<div className="flex items-center gap-2 rounded-lg border border-border px-3 py-2 bg-muted/30 max-w-[220px]">
<div className="flex items-center gap-3 rounded-xl border border-black/10 dark:border-white/10 px-3 py-2.5 bg-black/5 dark:bg-white/5 max-w-[220px]">
<FileIcon mimeType={file.mimeType} className="h-5 w-5 shrink-0 text-muted-foreground" />
<div className="min-w-0 overflow-hidden">
<p className="text-xs font-medium truncate">{file.fileName}</p>
@@ -469,7 +464,7 @@ function ImageThumbnail({
void filePath; void base64; void mimeType;
return (
<div
className="relative w-36 h-36 rounded-xl border overflow-hidden bg-muted group/img cursor-zoom-in"
className="relative w-36 h-36 rounded-xl border overflow-hidden border-black/10 dark:border-white/10 bg-black/5 dark:bg-white/5 group/img cursor-zoom-in"
onClick={onPreview}
>
<img src={src} alt={fileName} className="w-full h-full object-cover" />
@@ -500,7 +495,7 @@ function ImagePreviewCard({
void filePath; void base64; void mimeType;
return (
<div
className="relative max-w-xs rounded-lg border overflow-hidden group/img cursor-zoom-in"
className="relative max-w-xs rounded-xl border overflow-hidden border-black/10 dark:border-white/10 bg-black/5 dark:bg-white/5 group/img cursor-zoom-in"
onClick={onPreview}
>
<img src={src} alt={fileName} className="block w-full" />
@@ -595,7 +590,7 @@ function ToolCard({ name, input }: { name: string; input: unknown }) {
const [expanded, setExpanded] = useState(false);
return (
<div className="rounded-lg border border-border/50 bg-muted/20 text-sm">
<div className="rounded-xl border border-black/10 dark:border-white/10 bg-black/5 dark:bg-white/5 text-[14px]">
<button
className="flex items-center gap-2 w-full px-3 py-1.5 text-muted-foreground hover:text-foreground transition-colors"
onClick={() => setExpanded(!expanded)}

View File

@@ -77,18 +77,7 @@ export function Chat() {
}
}, [sending, streamingTimestamp]);
// Gateway not running
if (!isGatewayRunning) {
return (
<div className="flex h-[calc(100vh-8rem)] flex-col items-center justify-center text-center p-8">
<AlertCircle className="h-12 w-12 text-yellow-500 mb-4" />
<h2 className="text-xl font-semibold mb-2">{t('gatewayNotRunning')}</h2>
<p className="text-muted-foreground max-w-md">
{t('gatewayRequired')}
</p>
</div>
);
}
// Gateway not running block has been completely removed so the UI always renders.
const streamMsg = streamingMessage && typeof streamingMessage === 'object'
? streamingMessage as unknown as { role?: string; content?: unknown; timestamp?: number }