style(frontend): redesign with Miuix HyperOS design language
- Color system: Miuix dark tokens (primary #277AF7, bg #242424, surface #000) - Components: rounded-full buttons, squircle cards (18px), glass header - Messages: blue user bubbles, dark AI bubbles with typing animation - Admin dashboard: stat cards with real-time service status - Login: centered logo with gradient shadow, HyperOS input fields - Smooth transitions and scale-on-press interactions
This commit is contained in:
1 parent
17ef73bcbe
commit
bea29d3b73
9 files changed
+312
-99
No files matched your search
@@ -23,8 +23,11 @@ export default function ChatContainer() {
|
||||
addMessage(userMsg)
|
||||
setStreaming(true)
|
||||
|
||||
// Add placeholder for assistant response
|
||||
addMessage({ id: crypto.randomUUID(), session_id: currentSessionId, role: 'assistant', content: '', created_at: new Date().toISOString() })
|
||||
// Placeholder for assistant
|
||||
addMessage({
|
||||
id: crypto.randomUUID(), session_id: currentSessionId,
|
||||
role: 'assistant' as const, content: '', created_at: new Date().toISOString(),
|
||||
})
|
||||
|
||||
send({ type: 'message', content: inputText, session_id: currentSessionId, timestamp: Date.now() })
|
||||
setInput('')
|
||||
@@ -32,16 +35,29 @@ export default function ChatContainer() {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex-1 overflow-auto px-4 py-4">
|
||||
<MessageList messages={messages} />
|
||||
<div ref={bottomRef} />
|
||||
{/* Chat area */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
<div className="max-w-2xl mx-auto px-4 py-6">
|
||||
{messages.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center space-y-2 pt-32">
|
||||
<div className="w-16 h-16 rounded-[20px] bg-[#277AF7] flex items-center justify-center shadow-lg shadow-[#277AF7]/20 mb-2">
|
||||
<span className="text-2xl font-bold text-white">C</span>
|
||||
</div>
|
||||
<p className="text-white/50 text-sm">开始新对话吧~</p>
|
||||
</div>
|
||||
) : (
|
||||
<MessageList messages={messages} />
|
||||
)}
|
||||
<div ref={bottomRef} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Input bar */}
|
||||
<div className="shrink-0 border-t border-[#393939] bg-[#000]">
|
||||
<div className="max-w-2xl mx-auto px-4 py-3">
|
||||
<ChatInput value={inputText} onChange={setInput} onSend={handleSend} disabled={isStreaming} />
|
||||
</div>
|
||||
</div>
|
||||
<ChatInput
|
||||
value={inputText}
|
||||
onChange={setInput}
|
||||
onSend={handleSend}
|
||||
disabled={isStreaming}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -9,30 +9,30 @@ export default function ChatInput({ value, onChange, onSend, disabled }: ChatInp
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
onSend()
|
||||
if (!disabled && value.trim()) onSend()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border-t border-gray-800 p-3 shrink-0">
|
||||
<div className="max-w-3xl mx-auto flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={disabled}
|
||||
placeholder={disabled ? '思考中...' : '输入消息...'}
|
||||
className="flex-1 bg-gray-800 rounded-lg px-4 py-2.5 text-sm outline-none focus:ring-2 focus:ring-purple-500 disabled:opacity-50"
|
||||
/>
|
||||
<button
|
||||
onClick={onSend}
|
||||
disabled={disabled || !value.trim()}
|
||||
className="bg-purple-600 hover:bg-purple-500 disabled:bg-gray-700 rounded-lg px-5 py-2.5 text-sm font-medium transition-colors disabled:cursor-not-allowed"
|
||||
>
|
||||
发送
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-end gap-2 bg-[#000] rounded-[20px] border border-[#404040] focus-within:border-[#277AF7] transition-colors p-1.5">
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={disabled}
|
||||
placeholder="输入消息..."
|
||||
className="flex-1 bg-transparent px-3 py-1.5 text-sm text-[#F2F2F2] placeholder:text-white/30 outline-none disabled:opacity-40"
|
||||
/>
|
||||
<button
|
||||
onClick={onSend}
|
||||
disabled={disabled || !value.trim()}
|
||||
className="w-9 h-9 bg-[#277AF7] hover:bg-[#3482FF] active:scale-90 disabled:bg-[#253E64] text-white rounded-full flex items-center justify-center shrink-0 transition-all duration-200"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M2 8L14 2L8 14L6.5 9.5L2 8Z" fill="currentColor" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,16 +1,8 @@
|
||||
import type { Message } from '../../types/chat'
|
||||
|
||||
export default function MessageList({ messages }: { messages: Message[] }) {
|
||||
if (messages.length === 0) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full text-gray-500 text-sm">
|
||||
开始新对话吧~
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4 max-w-3xl mx-auto">
|
||||
<div className="space-y-3">
|
||||
{messages.map((msg) => (
|
||||
<MessageBubble key={msg.id} message={msg} />
|
||||
))}
|
||||
@@ -21,22 +13,56 @@ export default function MessageList({ messages }: { messages: Message[] }) {
|
||||
function MessageBubble({ message }: { message: Message }) {
|
||||
const isUser = message.role === 'user'
|
||||
const isAction = message.role === 'action'
|
||||
const isSystem = message.role === 'system'
|
||||
const isEmpty = message.role === 'assistant' && !message.content
|
||||
|
||||
if (isAction) {
|
||||
return <div className="text-center text-gray-500 text-sm italic py-1">{message.content}</div>
|
||||
return (
|
||||
<div className="flex justify-center py-1">
|
||||
<span className="text-xs text-[#4788FF] bg-[#2B3B54]/80 px-3 py-1 rounded-full">
|
||||
{message.content}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (isSystem) {
|
||||
return (
|
||||
<div className="flex justify-center py-1">
|
||||
<span className="text-xs text-[#ffdad6] bg-[#2E0603]/80 px-3 py-1 rounded-full">{message.content}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`flex ${isUser ? 'justify-end' : 'justify-start'}`}>
|
||||
<div className={`max-w-[70%] rounded-2xl px-4 py-2.5 text-sm leading-relaxed ${
|
||||
{/* Avatar for AI */}
|
||||
{!isUser && (
|
||||
<div className="w-8 h-8 rounded-xl bg-[#277AF7] flex items-center justify-center shrink-0 mr-2 mt-1">
|
||||
<span className="text-xs font-bold text-white">C</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`max-w-[75%] px-4 py-2.5 text-sm leading-relaxed ${
|
||||
isUser
|
||||
? 'bg-purple-600 text-white rounded-br-md'
|
||||
: message.role === 'system'
|
||||
? 'bg-red-900/50 text-red-300 mx-auto text-xs'
|
||||
: 'bg-gray-800 text-gray-100 rounded-bl-md'
|
||||
? 'bg-[#277AF7] text-white rounded-[18px] rounded-br-[6px]'
|
||||
: isEmpty
|
||||
? 'bg-transparent text-transparent'
|
||||
: 'bg-[#000] text-[#F2F2F2] rounded-[18px] rounded-bl-[6px] border border-[#404040]'
|
||||
}`}>
|
||||
{message.content || (message.role === 'assistant' ? <span className="animate-pulse">...</span> : null)}
|
||||
{isEmpty ? (
|
||||
<span className="flex gap-1.5 py-1">
|
||||
<span className="w-2 h-2 rounded-full bg-[#404040] animate-bounce" style={{animationDelay:'0ms'}} />
|
||||
<span className="w-2 h-2 rounded-full bg-[#404040] animate-bounce" style={{animationDelay:'150ms'}} />
|
||||
<span className="w-2 h-2 rounded-full bg-[#404040] animate-bounce" style={{animationDelay:'300ms'}} />
|
||||
</span>
|
||||
) : (
|
||||
message.content
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Spacer for user messages */}
|
||||
{isUser && <div className="w-8 shrink-0 ml-2" />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,18 +1,47 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import { useAuthStore } from '../../store/authStore'
|
||||
import Header from './Header'
|
||||
import Sidebar from './Sidebar'
|
||||
|
||||
export default function AppLayout({ children }: { children: ReactNode }) {
|
||||
const { username, logout } = useAuthStore()
|
||||
const location = useLocation()
|
||||
|
||||
return (
|
||||
<div className="h-screen flex flex-col bg-gray-950 text-gray-100">
|
||||
<Header username={username} onLogout={logout} />
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
<Sidebar />
|
||||
<main className="flex-1 overflow-auto">{children}</main>
|
||||
</div>
|
||||
<div className="h-screen flex flex-col bg-[#242424] text-[#F2F2F2]">
|
||||
{/* Header */}
|
||||
<header className="h-14 shrink-0 flex items-center justify-between px-4 border-b border-[#393939] bg-[#000]">
|
||||
<Link to="/chat" className="flex items-center gap-2">
|
||||
<div className="w-7 h-7 rounded-lg bg-[#277AF7] flex items-center justify-center">
|
||||
<span className="text-sm font-bold text-white">C</span>
|
||||
</div>
|
||||
<span className="font-semibold text-base">Cyrene</span>
|
||||
</Link>
|
||||
|
||||
<nav className="flex items-center gap-1">
|
||||
<Link to="/chat"
|
||||
className={`px-3 py-1.5 rounded-full text-sm transition-colors ${
|
||||
location.pathname.startsWith('/chat')
|
||||
? 'bg-[#277AF7]/15 text-[#4788FF]'
|
||||
: 'text-white/50 hover:text-white/80'
|
||||
}`}
|
||||
>聊天</Link>
|
||||
<Link to="/admin/dashboard"
|
||||
className={`px-3 py-1.5 rounded-full text-sm transition-colors ${
|
||||
location.pathname.startsWith('/admin')
|
||||
? 'bg-[#277AF7]/15 text-[#4788FF]'
|
||||
: 'text-white/50 hover:text-white/80'
|
||||
}`}
|
||||
>管理</Link>
|
||||
<div className="w-px h-4 bg-[#393939] mx-1" />
|
||||
<span className="text-xs text-white/40">{username}</span>
|
||||
<button onClick={logout}
|
||||
className="px-3 py-1.5 rounded-full text-sm text-white/40 hover:text-[#F12522] transition-colors"
|
||||
>退出</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
{/* Content */}
|
||||
<main className="flex-1 overflow-hidden">{children}</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
|
||||
export default function Header({ username, onLogout }: { username: string; onLogout: () => void }) {
|
||||
return (
|
||||
<header className="h-12 bg-gray-900 border-b border-gray-800 flex items-center px-4 justify-between shrink-0">
|
||||
<Link to="/chat" className="font-bold text-purple-400 text-lg">Cyrene</Link>
|
||||
<div className="flex items-center gap-3 text-sm text-gray-400">
|
||||
<Link to="/admin/dashboard" className="hover:text-gray-200">管理</Link>
|
||||
<span>{username}</span>
|
||||
<button onClick={onLogout} className="hover:text-red-400">退出</button>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -16,18 +16,22 @@ export default function Sidebar() {
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="w-56 bg-gray-900 border-r border-gray-800 flex flex-col shrink-0">
|
||||
<aside className="w-60 bg-[#000] border-r border-[#393939] shrink-0 flex flex-col">
|
||||
<div className="p-3">
|
||||
<button onClick={handleNewChat}
|
||||
className="w-full py-2 bg-purple-600 hover:bg-purple-500 rounded text-sm font-medium transition-colors">
|
||||
className="w-full py-2.5 bg-[#277AF7] hover:bg-[#3482FF] active:scale-[0.98] text-white font-medium text-sm rounded-full transition-all duration-200"
|
||||
>
|
||||
+ 新对话
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto px-2 text-sm text-gray-400">
|
||||
|
||||
<div className="flex-1 overflow-auto px-2 pb-2">
|
||||
{currentSessionId ? (
|
||||
<p className="px-2 py-1 truncate text-xs">会话: {currentSessionId.slice(0, 8)}...</p>
|
||||
<div className="px-3 py-2 bg-[#242424] rounded-[14px]">
|
||||
<p className="text-xs text-white/40 truncate">{currentSessionId.slice(0, 12)}...</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="px-2 py-1 text-xs">暂无对话</p>
|
||||
<p className="px-3 py-2 text-xs text-white/30 text-center">暂无对话</p>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
Reference in new issue
Block a user