style(frontend):优化前端效果

1.对于UI的配色和布局进行了优化
This commit is contained in:
2026-06-01 03:30:30 +00:00
parent 99520c69d7
commit f04fef916f
11 changed files with 452 additions and 416 deletions
+7 -12
View File
@@ -28,7 +28,6 @@ function App() {
const { loadSessions } = useChatStore(); const { loadSessions } = useChatStore();
// Initialize theme on mount
useEffect(() => { useEffect(() => {
applyTheme(); applyTheme();
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
@@ -37,14 +36,12 @@ function App() {
return () => mediaQuery.removeEventListener('change', handler); return () => mediaQuery.removeEventListener('change', handler);
}, [applyTheme]); }, [applyTheme]);
// Sync persisted locale to i18next on mount
useEffect(() => { useEffect(() => {
if (locale && i18n.language !== locale) { if (locale && i18n.language !== locale) {
i18n.changeLanguage(locale); i18n.changeLanguage(locale);
} }
}, [locale]); }, [locale]);
// Check auth and load sessions
useEffect(() => { useEffect(() => {
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
if (token) { if (token) {
@@ -75,11 +72,9 @@ function App() {
<div className="flex-1 flex overflow-hidden"> <div className="flex-1 flex overflow-hidden">
{mode === 'work' && workTab === 'chat' && ( {mode === 'work' && workTab === 'chat' && (
<div className="flex-1 p-4 flex overflow-hidden gap-4"> <div className="flex-1 flex overflow-hidden">
<div className="flex-1 flex bg-bg-card rounded-2xl shadow-sm border border-border-primary overflow-hidden relative"> <LeftPanel activeTab="chats" />
<LeftPanel activeTab="chats" /> <ChatPanel />
<ChatPanel />
</div>
</div> </div>
)} )}
@@ -101,22 +96,22 @@ function WorkflowShell() {
if (selectedWorkflow === 'new') { if (selectedWorkflow === 'new') {
return ( return (
<> <div className="flex-1 flex overflow-hidden">
<LeftPanel activeTab="workflows" /> <LeftPanel activeTab="workflows" />
<NewWorkflowDialog <NewWorkflowDialog
onClose={() => setSelectedWorkflow(null)} onClose={() => setSelectedWorkflow(null)}
onSuccess={(traceId: string) => setSelectedWorkflow(traceId)} onSuccess={(traceId: string) => setSelectedWorkflow(traceId)}
/> />
</> </div>
); );
} }
if (selectedWorkflow) { if (selectedWorkflow) {
return ( return (
<> <div className="flex-1 flex overflow-hidden">
<LeftPanel activeTab="workflows" /> <LeftPanel activeTab="workflows" />
<RightPanel selectedWorkflow={selectedWorkflow} /> <RightPanel selectedWorkflow={selectedWorkflow} />
</> </div>
); );
} }
+102 -89
View File
@@ -1,7 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import apiClient from '../../api/client'; import apiClient from '../../api/client';
import { Zap, ArrowRight, ShieldCheck } from 'lucide-react'; import { ArrowRight } from 'lucide-react';
interface AuthPageProps { interface AuthPageProps {
onLoginSuccess: () => void; onLoginSuccess: () => void;
@@ -49,96 +49,109 @@ export function AuthPage({ onLoginSuccess }: AuthPageProps) {
}; };
return ( return (
<div className="flex min-h-screen w-full relative overflow-hidden"> <div className="min-h-screen w-full bg-bg-primary flex items-center justify-center">
{/* Background decoration */} <div className="flex items-center gap-[70px] w-[900px]">
<div className="absolute inset-0 bg-bg-primary"> {/* Left: Brand */}
<div className="absolute top-0 left-1/4 w-96 h-96 bg-accent/5 rounded-full blur-3xl" /> <div className="flex-1">
<div className="absolute bottom-0 right-1/4 w-96 h-96 bg-glow-purple/5 rounded-full blur-3xl" /> <div className="w-10 h-[3px] rounded-sm bg-gradient-to-r from-accent to-clay mb-6" />
</div> <h1 className="text-[40px] font-bold tracking-tight leading-tight mb-3 text-text-primary">
Kilo<span className="not-italic text-accent">Star</span>
<div className="relative z-10 flex w-full items-center justify-center p-6"> </h1>
<div className="w-full max-w-md animate-fade-in-scale"> <p className="text-[15px] text-text-muted leading-relaxed mb-8">
{/* Logo */}
<div className="flex flex-col items-center mb-8">
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-accent to-glow-purple flex items-center justify-center text-white shadow-xl shadow-accent/20 mb-5">
<Zap size={28} fill="currentColor" />
</div>
<h1 className="text-2xl font-bold text-text-primary tracking-tight">{t('app.name')}</h1>
<p className="text-sm text-text-muted mt-1.5">{t('app.tagline')}</p>
</div>
<div className="bg-bg-card/80 backdrop-blur-xl rounded-2xl border border-border-primary shadow-xl shadow-black/5 p-8">
<div className="flex items-center gap-2 mb-6">
<ShieldCheck size={18} className="text-accent" />
<h2 className="text-lg font-semibold text-text-primary">
{isLogin ? t('auth.welcomeBack') : t('auth.createAccount')}
</h2>
</div>
{error && (
<div className={`mb-5 p-3 rounded-xl text-sm border ${error.includes('success') || error.includes('成功') ? 'bg-success-bg/50 text-success border-success/20' : 'bg-danger-bg/50 text-danger border-danger/20'}`}>
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('auth.username')}</label>
<input
type="text"
value={userName}
onChange={(e) => setUserName(e.target.value)}
className="w-full px-4 py-2.5 bg-bg-input border border-border-primary rounded-xl focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent transition-all text-text-primary placeholder:text-text-muted/60 text-sm"
placeholder={t('auth.usernamePlaceholder')}
required
/>
</div>
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('auth.password')}</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full px-4 py-2.5 bg-bg-input border border-border-primary rounded-xl focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent transition-all text-text-primary placeholder:text-text-muted/60 text-sm"
placeholder={t('auth.passwordPlaceholder')}
required
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full py-2.5 bg-accent text-white rounded-xl font-semibold hover:bg-accent-hover focus:outline-none focus:ring-2 focus:ring-accent/30 transition-all disabled:opacity-50 cursor-pointer text-sm flex items-center justify-center gap-2 group"
>
{loading ? (
<span className="flex items-center gap-2">
<span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
{t('auth.processing')}
</span>
) : (
<>
{isLogin ? t('auth.signIn') : t('auth.signUp')}
<ArrowRight size={16} className="transition-transform group-hover:translate-x-0.5" />
</>
)}
</button>
</form>
<div className="mt-6 text-center text-sm text-text-muted">
{isLogin ? t('auth.noAccount') : t('auth.hasAccount')}{' '}
<button
onClick={() => { setIsLogin(!isLogin); setError(''); }}
className="text-accent font-semibold hover:text-accent-hover transition-colors"
>
{isLogin ? t('auth.signUp') : t('auth.signIn')}
</button>
</div>
</div>
<p className="text-center text-xs text-text-muted/60 mt-6">
{t('app.tagline')} {t('app.tagline')}
</p> </p>
<div className="flex flex-col gap-2.5">
<div className="flex items-center gap-2.5 text-[13px] text-text-muted">
<div className="w-[5px] h-[5px] rounded-full bg-clay" />
<span>Multi-Agent Orchestration</span>
</div>
<div className="flex items-center gap-2.5 text-[13px] text-text-muted">
<div className="w-[5px] h-[5px] rounded-full bg-clay" />
<span>Visual Pipeline Builder</span>
</div>
<div className="flex items-center gap-2.5 text-[13px] text-text-muted">
<div className="w-[5px] h-[5px] rounded-full bg-clay" />
<span>Intelligent Monitoring</span>
</div>
</div>
</div>
{/* Right: Card */}
<div className="w-[380px] bg-bg-card rounded-[20px] p-10 shadow-[0_1px_3px_rgba(0,0,0,0.04),0_8px_24px_rgba(0,0,0,0.03)] border border-border-primary">
<h2 className="text-xl font-semibold text-text-primary mb-1">
{isLogin ? t('auth.welcomeBack') : t('auth.createAccount')}
</h2>
<p className="text-[13px] text-text-muted mb-7">
{isLogin ? t('auth.enterCredentials') : t('auth.signUpToStart')}
</p>
{error && (
<div className={`mb-5 p-3 rounded-[10px] text-sm border ${error.includes('success') || error.includes('成功') ? 'bg-success-bg/50 text-success border-success/20' : 'bg-danger-bg/50 text-danger border-danger/20'}`}>
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-[18px]">
<div>
<label className="block text-xs font-medium text-text-muted mb-1.5">
{t('auth.username')}
</label>
<input
type="text"
value={userName}
onChange={(e) => setUserName(e.target.value)}
className="w-full px-3.5 py-3 rounded-[10px] border border-border-primary bg-bg-primary text-sm text-text-primary placeholder:text-[#bbb5ae] outline-none transition-all focus:border-accent focus:shadow-[0_0_0_3px_rgba(156,175,136,0.1)]"
placeholder={t('auth.usernamePlaceholder')}
required
/>
</div>
<div>
<label className="block text-xs font-medium text-text-muted mb-1.5">
{t('auth.password')}
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full px-3.5 py-3 rounded-[10px] border border-border-primary bg-bg-primary text-sm text-text-primary placeholder:text-[#bbb5ae] outline-none transition-all focus:border-accent focus:shadow-[0_0_0_3px_rgba(156,175,136,0.1)]"
placeholder={t('auth.passwordPlaceholder')}
required
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full py-3 rounded-[10px] bg-accent text-white text-sm font-semibold hover:bg-accent-hover hover:-translate-y-px hover:shadow-[0_6px_20px_rgba(156,175,136,0.3)] transition-all disabled:opacity-50 cursor-pointer flex items-center justify-center gap-2"
>
{loading ? (
<span className="flex items-center gap-2">
<span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
{t('auth.processing')}
</span>
) : (
<>
{isLogin ? t('auth.signIn') : t('auth.signUp')}
<ArrowRight size={16} />
</>
)}
</button>
</form>
<div className="flex items-center gap-3.5 my-5 text-[#bbb5ae] text-xs before:flex-1 before:h-px before:bg-border-primary after:flex-1 after:h-px after:bg-border-primary">
or
</div>
<p className="text-center text-[13px] text-text-muted">
{isLogin ? t('auth.noAccount') : t('auth.hasAccount')}{' '}
<button
onClick={() => { setIsLogin(!isLogin); setError(''); }}
className="text-accent font-medium hover:text-accent-hover transition-colors"
>
{isLogin ? t('auth.signUp') : t('auth.signIn')}
</button>
</p>
</div> </div>
</div> </div>
</div> </div>
+49 -50
View File
@@ -1,6 +1,6 @@
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { MessageSquare, Activity, ArrowUp, Plus, Sparkles, Code, FileText, Search } from 'lucide-react'; import { MessageSquare, Activity, ArrowUp, Plus, Sparkles, Code, FileText, Search, User } from 'lucide-react';
import { useChatStore } from '../../store/useChatStore'; import { useChatStore } from '../../store/useChatStore';
export function ChatPanel() { export function ChatPanel() {
@@ -64,36 +64,32 @@ export function ChatPanel() {
if (!activeSessionId) { if (!activeSessionId) {
return ( return (
<div className="flex-1 flex flex-col items-center justify-center p-8 relative overflow-hidden"> <div className="flex-1 flex flex-col items-center justify-center p-10 relative overflow-hidden">
{/* Decorative background */}
<div className="absolute top-1/3 left-1/2 -translate-x-1/2 -translate-y-1/2 w-64 h-64 bg-accent/5 rounded-full blur-3xl" />
<div className="relative z-10 flex flex-col items-center animate-fade-in-scale"> <div className="relative z-10 flex flex-col items-center animate-fade-in-scale">
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-accent to-glow-purple flex items-center justify-center text-white shadow-xl shadow-accent/20 mb-6"> <div className="w-14 h-14 rounded-2xl bg-accent-light flex items-center justify-center text-accent mb-5">
<Activity size={32} /> <Activity size={24} />
</div> </div>
<h2 className="text-2xl font-bold text-text-primary mb-2">{t('chat.assistantName')}</h2> <h2 className="text-lg font-semibold text-text-primary mb-1.5">{t('chat.assistantName')}</h2>
<p className="text-text-muted text-sm mb-8 text-center max-w-sm"> <p className="text-text-secondary text-sm mb-7 text-center max-w-sm">
{t('chat.selectChat')} {t('chat.selectChat')}
</p> </p>
{/* Quick Actions */} <div className="grid grid-cols-2 gap-2.5 mb-7 w-full max-w-[380px]">
<div className="grid grid-cols-2 gap-3 mb-8 w-full max-w-md">
{quickActions.map((action) => ( {quickActions.map((action) => (
<button <button
key={action.label} key={action.label}
onClick={() => handleQuickAction(action.prompt)} onClick={() => handleQuickAction(action.prompt)}
className="flex items-center gap-2.5 px-4 py-3 bg-bg-card border border-border-primary rounded-xl text-left hover:border-accent hover:bg-bg-hover transition-all group" className="flex items-center gap-2.5 px-3.5 py-3 bg-bg-card border border-border-primary rounded-[10px] text-left hover:border-border-secondary hover:bg-bg-hover transition-all group shadow-[0_1px_2px_rgba(0,0,0,0.02)]"
> >
<action.icon size={16} className="text-text-muted group-hover:text-accent transition-colors" /> <action.icon size={14} className="text-accent flex-shrink-0" />
<span className="text-sm text-text-secondary group-hover:text-text-primary transition-colors">{action.label}</span> <span className="text-xs text-text-secondary group-hover:text-text-primary transition-colors">{action.label}</span>
</button> </button>
))} ))}
</div> </div>
<button <button
onClick={handleNewChat} onClick={handleNewChat}
className="px-6 py-2.5 bg-accent text-white rounded-xl font-medium hover:bg-accent-hover transition-all shadow-lg shadow-accent/20 text-sm flex items-center gap-2" className="px-5 py-2.5 bg-accent text-white rounded-xl font-medium hover:bg-accent-hover transition-all text-sm flex items-center gap-2"
> >
<Plus size={16} /> <Plus size={16} />
{t('chat.newChat')} {t('chat.newChat')}
@@ -104,19 +100,17 @@ export function ChatPanel() {
} }
return ( return (
<div className="flex-1 flex flex-col bg-bg-card overflow-hidden relative"> <div className="flex-1 flex flex-col bg-bg-primary overflow-hidden relative">
{/* Header */} {/* Header */}
<div className="h-12 border-b border-border-primary/60 bg-bg-card/80 backdrop-blur flex items-center justify-between px-5 z-10 shrink-0"> <div className="h-12 border-b border-border-primary flex items-center px-6 gap-2.5 bg-bg-primary/60 backdrop-blur-[8px] shrink-0">
<div className="flex items-center gap-2.5"> <div className="w-[26px] h-[26px] rounded-[7px] flex items-center justify-center bg-accent-light">
<div className="w-7 h-7 rounded-lg bg-accent-light flex items-center justify-center"> <MessageSquare size={12} className="text-accent" />
<MessageSquare size={14} className="text-accent" />
</div>
<h1 className="font-semibold text-sm text-text-primary">{activeSession?.title || t('chat.defaultTitle')}</h1>
</div> </div>
<span className="text-[13px] font-semibold text-text-primary">{activeSession?.title || t('chat.defaultTitle')}</span>
</div> </div>
{/* Messages */} {/* Messages */}
<div className="flex-1 overflow-y-auto px-4 py-6 space-y-5"> <div className="flex-1 overflow-y-auto px-8 py-6 flex flex-col gap-4.5">
{loadingMessages ? ( {loadingMessages ? (
<div className="flex justify-center items-center h-full"> <div className="flex justify-center items-center h-full">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
@@ -129,24 +123,29 @@ export function ChatPanel() {
messages.map((msg, idx) => ( messages.map((msg, idx) => (
<div <div
key={msg.id} key={msg.id}
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'} animate-fade-in`} className={`flex gap-2.5 max-w-[78%] ${msg.role === 'user' ? 'self-end flex-row-reverse' : ''} animate-fade-in`}
style={{ animationDelay: `${idx * 30}ms` }} style={{ animationDelay: `${idx * 30}ms` }}
> >
{msg.role === 'assistant' && ( <div
<div className="w-7 h-7 rounded-full bg-gradient-to-br from-accent to-glow-purple flex items-center justify-center mr-2.5 mt-0.5 shadow-sm flex-shrink-0"> className="w-7 h-7 rounded-full flex items-center justify-center flex-shrink-0"
<Activity size={13} className="text-white" /> style={{
</div> background: msg.role === 'assistant'
)} ? 'var(--accent)'
<div className={`max-w-[85%] ${msg.role === 'user' ? 'mr-1' : ''}`}> : 'linear-gradient(135deg, #6b6860, #8a8578)',
<div }}
className={`px-4 py-2.5 text-sm leading-relaxed whitespace-pre-wrap ${ >
msg.role === 'user' {msg.role === 'assistant' ? (
? 'bg-text-primary text-bg-primary rounded-2xl rounded-tr-sm' <Activity size={12} className="text-white" />
: 'bg-bg-secondary border border-border-primary rounded-2xl rounded-tl-sm' ) : (
}`} <User size={12} className="text-white" />
> )}
{msg.content} </div>
</div> <div className={`px-4 py-3 text-[13px] leading-[1.7] whitespace-pre-wrap ${
msg.role === 'user'
? 'bg-text-primary text-white rounded-[14px] rounded-br-[3px]'
: 'bg-bg-card border border-border-primary rounded-[14px] rounded-bl-[3px] shadow-[0_1px_3px_rgba(0,0,0,0.02)]'
}`}>
{msg.content}
</div> </div>
</div> </div>
)) ))
@@ -154,11 +153,11 @@ export function ChatPanel() {
{/* Typing indicator */} {/* Typing indicator */}
{activeSession && activeSession.messages.length > 0 && activeSession.messages[activeSession.messages.length - 1].role === 'user' && ( {activeSession && activeSession.messages.length > 0 && activeSession.messages[activeSession.messages.length - 1].role === 'user' && (
<div className="flex justify-start animate-fade-in"> <div className="flex gap-2.5 max-w-[78%] animate-fade-in">
<div className="w-7 h-7 rounded-full bg-gradient-to-br from-accent to-glow-purple flex items-center justify-center mr-2.5 mt-0.5 shadow-sm flex-shrink-0"> <div className="w-7 h-7 rounded-full bg-accent flex items-center justify-center flex-shrink-0">
<Activity size={13} className="text-white animate-pulse" /> <Activity size={12} className="text-white animate-pulse" />
</div> </div>
<div className="bg-bg-secondary border border-border-primary rounded-2xl rounded-tl-sm px-4 py-3"> <div className="bg-bg-card border border-border-primary rounded-[14px] rounded-bl-[3px] px-4 py-3 shadow-[0_1px_3px_rgba(0,0,0,0.02)]">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span className="w-1.5 h-1.5 rounded-full bg-accent animate-typing-dot" /> <span className="w-1.5 h-1.5 rounded-full bg-accent animate-typing-dot" />
<span className="w-1.5 h-1.5 rounded-full bg-accent animate-typing-dot" /> <span className="w-1.5 h-1.5 rounded-full bg-accent animate-typing-dot" />
@@ -171,12 +170,12 @@ export function ChatPanel() {
</div> </div>
{/* Input */} {/* Input */}
<div className="p-4 bg-bg-card border-t border-border-primary/60 shrink-0"> <div className="px-6 pt-3.5 pb-4.5 border-t border-border-primary bg-bg-primary/60 backdrop-blur-[8px] shrink-0">
<div className="relative flex items-end gap-2 max-w-3xl mx-auto"> <div className="flex items-end gap-2 max-w-[720px] mx-auto">
<input type="file" ref={fileInputRef} className="hidden" /> <input type="file" ref={fileInputRef} className="hidden" />
<button <button
onClick={() => fileInputRef.current?.click()} onClick={() => fileInputRef.current?.click()}
className="p-2.5 text-text-muted hover:text-accent hover:bg-accent-light rounded-xl transition-all flex-shrink-0 mb-0.5" className="w-10 h-10 rounded-xl text-text-muted hover:text-accent hover:bg-bg-hover transition-all flex items-center justify-center flex-shrink-0"
title={t('chat.addAttachment')} title={t('chat.addAttachment')}
> >
<Plus size={18} /> <Plus size={18} />
@@ -193,19 +192,19 @@ export function ChatPanel() {
}} }}
placeholder={t('chat.placeholder')} placeholder={t('chat.placeholder')}
rows={1} rows={1}
className="w-full bg-bg-input border border-border-primary rounded-xl pl-4 pr-12 py-3 focus:outline-none focus:ring-2 focus:ring-accent/15 focus:border-accent/40 transition-all text-text-primary placeholder:text-text-muted/50 text-sm resize-none min-h-[44px] max-h-[120px]" className="w-full bg-bg-card border border-border-primary rounded-xl pl-4 pr-4 py-3 focus:outline-none focus:border-accent focus:shadow-[0_0_0_3px_var(--accent-light),0_1px_3px_rgba(0,0,0,0.02)] transition-all text-text-primary placeholder:text-[#bbb5ae] text-[13px] resize-none min-h-[44px] max-h-[100px]"
style={{ height: 'auto' }} style={{ height: 'auto' }}
/> />
</div> </div>
<button <button
onClick={handleSendMessage} onClick={handleSendMessage}
disabled={!input.trim()} disabled={!input.trim()}
className="p-2.5 bg-accent text-white rounded-xl hover:bg-accent-hover transition-all shadow-lg shadow-accent/15 disabled:opacity-30 disabled:shadow-none disabled:hover:bg-accent flex-shrink-0 mb-0.5" className="w-10 h-10 rounded-[10px] bg-accent text-white hover:bg-accent-hover hover:-translate-y-px hover:shadow-[0_4px_12px_rgba(156,175,136,0.25)] transition-all disabled:opacity-30 flex items-center justify-center flex-shrink-0"
> >
<ArrowUp size={18} /> <ArrowUp size={16} />
</button> </button>
</div> </div>
<p className="text-center text-[10px] text-text-muted/50 mt-2"> <p className="text-center text-[10px] text-text-muted mt-2">
{t('chat.mistakeWarning')} {t('chat.mistakeWarning')}
</p> </p>
</div> </div>
+55 -56
View File
@@ -69,9 +69,9 @@ export function LeftPanel({ activeTab }: LeftPanelProps) {
const isChats = activeTab === 'chats'; const isChats = activeTab === 'chats';
return ( return (
<div className="w-64 bg-bg-sidebar border-r border-border-primary flex flex-col shrink-0"> <div className="w-[260px] bg-bg-sidebar border-r border-border-primary flex flex-col shrink-0">
<div className="flex items-center justify-between px-4 py-3 border-b border-border-primary"> <div className="flex items-center justify-between px-4 py-4">
<span className="text-[11px] font-bold text-text-muted uppercase tracking-widest"> <span className="text-[11px] font-semibold text-text-muted uppercase tracking-[1.5px]">
{isChats ? t('chat.chatHistory') : t('nav.workflow')} {isChats ? t('chat.chatHistory') : t('nav.workflow')}
</span> </span>
<button <button
@@ -79,47 +79,49 @@ export function LeftPanel({ activeTab }: LeftPanelProps) {
if (isChats) handleNewChat(); if (isChats) handleNewChat();
else setSelectedWorkflow('new'); else setSelectedWorkflow('new');
}} }}
className="p-1.5 rounded-lg bg-bg-hover text-text-muted hover:text-accent hover:bg-accent-light transition-all" className="w-[26px] h-[26px] rounded-md bg-bg-card text-text-secondary hover:bg-accent hover:text-white transition-all flex items-center justify-center shadow-[0_1px_2px_rgba(0,0,0,0.04)]"
title={isChats ? t('chat.newChat') : t('workflow.createWorkflow')} title={isChats ? t('chat.newChat') : t('workflow.createWorkflow')}
> >
<Plus size={14} /> <Plus size={12} />
</button> </button>
</div> </div>
<div className="flex-1 overflow-y-auto py-2 px-2 space-y-0.5"> <div className="flex-1 overflow-y-auto px-2 pb-2">
{isChats ? ( {isChats ? (
sessions.length === 0 ? ( sessions.length === 0 ? (
<div className="px-3 py-8 text-center text-text-muted text-xs"> <div className="px-3 py-8 text-center text-text-muted text-xs">
{t('chat.noHistory')} {t('chat.noHistory')}
</div> </div>
) : ( ) : (
sessions.map((session) => ( sessions.map((session) => {
<div const isActive = activeSessionId === session.id;
key={session.id} return (
onClick={() => setActiveSessionId(session.id)} <div
className={`group flex items-center gap-2.5 px-3 py-2.5 rounded-lg cursor-pointer transition-all ${ key={session.id}
activeSessionId === session.id onClick={() => setActiveSessionId(session.id)}
? 'bg-accent-light text-accent' className={`group flex items-center gap-2.5 px-2.5 py-2 rounded-lg cursor-pointer transition-all mb-px ${
: 'hover:bg-bg-hover text-text-secondary' isActive
}`} ? 'bg-bg-card shadow-[0_1px_3px_rgba(0,0,0,0.04)]'
> : 'hover:bg-white/60 dark:hover:bg-white/[0.04]'
<MessageSquare size={14} className={`flex-shrink-0 ${activeSessionId === session.id ? 'text-accent' : 'text-text-muted'}`} /> }`}
<div className="flex-1 min-w-0">
<h3 className={`text-xs font-medium truncate ${activeSessionId === session.id ? 'text-accent' : 'text-text-secondary'}`}>
{session.title}
</h3>
<p className="text-[10px] text-text-muted mt-0.5">
{new Date(session.updatedAt).toLocaleDateString()}
</p>
</div>
<button
onClick={(e) => handleDeleteChat(e, session.id)}
className="opacity-0 group-hover:opacity-100 p-1 rounded text-text-muted hover:text-danger hover:bg-danger-bg transition-all"
> >
<Trash2 size={12} /> <div className={`w-7 h-7 rounded-[7px] flex items-center justify-center flex-shrink-0 ${isActive ? 'bg-accent-light' : 'bg-bg-primary'}`}>
</button> <MessageSquare size={12} className={isActive ? 'text-accent' : 'text-text-muted'} />
</div> </div>
)) <div className="flex-1 min-w-0">
<h3 className={`text-xs truncate ${isActive ? 'text-text-primary font-medium' : 'text-text-secondary'}`}>
{session.title}
</h3>
</div>
<button
onClick={(e) => handleDeleteChat(e, session.id)}
className="opacity-0 group-hover:opacity-100 p-1 rounded text-text-muted hover:text-danger transition-all"
>
<Trash2 size={11} />
</button>
</div>
);
})
) )
) : ( ) : (
loadingWorkflows ? ( loadingWorkflows ? (
@@ -129,32 +131,29 @@ export function LeftPanel({ activeTab }: LeftPanelProps) {
{t('workflow.noWorkflows')} {t('workflow.noWorkflows')}
</div> </div>
) : ( ) : (
workflows.map((wf) => ( workflows.map((wf) => {
<div const isActive = selectedWorkflow === wf.trace_id;
key={wf.trace_id} return (
onClick={() => setSelectedWorkflow(wf.trace_id)} <div
className={`group flex items-center gap-2.5 px-3 py-2.5 rounded-lg cursor-pointer transition-all ${ key={wf.trace_id}
selectedWorkflow === wf.trace_id onClick={() => setSelectedWorkflow(wf.trace_id)}
? 'bg-accent-light' className={`group flex items-center gap-2.5 px-2.5 py-2 rounded-lg cursor-pointer transition-all mb-px ${
: 'hover:bg-bg-hover' isActive
}`} ? 'bg-bg-card shadow-[0_1px_3px_rgba(0,0,0,0.04)]'
> : 'hover:bg-white/60 dark:hover:bg-white/[0.04]'
<WorkflowIcon size={14} className={`flex-shrink-0 ${selectedWorkflow === wf.trace_id ? 'text-accent' : 'text-text-muted'}`} /> }`}
<div className="flex-1 min-w-0"> >
<h3 className={`text-xs font-medium truncate ${selectedWorkflow === wf.trace_id ? 'text-accent' : 'text-text-secondary'}`}> <div className={`w-7 h-7 rounded-[7px] flex items-center justify-center flex-shrink-0 ${isActive ? 'bg-accent-light' : 'bg-bg-primary'}`}>
{wf.title || t('common.unnamed')} <WorkflowIcon size={12} className={isActive ? 'text-accent' : 'text-text-muted'} />
</h3> </div>
<div className="flex items-center gap-1.5 mt-0.5"> <div className="flex-1 min-w-0">
<span className={`w-1.5 h-1.5 rounded-full ${ <h3 className={`text-xs truncate ${isActive ? 'text-text-primary font-medium' : 'text-text-secondary'}`}>
wf.status?.includes('working') ? 'bg-accent animate-pulse' : {wf.title || t('common.unnamed')}
wf.status === 'failed' ? 'bg-danger' : </h3>
wf.status === 'completed' ? 'bg-success' : 'bg-text-muted'
}`} />
<span className="text-[10px] text-text-muted font-mono truncate">{wf.trace_id.slice(-8)}</span>
</div> </div>
</div> </div>
</div> );
)) })
) )
)} )}
</div> </div>
@@ -2,7 +2,6 @@ import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import apiClient from '../../api/client'; import apiClient from '../../api/client';
import type { Workflow } from '../../types'; import type { Workflow } from '../../types';
import { PlayCircle, CheckCircle, XCircle, Clock, ArrowRight, Zap } from 'lucide-react';
interface WorkflowListViewProps { interface WorkflowListViewProps {
onSelectWorkflow: (id: string) => void; onSelectWorkflow: (id: string) => void;
@@ -37,11 +36,24 @@ export function WorkflowListView({ onSelectWorkflow }: WorkflowListViewProps) {
return () => clearInterval(intervalId); return () => clearInterval(intervalId);
}, []); }, []);
const getStatusMeta = (status?: string) => { const getStatusStyle = (status?: string) => {
if (status === 'completed') return { icon: CheckCircle, color: 'text-success', bg: 'bg-success-bg', border: 'border-success/20', glow: 'group-hover:shadow-success/20' }; if (status && status.includes('working')) {
if (status === 'failed') return { icon: XCircle, color: 'text-danger', bg: 'bg-danger-bg', border: 'border-danger/20', glow: 'group-hover:shadow-danger/20' }; return { label: t('workflow.status.running'), bg: 'bg-[rgba(156,175,136,0.12)]', text: 'text-[#7a8e6a]' };
if (status && status.includes('working')) return { icon: PlayCircle, color: 'text-accent', bg: 'bg-accent-light', border: 'border-accent/20', glow: 'group-hover:shadow-accent/20' }; }
return { icon: Clock, color: 'text-text-muted', bg: 'bg-bg-secondary', border: 'border-border-primary', glow: 'group-hover:shadow-border-primary' }; if (status === 'completed') {
return { label: t('workflow.status.completed'), bg: 'bg-[rgba(196,168,130,0.12)]', text: 'text-[#9a7d5e]' };
}
if (status === 'failed') {
return { label: t('workflow.status.failed'), bg: 'bg-[rgba(196,145,122,0.1)]', text: 'text-[#a0705a]' };
}
return { label: t('workflow.status.waiting'), bg: 'bg-[rgba(138,154,170,0.1)]', text: 'text-[#6e7d8d]' };
};
const stats = {
total: workflows.length,
running: workflows.filter((w) => w.status?.includes('working')).length,
completed: workflows.filter((w) => w.status === 'completed').length,
queued: workflows.filter((w) => !w.status || (!w.status.includes('working') && w.status !== 'completed' && w.status !== 'failed')).length,
}; };
if (loading) { if (loading) {
@@ -56,80 +68,85 @@ export function WorkflowListView({ onSelectWorkflow }: WorkflowListViewProps) {
} }
return ( return (
<div className="flex-1 flex flex-col p-8 overflow-auto"> <div className="flex-1 overflow-y-auto px-7 py-6">
<div className="max-w-6xl mx-auto w-full"> {/* Header */}
<div className="flex justify-between items-end mb-8"> <div className="flex justify-between items-end mb-6">
<div> <div>
<div className="flex items-center gap-2 mb-1"> <h1 className="text-xl font-semibold text-text-primary">{t('workflow.workflows')}</h1>
<Zap size={18} className="text-accent" /> <p className="text-[13px] text-text-muted mt-[3px]">{t('workflow.manageWorkflows')}</p>
<h1 className="text-2xl font-bold text-text-primary tracking-tight">{t('workflow.workflows')}</h1>
</div>
<p className="text-sm text-text-muted">{t('workflow.manageWorkflows')}</p>
</div>
<button
onClick={() => onSelectWorkflow('new')}
className="flex items-center gap-2 px-5 py-2.5 bg-accent text-white font-medium rounded-xl hover:bg-accent-hover transition-all shadow-lg shadow-accent/15 text-sm"
>
<span className="text-base leading-none">+</span>
{t('workflow.createWorkflow')}
</button>
</div> </div>
<button
{workflows.length === 0 ? ( onClick={() => onSelectWorkflow('new')}
<div className="flex flex-col items-center justify-center border border-dashed border-border-primary rounded-2xl bg-bg-card/50 p-16 text-center"> className="px-4 py-2 rounded-[10px] bg-accent text-white text-xs font-semibold hover:bg-accent-hover hover:-translate-y-px hover:shadow-[0_4px_16px_rgba(156,175,136,0.3)] transition-all"
<div className="w-14 h-14 bg-bg-secondary rounded-2xl flex items-center justify-center mb-4"> >
<PlayCircle size={28} className="text-text-muted" /> + {t('workflow.createWorkflow')}
</div> </button>
<h3 className="text-base font-semibold text-text-primary mb-1">{t('workflow.noWorkflows')}</h3>
<p className="text-sm text-text-muted max-w-xs">{t('workflow.workflowsAppearHere')}</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
{workflows.map((wf) => {
const meta = getStatusMeta(wf.status);
const Icon = meta.icon;
return (
<div
key={wf.trace_id}
onClick={() => onSelectWorkflow(wf.trace_id)}
className={`group relative bg-bg-card rounded-2xl p-5 border border-border-primary card-hover cursor-pointer overflow-hidden ${meta.glow}`}
>
{/* Status glow on hover */}
<div className={`absolute top-0 right-0 w-24 h-24 ${meta.bg} rounded-full blur-2xl opacity-0 group-hover:opacity-40 transition-opacity -translate-y-1/2 translate-x-1/2`} />
<div className="relative">
<div className="flex justify-between items-start mb-4">
<div className={`p-2 rounded-xl ${meta.bg} ${meta.border} border`}>
<Icon size={18} className={meta.color} />
</div>
<ArrowRight size={16} className="text-text-muted opacity-0 group-hover:opacity-100 transition-all group-hover:translate-x-0.5" />
</div>
<h3 className="text-base font-semibold text-text-primary mb-1 line-clamp-1" title={wf.title || t('common.unnamed')}>
{wf.title || t('common.unnamed')}
</h3>
{wf.command && (
<p className="text-xs text-text-muted line-clamp-2 mb-4">
{wf.command}
</p>
)}
<div className="flex items-center justify-between pt-3 border-t border-border-secondary">
<span className="text-[10px] font-mono text-text-muted bg-bg-secondary px-2 py-0.5 rounded">
{wf.trace_id.slice(0, 8)}...
</span>
{wf.created_at && (
<span className="text-[10px] text-text-muted">{new Date(wf.created_at).toLocaleDateString()}</span>
)}
</div>
</div>
</div>
);
})}
</div>
)}
</div> </div>
{/* Stats */}
<div className="grid grid-cols-4 gap-3 mb-6">
<div className="p-4 bg-bg-card rounded-xl border border-border-primary">
<div className="text-[10px] font-semibold uppercase tracking-[1px] text-text-muted mb-1.5">{t('workflow.total')}</div>
<div className="text-[22px] font-bold text-text-primary">{stats.total}</div>
</div>
<div className="p-4 bg-bg-card rounded-xl border border-border-primary">
<div className="text-[10px] font-semibold uppercase tracking-[1px] text-text-muted mb-1.5">{t('workflow.status.running')}</div>
<div className="text-[22px] font-bold text-[#7a8e6a]">{stats.running}</div>
</div>
<div className="p-4 bg-bg-card rounded-xl border border-border-primary">
<div className="text-[10px] font-semibold uppercase tracking-[1px] text-text-muted mb-1.5">{t('workflow.status.completed')}</div>
<div className="text-[22px] font-bold text-[#9a7d5e]">{stats.completed}</div>
</div>
<div className="p-4 bg-bg-card rounded-xl border border-border-primary">
<div className="text-[10px] font-semibold uppercase tracking-[1px] text-text-muted mb-1.5">{t('workflow.queued')}</div>
<div className="text-[22px] font-bold text-[#6e7d8d]">{stats.queued}</div>
</div>
</div>
{workflows.length === 0 ? (
<div className="flex flex-col items-center justify-center border border-dashed border-border-primary rounded-xl bg-bg-card/50 p-16 text-center">
<div className="w-14 h-14 bg-bg-secondary rounded-xl flex items-center justify-center mb-4">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-7 h-7 text-text-muted"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
</div>
<h3 className="text-base font-semibold text-text-primary mb-1">{t('workflow.noWorkflows')}</h3>
<p className="text-sm text-text-muted max-w-xs">{t('workflow.workflowsAppearHere')}</p>
</div>
) : (
<div className="grid grid-cols-[repeat(auto-fill,minmax(280px,1fr))] gap-3">
{workflows.map((wf) => {
const style = getStatusStyle(wf.status);
return (
<div
key={wf.trace_id}
onClick={() => onSelectWorkflow(wf.trace_id)}
className="p-4 bg-bg-card rounded-xl border border-border-primary cursor-pointer transition-all hover:shadow-[0_4px_16px_rgba(0,0,0,0.05)] hover:-translate-y-0.5 hover:border-[#d5d0ca] dark:hover:border-white/10"
>
<div className="flex justify-between items-center mb-3">
<span className={`text-[10px] font-semibold px-2.5 py-[3px] rounded-[20px] tracking-[0.3px] ${style.bg} ${style.text}`}>
{style.label}
</span>
</div>
<h3 className="text-[13px] font-semibold text-text-primary mb-[5px] line-clamp-1" title={wf.title || t('common.unnamed')}>
{wf.title || t('common.unnamed')}
</h3>
{wf.command && (
<p className="text-xs text-text-muted leading-relaxed mb-3.5 line-clamp-2">
{wf.command}
</p>
)}
<div className="flex justify-between items-center pt-3 border-t border-border-primary">
<span className="text-[10px] text-[#b5afa8] font-mono bg-bg-primary px-2 py-0.5 rounded">
{wf.trace_id.slice(-8)}
</span>
{wf.created_at && (
<span className="text-[11px] text-text-muted">{new Date(wf.created_at).toLocaleDateString()}</span>
)}
</div>
</div>
);
})}
</div>
)}
</div> </div>
); );
} }
@@ -33,7 +33,7 @@ export function CollapsibleSidebar() {
isSidebarOpen ? 'w-56' : 'w-14' isSidebarOpen ? 'w-56' : 'w-14'
}`} }`}
> >
<div className="flex-1 py-3 space-y-1"> <div className="flex-1 py-3 space-y-0.5">
{navItems.map((item) => { {navItems.map((item) => {
const isActive = activeTab === item.key; const isActive = activeTab === item.key;
return ( return (
@@ -42,18 +42,15 @@ export function CollapsibleSidebar() {
onClick={() => setTab(item.key as any)} onClick={() => setTab(item.key as any)}
className={`w-full flex items-center mx-1.5 rounded-lg transition-all duration-200 group ${ className={`w-full flex items-center mx-1.5 rounded-lg transition-all duration-200 group ${
isActive isActive
? 'bg-accent-light text-accent' ? 'bg-white/50 dark:bg-white/[0.06] text-text-primary shadow-[0_1px_3px_rgba(0,0,0,0.04)]'
: 'text-text-muted hover:text-text-secondary hover:bg-bg-hover' : 'text-text-muted hover:text-text-secondary hover:bg-white/40 dark:hover:bg-white/[0.04]'
} ${isSidebarOpen ? 'px-3 py-2.5 gap-3' : 'px-0 py-2.5 justify-center'}`} } ${isSidebarOpen ? 'px-3 py-2.5 gap-3' : 'px-0 py-2.5 justify-center'}`}
style={{ width: isSidebarOpen ? 'calc(100% - 12px)' : 'calc(100% - 12px)' }} style={{ width: isSidebarOpen ? 'calc(100% - 12px)' : 'calc(100% - 12px)' }}
> >
<item.icon size={18} className={`flex-shrink-0 transition-transform group-hover:scale-110 ${isActive ? 'text-accent' : ''}`} /> <item.icon size={16} className={`flex-shrink-0 transition-transform group-hover:scale-110 ${isActive ? 'text-accent' : ''}`} />
{isSidebarOpen && ( {isSidebarOpen && (
<span className="text-xs font-medium">{item.label}</span> <span className="text-xs font-medium">{item.label}</span>
)} )}
{isActive && isSidebarOpen && (
<div className="ml-auto w-1 h-1 rounded-full bg-accent" />
)}
</button> </button>
); );
})} })}
+39 -45
View File
@@ -36,83 +36,77 @@ export function TopBar() {
}; };
return ( return (
<div className="h-14 glass text-text-primary flex items-center justify-between px-5 shrink-0 z-50 relative"> <div className="h-[52px] glass text-text-primary flex items-center justify-between px-5 shrink-0 z-50 relative">
{/* Left: Logo with status */} {/* Left: Logo + Nav */}
<div className="flex items-center gap-3"> <div className="flex items-center">
<div className="relative"> <div className="flex items-center gap-2.5">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-accent to-glow-purple flex items-center justify-center text-white shadow-lg"> <div className="w-7 h-7 rounded-[7px] bg-accent flex items-center justify-center">
<Zap size={18} fill="currentColor" /> <Zap size={14} className="text-white" />
</div> </div>
<span className="status-dot absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full bg-success border-2 border-bg-card" /> <span className="text-[15px] font-bold tracking-[-0.3px] text-text-primary">{t('app.name')}</span>
</div> </div>
<div className="flex flex-col leading-none">
<span className="font-bold text-sm tracking-tight text-text-primary">{t('app.name')}</span>
<span className="text-[10px] text-text-muted font-medium tracking-wide">{t('app.tagline')}</span>
</div>
</div>
{/* Center: Mode Switch */} {/* Center: Pill Nav */}
<div className="hidden md:flex items-center bg-bg-secondary/80 rounded-full p-0.5 border border-border-primary"> <div className="hidden md:flex items-center gap-px p-0.5 bg-bg-input rounded-lg ml-5">
<button <button
onClick={() => { setMode('work'); setShowSettings(false); }} onClick={() => { setMode('work'); setShowSettings(false); }}
className={`px-4 py-1.5 rounded-full text-xs font-semibold transition-all duration-200 ${ className={`px-3.5 py-[5px] rounded-md text-xs font-semibold transition-all ${
mode === 'work' && !showSettings mode === 'work' && !showSettings
? 'bg-bg-card text-accent shadow-sm' ? 'bg-bg-card text-text-primary shadow-[0_1px_3px_rgba(0,0,0,0.06)]'
: 'text-text-muted hover:text-text-secondary' : 'text-text-muted hover:text-text-secondary'
}`} }`}
> >
{t('nav.work')} {t('nav.work')}
</button> </button>
<button <button
onClick={() => { setMode('agent'); setShowSettings(false); }} onClick={() => { setMode('agent'); setShowSettings(false); }}
className={`px-4 py-1.5 rounded-full text-xs font-semibold transition-all duration-200 ${ className={`px-3.5 py-[5px] rounded-md text-xs font-semibold transition-all ${
mode === 'agent' && !showSettings mode === 'agent' && !showSettings
? 'bg-bg-card text-accent shadow-sm' ? 'bg-bg-card text-text-primary shadow-[0_1px_3px_rgba(0,0,0,0.06)]'
: 'text-text-muted hover:text-text-secondary' : 'text-text-muted hover:text-text-secondary'
}`} }`}
> >
{t('nav.agent')} {t('nav.agent')}
</button> </button>
</div>
</div> </div>
{/* Right: Actions */} {/* Right: Actions */}
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<button <button
onClick={toggleLanguage} onClick={toggleLanguage}
className="p-2 rounded-lg text-text-muted hover:text-text-primary hover:bg-bg-hover transition-all" className="w-8 h-8 rounded-lg text-text-muted hover:text-text-secondary hover:bg-bg-hover transition-all flex items-center justify-center"
title={i18n.language.startsWith('zh') ? t('topbar.switchToEn') : t('topbar.switchToZh')} title={i18n.language.startsWith('zh') ? t('topbar.switchToEn') : t('topbar.switchToZh')}
> >
<Globe size={16} /> <Globe size={15} />
</button> </button>
<button <button
onClick={toggleTheme} onClick={toggleTheme}
className="p-2 rounded-lg text-text-muted hover:text-text-primary hover:bg-bg-hover transition-all" className="w-8 h-8 rounded-lg text-text-muted hover:text-text-secondary hover:bg-bg-hover transition-all flex items-center justify-center"
title={resolvedTheme === 'dark' ? t('topbar.lightMode') : t('topbar.darkMode')} title={resolvedTheme === 'dark' ? t('topbar.lightMode') : t('topbar.darkMode')}
> >
{resolvedTheme === 'dark' ? <Sun size={16} /> : <Moon size={16} />} {resolvedTheme === 'dark' ? <Sun size={15} /> : <Moon size={15} />}
</button> </button>
<div className="w-px h-4 bg-border-primary mx-1" />
<button <button
onClick={() => setShowSettings(!showSettings)} onClick={() => setShowSettings(!showSettings)}
className={`p-2 rounded-lg transition-all ${ className={`w-8 h-8 rounded-lg transition-all flex items-center justify-center ${
showSettings showSettings
? 'bg-accent-light text-accent' ? 'bg-accent-light text-accent'
: 'text-text-muted hover:text-text-primary hover:bg-bg-hover' : 'text-text-muted hover:text-text-secondary hover:bg-bg-hover'
}`} }`}
title={t('nav.settings')} title={t('nav.settings')}
> >
<Settings size={16} /> <Settings size={15} />
</button> </button>
<button <button
onClick={handleLogout} onClick={handleLogout}
className="p-2 rounded-lg text-text-muted hover:text-danger hover:bg-danger-bg transition-all ml-0.5" className="w-8 h-8 rounded-lg text-text-muted hover:text-danger hover:bg-danger-bg transition-all flex items-center justify-center"
title={t('topbar.logout')} title={t('topbar.logout')}
> >
<LogOut size={16} /> <LogOut size={15} />
</button> </button>
</div> </div>
</div> </div>
@@ -14,23 +14,25 @@ export function SettingsLayout() {
]; ];
return ( return (
<div className="flex-1 flex bg-bg-secondary overflow-hidden"> <div className="flex-1 flex bg-bg-primary overflow-hidden">
<div className="w-56 bg-bg-sidebar border-r border-border-primary flex flex-col"> <div className="w-56 bg-bg-sidebar border-r border-border-primary flex flex-col">
<div className="px-5 py-4 border-b border-border-primary"> <div className="px-4 py-3.5">
<h2 className="text-sm font-bold text-text-primary">{t('settings.settings')}</h2> <span className="text-[10px] font-semibold text-text-muted uppercase tracking-[1.5px]">
{t('settings.settings')}
</span>
</div> </div>
<div className="p-2 space-y-0.5"> <div className="px-1.5 pb-2 space-y-0.5">
{tabs.map((tab) => ( {tabs.map((tab) => (
<button <button
key={tab.key} key={tab.key}
onClick={() => setSettingsTab(tab.key)} onClick={() => setSettingsTab(tab.key)}
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-xs font-medium transition-all ${ className={`w-full flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-xs font-medium transition-all ${
settingsTab === tab.key settingsTab === tab.key
? 'bg-accent-light text-accent' ? 'bg-bg-card text-text-primary shadow-[0_1px_3px_rgba(0,0,0,0.04)]'
: 'text-text-muted hover:text-text-secondary hover:bg-bg-hover' : 'text-text-muted hover:text-text-secondary hover:bg-white/50 dark:hover:bg-white/[0.04]'
}`} }`}
> >
<tab.icon size={15} /> <tab.icon size={14} />
{tab.label} {tab.label}
</button> </button>
))} ))}
+3 -1
View File
@@ -88,7 +88,9 @@
"running": "Running", "running": "Running",
"completed": "Completed", "completed": "Completed",
"failed": "Failed" "failed": "Failed"
} },
"total": "Total",
"queued": "Queued"
}, },
"settings": { "settings": {
"settings": "Settings", "settings": "Settings",
+3 -1
View File
@@ -88,7 +88,9 @@
"running": "运行中", "running": "运行中",
"completed": "已完成", "completed": "已完成",
"failed": "失败" "failed": "失败"
} },
"total": "总数",
"queued": "排队中"
}, },
"settings": { "settings": {
"settings": "设置", "settings": "设置",
+85 -69
View File
@@ -38,72 +38,83 @@
--color-warning-bg: var(--warning-bg); --color-warning-bg: var(--warning-bg);
--color-glow-purple: var(--glow-purple); --color-glow-purple: var(--glow-purple);
--color-glow-cyan: var(--glow-cyan); --color-glow-cyan: var(--glow-cyan);
--color-clay: var(--clay);
--color-slate: var(--slate);
--color-terracotta: var(--terracotta);
--font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; --font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace; --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
} }
/* ===== Morandi Light ===== */
:root { :root {
--bg-primary: #fafafa; --bg-primary: #f2f0ed;
--bg-secondary: #f4f4f5; --bg-secondary: #eae8e4;
--bg-tertiary: #e4e4e7; --bg-tertiary: #e0ddd8;
--bg-card: #ffffff; --bg-card: #faf9f7;
--bg-sidebar: #f4f4f5; --bg-sidebar: #eae8e4;
--bg-topbar: rgba(255, 255, 255, 0.72); --bg-topbar: rgba(250, 249, 247, 0.85);
--bg-input: #f4f4f5; --bg-input: #f2f0ed;
--bg-hover: #f4f4f5; --bg-hover: rgba(255, 255, 255, 0.4);
--bg-active: #eff6ff; --bg-active: rgba(156, 175, 136, 0.08);
--bg-glass: rgba(255, 255, 255, 0.6); --bg-glass: rgba(250, 249, 247, 0.72);
--border-primary: #e4e4e7; --border-primary: #e0ddd8;
--border-secondary: #f4f4f5; --border-secondary: #eae8e4;
--text-primary: #18181b; --text-primary: #3d3d3d;
--text-secondary: #3f3f46; --text-secondary: #5a5a5a;
--text-tertiary: #52525b; --text-tertiary: #8c8680;
--text-muted: #a1a1aa; --text-muted: #b5afa8;
--accent: #4f46e5; --accent: #9caf88;
--accent-hover: #4338ca; --accent-hover: #8a9e78;
--accent-light: #e0e7ff; --accent-light: rgba(156, 175, 136, 0.12);
--accent-text: #3730a3; --accent-text: #7a8e6a;
--accent-glow: rgba(79, 70, 229, 0.15); --accent-glow: rgba(156, 175, 136, 0.2);
--danger: #dc2626; --danger: #c4917a;
--danger-bg: #fef2f2; --danger-bg: rgba(196, 145, 122, 0.08);
--success: #16a34a; --success: #7a8e6a;
--success-bg: #f0fdf4; --success-bg: rgba(122, 142, 106, 0.08);
--warning: #d97706; --warning: #c4a882;
--warning-bg: #fffbeb; --warning-bg: rgba(196, 168, 130, 0.08);
--glow-purple: #a855f7; --glow-purple: #8a9aaa;
--glow-cyan: #06b6d4; --glow-cyan: #c4a882;
--clay: #c4a882;
--slate: #8a9aaa;
--terracotta: #c4917a;
} }
/* ===== Morandi Dark ===== */
.dark { .dark {
--bg-primary: #09090b; --bg-primary: #1c1b19;
--bg-secondary: #0f0f11; --bg-secondary: #232220;
--bg-tertiary: #18181b; --bg-tertiary: #2d2b28;
--bg-card: #131316; --bg-card: #252421;
--bg-sidebar: #0c0c0e; --bg-sidebar: #1e1d1b;
--bg-topbar: rgba(9, 9, 11, 0.72); --bg-topbar: rgba(28, 27, 25, 0.85);
--bg-input: #18181b; --bg-input: #2d2b28;
--bg-hover: #1c1c1f; --bg-hover: rgba(255, 255, 255, 0.04);
--bg-active: rgba(79, 70, 229, 0.12); --bg-active: rgba(156, 175, 136, 0.1);
--bg-glass: rgba(19, 19, 22, 0.6); --bg-glass: rgba(37, 36, 33, 0.72);
--border-primary: rgba(255, 255, 255, 0.08); --border-primary: rgba(255, 255, 255, 0.06);
--border-secondary: rgba(255, 255, 255, 0.04); --border-secondary: rgba(255, 255, 255, 0.03);
--text-primary: #fafafa; --text-primary: #e8e6e3;
--text-secondary: #e4e4e7; --text-secondary: #c8c5c0;
--text-tertiary: #a1a1aa; --text-tertiary: #a09c96;
--text-muted: #71717a; --text-muted: #7a7772;
--accent: #6366f1; --accent: #a8bc94;
--accent-hover: #818cf8; --accent-hover: #b8caa6;
--accent-light: rgba(99, 102, 241, 0.15); --accent-light: rgba(156, 175, 136, 0.15);
--accent-text: #a5b4fc; --accent-text: #c4d4b4;
--accent-glow: rgba(99, 102, 241, 0.25); --accent-glow: rgba(156, 175, 136, 0.2);
--danger: #f87171; --danger: #d4a894;
--danger-bg: rgba(248, 113, 113, 0.1); --danger-bg: rgba(196, 145, 122, 0.1);
--success: #4ade80; --success: #9caf88;
--success-bg: rgba(74, 222, 128, 0.1); --success-bg: rgba(156, 175, 136, 0.1);
--warning: #fbbf24; --warning: #c4a882;
--warning-bg: rgba(251, 191, 36, 0.1); --warning-bg: rgba(196, 168, 130, 0.1);
--glow-purple: #c084fc; --glow-purple: #a0aab8;
--glow-cyan: #67e8f9; --glow-cyan: #c4b89e;
--clay: #c4a882;
--slate: #8a9aaa;
--terracotta: #c4917a;
} }
html { html {
@@ -118,16 +129,19 @@ body {
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
/* Dot pattern background for dark mode */ /* Dot pattern background */
.dark body::before { body::before {
content: ""; content: "";
position: fixed; position: fixed;
inset: 0; inset: 0;
background-image: radial-gradient(circle at 1px 1px, rgba(255,255,255,0.04) 1px, transparent 0); background-image: radial-gradient(circle at 1px 1px, rgba(0, 0, 0, 0.012) 1px, transparent 0);
background-size: 24px 24px; background-size: 24px 24px;
pointer-events: none; pointer-events: none;
z-index: 0; z-index: 0;
} }
.dark body::before {
background-image: radial-gradient(circle at 1px 1px, rgba(255, 255, 255, 0.015) 1px, transparent 0);
}
/* Custom scrollbar */ /* Custom scrollbar */
::-webkit-scrollbar { ::-webkit-scrollbar {
@@ -139,7 +153,7 @@ body {
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: var(--border-primary); background: var(--border-primary);
border-radius: 10px; border-radius: 4px;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: var(--text-muted); background: var(--text-muted);
@@ -209,21 +223,23 @@ body {
/* Glass effect */ /* Glass effect */
.glass { .glass {
background: var(--bg-glass); background: var(--bg-glass);
backdrop-filter: blur(16px) saturate(180%); backdrop-filter: blur(16px) saturate(140%);
-webkit-backdrop-filter: blur(16px) saturate(180%); -webkit-backdrop-filter: blur(16px) saturate(140%);
border-bottom: 1px solid var(--border-primary); border-bottom: 1px solid var(--border-primary);
} }
/* Modern card hover */ /* Card hover */
.card-hover { .card-hover {
transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.2s ease, border-color 0.2s ease; transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.25s ease, border-color 0.25s ease;
} }
.card-hover:hover { .card-hover:hover {
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 8px 30px -8px rgba(0, 0, 0, 0.12); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.05);
border-color: #d5d0ca;
} }
.dark .card-hover:hover { .dark .card-hover:hover {
box-shadow: 0 8px 30px -8px rgba(0, 0, 0, 0.4); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
border-color: rgba(255, 255, 255, 0.1);
} }
/* Glow effects */ /* Glow effects */
@@ -231,7 +247,7 @@ body {
box-shadow: 0 0 20px -4px var(--accent-glow); box-shadow: 0 0 20px -4px var(--accent-glow);
} }
.glow-purple { .glow-purple {
box-shadow: 0 0 30px -6px rgba(168, 85, 247, 0.3); box-shadow: 0 0 30px -6px rgba(138, 154, 170, 0.2);
} }
/* Status indicator pulse */ /* Status indicator pulse */