美化页面

This commit is contained in:
2025-08-08 00:21:10 +08:00
parent ca56cc83f0
commit ef2e30401e
7 changed files with 334 additions and 101 deletions

View File

@@ -0,0 +1,214 @@
use std::fs;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use dirs::home_dir;
use crate::commands::relay_stations::RelayStation;
/// Claude 配置文件结构
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClaudeConfig {
#[serde(default)]
pub env: ClaudeEnv,
#[serde(default)]
pub permissions: Option<ClaudePermissions>,
#[serde(default)]
pub model: Option<String>,
#[serde(rename = "apiKeyHelper")]
pub api_key_helper: Option<String>,
#[serde(flatten)]
pub other: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ClaudeEnv {
#[serde(rename = "ANTHROPIC_AUTH_TOKEN")]
pub anthropic_auth_token: Option<String>,
#[serde(rename = "ANTHROPIC_BASE_URL")]
pub anthropic_base_url: Option<String>,
#[serde(rename = "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC")]
pub disable_nonessential_traffic: Option<String>,
#[serde(flatten)]
pub other: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ClaudePermissions {
#[serde(default)]
pub allow: Vec<String>,
#[serde(default)]
pub deny: Vec<String>,
}
/// 获取 Claude 配置文件路径
pub fn get_claude_config_path() -> Result<PathBuf, String> {
let home = home_dir().ok_or_else(|| "无法获取主目录".to_string())?;
Ok(home.join(".claude").join("settings.json"))
}
/// 获取配置备份文件路径
pub fn get_config_backup_path() -> Result<PathBuf, String> {
let home = home_dir().ok_or_else(|| "无法获取主目录".to_string())?;
Ok(home.join(".claude").join("settings.backup.json"))
}
/// 读取 Claude 配置文件
pub fn read_claude_config() -> Result<ClaudeConfig, String> {
let config_path = get_claude_config_path()?;
if !config_path.exists() {
// 如果配置文件不存在,创建默认配置
return Ok(ClaudeConfig {
env: ClaudeEnv::default(),
permissions: Some(ClaudePermissions::default()),
model: None,
api_key_helper: None,
other: json!({}),
});
}
let content = fs::read_to_string(&config_path)
.map_err(|e| format!("读取配置文件失败: {}", e))?;
serde_json::from_str(&content)
.map_err(|e| format!("解析配置文件失败: {}", e))
}
/// 写入 Claude 配置文件
pub fn write_claude_config(config: &ClaudeConfig) -> Result<(), String> {
let config_path = get_claude_config_path()?;
// 确保目录存在
if let Some(parent) = config_path.parent() {
fs::create_dir_all(parent)
.map_err(|e| format!("创建配置目录失败: {}", e))?;
}
let content = serde_json::to_string_pretty(config)
.map_err(|e| format!("序列化配置失败: {}", e))?;
fs::write(&config_path, content)
.map_err(|e| format!("写入配置文件失败: {}", e))
}
/// 备份当前配置
pub fn backup_claude_config() -> Result<(), String> {
let config_path = get_claude_config_path()?;
let backup_path = get_config_backup_path()?;
if config_path.exists() {
fs::copy(&config_path, &backup_path)
.map_err(|e| format!("备份配置文件失败: {}", e))?;
}
Ok(())
}
/// 恢复配置备份
pub fn restore_claude_config() -> Result<(), String> {
let config_path = get_claude_config_path()?;
let backup_path = get_config_backup_path()?;
if !backup_path.exists() {
return Err("备份文件不存在".to_string());
}
fs::copy(&backup_path, &config_path)
.map_err(|e| format!("恢复配置文件失败: {}", e))?;
Ok(())
}
/// 根据中转站配置更新 Claude 配置
pub fn apply_relay_station_to_config(station: &RelayStation) -> Result<(), String> {
// 先备份当前配置
backup_claude_config()?;
// 读取当前配置
let mut config = read_claude_config()?;
// 更新 API URL
config.env.anthropic_base_url = Some(station.api_url.clone());
// 更新 API Token
config.env.anthropic_auth_token = Some(station.system_token.clone());
// 将中转站的 token 也设置到 apiKeyHelper
// 格式echo 'token'
config.api_key_helper = Some(format!("echo '{}'", station.system_token));
// 如果是自定义适配器,可能需要特殊处理
match station.adapter.as_str() {
"newapi" | "oneapi" => {
// NewAPI 和 OneAPI 兼容 OpenAI 格式,不需要特殊处理
}
"yourapi" => {
// YourAPI 可能需要特殊的路径格式
if !station.api_url.ends_with("/v1") {
config.env.anthropic_base_url = Some(format!("{}/v1", station.api_url));
}
}
"custom" => {
// 自定义适配器,使用原始配置
}
_ => {}
}
// 写入更新后的配置
write_claude_config(&config)?;
log::info!("已将中转站 {} 的配置应用到 Claude 配置文件", station.name);
Ok(())
}
/// 清除中转站配置(恢复默认)
pub fn clear_relay_station_from_config() -> Result<(), String> {
// 尝试从备份恢复原始的配置
let backup_config = if let Ok(backup_path) = get_config_backup_path() {
if backup_path.exists() {
let content = fs::read_to_string(&backup_path).ok();
content.and_then(|c| serde_json::from_str::<ClaudeConfig>(&c).ok())
} else {
None
}
} else {
None
};
// 读取当前配置
let mut config = read_claude_config()?;
// 清除 API URL 和 Token
config.env.anthropic_base_url = None;
config.env.anthropic_auth_token = None;
// 恢复原始的 apiKeyHelper如果有备份的话
if let Some(backup) = backup_config {
config.api_key_helper = backup.api_key_helper;
// 如果备份中有 ANTHROPIC_AUTH_TOKEN也恢复它
if backup.env.anthropic_auth_token.is_some() {
config.env.anthropic_auth_token = backup.env.anthropic_auth_token;
}
} else {
// 如果没有备份,清除 apiKeyHelper
config.api_key_helper = None;
}
// 写入更新后的配置
write_claude_config(&config)?;
log::info!("已清除 Claude 配置文件中的中转站设置");
Ok(())
}
/// 获取当前配置中的 API URL
pub fn get_current_api_url() -> Result<Option<String>, String> {
let config = read_claude_config()?;
Ok(config.env.anthropic_base_url)
}
/// 获取当前配置中的 API Token
pub fn get_current_api_token() -> Result<Option<String>, String> {
let config = read_claude_config()?;
Ok(config.env.anthropic_auth_token)
}

View File

@@ -29,6 +29,7 @@ import { useAppLifecycle, useTrackEvent } from "@/hooks";
import { useTranslation } from "@/hooks/useTranslation";
import { WelcomePage } from "@/components/WelcomePage";
import RelayStationManager from "@/components/RelayStationManager";
import i18n from "@/lib/i18n";
type View =
| "welcome"
@@ -75,6 +76,25 @@ function AppContent() {
const [hasTrackedFirstChat] = useState(false);
// const [hasTrackedFirstAgent] = useState(false);
// Initialize backend language on app startup
useEffect(() => {
const initializeBackendLanguage = async () => {
try {
// Get the current frontend language
const frontendLang = i18n.language;
// Map to backend format
const backendLocale = frontendLang === 'zh' ? 'zh-CN' : 'en-US';
// Sync to backend
await api.setLanguage(backendLocale);
console.log('Backend language initialized to:', backendLocale);
} catch (error) {
console.error('Failed to initialize backend language:', error);
}
};
initializeBackendLanguage();
}, []); // Run once on app startup
// Track when user reaches different journey stages
useEffect(() => {
if (view === "projects" && projects.length > 0 && !hasTrackedFirstChat) {

View File

@@ -159,9 +159,17 @@ export const AnalyticsConsentBanner: React.FC<AnalyticsConsentBannerProps> = ({
const checkConsent = async () => {
if (hasChecked) return;
// Check if we've already shown the consent dialog before
const hasShownBefore = localStorage.getItem('claudia-analytics-consent-shown');
if (hasShownBefore === 'true') {
setHasChecked(true);
return;
}
await analytics.initialize();
const settings = analytics.getSettings();
// Only show if user hasn't made a decision yet
if (!settings?.hasConsented) {
setVisible(true);
}
@@ -175,11 +183,21 @@ export const AnalyticsConsentBanner: React.FC<AnalyticsConsentBannerProps> = ({
const handleAccept = async () => {
await analytics.enable();
// Mark that we've shown the consent dialog
localStorage.setItem('claudia-analytics-consent-shown', 'true');
setVisible(false);
};
const handleDecline = async () => {
await analytics.disable();
// Mark that we've shown the consent dialog
localStorage.setItem('claudia-analytics-consent-shown', 'true');
setVisible(false);
};
const handleClose = () => {
// Even if they close without choosing, mark as shown
localStorage.setItem('claudia-analytics-consent-shown', 'true');
setVisible(false);
};
@@ -223,7 +241,7 @@ export const AnalyticsConsentBanner: React.FC<AnalyticsConsentBannerProps> = ({
</div>
</div>
<button
onClick={() => setVisible(false)}
onClick={handleClose}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<X className="h-4 w-4" />

View File

@@ -121,78 +121,59 @@ export function ClaudiaLogoMinimal({ size = 48, className = "" }: ClaudiaLogoPro
className={`relative inline-flex items-center justify-center ${className}`}
style={{ width: size, height: size }}
animate={{
rotate: [0, 5, -5, 5, 0],
rotate: [0, 3, -3, 3, 0],
}}
transition={{
duration: 6,
duration: 8,
repeat: Infinity,
ease: "easeInOut",
}}
>
{/* Gradient background */}
{/* Simple orange circle background */}
<motion.div
className="absolute inset-0 rounded-2xl bg-gradient-to-br from-orange-400 via-orange-500 to-orange-600"
className="absolute inset-0 rounded-2xl bg-orange-500"
animate={{
scale: [1, 1.1, 1],
scale: [1, 1.05, 1],
}}
transition={{
duration: 4,
repeat: Infinity,
ease: "easeInOut",
}}
/>
{/* Subtle inner shadow for depth */}
<div className="absolute inset-0 rounded-2xl bg-gradient-to-br from-transparent to-black/10" />
{/* Letter C - clean and simple */}
<motion.div
className="relative z-10 text-white font-bold flex items-center justify-center"
style={{ fontSize: size * 0.5, fontFamily: 'system-ui, -apple-system, sans-serif' }}
animate={{
scale: [1, 1.05, 1],
}}
transition={{
duration: 3,
repeat: Infinity,
ease: "easeInOut",
}}
/>
{/* Inner light effect */}
<motion.div
className="absolute inset-1 rounded-xl bg-gradient-to-br from-orange-300/50 to-transparent"
animate={{
opacity: [0.5, 1, 0.5],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut",
}}
/>
{/* Letter C with animation */}
<motion.div
className="relative z-10 text-white font-bold flex items-center justify-center"
style={{ fontSize: size * 0.5 }}
animate={{
scale: [1, 1.1, 1],
textShadow: [
"0 0 10px rgba(255,255,255,0.5)",
"0 0 20px rgba(255,255,255,0.8)",
"0 0 10px rgba(255,255,255,0.5)",
],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut",
}}
>
C
</motion.div>
{/* Pulse rings */}
{[...Array(2)].map((_, i) => (
{/* Single subtle pulse ring */}
<motion.div
key={i}
className="absolute inset-0 rounded-2xl border-2 border-orange-400"
className="absolute inset-0 rounded-2xl border border-orange-400/30"
animate={{
scale: [1, 1.5, 2],
opacity: [0.5, 0.2, 0],
scale: [1, 1.3, 1.5],
opacity: [0.3, 0.1, 0],
}}
transition={{
duration: 3,
duration: 4,
repeat: Infinity,
delay: i * 1.5,
ease: "easeOut",
}}
/>
))}
</motion.div>
);
}

View File

@@ -3,7 +3,6 @@ import { Bot, FolderCode, BarChart, ServerCog, FileText, Settings, Network } fro
import { useTranslation } from "@/hooks/useTranslation";
import { Button } from "@/components/ui/button";
import { ClaudiaLogoMinimal } from "@/components/ClaudiaLogo";
import { BorderGlowCard } from "@/components/ui/glow-card";
interface WelcomePageProps {
onNavigate: (view: string) => void;
@@ -19,8 +18,8 @@ export function WelcomePage({ onNavigate, onNewSession }: WelcomePageProps) {
icon: Network,
title: t("welcome.relayStationManagement"),
subtitle: t("welcome.relayStationManagementDesc"),
color: "text-indigo-500",
bgColor: "bg-indigo-500/10",
color: "text-orange-500",
bgColor: "bg-orange-500/10",
view: "relay-stations"
},
{
@@ -37,8 +36,8 @@ export function WelcomePage({ onNavigate, onNewSession }: WelcomePageProps) {
icon: FolderCode,
title: t("welcome.projectManagement"),
subtitle: t("welcome.projectManagementDesc"),
color: "text-blue-500",
bgColor: "bg-blue-500/10",
color: "text-orange-500",
bgColor: "bg-orange-500/10",
view: "projects"
}
];
@@ -49,8 +48,8 @@ export function WelcomePage({ onNavigate, onNewSession }: WelcomePageProps) {
icon: BarChart,
title: t("welcome.usageStatistics"),
subtitle: t("welcome.usageStatisticsDesc"),
color: "text-green-500",
bgColor: "bg-green-500/10",
color: "text-orange-500",
bgColor: "bg-orange-500/10",
view: "usage-dashboard"
},
{
@@ -58,8 +57,8 @@ export function WelcomePage({ onNavigate, onNewSession }: WelcomePageProps) {
icon: ServerCog,
title: t("welcome.mcpBroker"),
subtitle: t("welcome.mcpBrokerDesc"),
color: "text-purple-500",
bgColor: "bg-purple-500/10",
color: "text-orange-500",
bgColor: "bg-orange-500/10",
view: "mcp"
},
{
@@ -67,8 +66,8 @@ export function WelcomePage({ onNavigate, onNewSession }: WelcomePageProps) {
icon: FileText,
title: t("welcome.claudeMd"),
subtitle: t("welcome.claudeMdDesc"),
color: "text-cyan-500",
bgColor: "bg-cyan-500/10",
color: "text-orange-500",
bgColor: "bg-orange-500/10",
view: "editor"
},
{
@@ -76,8 +75,8 @@ export function WelcomePage({ onNavigate, onNewSession }: WelcomePageProps) {
icon: Settings,
title: t("welcome.settings"),
subtitle: t("welcome.settingsDesc"),
color: "text-gray-500",
bgColor: "bg-gray-500/10",
color: "text-orange-500",
bgColor: "bg-orange-500/10",
view: "settings"
}
];
@@ -92,25 +91,25 @@ export function WelcomePage({ onNavigate, onNewSession }: WelcomePageProps) {
return (
<div className="flex items-center justify-center min-h-screen bg-background overflow-hidden">
<div className="w-full max-w-6xl px-8">
<div className="w-full max-w-6xl px-8 -mt-20">
{/* Header */}
<motion.div
className="text-center mb-16"
className="text-center mb-10"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<h1 className="text-5xl font-bold mb-4 flex items-center justify-center gap-4 bg-gradient-to-r from-orange-400 via-pink-500 to-purple-600 bg-clip-text text-transparent">
<h1 className="text-5xl font-bold mb-4 flex items-center justify-center gap-4 text-white">
<ClaudiaLogoMinimal size={56} />
{t("app.welcome")}
</h1>
<p className="text-muted-foreground text-xl">
<p className="text-white/90 text-xl">
{t("app.tagline")}
</p>
</motion.div>
{/* Main Feature Cards */}
<div className="grid grid-cols-3 gap-8 mb-12">
<div className="grid grid-cols-3 gap-8 mb-10">
{mainFeatures.map((feature, index) => (
<motion.div
key={feature.id}
@@ -123,8 +122,8 @@ export function WelcomePage({ onNavigate, onNewSession }: WelcomePageProps) {
stiffness: 100
}}
>
<BorderGlowCard
className="h-full group"
<div
className="h-full group bg-white/5 border border-white/10 rounded-lg hover:bg-white/10 hover:border-orange-500/50 transition-all duration-300 cursor-pointer"
onClick={() => handleCardClick(feature.view)}
>
<div className="p-10">
@@ -133,22 +132,22 @@ export function WelcomePage({ onNavigate, onNewSession }: WelcomePageProps) {
<feature.icon className={`h-10 w-10 ${feature.color}`} strokeWidth={1.5} />
</div>
<div className="flex-1">
<h2 className="text-2xl font-bold mb-3 group-hover:text-primary transition-colors">
<h2 className="text-2xl font-bold mb-3 text-white group-hover:text-primary transition-colors">
{feature.title}
</h2>
<p className="text-muted-foreground text-base leading-relaxed">
<p className="text-white/80 text-base leading-relaxed">
{feature.subtitle}
</p>
</div>
</div>
</div>
</BorderGlowCard>
</div>
</motion.div>
))}
</div>
{/* Bottom Feature Cards */}
<div className="grid grid-cols-4 gap-6 mb-12">
<div className="grid grid-cols-4 gap-6 mb-10">
{bottomFeatures.map((feature, index) => (
<motion.div
key={feature.id}
@@ -161,22 +160,24 @@ export function WelcomePage({ onNavigate, onNewSession }: WelcomePageProps) {
stiffness: 100
}}
>
<BorderGlowCard
className="h-36 group"
<div
className="h-32 group bg-white/5 border border-white/10 rounded-lg hover:bg-white/10 hover:border-orange-500/50 transition-all duration-300 cursor-pointer"
onClick={() => handleCardClick(feature.view)}
>
<div className="h-full flex flex-col items-center justify-center p-6">
<div className={`p-3 ${feature.bgColor} rounded-xl mb-3 transition-transform duration-300 group-hover:scale-110 group-hover:rotate-6`}>
<feature.icon className={`h-8 w-8 ${feature.color}`} strokeWidth={1.5} />
<div className="h-full flex items-center p-6">
<div className={`p-3 ${feature.bgColor} rounded-xl transition-transform duration-300 group-hover:scale-110 group-hover:rotate-6 mr-4 flex-shrink-0`}>
<feature.icon className={`h-7 w-7 ${feature.color}`} strokeWidth={1.5} />
</div>
<h3 className="text-sm font-semibold mb-1 group-hover:text-primary transition-colors">
<div className="flex-1 min-w-0">
<h3 className="text-sm font-semibold mb-1 text-white group-hover:text-primary transition-colors truncate">
{feature.title}
</h3>
<p className="text-xs text-muted-foreground text-center line-clamp-2">
<p className="text-xs text-white/70 line-clamp-2">
{feature.subtitle}
</p>
</div>
</BorderGlowCard>
</div>
</div>
</motion.div>
))}
</div>
@@ -195,7 +196,7 @@ export function WelcomePage({ onNavigate, onNewSession }: WelcomePageProps) {
>
<Button
size="lg"
className="relative px-10 py-7 text-lg font-semibold bg-gradient-to-r from-orange-500 to-pink-500 hover:from-orange-600 hover:to-pink-600 text-white border-0 shadow-2xl hover:shadow-orange-500/25 transition-all duration-300 hover:scale-105 rounded-2xl group overflow-hidden"
className="relative px-10 py-7 text-lg font-semibold bg-orange-500 hover:bg-orange-600 text-white border-0 shadow-2xl hover:shadow-orange-500/25 transition-all duration-300 hover:scale-105 rounded-2xl group overflow-hidden"
onClick={handleButtonClick}
>
{/* Shimmer effect on button */}
@@ -203,10 +204,9 @@ export function WelcomePage({ onNavigate, onNewSession }: WelcomePageProps) {
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent animate-shimmer" />
</div>
<span className="relative z-10 flex items-center gap-3">
<span className="text-2xl"></span>
<span className="relative z-10 flex items-center gap-2">
<span className="text-2xl">+</span>
{t("welcome.quickStartSession")}
<span className="text-2xl">🚀</span>
</span>
</Button>
</motion.div>

View File

@@ -5,7 +5,7 @@
"ccProjects": "CC Projects",
"browseClaudeCodeSessions": "Browse your Claude Code sessions",
"newClaudeCodeSession": "New Claude Code session",
"ccAgents": "CC Agents",
"ccAgents": "Agent Management",
"mcpServers": "MCP Servers",
"manageMcpServers": "Manage Model Context Protocol servers",
"app": {
@@ -38,7 +38,7 @@
},
"navigation": {
"projects": "CC Projects",
"agents": "CC Agents",
"agents": "Agent Management",
"settings": "Settings",
"usage": "Usage Dashboard",
"mcp": "MCP Manager",
@@ -74,7 +74,7 @@
"sessionHistory": "Session History"
},
"agents": {
"title": "CC Agents",
"title": "Agent Management",
"newAgent": "New Agent",
"createAgent": "Create Agent",
"editAgent": "Edit Agent",

View File

@@ -5,7 +5,7 @@
"ccProjects": "Claude Code 项目",
"browseClaudeCodeSessions": "浏览您的 Claude Code 会话",
"newClaudeCodeSession": "新建 Claude Code 会话",
"ccAgents": "CC 智能体",
"ccAgents": "Agent 管理",
"mcpServers": "MCP 服务器",
"manageMcpServers": "管理模型上下文协议服务器",
"app": {
@@ -35,7 +35,7 @@
},
"navigation": {
"projects": "Claude Code 项目",
"agents": "CC 智能体",
"agents": "Agent 管理",
"settings": "设置",
"usage": "用量仪表板",
"mcp": "MCP 管理器",
@@ -71,7 +71,7 @@
"sessionHistory": "会话历史"
},
"agents": {
"title": "CC 智能体",
"title": "Agent 管理",
"newAgent": "新建智能体",
"createAgent": "创建智能体",
"editAgent": "编辑智能体",