Files
gooseek/backend/webui/src/app/(main)/medicine/page.tsx
home 08bd41e75c feat: travel service with 2GIS routing, POI, hotels + finance providers + UI overhaul
- Add travel-svc microservice (Amadeus, TravelPayouts, 2GIS, OpenRouteService)
- Add travel orchestrator with parallel collectors (events, POI, hotels, flights)
- Add 2GIS road routing with transport cost calculation (car/bus/taxi)
- Add TravelMap (2GIS MapGL) and TravelWidgets components
- Add useTravelChat hook for streaming travel agent responses
- Add finance heatmap providers refactor
- Add SearXNG settings, API proxy routes, Docker compose updates
- Update Dockerfiles, config, types, and all UI pages for consistency

Made-with: Cursor
2026-03-01 21:58:32 +03:00

299 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useState, useCallback } from 'react';
import { motion } from 'framer-motion';
import {
HeartPulse,
Search,
Pill,
Stethoscope,
FileText,
AlertTriangle,
Clock,
Loader2,
Sparkles,
Activity,
Thermometer,
Brain,
Eye,
Bone,
Wind,
Droplet,
Shield,
} from 'lucide-react';
interface Symptom {
id: string;
name: string;
icon: React.ElementType;
category: string;
}
interface Article {
id: string;
title: string;
category: string;
readTime: string;
icon: string;
}
const commonSymptoms: Symptom[] = [
{ id: '1', name: 'Головная боль', icon: Brain, category: 'Неврология' },
{ id: '2', name: 'Температура', icon: Thermometer, category: 'Общие' },
{ id: '3', name: 'Боль в горле', icon: Wind, category: 'ЛОР' },
{ id: '4', name: 'Боль в спине', icon: Bone, category: 'Ортопедия' },
{ id: '5', name: 'Проблемы со зрением', icon: Eye, category: 'Офтальмология' },
{ id: '6', name: 'Давление', icon: Activity, category: 'Кардиология' },
{ id: '7', name: 'Аллергия', icon: Droplet, category: 'Аллергология' },
{ id: '8', name: 'Усталость', icon: HeartPulse, category: 'Общие' },
];
const healthArticles: Article[] = [
{
id: '1',
title: 'Как укрепить иммунитет зимой',
category: 'Профилактика',
readTime: '5 мин',
icon: '🛡️',
},
{
id: '2',
title: 'Правильное питание для сердца',
category: 'Кардиология',
readTime: '7 мин',
icon: '❤️',
},
{
id: '3',
title: 'Упражнения для здоровой спины',
category: 'Ортопедия',
readTime: '6 мин',
icon: '🏃',
},
{
id: '4',
title: 'Как справиться со стрессом',
category: 'Психология',
readTime: '8 мин',
icon: '🧘',
},
];
const quickServices = [
{ icon: Stethoscope, label: 'Найти врача', color: 'bg-blue-500/10 text-blue-600' },
{ icon: Pill, label: 'Справочник лекарств', color: 'bg-green-500/10 text-green-600' },
{ icon: FileText, label: 'Анализы', color: 'bg-purple-500/10 text-purple-600' },
{ icon: Sparkles, label: 'AI Консультант', color: 'active-gradient text-gradient' },
];
function SymptomButton({ symptom, onClick }: { symptom: Symptom; onClick: () => void }) {
return (
<button
onClick={onClick}
className="flex items-center gap-2 px-3 py-2 bg-surface/50 border border-border/40 rounded-lg hover:border-border hover:bg-surface/70 transition-all text-left"
>
<symptom.icon className="w-4 h-4 text-muted flex-shrink-0" />
<span className="text-sm text-secondary truncate">{symptom.name}</span>
</button>
);
}
function ArticleCard({ article, delay }: { article: Article; delay: number }) {
return (
<motion.div
initial={{ opacity: 0, x: -8 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay }}
className="flex items-center gap-3 p-3 bg-elevated/40 border border-border/40 rounded-xl hover:border-border transition-all cursor-pointer group"
>
<div className="w-10 h-10 rounded-xl bg-surface/60 flex items-center justify-center text-xl flex-shrink-0">
{article.icon}
</div>
<div className="flex-1 min-w-0">
<h3 className="text-sm font-medium text-primary group-hover:text-gradient transition-colors truncate">
{article.title}
</h3>
<div className="flex items-center gap-2 text-xs text-muted">
<span>{article.category}</span>
<span></span>
<span className="flex items-center gap-1">
<Clock className="w-3 h-3" />
{article.readTime}
</span>
</div>
</div>
</motion.div>
);
}
export default function MedicinePage() {
const [searchQuery, setSearchQuery] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [aiResponse, setAiResponse] = useState<string | null>(null);
const handleAIConsult = useCallback(async () => {
if (!searchQuery.trim()) return;
setIsLoading(true);
setAiResponse(null);
await new Promise((resolve) => setTimeout(resolve, 2000));
setAiResponse(
'На основе описанных симптомов рекомендую обратиться к терапевту для первичного осмотра. ' +
'Это может быть связано с несколькими причинами, которые требуют диагностики. ' +
'До визита к врачу: пейте больше жидкости, отдыхайте, избегайте переохлаждения.'
);
setIsLoading(false);
}, [searchQuery]);
const handleSymptomClick = (symptomName: string) => {
setSearchQuery(symptomName);
};
return (
<div className="h-full overflow-y-auto">
<div className="max-w-2xl mx-auto px-4 sm:px-6 py-6 sm:py-8">
{/* Header */}
<div className="mb-6 sm:mb-8">
<h1 className="text-xl sm:text-2xl font-semibold text-primary mb-1 sm:mb-2">Медицина</h1>
<p className="text-sm text-secondary">AI-помощник по здоровью и медицине</p>
</div>
{/* Disclaimer */}
<div className="flex items-start gap-3 p-4 bg-amber-500/5 border border-amber-500/20 rounded-xl mb-6">
<AlertTriangle className="w-5 h-5 text-amber-600 flex-shrink-0 mt-0.5" />
<div>
<p className="text-sm font-medium text-amber-600 mb-1">Важно</p>
<p className="text-xs text-muted">
Информация носит справочный характер и не заменяет консультацию врача.
При серьёзных симптомах обратитесь к специалисту.
</p>
</div>
</div>
{/* AI Search */}
<div className="bg-elevated/40 border border-border/40 rounded-xl p-4 mb-6">
<div className="relative mb-4">
<Sparkles className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gradient" />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleAIConsult()}
placeholder="Опишите симптомы или задайте вопрос о здоровье..."
className="w-full h-12 pl-12 pr-4 bg-surface/50 border border-border/50 rounded-xl text-sm text-primary placeholder:text-muted focus:outline-none focus:border-accent/50 focus:ring-1 focus:ring-accent/20 transition-all"
/>
</div>
<button
onClick={handleAIConsult}
disabled={isLoading || !searchQuery.trim()}
className="w-full h-11 flex items-center justify-center gap-2 active-gradient text-gradient font-medium text-sm rounded-xl disabled:opacity-50 disabled:cursor-not-allowed transition-opacity"
>
{isLoading ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
AI анализирует...
</>
) : (
<>
<Search className="w-4 h-4" />
Получить консультацию AI
</>
)}
</button>
{/* AI Response */}
{aiResponse && (
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
className="mt-4 p-4 bg-surface/50 border border-border/40 rounded-xl"
>
<div className="flex items-center gap-2 mb-2">
<Shield className="w-4 h-4 text-gradient" />
<span className="text-xs font-medium text-gradient">Рекомендация AI</span>
</div>
<p className="text-sm text-secondary leading-relaxed">{aiResponse}</p>
</motion.div>
)}
</div>
{/* Quick Services */}
<div className="grid grid-cols-4 gap-2 sm:gap-3 mb-6 sm:mb-8">
{quickServices.map((service) => (
<button
key={service.label}
className={`flex flex-col items-center gap-2 p-3 sm:p-4 rounded-xl border border-border/40 hover:border-border transition-all ${service.color}`}
>
<service.icon className="w-5 h-5 sm:w-6 sm:h-6" />
<span className="text-[10px] sm:text-xs font-medium text-center leading-tight">
{service.label}
</span>
</button>
))}
</div>
{/* Common Symptoms */}
<div className="mb-6 sm:mb-8">
<div className="flex items-center gap-3 mb-4">
<div className="w-9 h-9 rounded-xl bg-red-500/10 flex items-center justify-center">
<HeartPulse className="w-4 h-4 text-red-600" />
</div>
<h2 className="text-sm font-medium text-primary">Частые симптомы</h2>
</div>
<div className="grid grid-cols-2 gap-2">
{commonSymptoms.map((symptom) => (
<SymptomButton
key={symptom.id}
symptom={symptom}
onClick={() => handleSymptomClick(symptom.name)}
/>
))}
</div>
</div>
{/* Health Articles */}
<div className="mb-6">
<div className="flex items-center gap-3 mb-4">
<div className="w-9 h-9 rounded-xl bg-green-500/10 flex items-center justify-center">
<FileText className="w-4 h-4 text-green-600" />
</div>
<h2 className="text-sm font-medium text-primary">Полезные статьи</h2>
</div>
<div className="space-y-2">
{healthArticles.map((article, i) => (
<ArticleCard key={article.id} article={article} delay={i * 0.05} />
))}
</div>
</div>
{/* Emergency Info */}
<div className="bg-red-500/5 border border-red-500/20 rounded-xl p-4">
<div className="flex items-center gap-2 mb-2">
<AlertTriangle className="w-4 h-4 text-red-600" />
<span className="text-sm font-medium text-red-600">Экстренная помощь</span>
</div>
<p className="text-sm text-muted mb-3">
При угрозе жизни немедленно вызывайте скорую помощь
</p>
<div className="flex gap-3">
<a
href="tel:103"
className="flex-1 h-10 flex items-center justify-center gap-2 bg-red-500/10 border border-red-500/30 text-red-600 font-medium text-sm rounded-lg hover:bg-red-500/20 transition-colors"
>
📞 103
</a>
<a
href="tel:112"
className="flex-1 h-10 flex items-center justify-center gap-2 bg-red-500/10 border border-red-500/30 text-red-600 font-medium text-sm rounded-lg hover:bg-red-500/20 transition-colors"
>
📞 112
</a>
</div>
</div>
</div>
</div>
);
}