style(frontend):优化前端效果
1.对于UI的配色和布局进行了优化
This commit is contained in:
+5
-10
@@ -28,7 +28,6 @@ function App() {
|
||||
|
||||
const { loadSessions } = useChatStore();
|
||||
|
||||
// Initialize theme on mount
|
||||
useEffect(() => {
|
||||
applyTheme();
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
@@ -37,14 +36,12 @@ function App() {
|
||||
return () => mediaQuery.removeEventListener('change', handler);
|
||||
}, [applyTheme]);
|
||||
|
||||
// Sync persisted locale to i18next on mount
|
||||
useEffect(() => {
|
||||
if (locale && i18n.language !== locale) {
|
||||
i18n.changeLanguage(locale);
|
||||
}
|
||||
}, [locale]);
|
||||
|
||||
// Check auth and load sessions
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
@@ -75,12 +72,10 @@ function App() {
|
||||
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{mode === 'work' && workTab === 'chat' && (
|
||||
<div className="flex-1 p-4 flex overflow-hidden gap-4">
|
||||
<div className="flex-1 flex bg-bg-card rounded-2xl shadow-sm border border-border-primary overflow-hidden relative">
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
<LeftPanel activeTab="chats" />
|
||||
<ChatPanel />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode === 'work' && workTab === 'workflow' && <WorkflowShell />}
|
||||
@@ -101,22 +96,22 @@ function WorkflowShell() {
|
||||
|
||||
if (selectedWorkflow === 'new') {
|
||||
return (
|
||||
<>
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
<LeftPanel activeTab="workflows" />
|
||||
<NewWorkflowDialog
|
||||
onClose={() => setSelectedWorkflow(null)}
|
||||
onSuccess={(traceId: string) => setSelectedWorkflow(traceId)}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedWorkflow) {
|
||||
return (
|
||||
<>
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
<LeftPanel activeTab="workflows" />
|
||||
<RightPanel selectedWorkflow={selectedWorkflow} />
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import apiClient from '../../api/client';
|
||||
import { Zap, ArrowRight, ShieldCheck } from 'lucide-react';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
|
||||
interface AuthPageProps {
|
||||
onLoginSuccess: () => void;
|
||||
@@ -49,58 +49,72 @@ export function AuthPage({ onLoginSuccess }: AuthPageProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen w-full relative overflow-hidden">
|
||||
{/* Background decoration */}
|
||||
<div className="absolute inset-0 bg-bg-primary">
|
||||
<div className="absolute top-0 left-1/4 w-96 h-96 bg-accent/5 rounded-full blur-3xl" />
|
||||
<div className="absolute bottom-0 right-1/4 w-96 h-96 bg-glow-purple/5 rounded-full blur-3xl" />
|
||||
<div className="min-h-screen w-full bg-bg-primary flex items-center justify-center">
|
||||
<div className="flex items-center gap-[70px] w-[900px]">
|
||||
{/* Left: Brand */}
|
||||
<div className="flex-1">
|
||||
<div className="w-10 h-[3px] rounded-sm bg-gradient-to-r from-accent to-clay mb-6" />
|
||||
<h1 className="text-[40px] font-bold tracking-tight leading-tight mb-3 text-text-primary">
|
||||
Kilo<span className="not-italic text-accent">Star</span>
|
||||
</h1>
|
||||
<p className="text-[15px] text-text-muted leading-relaxed mb-8">
|
||||
{t('app.tagline')}
|
||||
</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>
|
||||
|
||||
<div className="relative z-10 flex w-full items-center justify-center p-6">
|
||||
<div className="w-full max-w-md animate-fade-in-scale">
|
||||
{/* 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">
|
||||
{/* 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>
|
||||
</div>
|
||||
<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-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'}`}>
|
||||
<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-4">
|
||||
<form onSubmit={handleSubmit} className="space-y-[18px]">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('auth.username')}</label>
|
||||
<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-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"
|
||||
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-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('auth.password')}</label>
|
||||
<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-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"
|
||||
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
|
||||
/>
|
||||
@@ -109,7 +123,7 @@ export function AuthPage({ onLoginSuccess }: AuthPageProps) {
|
||||
<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"
|
||||
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">
|
||||
@@ -119,25 +133,24 @@ export function AuthPage({ onLoginSuccess }: AuthPageProps) {
|
||||
) : (
|
||||
<>
|
||||
{isLogin ? t('auth.signIn') : t('auth.signUp')}
|
||||
<ArrowRight size={16} className="transition-transform group-hover:translate-x-0.5" />
|
||||
<ArrowRight size={16} />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 text-center text-sm text-text-muted">
|
||||
<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-semibold hover:text-accent-hover transition-colors"
|
||||
className="text-accent font-medium 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')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
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';
|
||||
|
||||
export function ChatPanel() {
|
||||
@@ -64,36 +64,32 @@ export function ChatPanel() {
|
||||
|
||||
if (!activeSessionId) {
|
||||
return (
|
||||
<div className="flex-1 flex flex-col items-center justify-center p-8 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="flex-1 flex flex-col items-center justify-center p-10 relative overflow-hidden">
|
||||
<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">
|
||||
<Activity size={32} />
|
||||
<div className="w-14 h-14 rounded-2xl bg-accent-light flex items-center justify-center text-accent mb-5">
|
||||
<Activity size={24} />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-text-primary mb-2">{t('chat.assistantName')}</h2>
|
||||
<p className="text-text-muted text-sm mb-8 text-center max-w-sm">
|
||||
<h2 className="text-lg font-semibold text-text-primary mb-1.5">{t('chat.assistantName')}</h2>
|
||||
<p className="text-text-secondary text-sm mb-7 text-center max-w-sm">
|
||||
{t('chat.selectChat')}
|
||||
</p>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="grid grid-cols-2 gap-3 mb-8 w-full max-w-md">
|
||||
<div className="grid grid-cols-2 gap-2.5 mb-7 w-full max-w-[380px]">
|
||||
{quickActions.map((action) => (
|
||||
<button
|
||||
key={action.label}
|
||||
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" />
|
||||
<span className="text-sm text-text-secondary group-hover:text-text-primary transition-colors">{action.label}</span>
|
||||
<action.icon size={14} className="text-accent flex-shrink-0" />
|
||||
<span className="text-xs text-text-secondary group-hover:text-text-primary transition-colors">{action.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
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} />
|
||||
{t('chat.newChat')}
|
||||
@@ -104,19 +100,17 @@ export function ChatPanel() {
|
||||
}
|
||||
|
||||
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 */}
|
||||
<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="flex items-center gap-2.5">
|
||||
<div className="w-7 h-7 rounded-lg bg-accent-light flex items-center justify-center">
|
||||
<MessageSquare size={14} className="text-accent" />
|
||||
</div>
|
||||
<h1 className="font-semibold text-sm text-text-primary">{activeSession?.title || t('chat.defaultTitle')}</h1>
|
||||
<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="w-[26px] h-[26px] rounded-[7px] flex items-center justify-center bg-accent-light">
|
||||
<MessageSquare size={12} className="text-accent" />
|
||||
</div>
|
||||
<span className="text-[13px] font-semibold text-text-primary">{activeSession?.title || t('chat.defaultTitle')}</span>
|
||||
</div>
|
||||
|
||||
{/* 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 ? (
|
||||
<div className="flex justify-center items-center h-full">
|
||||
<div className="flex items-center gap-1.5">
|
||||
@@ -129,24 +123,29 @@ export function ChatPanel() {
|
||||
messages.map((msg, idx) => (
|
||||
<div
|
||||
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` }}
|
||||
>
|
||||
{msg.role === 'assistant' && (
|
||||
<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">
|
||||
<Activity size={13} className="text-white" />
|
||||
</div>
|
||||
)}
|
||||
<div className={`max-w-[85%] ${msg.role === 'user' ? 'mr-1' : ''}`}>
|
||||
<div
|
||||
className={`px-4 py-2.5 text-sm leading-relaxed whitespace-pre-wrap ${
|
||||
msg.role === 'user'
|
||||
? 'bg-text-primary text-bg-primary rounded-2xl rounded-tr-sm'
|
||||
: 'bg-bg-secondary border border-border-primary rounded-2xl rounded-tl-sm'
|
||||
}`}
|
||||
className="w-7 h-7 rounded-full flex items-center justify-center flex-shrink-0"
|
||||
style={{
|
||||
background: msg.role === 'assistant'
|
||||
? 'var(--accent)'
|
||||
: 'linear-gradient(135deg, #6b6860, #8a8578)',
|
||||
}}
|
||||
>
|
||||
{msg.content}
|
||||
{msg.role === 'assistant' ? (
|
||||
<Activity size={12} className="text-white" />
|
||||
) : (
|
||||
<User size={12} className="text-white" />
|
||||
)}
|
||||
</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>
|
||||
))
|
||||
@@ -154,11 +153,11 @@ export function ChatPanel() {
|
||||
|
||||
{/* Typing indicator */}
|
||||
{activeSession && activeSession.messages.length > 0 && activeSession.messages[activeSession.messages.length - 1].role === 'user' && (
|
||||
<div className="flex justify-start 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">
|
||||
<Activity size={13} className="text-white animate-pulse" />
|
||||
<div className="flex gap-2.5 max-w-[78%] animate-fade-in">
|
||||
<div className="w-7 h-7 rounded-full bg-accent flex items-center justify-center flex-shrink-0">
|
||||
<Activity size={12} className="text-white animate-pulse" />
|
||||
</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">
|
||||
<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>
|
||||
|
||||
{/* Input */}
|
||||
<div className="p-4 bg-bg-card border-t border-border-primary/60 shrink-0">
|
||||
<div className="relative flex items-end gap-2 max-w-3xl mx-auto">
|
||||
<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="flex items-end gap-2 max-w-[720px] mx-auto">
|
||||
<input type="file" ref={fileInputRef} className="hidden" />
|
||||
<button
|
||||
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')}
|
||||
>
|
||||
<Plus size={18} />
|
||||
@@ -193,19 +192,19 @@ export function ChatPanel() {
|
||||
}}
|
||||
placeholder={t('chat.placeholder')}
|
||||
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' }}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSendMessage}
|
||||
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>
|
||||
</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')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -69,9 +69,9 @@ export function LeftPanel({ activeTab }: LeftPanelProps) {
|
||||
const isChats = activeTab === 'chats';
|
||||
|
||||
return (
|
||||
<div className="w-64 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">
|
||||
<span className="text-[11px] font-bold text-text-muted uppercase tracking-widest">
|
||||
<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-4">
|
||||
<span className="text-[11px] font-semibold text-text-muted uppercase tracking-[1.5px]">
|
||||
{isChats ? t('chat.chatHistory') : t('nav.workflow')}
|
||||
</span>
|
||||
<button
|
||||
@@ -79,47 +79,49 @@ export function LeftPanel({ activeTab }: LeftPanelProps) {
|
||||
if (isChats) handleNewChat();
|
||||
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')}
|
||||
>
|
||||
<Plus size={14} />
|
||||
<Plus size={12} />
|
||||
</button>
|
||||
</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 ? (
|
||||
sessions.length === 0 ? (
|
||||
<div className="px-3 py-8 text-center text-text-muted text-xs">
|
||||
{t('chat.noHistory')}
|
||||
</div>
|
||||
) : (
|
||||
sessions.map((session) => (
|
||||
sessions.map((session) => {
|
||||
const isActive = activeSessionId === session.id;
|
||||
return (
|
||||
<div
|
||||
key={session.id}
|
||||
onClick={() => setActiveSessionId(session.id)}
|
||||
className={`group flex items-center gap-2.5 px-3 py-2.5 rounded-lg cursor-pointer transition-all ${
|
||||
activeSessionId === session.id
|
||||
? 'bg-accent-light text-accent'
|
||||
: 'hover:bg-bg-hover text-text-secondary'
|
||||
className={`group flex items-center gap-2.5 px-2.5 py-2 rounded-lg cursor-pointer transition-all mb-px ${
|
||||
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={`w-7 h-7 rounded-[7px] flex items-center justify-center flex-shrink-0 ${isActive ? 'bg-accent-light' : 'bg-bg-primary'}`}>
|
||||
<MessageSquare size={12} className={isActive ? 'text-accent' : 'text-text-muted'} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className={`text-xs font-medium truncate ${activeSessionId === session.id ? 'text-accent' : 'text-text-secondary'}`}>
|
||||
<h3 className={`text-xs truncate ${isActive ? 'text-text-primary font-medium' : '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"
|
||||
className="opacity-0 group-hover:opacity-100 p-1 rounded text-text-muted hover:text-danger transition-all"
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
<Trash2 size={11} />
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
);
|
||||
})
|
||||
)
|
||||
) : (
|
||||
loadingWorkflows ? (
|
||||
@@ -129,32 +131,29 @@ export function LeftPanel({ activeTab }: LeftPanelProps) {
|
||||
{t('workflow.noWorkflows')}
|
||||
</div>
|
||||
) : (
|
||||
workflows.map((wf) => (
|
||||
workflows.map((wf) => {
|
||||
const isActive = selectedWorkflow === wf.trace_id;
|
||||
return (
|
||||
<div
|
||||
key={wf.trace_id}
|
||||
onClick={() => setSelectedWorkflow(wf.trace_id)}
|
||||
className={`group flex items-center gap-2.5 px-3 py-2.5 rounded-lg cursor-pointer transition-all ${
|
||||
selectedWorkflow === wf.trace_id
|
||||
? 'bg-accent-light'
|
||||
: 'hover:bg-bg-hover'
|
||||
className={`group flex items-center gap-2.5 px-2.5 py-2 rounded-lg cursor-pointer transition-all mb-px ${
|
||||
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={`w-7 h-7 rounded-[7px] flex items-center justify-center flex-shrink-0 ${isActive ? 'bg-accent-light' : 'bg-bg-primary'}`}>
|
||||
<WorkflowIcon size={12} className={isActive ? 'text-accent' : 'text-text-muted'} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className={`text-xs font-medium truncate ${selectedWorkflow === wf.trace_id ? 'text-accent' : 'text-text-secondary'}`}>
|
||||
<h3 className={`text-xs truncate ${isActive ? 'text-text-primary font-medium' : 'text-text-secondary'}`}>
|
||||
{wf.title || t('common.unnamed')}
|
||||
</h3>
|
||||
<div className="flex items-center gap-1.5 mt-0.5">
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${
|
||||
wf.status?.includes('working') ? 'bg-accent animate-pulse' :
|
||||
wf.status === 'failed' ? 'bg-danger' :
|
||||
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>
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import apiClient from '../../api/client';
|
||||
import type { Workflow } from '../../types';
|
||||
import { PlayCircle, CheckCircle, XCircle, Clock, ArrowRight, Zap } from 'lucide-react';
|
||||
|
||||
interface WorkflowListViewProps {
|
||||
onSelectWorkflow: (id: string) => void;
|
||||
@@ -37,11 +36,24 @@ export function WorkflowListView({ onSelectWorkflow }: WorkflowListViewProps) {
|
||||
return () => clearInterval(intervalId);
|
||||
}, []);
|
||||
|
||||
const getStatusMeta = (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 === 'failed') return { icon: XCircle, color: 'text-danger', bg: 'bg-danger-bg', border: 'border-danger/20', glow: 'group-hover:shadow-danger/20' };
|
||||
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' };
|
||||
const getStatusStyle = (status?: string) => {
|
||||
if (status && status.includes('working')) {
|
||||
return { label: t('workflow.status.running'), bg: 'bg-[rgba(156,175,136,0.12)]', text: 'text-[#7a8e6a]' };
|
||||
}
|
||||
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) {
|
||||
@@ -56,80 +68,85 @@ export function WorkflowListView({ onSelectWorkflow }: WorkflowListViewProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col p-8 overflow-auto">
|
||||
<div className="max-w-6xl mx-auto w-full">
|
||||
<div className="flex justify-between items-end mb-8">
|
||||
<div className="flex-1 overflow-y-auto px-7 py-6">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-end mb-6">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Zap size={18} className="text-accent" />
|
||||
<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>
|
||||
<h1 className="text-xl font-semibold text-text-primary">{t('workflow.workflows')}</h1>
|
||||
<p className="text-[13px] text-text-muted mt-[3px]">{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"
|
||||
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"
|
||||
>
|
||||
<span className="text-base leading-none">+</span>
|
||||
{t('workflow.createWorkflow')}
|
||||
+ {t('workflow.createWorkflow')}
|
||||
</button>
|
||||
</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-2xl bg-bg-card/50 p-16 text-center">
|
||||
<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" />
|
||||
<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-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-[repeat(auto-fill,minmax(280px,1fr))] gap-3">
|
||||
{workflows.map((wf) => {
|
||||
const meta = getStatusMeta(wf.status);
|
||||
const Icon = meta.icon;
|
||||
const style = getStatusStyle(wf.status);
|
||||
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}`}
|
||||
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"
|
||||
>
|
||||
{/* 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 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>
|
||||
<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')}>
|
||||
<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 line-clamp-2 mb-4">
|
||||
<p className="text-xs text-text-muted leading-relaxed mb-3.5 line-clamp-2">
|
||||
{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)}...
|
||||
<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-[10px] text-text-muted">{new Date(wf.created_at).toLocaleDateString()}</span>
|
||||
<span className="text-[11px] text-text-muted">{new Date(wf.created_at).toLocaleDateString()}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export function CollapsibleSidebar() {
|
||||
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) => {
|
||||
const isActive = activeTab === item.key;
|
||||
return (
|
||||
@@ -42,18 +42,15 @@ export function CollapsibleSidebar() {
|
||||
onClick={() => setTab(item.key as any)}
|
||||
className={`w-full flex items-center mx-1.5 rounded-lg transition-all duration-200 group ${
|
||||
isActive
|
||||
? 'bg-accent-light text-accent'
|
||||
: 'text-text-muted hover:text-text-secondary hover:bg-bg-hover'
|
||||
? '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-white/40 dark:hover:bg-white/[0.04]'
|
||||
} ${isSidebarOpen ? 'px-3 py-2.5 gap-3' : 'px-0 py-2.5 justify-center'}`}
|
||||
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 && (
|
||||
<span className="text-xs font-medium">{item.label}</span>
|
||||
)}
|
||||
{isActive && isSidebarOpen && (
|
||||
<div className="ml-auto w-1 h-1 rounded-full bg-accent" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -36,28 +36,23 @@ export function TopBar() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-14 glass text-text-primary flex items-center justify-between px-5 shrink-0 z-50 relative">
|
||||
{/* Left: Logo with status */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
<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">
|
||||
<Zap size={18} fill="currentColor" />
|
||||
</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" />
|
||||
</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 className="h-[52px] glass text-text-primary flex items-center justify-between px-5 shrink-0 z-50 relative">
|
||||
{/* Left: Logo + Nav */}
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="w-7 h-7 rounded-[7px] bg-accent flex items-center justify-center">
|
||||
<Zap size={14} className="text-white" />
|
||||
</div>
|
||||
<span className="text-[15px] font-bold tracking-[-0.3px] text-text-primary">{t('app.name')}</span>
|
||||
</div>
|
||||
|
||||
{/* Center: Mode Switch */}
|
||||
<div className="hidden md:flex items-center bg-bg-secondary/80 rounded-full p-0.5 border border-border-primary">
|
||||
{/* Center: Pill Nav */}
|
||||
<div className="hidden md:flex items-center gap-px p-0.5 bg-bg-input rounded-lg ml-5">
|
||||
<button
|
||||
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
|
||||
? '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'
|
||||
}`}
|
||||
>
|
||||
@@ -65,54 +60,53 @@ export function TopBar() {
|
||||
</button>
|
||||
<button
|
||||
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
|
||||
? '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'
|
||||
}`}
|
||||
>
|
||||
{t('nav.agent')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Actions */}
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
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')}
|
||||
>
|
||||
<Globe size={16} />
|
||||
<Globe size={15} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
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')}
|
||||
>
|
||||
{resolvedTheme === 'dark' ? <Sun size={16} /> : <Moon size={16} />}
|
||||
{resolvedTheme === 'dark' ? <Sun size={15} /> : <Moon size={15} />}
|
||||
</button>
|
||||
|
||||
<div className="w-px h-4 bg-border-primary mx-1" />
|
||||
|
||||
<button
|
||||
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
|
||||
? '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')}
|
||||
>
|
||||
<Settings size={16} />
|
||||
<Settings size={15} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
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')}
|
||||
>
|
||||
<LogOut size={16} />
|
||||
<LogOut size={15} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,23 +14,25 @@ export function SettingsLayout() {
|
||||
];
|
||||
|
||||
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="px-5 py-4 border-b border-border-primary">
|
||||
<h2 className="text-sm font-bold text-text-primary">{t('settings.settings')}</h2>
|
||||
<div className="px-4 py-3.5">
|
||||
<span className="text-[10px] font-semibold text-text-muted uppercase tracking-[1.5px]">
|
||||
{t('settings.settings')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="p-2 space-y-0.5">
|
||||
<div className="px-1.5 pb-2 space-y-0.5">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={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
|
||||
? 'bg-accent-light text-accent'
|
||||
: 'text-text-muted hover:text-text-secondary hover:bg-bg-hover'
|
||||
? '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-white/50 dark:hover:bg-white/[0.04]'
|
||||
}`}
|
||||
>
|
||||
<tab.icon size={15} />
|
||||
<tab.icon size={14} />
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
|
||||
@@ -88,7 +88,9 @@
|
||||
"running": "Running",
|
||||
"completed": "Completed",
|
||||
"failed": "Failed"
|
||||
}
|
||||
},
|
||||
"total": "Total",
|
||||
"queued": "Queued"
|
||||
},
|
||||
"settings": {
|
||||
"settings": "Settings",
|
||||
|
||||
@@ -88,7 +88,9 @@
|
||||
"running": "运行中",
|
||||
"completed": "已完成",
|
||||
"failed": "失败"
|
||||
}
|
||||
},
|
||||
"total": "总数",
|
||||
"queued": "排队中"
|
||||
},
|
||||
"settings": {
|
||||
"settings": "设置",
|
||||
|
||||
+85
-69
@@ -38,72 +38,83 @@
|
||||
--color-warning-bg: var(--warning-bg);
|
||||
--color-glow-purple: var(--glow-purple);
|
||||
--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-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
}
|
||||
|
||||
/* ===== Morandi Light ===== */
|
||||
:root {
|
||||
--bg-primary: #fafafa;
|
||||
--bg-secondary: #f4f4f5;
|
||||
--bg-tertiary: #e4e4e7;
|
||||
--bg-card: #ffffff;
|
||||
--bg-sidebar: #f4f4f5;
|
||||
--bg-topbar: rgba(255, 255, 255, 0.72);
|
||||
--bg-input: #f4f4f5;
|
||||
--bg-hover: #f4f4f5;
|
||||
--bg-active: #eff6ff;
|
||||
--bg-glass: rgba(255, 255, 255, 0.6);
|
||||
--border-primary: #e4e4e7;
|
||||
--border-secondary: #f4f4f5;
|
||||
--text-primary: #18181b;
|
||||
--text-secondary: #3f3f46;
|
||||
--text-tertiary: #52525b;
|
||||
--text-muted: #a1a1aa;
|
||||
--accent: #4f46e5;
|
||||
--accent-hover: #4338ca;
|
||||
--accent-light: #e0e7ff;
|
||||
--accent-text: #3730a3;
|
||||
--accent-glow: rgba(79, 70, 229, 0.15);
|
||||
--danger: #dc2626;
|
||||
--danger-bg: #fef2f2;
|
||||
--success: #16a34a;
|
||||
--success-bg: #f0fdf4;
|
||||
--warning: #d97706;
|
||||
--warning-bg: #fffbeb;
|
||||
--glow-purple: #a855f7;
|
||||
--glow-cyan: #06b6d4;
|
||||
--bg-primary: #f2f0ed;
|
||||
--bg-secondary: #eae8e4;
|
||||
--bg-tertiary: #e0ddd8;
|
||||
--bg-card: #faf9f7;
|
||||
--bg-sidebar: #eae8e4;
|
||||
--bg-topbar: rgba(250, 249, 247, 0.85);
|
||||
--bg-input: #f2f0ed;
|
||||
--bg-hover: rgba(255, 255, 255, 0.4);
|
||||
--bg-active: rgba(156, 175, 136, 0.08);
|
||||
--bg-glass: rgba(250, 249, 247, 0.72);
|
||||
--border-primary: #e0ddd8;
|
||||
--border-secondary: #eae8e4;
|
||||
--text-primary: #3d3d3d;
|
||||
--text-secondary: #5a5a5a;
|
||||
--text-tertiary: #8c8680;
|
||||
--text-muted: #b5afa8;
|
||||
--accent: #9caf88;
|
||||
--accent-hover: #8a9e78;
|
||||
--accent-light: rgba(156, 175, 136, 0.12);
|
||||
--accent-text: #7a8e6a;
|
||||
--accent-glow: rgba(156, 175, 136, 0.2);
|
||||
--danger: #c4917a;
|
||||
--danger-bg: rgba(196, 145, 122, 0.08);
|
||||
--success: #7a8e6a;
|
||||
--success-bg: rgba(122, 142, 106, 0.08);
|
||||
--warning: #c4a882;
|
||||
--warning-bg: rgba(196, 168, 130, 0.08);
|
||||
--glow-purple: #8a9aaa;
|
||||
--glow-cyan: #c4a882;
|
||||
--clay: #c4a882;
|
||||
--slate: #8a9aaa;
|
||||
--terracotta: #c4917a;
|
||||
}
|
||||
|
||||
/* ===== Morandi Dark ===== */
|
||||
.dark {
|
||||
--bg-primary: #09090b;
|
||||
--bg-secondary: #0f0f11;
|
||||
--bg-tertiary: #18181b;
|
||||
--bg-card: #131316;
|
||||
--bg-sidebar: #0c0c0e;
|
||||
--bg-topbar: rgba(9, 9, 11, 0.72);
|
||||
--bg-input: #18181b;
|
||||
--bg-hover: #1c1c1f;
|
||||
--bg-active: rgba(79, 70, 229, 0.12);
|
||||
--bg-glass: rgba(19, 19, 22, 0.6);
|
||||
--border-primary: rgba(255, 255, 255, 0.08);
|
||||
--border-secondary: rgba(255, 255, 255, 0.04);
|
||||
--text-primary: #fafafa;
|
||||
--text-secondary: #e4e4e7;
|
||||
--text-tertiary: #a1a1aa;
|
||||
--text-muted: #71717a;
|
||||
--accent: #6366f1;
|
||||
--accent-hover: #818cf8;
|
||||
--accent-light: rgba(99, 102, 241, 0.15);
|
||||
--accent-text: #a5b4fc;
|
||||
--accent-glow: rgba(99, 102, 241, 0.25);
|
||||
--danger: #f87171;
|
||||
--danger-bg: rgba(248, 113, 113, 0.1);
|
||||
--success: #4ade80;
|
||||
--success-bg: rgba(74, 222, 128, 0.1);
|
||||
--warning: #fbbf24;
|
||||
--warning-bg: rgba(251, 191, 36, 0.1);
|
||||
--glow-purple: #c084fc;
|
||||
--glow-cyan: #67e8f9;
|
||||
--bg-primary: #1c1b19;
|
||||
--bg-secondary: #232220;
|
||||
--bg-tertiary: #2d2b28;
|
||||
--bg-card: #252421;
|
||||
--bg-sidebar: #1e1d1b;
|
||||
--bg-topbar: rgba(28, 27, 25, 0.85);
|
||||
--bg-input: #2d2b28;
|
||||
--bg-hover: rgba(255, 255, 255, 0.04);
|
||||
--bg-active: rgba(156, 175, 136, 0.1);
|
||||
--bg-glass: rgba(37, 36, 33, 0.72);
|
||||
--border-primary: rgba(255, 255, 255, 0.06);
|
||||
--border-secondary: rgba(255, 255, 255, 0.03);
|
||||
--text-primary: #e8e6e3;
|
||||
--text-secondary: #c8c5c0;
|
||||
--text-tertiary: #a09c96;
|
||||
--text-muted: #7a7772;
|
||||
--accent: #a8bc94;
|
||||
--accent-hover: #b8caa6;
|
||||
--accent-light: rgba(156, 175, 136, 0.15);
|
||||
--accent-text: #c4d4b4;
|
||||
--accent-glow: rgba(156, 175, 136, 0.2);
|
||||
--danger: #d4a894;
|
||||
--danger-bg: rgba(196, 145, 122, 0.1);
|
||||
--success: #9caf88;
|
||||
--success-bg: rgba(156, 175, 136, 0.1);
|
||||
--warning: #c4a882;
|
||||
--warning-bg: rgba(196, 168, 130, 0.1);
|
||||
--glow-purple: #a0aab8;
|
||||
--glow-cyan: #c4b89e;
|
||||
--clay: #c4a882;
|
||||
--slate: #8a9aaa;
|
||||
--terracotta: #c4917a;
|
||||
}
|
||||
|
||||
html {
|
||||
@@ -118,16 +129,19 @@ body {
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* Dot pattern background for dark mode */
|
||||
.dark body::before {
|
||||
/* Dot pattern background */
|
||||
body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
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;
|
||||
pointer-events: none;
|
||||
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 */
|
||||
::-webkit-scrollbar {
|
||||
@@ -139,7 +153,7 @@ body {
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--border-primary);
|
||||
border-radius: 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--text-muted);
|
||||
@@ -209,21 +223,23 @@ body {
|
||||
/* Glass effect */
|
||||
.glass {
|
||||
background: var(--bg-glass);
|
||||
backdrop-filter: blur(16px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(16px) saturate(180%);
|
||||
backdrop-filter: blur(16px) saturate(140%);
|
||||
-webkit-backdrop-filter: blur(16px) saturate(140%);
|
||||
border-bottom: 1px solid var(--border-primary);
|
||||
}
|
||||
|
||||
/* Modern 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 {
|
||||
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 {
|
||||
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 */
|
||||
@@ -231,7 +247,7 @@ body {
|
||||
box-shadow: 0 0 20px -4px var(--accent-glow);
|
||||
}
|
||||
.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 */
|
||||
|
||||
Reference in New Issue
Block a user