diff --git a/frontend/src/components/agents/AgentDetailModal.module.css b/frontend/src/components/agents/AgentDetailModal.module.css index 4e0cf9af..b54e872b 100644 --- a/frontend/src/components/agents/AgentDetailModal.module.css +++ b/frontend/src/components/agents/AgentDetailModal.module.css @@ -161,3 +161,10 @@ grid-template-columns: 1fr; } } + +.chartsContainer { + display: flex; + flex-direction: column; + gap: 1.5rem; + margin-top: 0.5rem; +} diff --git a/frontend/src/components/agents/AgentDetailModal.tsx b/frontend/src/components/agents/AgentDetailModal.tsx index f319f4cb..952679c6 100644 --- a/frontend/src/components/agents/AgentDetailModal.tsx +++ b/frontend/src/components/agents/AgentDetailModal.tsx @@ -3,6 +3,9 @@ import { useTranslation } from 'react-i18next' import { ExternalLink, X } from 'lucide-react' import type { AgentRecord } from '../../types/api' import { ReputationStars } from './ReputationStars' +import { useAgentReputation } from '../../hooks/useAgentReputation' +import { AgentReputationRadar } from './AgentReputationRadar' +import { AgentReputationTrend } from './AgentReputationTrend' import styles from './AgentDetailModal.module.css' const STELLAR_EXPLORER = 'https://stellar.expert/explorer/testnet' @@ -123,6 +126,22 @@ export function AgentDetailModal({ agent, onClose }: AgentDetailModalProps) { )} + +
+
Reputation Details
+
+ {reputationLoading ? ( +
Loading charts...
+ ) : reputationData ? ( +
+ + +
+ ) : ( +
No detailed reputation data available
+ )} +
+
diff --git a/frontend/src/components/agents/AgentReputationRadar.module.css b/frontend/src/components/agents/AgentReputationRadar.module.css new file mode 100644 index 00000000..cdb73207 --- /dev/null +++ b/frontend/src/components/agents/AgentReputationRadar.module.css @@ -0,0 +1,6 @@ +.container { + width: 100%; + display: flex; + justify-content: center; + align-items: center; +} diff --git a/frontend/src/components/agents/AgentReputationRadar.test.tsx b/frontend/src/components/agents/AgentReputationRadar.test.tsx new file mode 100644 index 00000000..f2696827 --- /dev/null +++ b/frontend/src/components/agents/AgentReputationRadar.test.tsx @@ -0,0 +1,34 @@ +import { render } from '@testing-library/react'; +import { AgentReputationRadar } from './AgentReputationRadar'; +import { vi, describe, it, expect } from 'vitest'; + +// Mock recharts because ResponsiveContainer doesn't work well in JSDOM +vi.mock('recharts', async () => { + const OriginalRecharts = await vi.importActual('recharts'); + return { + ...OriginalRecharts, + ResponsiveContainer: ({ children }: any) => ( +
{children}
+ ), + }; +}); + +describe('AgentReputationRadar', () => { + it('renders the radar chart with given dimensions', () => { + const dimensions = { + quality: 90, + speed: 85, + reliability: 95, + cost: 80, + }; + + const { container } = render(); + + // Check if the container is rendered + expect(container.firstChild).toBeInTheDocument(); + + // In a real JSDOM environment with SVG, we could check for specific SVG elements. + // For this test, we ensure it renders without crashing and contains the ResponsiveContainer div. + expect(container.querySelector('.recharts-wrapper')).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/components/agents/AgentReputationRadar.tsx b/frontend/src/components/agents/AgentReputationRadar.tsx new file mode 100644 index 00000000..00cd7882 --- /dev/null +++ b/frontend/src/components/agents/AgentReputationRadar.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { Radar, RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, ResponsiveContainer, Tooltip } from 'recharts'; +import type { ReputationDimensions } from '../../types/agent'; +import styles from './AgentReputationRadar.module.css'; + +interface AgentReputationRadarProps { + dimensions: ReputationDimensions; +} + +export const AgentReputationRadar: React.FC = ({ dimensions }) => { + const data = [ + { subject: 'Quality', A: dimensions.quality, fullMark: 100 }, + { subject: 'Speed', A: dimensions.speed, fullMark: 100 }, + { subject: 'Reliability', A: dimensions.reliability, fullMark: 100 }, + { subject: 'Cost', A: dimensions.cost, fullMark: 100 }, + ]; + + return ( +
+ + + + + + + + + +
+ ); +}; diff --git a/frontend/src/components/agents/AgentReputationTrend.module.css b/frontend/src/components/agents/AgentReputationTrend.module.css new file mode 100644 index 00000000..97e43ea5 --- /dev/null +++ b/frontend/src/components/agents/AgentReputationTrend.module.css @@ -0,0 +1,4 @@ +.container { + width: 100%; + margin-top: 1rem; +} diff --git a/frontend/src/components/agents/AgentReputationTrend.tsx b/frontend/src/components/agents/AgentReputationTrend.tsx new file mode 100644 index 00000000..a44af598 --- /dev/null +++ b/frontend/src/components/agents/AgentReputationTrend.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Brush } from 'recharts'; +import type { ReputationHistory } from '../../types/agent'; +import styles from './AgentReputationTrend.module.css'; + +interface AgentReputationTrendProps { + history: ReputationHistory[]; +} + +export const AgentReputationTrend: React.FC = ({ history }) => { + // Format dates for display + const data = history.map(item => { + const d = new Date(item.date); + return { + ...item, + displayDate: `${d.getMonth() + 1}/${d.getDate()}` + }; + }); + + return ( +
+ + + + + + + + + + +
+ ); +}; diff --git a/frontend/src/components/landing/Hero.tsx b/frontend/src/components/landing/Hero.tsx index ad506a2a..a1b9e2e3 100644 --- a/frontend/src/components/landing/Hero.tsx +++ b/frontend/src/components/landing/Hero.tsx @@ -1,25 +1,11 @@ import React from 'react' import { useTranslation, Trans } from 'react-i18next' import { useNavigate } from 'react-router-dom' -import { motion } from 'framer-motion' import { Sparkles, ArrowRight } from 'lucide-react' import { useParticles } from '../../hooks/useParticles' import { useTypingAnimation } from '../../hooks/useTypingAnimation' import styles from './Hero.module.css' -const containerVariants = { - hidden: { opacity: 0 }, - visible: { - opacity: 1, - transition: { staggerChildren: 0.12, delayChildren: 0.2 }, - }, -} as const - -const itemVariants = { - hidden: { opacity: 0, y: 20 }, - visible: { opacity: 1, y: 0, transition: { duration: 0.5, ease: 'easeOut' as const } }, -} as const - const Hero: React.FC = () => { const { t } = useTranslation() const navigate = useNavigate() @@ -45,29 +31,29 @@ const Hero: React.FC = () => { {/* Centered Logo Mark */} -
a
- +
{/* Status Pill */} -
{t('landing.hero.badge')} {/* Headline */} - { {/* Subtext */} - {t('landing.hero.subtitle')} {/* CTAs */} - - - +
+ ) } diff --git a/frontend/src/hooks/useAgentReputation.ts b/frontend/src/hooks/useAgentReputation.ts new file mode 100644 index 00000000..2bc804de --- /dev/null +++ b/frontend/src/hooks/useAgentReputation.ts @@ -0,0 +1,41 @@ +import { useState, useEffect } from 'react'; +import { getAgentReputation } from '../services/api'; +import type { AgentReputation } from '../types/agent'; + +export function useAgentReputation(agentId: string) { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (!agentId) return; + + let mounted = true; + const fetchReputation = async () => { + setLoading(true); + setError(null); + try { + const result = await getAgentReputation(agentId); + if (mounted) { + setData(result); + } + } catch (err) { + if (mounted) { + setError(err instanceof Error ? err.message : 'Failed to fetch reputation data'); + } + } finally { + if (mounted) { + setLoading(false); + } + } + }; + + fetchReputation(); + + return () => { + mounted = false; + }; + }, [agentId]); + + return { data, loading, error }; +} diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index b2479ea6..37b95d4c 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -114,3 +114,7 @@ export const getRecentTasks = async (walletAddress: string): Promise => { return apiClient.get('/api/agents'); }; + +export const getAgentReputation = async (id: string): Promise => { + return apiClient.get(`/api/agents/${id}/reputation`); +}; diff --git a/frontend/src/styles/animations.css b/frontend/src/styles/animations.css new file mode 100644 index 00000000..ac315e77 --- /dev/null +++ b/frontend/src/styles/animations.css @@ -0,0 +1,70 @@ +.fade-in { + animation: fadeIn 0.3s ease forwards; +} + +.slide-up { + animation: slideUp 0.3s ease forwards; +} + +.scale-in { + animation: scaleIn 0.3s ease forwards; +} + +.pulse { + animation: pulse 2s infinite ease-in-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes scaleIn { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes pulse { + 0% { + opacity: 1; + } + 50% { + opacity: 0.6; + } + 100% { + opacity: 1; + } +} + +@media (prefers-reduced-motion: reduce) { + .fade-in, + .slide-up, + .scale-in, + .pulse { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + animation-delay: 0.01ms !important; + } +} diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index 73998584..32cbeea1 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -2,6 +2,9 @@ @tailwind components; @tailwind utilities; +@import './animations.css'; +@import './micro-interactions.css'; + :root { --font-sans: 'Outfit', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; --bg-primary: #0A0E14; diff --git a/frontend/src/styles/micro-interactions.css b/frontend/src/styles/micro-interactions.css new file mode 100644 index 00000000..14a7aa7a --- /dev/null +++ b/frontend/src/styles/micro-interactions.css @@ -0,0 +1,62 @@ +.hover-lift { + transition-property: transform, box-shadow; + transition-duration: 300ms; + transition-timing-function: ease; +} + +.hover-lift:hover { + transform: translateY(-4px); + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.15); +} + +.hover-glow { + transition-property: box-shadow; + transition-duration: 300ms; + transition-timing-function: ease; +} + +.hover-glow:hover { + box-shadow: 0 0 15px var(--accent-cyan); +} + +.hover-scale { + transition-property: transform; + transition-duration: 300ms; + transition-timing-function: ease; +} + +.hover-scale:hover { + transform: scale(1.05); +} + +.focus-ring { + outline: none; +} + +.focus-ring:focus-visible { + outline: 2px solid var(--accent-cyan); + outline-offset: 2px; +} + +.transition-fast { + transition-duration: 150ms; +} + +.transition-normal { + transition-duration: 300ms; +} + +.transition-slow { + transition-duration: 500ms; +} + +@media (prefers-reduced-motion: reduce) { + .hover-lift, + .hover-glow, + .hover-scale, + .transition-fast, + .transition-normal, + .transition-slow { + transition-duration: 0.01ms !important; + } +} diff --git a/frontend/src/types/agent.ts b/frontend/src/types/agent.ts index c9758f36..2e614d8c 100644 --- a/frontend/src/types/agent.ts +++ b/frontend/src/types/agent.ts @@ -80,3 +80,21 @@ export type AgentResult = | DesignResult | null | undefined; + +export interface ReputationDimensions { + quality: number; + speed: number; + reliability: number; + cost: number; +} + +export interface ReputationHistory { + date: string; + score: number; +} + +export interface AgentReputation { + id: string; + dimensions: ReputationDimensions; + history: ReputationHistory[]; +}