mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-18 18:04:32 +08:00
feat(ui): final polish — ChatGPT-style welcome screen, enhanced markdown, typing indicator
- Welcome state: centered greeting with 4 suggestion cards (ChatGPT style grid) - Enhanced markdown: headers, numbered/bullet lists, links, italic, HR support - Typing indicator: bouncing dots animation while waiting for response - Refined dark theme: subtle rgba borders, backdrop blur, better contrast hierarchy - Improved sidebar: slimmer 280px, glass morphism effect, smoother hover states - MarketTicker: crypto icons (₿/Ξ/◎), skeleton loading, monospace prices - Input area: ArrowUp send icon, ⌘K shortcut hint, focus ring glow - Keyboard shortcuts: Cmd+K focus input, Esc close mobile sidebar - Custom scrollbar styling, hidden scrollbar for quick actions bar - Removed initial bot message in favor of interactive welcome state
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { TrendingUp, TrendingDown, RefreshCw } from 'lucide-react'
|
||||
// icons reserved for future use
|
||||
|
||||
interface TickerData {
|
||||
symbol: string
|
||||
@@ -12,9 +12,16 @@ interface TickerData {
|
||||
|
||||
const SYMBOLS = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT']
|
||||
|
||||
const SYMBOL_ICONS: Record<string, string> = {
|
||||
BTC: '₿',
|
||||
ETH: 'Ξ',
|
||||
SOL: '◎',
|
||||
}
|
||||
|
||||
export function MarketTicker() {
|
||||
const [tickers, setTickers] = useState<Record<string, TickerData>>({})
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [, setRefreshing] = useState(false)
|
||||
|
||||
const fetchTickers = async () => {
|
||||
try {
|
||||
@@ -34,6 +41,7 @@ export function MarketTicker() {
|
||||
// ignore
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setRefreshing(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,9 +68,33 @@ export function MarketTicker() {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ padding: '12px 14px', color: '#5c5c72', fontSize: 12 }}>
|
||||
<RefreshCw size={14} className="animate-spin inline mr-2" />
|
||||
Loading market data...
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
{SYMBOLS.map((sym) => (
|
||||
<div
|
||||
key={sym}
|
||||
style={{
|
||||
padding: '12px',
|
||||
background: 'rgba(255,255,255,0.02)',
|
||||
borderRadius: 10,
|
||||
border: '1px solid rgba(255,255,255,0.04)',
|
||||
height: 56,
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
width: '60%',
|
||||
height: 10,
|
||||
background: 'rgba(255,255,255,0.04)',
|
||||
borderRadius: 4,
|
||||
animation: 'pulse 1.5s infinite',
|
||||
}} />
|
||||
</div>
|
||||
))}
|
||||
<style>{`
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 0.4; }
|
||||
50% { opacity: 0.8; }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -73,9 +105,12 @@ export function MarketTicker() {
|
||||
const t = tickers[sym]
|
||||
if (!t) return null
|
||||
const pct = parseFloat(t.priceChangePercent)
|
||||
const isUp = pct >= 0
|
||||
const color = isUp ? '#00e5a0' : '#F6465D'
|
||||
const isUp = pct > 0
|
||||
const isDown = pct < 0
|
||||
const color = isUp ? '#00e5a0' : isDown ? '#F6465D' : '#6c6c82'
|
||||
const bgColor = isUp ? 'rgba(0,229,160,0.06)' : isDown ? 'rgba(246,70,93,0.06)' : 'rgba(108,108,130,0.06)'
|
||||
const label = sym.replace('USDT', '')
|
||||
const icon = SYMBOL_ICONS[label] || label[0]
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -84,10 +119,20 @@ export function MarketTicker() {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '10px 12px',
|
||||
background: '#0d0d15',
|
||||
padding: '10px 11px',
|
||||
background: 'rgba(255,255,255,0.02)',
|
||||
borderRadius: 10,
|
||||
border: '1px solid #1a1a28',
|
||||
border: '1px solid rgba(255,255,255,0.04)',
|
||||
transition: 'all 0.15s ease',
|
||||
cursor: 'default',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = 'rgba(255,255,255,0.04)'
|
||||
e.currentTarget.style.borderColor = 'rgba(255,255,255,0.08)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'rgba(255,255,255,0.02)'
|
||||
e.currentTarget.style.borderColor = 'rgba(255,255,255,0.04)'
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
@@ -95,32 +140,37 @@ export function MarketTicker() {
|
||||
style={{
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 7,
|
||||
background: isUp ? 'rgba(0,229,160,0.08)' : 'rgba(246,70,93,0.08)',
|
||||
borderRadius: 8,
|
||||
background: bgColor,
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
fontSize: 13,
|
||||
fontWeight: 700,
|
||||
color: color,
|
||||
fontFamily: 'system-ui',
|
||||
}}
|
||||
>
|
||||
{isUp ? (
|
||||
<TrendingUp size={14} color={color} />
|
||||
) : (
|
||||
<TrendingDown size={14} color={color} />
|
||||
)}
|
||||
{icon}
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 13, fontWeight: 600, color: '#eaeaf0' }}>
|
||||
<div style={{ fontSize: 12.5, fontWeight: 600, color: '#e0e0ec', letterSpacing: '-0.01em' }}>
|
||||
{label}
|
||||
</div>
|
||||
<div style={{ fontSize: 10, color: '#5c5c72' }}>
|
||||
<div style={{ fontSize: 10, color: '#4c4c62' }}>
|
||||
Vol {formatVolume(t.volume)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 600, color: '#eaeaf0' }}>
|
||||
<div style={{ fontSize: 12.5, fontWeight: 600, color: '#e0e0ec', fontFamily: '"IBM Plex Mono", monospace', letterSpacing: '-0.02em' }}>
|
||||
${formatPrice(t.lastPrice)}
|
||||
</div>
|
||||
<div style={{ fontSize: 11, fontWeight: 500, color }}>
|
||||
<div style={{
|
||||
fontSize: 10.5,
|
||||
fontWeight: 600,
|
||||
color,
|
||||
fontFamily: '"IBM Plex Mono", monospace',
|
||||
}}>
|
||||
{isUp ? '+' : ''}{pct.toFixed(2)}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,8 +8,11 @@ import {
|
||||
Bot,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Send,
|
||||
Sparkles,
|
||||
ArrowUp,
|
||||
Zap,
|
||||
BarChart3,
|
||||
Lightbulb,
|
||||
Search,
|
||||
} from 'lucide-react'
|
||||
import { useLanguage } from '../contexts/LanguageContext'
|
||||
import { MarketTicker } from '../components/agent/MarketTicker'
|
||||
@@ -24,62 +27,186 @@ interface Message {
|
||||
streaming?: boolean
|
||||
}
|
||||
|
||||
// Simple markdown-ish renderer: bold, code, newlines
|
||||
// Enhanced markdown renderer: headers, bold, italic, code, lists, links
|
||||
function renderMessageContent(text: string) {
|
||||
// Split by code blocks first
|
||||
const parts = text.split(/(```[\s\S]*?```|`[^`]+`)/g)
|
||||
return parts.map((part, i) => {
|
||||
if (part.startsWith('```') && part.endsWith('```')) {
|
||||
const code = part.slice(3, -3).replace(/^\w+\n/, '') // strip language hint
|
||||
return (
|
||||
const lines = text.split('\n')
|
||||
const elements: JSX.Element[] = []
|
||||
let inCodeBlock = false
|
||||
let codeContent = ''
|
||||
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]
|
||||
|
||||
// Code block toggle
|
||||
if (line.startsWith('```')) {
|
||||
if (inCodeBlock) {
|
||||
elements.push(
|
||||
<pre
|
||||
key={i}
|
||||
key={`code-${i}`}
|
||||
style={{
|
||||
background: '#0a0a12',
|
||||
border: '1px solid #1f1f2c',
|
||||
borderRadius: 8,
|
||||
padding: '10px 12px',
|
||||
border: '1px solid rgba(255,255,255,0.06)',
|
||||
borderRadius: 10,
|
||||
padding: '12px 14px',
|
||||
fontSize: 12,
|
||||
overflowX: 'auto',
|
||||
margin: '6px 0',
|
||||
fontFamily: 'monospace',
|
||||
margin: '8px 0',
|
||||
fontFamily: '"IBM Plex Mono", monospace',
|
||||
color: '#c0c0d0',
|
||||
lineHeight: 1.6,
|
||||
}}
|
||||
>
|
||||
{code}
|
||||
{codeContent.trim()}
|
||||
</pre>
|
||||
)
|
||||
codeContent = ''
|
||||
inCodeBlock = false
|
||||
} else {
|
||||
inCodeBlock = true
|
||||
// language hint (reserved for syntax highlighting)
|
||||
}
|
||||
if (part.startsWith('`') && part.endsWith('`')) {
|
||||
return (
|
||||
continue
|
||||
}
|
||||
|
||||
if (inCodeBlock) {
|
||||
codeContent += (codeContent ? '\n' : '') + line
|
||||
continue
|
||||
}
|
||||
|
||||
// Headers
|
||||
if (line.startsWith('### ')) {
|
||||
elements.push(
|
||||
<div key={i} style={{ fontSize: 14, fontWeight: 700, color: '#f0f0f8', margin: '12px 0 6px', letterSpacing: '-0.01em' }}>
|
||||
{renderInline(line.slice(4))}
|
||||
</div>
|
||||
)
|
||||
continue
|
||||
}
|
||||
if (line.startsWith('## ')) {
|
||||
elements.push(
|
||||
<div key={i} style={{ fontSize: 15, fontWeight: 700, color: '#f0f0f8', margin: '14px 0 6px', letterSpacing: '-0.01em' }}>
|
||||
{renderInline(line.slice(3))}
|
||||
</div>
|
||||
)
|
||||
continue
|
||||
}
|
||||
if (line.startsWith('# ')) {
|
||||
elements.push(
|
||||
<div key={i} style={{ fontSize: 16, fontWeight: 700, color: '#f0f0f8', margin: '16px 0 8px', letterSpacing: '-0.02em' }}>
|
||||
{renderInline(line.slice(2))}
|
||||
</div>
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
// Bullet lists
|
||||
if (line.match(/^[-•*]\s/)) {
|
||||
elements.push(
|
||||
<div key={i} style={{ display: 'flex', gap: 8, padding: '2px 0', lineHeight: 1.65 }}>
|
||||
<span style={{ color: '#F0B90B', flexShrink: 0, fontSize: 8, marginTop: 7 }}>●</span>
|
||||
<span>{renderInline(line.replace(/^[-•*]\s/, ''))}</span>
|
||||
</div>
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
// Numbered lists
|
||||
if (line.match(/^\d+\.\s/)) {
|
||||
const num = line.match(/^(\d+)\./)?.[1]
|
||||
elements.push(
|
||||
<div key={i} style={{ display: 'flex', gap: 8, padding: '2px 0', lineHeight: 1.65 }}>
|
||||
<span style={{ color: '#8a8aa0', flexShrink: 0, fontSize: 12, fontWeight: 600, minWidth: 16, fontFamily: '"IBM Plex Mono", monospace' }}>{num}.</span>
|
||||
<span>{renderInline(line.replace(/^\d+\.\s/, ''))}</span>
|
||||
</div>
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
// Horizontal rule
|
||||
if (line.match(/^---+$/)) {
|
||||
elements.push(
|
||||
<hr key={i} style={{ border: 'none', borderTop: '1px solid rgba(255,255,255,0.06)', margin: '12px 0' }} />
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
// Empty line → small gap
|
||||
if (line.trim() === '') {
|
||||
elements.push(<div key={i} style={{ height: 6 }} />)
|
||||
continue
|
||||
}
|
||||
|
||||
// Regular paragraph
|
||||
elements.push(
|
||||
<div key={i} style={{ lineHeight: 1.7, padding: '1px 0' }}>
|
||||
{renderInline(line)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return elements
|
||||
}
|
||||
|
||||
// Inline formatting: bold, italic, code, links
|
||||
function renderInline(text: string): (string | JSX.Element)[] {
|
||||
const parts = text.split(/(```[\s\S]*?```|`[^`]+`|\*\*[^*]+\*\*|\*[^*]+\*|\[([^\]]+)\]\(([^)]+)\))/g)
|
||||
const result: (string | JSX.Element)[] = []
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i]
|
||||
if (!part) continue
|
||||
|
||||
if (part.startsWith('`') && part.endsWith('`') && !part.startsWith('```')) {
|
||||
result.push(
|
||||
<code
|
||||
key={i}
|
||||
style={{
|
||||
background: '#1a1a28',
|
||||
padding: '1px 5px',
|
||||
borderRadius: 4,
|
||||
fontSize: '0.9em',
|
||||
fontFamily: 'monospace',
|
||||
color: '#d0d0e0',
|
||||
background: 'rgba(240,185,11,0.08)',
|
||||
padding: '2px 6px',
|
||||
borderRadius: 5,
|
||||
fontSize: '0.88em',
|
||||
fontFamily: '"IBM Plex Mono", monospace',
|
||||
color: '#F0B90B',
|
||||
border: '1px solid rgba(240,185,11,0.12)',
|
||||
}}
|
||||
>
|
||||
{part.slice(1, -1)}
|
||||
</code>
|
||||
)
|
||||
}
|
||||
// Handle bold **text**
|
||||
const boldParts = part.split(/(\*\*[^*]+\*\*)/g)
|
||||
return boldParts.map((bp, j) => {
|
||||
if (bp.startsWith('**') && bp.endsWith('**')) {
|
||||
return (
|
||||
<strong key={`${i}-${j}`} style={{ fontWeight: 600, color: '#f0f0f8' }}>
|
||||
{bp.slice(2, -2)}
|
||||
} else if (part.startsWith('**') && part.endsWith('**')) {
|
||||
result.push(
|
||||
<strong key={i} style={{ fontWeight: 600, color: '#f0f0f8' }}>
|
||||
{part.slice(2, -2)}
|
||||
</strong>
|
||||
)
|
||||
} else if (part.startsWith('*') && part.endsWith('*') && !part.startsWith('**')) {
|
||||
result.push(
|
||||
<em key={i} style={{ fontStyle: 'italic', color: '#d0d0e0' }}>
|
||||
{part.slice(1, -1)}
|
||||
</em>
|
||||
)
|
||||
} else if (part.match(/^\[([^\]]+)\]\(([^)]+)\)$/)) {
|
||||
const match = part.match(/^\[([^\]]+)\]\(([^)]+)\)$/)
|
||||
if (match) {
|
||||
result.push(
|
||||
<a
|
||||
key={i}
|
||||
href={match[2]}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: '#F0B90B', textDecoration: 'underline', textUnderlineOffset: 2 }}
|
||||
>
|
||||
{match[1]}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
return <span key={`${i}-${j}`}>{bp}</span>
|
||||
})
|
||||
})
|
||||
} else {
|
||||
result.push(part)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
let msgIdCounter = 0
|
||||
@@ -87,23 +214,18 @@ function nextId() {
|
||||
return `msg-${Date.now()}-${++msgIdCounter}`
|
||||
}
|
||||
|
||||
// Suggestion cards for welcome state
|
||||
interface SuggestionCard {
|
||||
icon: JSX.Element
|
||||
title: string
|
||||
subtitle: string
|
||||
cmd: string
|
||||
}
|
||||
|
||||
export function AgentChatPage() {
|
||||
const { language } = useLanguage()
|
||||
const [sidebarOpen, setSidebarOpen] = useState(() => window.innerWidth > 1024)
|
||||
const [messages, setMessages] = useState<Message[]>([
|
||||
{
|
||||
id: nextId(),
|
||||
role: 'bot',
|
||||
text:
|
||||
language === 'zh'
|
||||
? '👋 你好!我是 NOFXi,你的 AI 交易 Agent。\n\n跟我说话就行,我能帮你分析市场、管理交易、配置策略。\n\n试试: "分析一下BTC" 或 /help'
|
||||
: "👋 Hi! I'm NOFXi, your AI trading agent.\n\nJust talk to me. I can analyze markets, manage trades, and configure strategies.\n\nTry: \"Analyze BTC\" or /help",
|
||||
time: new Date().toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}),
|
||||
},
|
||||
])
|
||||
const [messages, setMessages] = useState<Message[]>([])
|
||||
const [input, setInput] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [composing, setComposing] = useState(false)
|
||||
@@ -114,7 +236,7 @@ export function AgentChatPage() {
|
||||
const [sections, setSections] = useState({
|
||||
market: true,
|
||||
positions: true,
|
||||
traders: true,
|
||||
traders: false,
|
||||
})
|
||||
|
||||
const toggleSection = (key: keyof typeof sections) => {
|
||||
@@ -135,13 +257,28 @@ export function AgentChatPage() {
|
||||
return () => window.removeEventListener('resize', handleResize)
|
||||
}, [])
|
||||
|
||||
// Keyboard shortcut: Cmd+K to focus input
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
||||
e.preventDefault()
|
||||
inputRef.current?.focus()
|
||||
}
|
||||
if (e.key === 'Escape' && window.innerWidth <= 768) {
|
||||
setSidebarOpen(false)
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||
}, [])
|
||||
|
||||
// Auto-resize textarea
|
||||
const handleInputChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setInput(e.target.value)
|
||||
const el = e.target
|
||||
el.style.height = 'auto'
|
||||
el.style.height = Math.min(el.scrollHeight, 120) + 'px'
|
||||
el.style.height = Math.min(el.scrollHeight, 150) + 'px'
|
||||
},
|
||||
[]
|
||||
)
|
||||
@@ -202,7 +339,7 @@ export function AgentChatPage() {
|
||||
)
|
||||
)
|
||||
if (i < words.length - 1) {
|
||||
await new Promise((r) => setTimeout(r, 12))
|
||||
await new Promise((r) => setTimeout(r, 10))
|
||||
}
|
||||
}
|
||||
// Mark streaming done
|
||||
@@ -230,11 +367,23 @@ export function AgentChatPage() {
|
||||
inputRef.current?.focus()
|
||||
}
|
||||
|
||||
const quickActions =
|
||||
language === 'zh'
|
||||
// Welcome suggestions (ChatGPT style)
|
||||
const suggestions: SuggestionCard[] = language === 'zh'
|
||||
? [
|
||||
{ icon: <BarChart3 size={18} />, title: '分析 BTC 走势', subtitle: '技术分析 + 市场情绪', cmd: '分析一下 BTC 的走势' },
|
||||
{ icon: <Zap size={18} />, title: '做多 ETH', subtitle: 'Agent 帮你自动下单', cmd: '帮我做多 ETH 0.01 手' },
|
||||
{ icon: <Search size={18} />, title: '搜索股票', subtitle: '输入名称或代码即可', cmd: '搜索一下中远海控' },
|
||||
{ icon: <Lightbulb size={18} />, title: '策略建议', subtitle: '根据当前市场给出建议', cmd: '当前市场适合什么策略?' },
|
||||
]
|
||||
: [
|
||||
{ icon: <BarChart3 size={18} />, title: 'Analyze BTC', subtitle: 'Technical analysis + sentiment', cmd: 'Analyze BTC price action' },
|
||||
{ icon: <Zap size={18} />, title: 'Trade ETH', subtitle: 'Agent executes for you', cmd: 'Open a long position on ETH 0.01' },
|
||||
{ icon: <Search size={18} />, title: 'Search Stocks', subtitle: 'Enter name or ticker', cmd: 'Search for NVIDIA stock' },
|
||||
{ icon: <Lightbulb size={18} />, title: 'Strategy Ideas', subtitle: 'Market-based suggestions', cmd: 'What strategy fits the current market?' },
|
||||
]
|
||||
|
||||
const quickActions = language === 'zh'
|
||||
? [
|
||||
{ label: '📊 分析 BTC', cmd: '/analyze BTC' },
|
||||
{ label: '📊 分析 ETH', cmd: '/analyze ETH' },
|
||||
{ label: '💼 持仓', cmd: '/positions' },
|
||||
{ label: '💰 余额', cmd: '/balance' },
|
||||
{ label: '📋 Traders', cmd: '/traders' },
|
||||
@@ -242,8 +391,6 @@ export function AgentChatPage() {
|
||||
{ label: '❓ 帮助', cmd: '/help' },
|
||||
]
|
||||
: [
|
||||
{ label: '📊 Analyze BTC', cmd: '/analyze BTC' },
|
||||
{ label: '📊 Analyze ETH', cmd: '/analyze ETH' },
|
||||
{ label: '💼 Positions', cmd: '/positions' },
|
||||
{ label: '💰 Balance', cmd: '/balance' },
|
||||
{ label: '📋 Traders', cmd: '/traders' },
|
||||
@@ -267,11 +414,13 @@ export function AgentChatPage() {
|
||||
{
|
||||
key: 'traders' as const,
|
||||
icon: <Bot size={14} />,
|
||||
title: language === 'zh' ? 'Traders' : 'Traders',
|
||||
title: 'Traders',
|
||||
component: <TraderStatusPanel />,
|
||||
},
|
||||
]
|
||||
|
||||
const isWelcomeState = messages.length === 0
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -291,51 +440,43 @@ export function AgentChatPage() {
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{/* Quick actions bar */}
|
||||
{/* Top bar with quick actions */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
padding: '8px 16px',
|
||||
borderBottom: '1px solid #1a1a28',
|
||||
borderBottom: '1px solid rgba(255,255,255,0.04)',
|
||||
overflowX: 'auto',
|
||||
flexShrink: 0,
|
||||
backdropFilter: 'blur(12px)',
|
||||
background: 'rgba(9,9,11,0.8)',
|
||||
}}
|
||||
className="hide-scrollbar"
|
||||
>
|
||||
<Sparkles size={14} color="#F0B90B" style={{ flexShrink: 0 }} />
|
||||
{quickActions.map((a, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => send(a.cmd)}
|
||||
className="quick-action-btn"
|
||||
style={{
|
||||
padding: '5px 12px',
|
||||
background: '#111118',
|
||||
border: '1px solid #1f1f2c',
|
||||
background: 'rgba(255,255,255,0.03)',
|
||||
border: '1px solid rgba(255,255,255,0.06)',
|
||||
borderRadius: 20,
|
||||
color: '#8a8aa0',
|
||||
color: '#6c6c82',
|
||||
fontSize: 12,
|
||||
cursor: 'pointer',
|
||||
whiteSpace: 'nowrap',
|
||||
fontFamily: 'inherit',
|
||||
transition: 'all 0.15s',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = '#F0B90B40'
|
||||
e.currentTarget.style.color = '#F0B90B'
|
||||
e.currentTarget.style.background = '#F0B90B08'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = '#1f1f2c'
|
||||
e.currentTarget.style.color = '#8a8aa0'
|
||||
e.currentTarget.style.background = '#111118'
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
>
|
||||
{a.label}
|
||||
</button>
|
||||
))}
|
||||
|
||||
{/* Sidebar toggle (desktop) */}
|
||||
<button
|
||||
onClick={() => setSidebarOpen(!sidebarOpen)}
|
||||
style={{
|
||||
@@ -343,14 +484,17 @@ export function AgentChatPage() {
|
||||
padding: 6,
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
color: '#5c5c72',
|
||||
color: '#4c4c62',
|
||||
cursor: 'pointer',
|
||||
borderRadius: 6,
|
||||
borderRadius: 8,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexShrink: 0,
|
||||
transition: 'color 0.2s',
|
||||
}}
|
||||
title={sidebarOpen ? 'Hide sidebar' : 'Show sidebar'}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.color = '#8a8aa0' }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.color = '#4c4c62' }}
|
||||
>
|
||||
{sidebarOpen ? (
|
||||
<PanelRightClose size={18} />
|
||||
@@ -360,104 +504,226 @@ export function AgentChatPage() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Messages area */}
|
||||
{/* Messages area or Welcome state */}
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
overflowY: 'auto',
|
||||
padding: '20px 0',
|
||||
}}
|
||||
className="custom-scrollbar"
|
||||
>
|
||||
{isWelcomeState ? (
|
||||
/* ========== WELCOME STATE ========== */
|
||||
<div style={{
|
||||
maxWidth: 640,
|
||||
margin: '0 auto',
|
||||
padding: '0 20px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
minHeight: 400,
|
||||
}}>
|
||||
{/* Logo / greeting */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, ease: 'easeOut' }}
|
||||
style={{ textAlign: 'center', marginBottom: 40 }}
|
||||
>
|
||||
<div style={{
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 16,
|
||||
background: 'linear-gradient(135deg, rgba(240,185,11,0.12), rgba(0,229,160,0.06))',
|
||||
border: '1px solid rgba(240,185,11,0.15)',
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
margin: '0 auto 16px',
|
||||
fontSize: 24,
|
||||
}}>
|
||||
⚡
|
||||
</div>
|
||||
<h1 style={{
|
||||
fontSize: 22,
|
||||
fontWeight: 700,
|
||||
color: '#f0f0f8',
|
||||
margin: '0 0 8px',
|
||||
letterSpacing: '-0.02em',
|
||||
}}>
|
||||
{language === 'zh' ? '跟 NOFXi 聊点什么' : 'What can I help with?'}
|
||||
</h1>
|
||||
<p style={{
|
||||
fontSize: 13.5,
|
||||
color: '#5c5c72',
|
||||
margin: 0,
|
||||
lineHeight: 1.5,
|
||||
}}>
|
||||
{language === 'zh'
|
||||
? '分析行情、执行交易、搜索股票 — 用自然语言就行'
|
||||
: 'Analyze markets, execute trades, search stocks — just ask'}
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Suggestion cards grid */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 16 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.1, ease: 'easeOut' }}
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(2, 1fr)',
|
||||
gap: 10,
|
||||
width: '100%',
|
||||
maxWidth: 520,
|
||||
}}
|
||||
>
|
||||
{suggestions.map((s, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => send(s.cmd)}
|
||||
className="suggestion-card"
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
gap: 6,
|
||||
padding: '16px 14px',
|
||||
background: 'rgba(255,255,255,0.02)',
|
||||
border: '1px solid rgba(255,255,255,0.06)',
|
||||
borderRadius: 14,
|
||||
cursor: 'pointer',
|
||||
textAlign: 'left',
|
||||
fontFamily: 'inherit',
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
>
|
||||
<div style={{ color: '#F0B90B', opacity: 0.7 }}>
|
||||
{s.icon}
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 13, fontWeight: 600, color: '#d0d0e0', marginBottom: 2 }}>
|
||||
{s.title}
|
||||
</div>
|
||||
<div style={{ fontSize: 11.5, color: '#5c5c72' }}>
|
||||
{s.subtitle}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
) : (
|
||||
/* ========== MESSAGES ========== */
|
||||
<div style={{ maxWidth: 720, margin: '0 auto', padding: '0 20px' }}>
|
||||
{messages.map((m) => (
|
||||
<motion.div
|
||||
key={m.id}
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
initial={{ opacity: 0, y: 6 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 12,
|
||||
marginBottom: 20,
|
||||
marginBottom: 24,
|
||||
flexDirection: m.role === 'user' ? 'row-reverse' : 'row',
|
||||
}}
|
||||
>
|
||||
{/* Avatar */}
|
||||
<div
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: 10,
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
fontSize: 15,
|
||||
fontSize: 14,
|
||||
flexShrink: 0,
|
||||
marginTop: 2,
|
||||
background:
|
||||
m.role === 'user'
|
||||
? 'linear-gradient(135deg, rgba(139,92,246,.15), rgba(139,92,246,.05))'
|
||||
: 'linear-gradient(135deg, rgba(240,185,11,.1), rgba(0,229,160,.05))',
|
||||
? 'linear-gradient(135deg, rgba(139,92,246,.12), rgba(139,92,246,.04))'
|
||||
: 'linear-gradient(135deg, rgba(240,185,11,.08), rgba(0,229,160,.04))',
|
||||
border:
|
||||
'1px solid ' +
|
||||
(m.role === 'user'
|
||||
? 'rgba(139,92,246,.2)'
|
||||
: 'rgba(240,185,11,.15)'),
|
||||
? 'rgba(139,92,246,.15)'
|
||||
: 'rgba(240,185,11,.1)'),
|
||||
}}
|
||||
>
|
||||
{m.role === 'user' ? '👤' : '⚡'}
|
||||
</div>
|
||||
|
||||
{/* Message bubble */}
|
||||
<div style={{ maxWidth: '75%', minWidth: 0 }}>
|
||||
{/* Message content */}
|
||||
<div style={{ maxWidth: '78%', minWidth: 0 }}>
|
||||
{m.role === 'user' ? (
|
||||
<div
|
||||
style={{
|
||||
padding: '12px 16px',
|
||||
borderRadius: 16,
|
||||
padding: '10px 16px',
|
||||
borderRadius: 18,
|
||||
borderTopRightRadius: 4,
|
||||
fontSize: 13.5,
|
||||
lineHeight: 1.7,
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
background:
|
||||
m.role === 'user'
|
||||
? 'linear-gradient(135deg, #8b5cf6, #6d28d9)'
|
||||
: '#111118',
|
||||
color: m.role === 'user' ? '#fff' : '#eaeaf0',
|
||||
border:
|
||||
m.role === 'bot' ? '1px solid #1f1f2c' : 'none',
|
||||
borderTopLeftRadius: m.role === 'bot' ? 4 : 16,
|
||||
borderTopRightRadius: m.role === 'user' ? 4 : 16,
|
||||
background: 'linear-gradient(135deg, #7c3aed, #6d28d9)',
|
||||
color: '#fff',
|
||||
}}
|
||||
>
|
||||
{m.role === 'bot'
|
||||
? renderMessageContent(m.text)
|
||||
: m.text}
|
||||
{m.streaming && (
|
||||
{m.text}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
padding: '12px 16px',
|
||||
borderRadius: 18,
|
||||
borderTopLeftRadius: 4,
|
||||
fontSize: 13.5,
|
||||
lineHeight: 1.7,
|
||||
wordBreak: 'break-word',
|
||||
background: 'rgba(255,255,255,0.03)',
|
||||
color: '#dcdce8',
|
||||
border: '1px solid rgba(255,255,255,0.05)',
|
||||
}}
|
||||
>
|
||||
{renderMessageContent(m.text)}
|
||||
{m.streaming && m.text === '' && (
|
||||
<div style={{ display: 'flex', gap: 4, padding: '4px 0' }}>
|
||||
<span className="typing-dot" style={{ animationDelay: '0ms' }} />
|
||||
<span className="typing-dot" style={{ animationDelay: '150ms' }} />
|
||||
<span className="typing-dot" style={{ animationDelay: '300ms' }} />
|
||||
</div>
|
||||
)}
|
||||
{m.streaming && m.text !== '' && (
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: 6,
|
||||
height: 14,
|
||||
width: 2,
|
||||
height: 15,
|
||||
background: '#F0B90B',
|
||||
marginLeft: 2,
|
||||
marginLeft: 1,
|
||||
borderRadius: 1,
|
||||
animation: 'blink 1s infinite',
|
||||
animation: 'blink 0.8s infinite',
|
||||
verticalAlign: 'text-bottom',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{m.time && (
|
||||
)}
|
||||
{m.time && !m.streaming && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: 10,
|
||||
color: '#3c3c52',
|
||||
color: '#2c2c42',
|
||||
marginTop: 4,
|
||||
textAlign: m.role === 'user' ? 'right' : 'left',
|
||||
paddingLeft: m.role === 'bot' ? 4 : 0,
|
||||
paddingRight: m.role === 'user' ? 4 : 0,
|
||||
}}
|
||||
>
|
||||
{m.role === 'bot' && 'NOFXi · '}
|
||||
{m.time}
|
||||
{m.role === 'bot' && 'NOFXi · '}{m.time}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -465,37 +731,30 @@ export function AgentChatPage() {
|
||||
))}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Input area */}
|
||||
<div
|
||||
style={{
|
||||
padding: '12px 16px 20px',
|
||||
borderTop: '1px solid #1a1a28',
|
||||
background:
|
||||
'linear-gradient(to top, #09090b 60%, transparent)',
|
||||
borderTop: '1px solid rgba(255,255,255,0.04)',
|
||||
background: 'linear-gradient(to top, #09090b 80%, transparent)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="chat-input-wrapper"
|
||||
style={{
|
||||
maxWidth: 720,
|
||||
margin: '0 auto',
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
background: '#111118',
|
||||
border: '1px solid #1f1f2c',
|
||||
borderRadius: 16,
|
||||
background: 'rgba(255,255,255,0.03)',
|
||||
border: '1px solid rgba(255,255,255,0.07)',
|
||||
borderRadius: 18,
|
||||
padding: '4px 4px 4px 16px',
|
||||
alignItems: 'flex-end',
|
||||
transition: 'border-color 0.2s',
|
||||
}}
|
||||
onFocus={(e) => {
|
||||
;(e.currentTarget as HTMLElement).style.borderColor =
|
||||
'#F0B90B40'
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
;(e.currentTarget as HTMLElement).style.borderColor =
|
||||
'#1f1f2c'
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
>
|
||||
<textarea
|
||||
@@ -505,19 +764,15 @@ export function AgentChatPage() {
|
||||
onCompositionStart={() => setComposing(true)}
|
||||
onCompositionEnd={() => setComposing(false)}
|
||||
onKeyDown={(e) => {
|
||||
if (
|
||||
e.key === 'Enter' &&
|
||||
!e.shiftKey &&
|
||||
!composing
|
||||
) {
|
||||
if (e.key === 'Enter' && !e.shiftKey && !composing) {
|
||||
e.preventDefault()
|
||||
send()
|
||||
}
|
||||
}}
|
||||
placeholder={
|
||||
language === 'zh'
|
||||
? '跟 NOFXi 聊点什么...'
|
||||
: 'Ask NOFXi anything...'
|
||||
? '跟 NOFXi 聊点什么... ⌘K'
|
||||
: 'Ask NOFXi anything... ⌘K'
|
||||
}
|
||||
rows={1}
|
||||
style={{
|
||||
@@ -531,33 +786,30 @@ export function AgentChatPage() {
|
||||
fontFamily: 'inherit',
|
||||
resize: 'none',
|
||||
lineHeight: 1.5,
|
||||
maxHeight: 120,
|
||||
maxHeight: 150,
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={() => send()}
|
||||
disabled={loading || !input.trim()}
|
||||
style={{
|
||||
width: 38,
|
||||
height: 38,
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 12,
|
||||
border: 'none',
|
||||
background:
|
||||
loading || !input.trim()
|
||||
? '#1a1a28'
|
||||
? 'rgba(255,255,255,0.04)'
|
||||
: 'linear-gradient(135deg, #F0B90B, #d4a30a)',
|
||||
color: loading || !input.trim() ? '#3c3c52' : '#000',
|
||||
cursor:
|
||||
loading || !input.trim()
|
||||
? 'not-allowed'
|
||||
: 'pointer',
|
||||
cursor: loading || !input.trim() ? 'not-allowed' : 'pointer',
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
flexShrink: 0,
|
||||
transition: 'all 0.2s',
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
>
|
||||
<Send size={16} />
|
||||
<ArrowUp size={16} strokeWidth={2.5} />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
@@ -566,7 +818,7 @@ export function AgentChatPage() {
|
||||
margin: '6px auto 0',
|
||||
textAlign: 'center',
|
||||
fontSize: 10,
|
||||
color: '#2c2c42',
|
||||
color: '#1e1e32',
|
||||
}}
|
||||
>
|
||||
NOFXi may make mistakes. Always verify trading decisions.
|
||||
@@ -579,35 +831,37 @@ export function AgentChatPage() {
|
||||
{sidebarOpen && (
|
||||
<motion.div
|
||||
initial={{ width: 0, opacity: 0 }}
|
||||
animate={{ width: 300, opacity: 1 }}
|
||||
animate={{ width: 280, opacity: 1 }}
|
||||
exit={{ width: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.2, ease: 'easeInOut' }}
|
||||
style={{
|
||||
borderLeft: '1px solid #1a1a28',
|
||||
background: '#0b0b13',
|
||||
borderLeft: '1px solid rgba(255,255,255,0.04)',
|
||||
background: 'rgba(11,11,19,0.6)',
|
||||
backdropFilter: 'blur(12px)',
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
className="custom-scrollbar"
|
||||
>
|
||||
<div style={{ padding: '12px 12px 20px', width: 300 }}>
|
||||
<div style={{ padding: '12px 10px 20px', width: 280 }}>
|
||||
{/* Sidebar header */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 14,
|
||||
padding: '0 4px',
|
||||
marginBottom: 12,
|
||||
padding: '4px 6px',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 11,
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
color: '#5c5c72',
|
||||
color: '#4c4c62',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 1,
|
||||
letterSpacing: 1.5,
|
||||
}}
|
||||
>
|
||||
{language === 'zh' ? '交易面板' : 'Trading Panel'}
|
||||
@@ -616,7 +870,7 @@ export function AgentChatPage() {
|
||||
|
||||
{/* Sidebar sections */}
|
||||
{sidebarSections.map((section) => (
|
||||
<div key={section.key} style={{ marginBottom: 10 }}>
|
||||
<div key={section.key} style={{ marginBottom: 8 }}>
|
||||
<button
|
||||
onClick={() => toggleSection(section.key)}
|
||||
style={{
|
||||
@@ -624,27 +878,29 @@ export function AgentChatPage() {
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
width: '100%',
|
||||
padding: '8px 8px',
|
||||
padding: '7px 8px',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
color: '#8a8aa0',
|
||||
color: '#7a7a90',
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
borderRadius: 8,
|
||||
transition: 'background 0.15s',
|
||||
transition: 'all 0.15s ease',
|
||||
fontFamily: 'inherit',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = '#111118'
|
||||
e.currentTarget.style.background = 'rgba(255,255,255,0.03)'
|
||||
e.currentTarget.style.color = '#a0a0b0'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'transparent'
|
||||
e.currentTarget.style.color = '#7a7a90'
|
||||
}}
|
||||
>
|
||||
{section.icon}
|
||||
<span>{section.title}</span>
|
||||
<span style={{ marginLeft: 'auto' }}>
|
||||
<span style={{ marginLeft: 'auto', transition: 'transform 0.2s' }}>
|
||||
{sections[section.key] ? (
|
||||
<ChevronDown size={14} />
|
||||
) : (
|
||||
@@ -674,12 +930,71 @@ export function AgentChatPage() {
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Blinking cursor animation */}
|
||||
{/* Animations */}
|
||||
<style>{`
|
||||
@keyframes blink {
|
||||
0%, 50% { opacity: 1; }
|
||||
51%, 100% { opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes typingBounce {
|
||||
0%, 60%, 100% { transform: translateY(0); opacity: 0.3; }
|
||||
30% { transform: translateY(-4px); opacity: 0.8; }
|
||||
}
|
||||
|
||||
.typing-dot {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: #F0B90B;
|
||||
display: inline-block;
|
||||
animation: typingBounce 1.2s infinite;
|
||||
}
|
||||
|
||||
.suggestion-card:hover {
|
||||
background: rgba(240,185,11,0.04) !important;
|
||||
border-color: rgba(240,185,11,0.15) !important;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.quick-action-btn:hover {
|
||||
border-color: rgba(240,185,11,0.2) !important;
|
||||
color: #F0B90B !important;
|
||||
background: rgba(240,185,11,0.04) !important;
|
||||
}
|
||||
|
||||
.chat-input-wrapper:focus-within {
|
||||
border-color: rgba(240,185,11,0.25) !important;
|
||||
box-shadow: 0 0 0 1px rgba(240,185,11,0.08);
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: rgba(255,255,255,0.06);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.hide-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.hide-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.suggestion-card {
|
||||
padding: 12px !important;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user