feat: Go backend, enhanced search, new widgets, Docker deploy
Major changes: - Add Go backend (backend/) with microservices architecture - Enhanced master-agents-svc: reranker, content-classifier, stealth-crawler, proxy-manager, media-search, fastClassifier, language detection - New web-svc widgets: KnowledgeCard, ProductCard, ProfileCard, VideoCard, UnifiedCard, CardGallery, InlineImageGallery, SourcesPanel, RelatedQuestions - Improved discover-svc with discover-db integration - Docker deployment improvements (Caddyfile, vendor.sh, BUILD.md) - Library-svc: project_id schema migration - Remove deprecated finance-svc and travel-svc - Localization improvements across services Made-with: Cursor
This commit is contained in:
153
backend/webui/src/app/(main)/discover/page.tsx
Normal file
153
backend/webui/src/app/(main)/discover/page.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { RefreshCw, ExternalLink, Loader2, Sparkles, Globe, Cpu, DollarSign, Dumbbell } from 'lucide-react';
|
||||
import * as Tabs from '@radix-ui/react-tabs';
|
||||
import { fetchDiscover } from '@/lib/api';
|
||||
import type { DiscoverItem } from '@/lib/types';
|
||||
|
||||
const topics = [
|
||||
{ id: 'tech', label: 'Технологии', icon: Cpu },
|
||||
{ id: 'finance', label: 'Финансы', icon: DollarSign },
|
||||
{ id: 'sports', label: 'Спорт', icon: Dumbbell },
|
||||
] as const;
|
||||
|
||||
export default function DiscoverPage() {
|
||||
const [items, setItems] = useState<DiscoverItem[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [topic, setTopic] = useState<string>('tech');
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const data = await fetchDiscover(topic, 'russia');
|
||||
setItems(data);
|
||||
} catch {
|
||||
setItems([]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [topic]);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-y-auto">
|
||||
<div className="max-w-3xl mx-auto px-4 sm:px-6 py-6 sm:py-8">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6 sm:mb-8">
|
||||
<div>
|
||||
<h1 className="text-xl sm:text-2xl font-semibold text-primary mb-1 sm:mb-2">Discover</h1>
|
||||
<p className="text-sm text-secondary">Актуальные новости и события</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={load}
|
||||
disabled={isLoading}
|
||||
className="flex items-center gap-2 px-4 py-2.5 text-sm text-secondary hover:text-primary bg-surface/40 border border-border/50 hover:border-accent/30 rounded-xl transition-all w-fit"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />
|
||||
<span className="hidden sm:inline">Обновить</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Topic Tabs */}
|
||||
<Tabs.Root value={topic} onValueChange={setTopic}>
|
||||
<Tabs.List className="flex gap-2 mb-6 sm:mb-8 overflow-x-auto pb-1 -mx-4 px-4 sm:mx-0 sm:px-0">
|
||||
{topics.map((t) => (
|
||||
<Tabs.Trigger
|
||||
key={t.id}
|
||||
value={t.id}
|
||||
className="flex items-center gap-2 px-4 py-2.5 text-sm rounded-xl border transition-all whitespace-nowrap data-[state=active]:bg-accent/10 data-[state=active]:border-accent/30 data-[state=active]:text-primary bg-surface/30 border-border/30 text-muted hover:text-secondary hover:border-border"
|
||||
>
|
||||
<t.icon className="w-4 h-4" />
|
||||
{t.label}
|
||||
</Tabs.Trigger>
|
||||
))}
|
||||
</Tabs.List>
|
||||
</Tabs.Root>
|
||||
|
||||
{/* Content */}
|
||||
{isLoading ? (
|
||||
<div className="flex flex-col items-center justify-center py-16 sm:py-20">
|
||||
<div className="relative">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-accent/60" />
|
||||
<div className="absolute inset-0 blur-xl bg-accent/20" />
|
||||
</div>
|
||||
<p className="text-sm text-muted mt-4">Загрузка новостей...</p>
|
||||
</div>
|
||||
) : items.length === 0 ? (
|
||||
<div className="text-center py-16 sm:py-20">
|
||||
<Globe className="w-12 h-12 mx-auto mb-4 text-muted" />
|
||||
<p className="text-secondary">Нет новостей по выбранной теме</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 sm:space-y-4">
|
||||
{items.map((item, i) => (
|
||||
<motion.article
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.05 }}
|
||||
className="group relative flex flex-col sm:flex-row gap-3 sm:gap-4 p-3 sm:p-4 bg-elevated/40 border border-border/40 rounded-xl hover:border-accent/25 hover:bg-elevated/60 transition-all duration-200"
|
||||
>
|
||||
{/* Thumbnail */}
|
||||
{item.thumbnail && (
|
||||
<div className="relative w-full sm:w-28 h-40 sm:h-20 rounded-lg overflow-hidden flex-shrink-0 bg-surface/50">
|
||||
<img
|
||||
src={item.thumbnail}
|
||||
alt=""
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<a
|
||||
href={item.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm font-medium text-primary line-clamp-2 group-hover:text-accent transition-colors mb-2"
|
||||
>
|
||||
{item.title}
|
||||
</a>
|
||||
<p className="text-xs text-muted line-clamp-2 mb-3">
|
||||
{item.content}
|
||||
</p>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex flex-wrap items-center gap-3 sm:gap-4">
|
||||
<a
|
||||
href={item.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1.5 text-xs text-secondary hover:text-primary transition-colors"
|
||||
>
|
||||
<ExternalLink className="w-3.5 h-3.5" />
|
||||
Читать
|
||||
</a>
|
||||
<button className="flex items-center gap-1.5 text-xs text-accent/80 hover:text-accent transition-colors">
|
||||
<Sparkles className="w-3.5 h-3.5" />
|
||||
AI Саммари
|
||||
</button>
|
||||
{item.sourcesCount && item.sourcesCount > 1 && (
|
||||
<span className="text-xs text-faint">
|
||||
{item.sourcesCount} источников
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.article>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user