style: refine chat UI consistency and enhance dark mode (#393)
This commit is contained in:
committed by
GitHub
Unverified
parent
99681777a0
commit
9502d9b1c5
@@ -319,7 +319,7 @@ function ProviderCard({
|
|||||||
setFallbackModelsText(normalizeFallbackModels(account.fallbackModels).join('\n'));
|
setFallbackModelsText(normalizeFallbackModels(account.fallbackModels).join('\n'));
|
||||||
setFallbackProviderIds(normalizeFallbackProviderIds(account.fallbackAccountIds));
|
setFallbackProviderIds(normalizeFallbackProviderIds(account.fallbackAccountIds));
|
||||||
}
|
}
|
||||||
}, [isEditing, account.baseUrl, account.fallbackModels, account.fallbackAccountIds, account.model]);
|
}, [isEditing, account.baseUrl, account.fallbackModels, account.fallbackAccountIds, account.model, account.apiProtocol]);
|
||||||
|
|
||||||
const fallbackOptions = allProviders.filter((candidate) => candidate.account.id !== account.id);
|
const fallbackOptions = allProviders.filter((candidate) => candidate.account.id !== account.id);
|
||||||
|
|
||||||
@@ -408,8 +408,8 @@ function ProviderCard({
|
|||||||
className={cn(
|
className={cn(
|
||||||
"group flex flex-col p-4 rounded-2xl transition-all relative overflow-hidden",
|
"group flex flex-col p-4 rounded-2xl transition-all relative overflow-hidden",
|
||||||
isDefault
|
isDefault
|
||||||
? "bg-white dark:bg-[#1a1a19] border border-blue-500/20 shadow-sm ring-1 ring-blue-500/20"
|
? "bg-white dark:bg-[#1a1a19] border border-black/10 dark:border-white/10 shadow-sm"
|
||||||
: "bg-black/5 dark:bg-white/5 border border-transparent"
|
: "bg-transparent border border-black/10 dark:border-white/10"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -502,9 +502,9 @@ function ProviderCard({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isEditing && (
|
{isEditing && (
|
||||||
<div className="space-y-4 mt-4 pt-4 border-t border-black/5 dark:border-white/5">
|
<div className="space-y-6 mt-4 pt-4 border-t border-black/5 dark:border-white/5">
|
||||||
{canEditModelConfig && (
|
{canEditModelConfig && (
|
||||||
<div className="space-y-3 rounded-xl bg-[#eeece3] dark:bg-[#151514] border border-black/5 dark:border-white/5 p-4">
|
<div className="space-y-3">
|
||||||
<p className="text-[14px] font-bold text-foreground/80">{t('aiProviders.sections.model')}</p>
|
<p className="text-[14px] font-bold text-foreground/80">{t('aiProviders.sections.model')}</p>
|
||||||
{typeInfo?.showBaseUrl && (
|
{typeInfo?.showBaseUrl && (
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
@@ -551,7 +551,7 @@ function ProviderCard({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="space-y-3 rounded-xl bg-[#eeece3] dark:bg-[#151514] border border-black/5 dark:border-white/5 p-4">
|
<div className="space-y-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowFallback(!showFallback)}
|
onClick={() => setShowFallback(!showFallback)}
|
||||||
className="flex items-center justify-between w-full text-[14px] font-bold text-foreground/80 hover:text-foreground transition-colors"
|
className="flex items-center justify-between w-full text-[14px] font-bold text-foreground/80 hover:text-foreground transition-colors"
|
||||||
@@ -599,7 +599,7 @@ function ProviderCard({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3 rounded-xl bg-[#eeece3] dark:bg-[#151514] border border-black/5 dark:border-white/5 p-4">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<Label className="text-[14px] font-bold text-foreground/80">{t('aiProviders.dialog.apiKey')}</Label>
|
<Label className="text-[14px] font-bold text-foreground/80">{t('aiProviders.dialog.apiKey')}</Label>
|
||||||
|
|||||||
@@ -353,7 +353,7 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false, i
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Input Row */}
|
{/* 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 */}
|
{/* Attach Button */}
|
||||||
<Button
|
<Button
|
||||||
@@ -383,7 +383,7 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false, i
|
|||||||
onPaste={handlePaste}
|
onPaste={handlePaste}
|
||||||
placeholder={disabled ? 'Gateway not connected...' : ''}
|
placeholder={disabled ? 'Gateway not connected...' : ''}
|
||||||
disabled={disabled}
|
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}
|
rows={1}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* with markdown, thinking sections, images, and tool cards.
|
* with markdown, thinking sections, images, and tool cards.
|
||||||
*/
|
*/
|
||||||
import { useState, useCallback, useEffect, memo } from 'react';
|
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 ReactMarkdown from 'react-markdown';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
@@ -71,16 +71,11 @@ export const ChatMessage = memo(function ChatMessage({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Avatar */}
|
{/* Avatar */}
|
||||||
<div
|
{!isUser && (
|
||||||
className={cn(
|
<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">
|
||||||
'flex h-8 w-8 shrink-0 items-center justify-center rounded-full mt-1',
|
<Sparkles className="h-4 w-4" />
|
||||||
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>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div
|
<div
|
||||||
@@ -148,7 +143,7 @@ export const ChatMessage = memo(function ChatMessage({
|
|||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
key={`local-${i}`}
|
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" />
|
<File className="h-8 w-8" />
|
||||||
</div>
|
</div>
|
||||||
@@ -209,7 +204,7 @@ export const ChatMessage = memo(function ChatMessage({
|
|||||||
}
|
}
|
||||||
if (isImage && !file.preview) {
|
if (isImage && !file.preview) {
|
||||||
return (
|
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" />
|
<File className="h-8 w-8" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -286,7 +281,7 @@ function ToolStatusBar({
|
|||||||
{isError && <AlertCircle className="h-3.5 w-3.5 text-destructive shrink-0" />}
|
{isError && <AlertCircle className="h-3.5 w-3.5 text-destructive shrink-0" />}
|
||||||
<Wrench className="h-3 w-3 shrink-0 opacity-60" />
|
<Wrench className="h-3 w-3 shrink-0 opacity-60" />
|
||||||
<span className="font-mono text-[12px] font-medium">{tool.name}</span>
|
<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 && (
|
{tool.summary && (
|
||||||
<span className="truncate text-[11px] opacity-70">{tool.summary}</span>
|
<span className="truncate text-[11px] opacity-70">{tool.summary}</span>
|
||||||
)}
|
)}
|
||||||
@@ -342,8 +337,8 @@ function MessageBubble({
|
|||||||
'relative rounded-2xl px-4 py-3',
|
'relative rounded-2xl px-4 py-3',
|
||||||
!isUser && 'w-full',
|
!isUser && 'w-full',
|
||||||
isUser
|
isUser
|
||||||
? 'bg-primary text-primary-foreground'
|
? 'bg-[#0a84ff] text-white shadow-sm'
|
||||||
: 'bg-muted',
|
: 'bg-black/5 dark:bg-white/5 text-foreground',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isUser ? (
|
{isUser ? (
|
||||||
@@ -398,7 +393,7 @@ function ThinkingBlock({ content }: { content: string }) {
|
|||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
|
||||||
return (
|
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
|
<button
|
||||||
className="flex items-center gap-2 w-full px-3 py-2 text-muted-foreground hover:text-foreground transition-colors"
|
className="flex items-center gap-2 w-full px-3 py-2 text-muted-foreground hover:text-foreground transition-colors"
|
||||||
onClick={() => setExpanded(!expanded)}
|
onClick={() => setExpanded(!expanded)}
|
||||||
@@ -437,7 +432,7 @@ function FileIcon({ mimeType, className }: { mimeType: string; className?: strin
|
|||||||
|
|
||||||
function FileCard({ file }: { file: AttachedFileMeta }) {
|
function FileCard({ file }: { file: AttachedFileMeta }) {
|
||||||
return (
|
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" />
|
<FileIcon mimeType={file.mimeType} className="h-5 w-5 shrink-0 text-muted-foreground" />
|
||||||
<div className="min-w-0 overflow-hidden">
|
<div className="min-w-0 overflow-hidden">
|
||||||
<p className="text-xs font-medium truncate">{file.fileName}</p>
|
<p className="text-xs font-medium truncate">{file.fileName}</p>
|
||||||
@@ -469,7 +464,7 @@ function ImageThumbnail({
|
|||||||
void filePath; void base64; void mimeType;
|
void filePath; void base64; void mimeType;
|
||||||
return (
|
return (
|
||||||
<div
|
<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}
|
onClick={onPreview}
|
||||||
>
|
>
|
||||||
<img src={src} alt={fileName} className="w-full h-full object-cover" />
|
<img src={src} alt={fileName} className="w-full h-full object-cover" />
|
||||||
@@ -500,7 +495,7 @@ function ImagePreviewCard({
|
|||||||
void filePath; void base64; void mimeType;
|
void filePath; void base64; void mimeType;
|
||||||
return (
|
return (
|
||||||
<div
|
<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}
|
onClick={onPreview}
|
||||||
>
|
>
|
||||||
<img src={src} alt={fileName} className="block w-full" />
|
<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);
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
|
||||||
return (
|
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
|
<button
|
||||||
className="flex items-center gap-2 w-full px-3 py-1.5 text-muted-foreground hover:text-foreground transition-colors"
|
className="flex items-center gap-2 w-full px-3 py-1.5 text-muted-foreground hover:text-foreground transition-colors"
|
||||||
onClick={() => setExpanded(!expanded)}
|
onClick={() => setExpanded(!expanded)}
|
||||||
|
|||||||
@@ -77,18 +77,7 @@ export function Chat() {
|
|||||||
}
|
}
|
||||||
}, [sending, streamingTimestamp]);
|
}, [sending, streamingTimestamp]);
|
||||||
|
|
||||||
// Gateway not running
|
// Gateway not running block has been completely removed so the UI always renders.
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const streamMsg = streamingMessage && typeof streamingMessage === 'object'
|
const streamMsg = streamingMessage && typeof streamingMessage === 'object'
|
||||||
? streamingMessage as unknown as { role?: string; content?: unknown; timestamp?: number }
|
? streamingMessage as unknown as { role?: string; content?: unknown; timestamp?: number }
|
||||||
|
|||||||
Reference in New Issue
Block a user