fix(computer-svc): stream для завершенных задач + Timeweb env vars
- Исправлен Stream() в computer.go: для completed/failed/cancelled задач сразу отправляется финальное событие и канал закрывается (ранее соединение зависало с socket hang up) - Добавлены TIMEWEB_* переменные в docker-compose.yml для computer-svc (LLM через Timeweb Cloud AI для России) - UI компоненты webui обновлены Made-with: Cursor
This commit is contained in:
@@ -104,9 +104,9 @@ const statusConfig: Record<ComputerTaskStatus, { color: string; bg: string; icon
|
||||
completed: { color: 'text-success', bg: 'bg-success/10', icon: CheckCircle2, label: 'Завершено' },
|
||||
failed: { color: 'text-error', bg: 'bg-error/10', icon: XCircle, label: 'Ошибка' },
|
||||
cancelled: { color: 'text-muted', bg: 'bg-surface/40', icon: Square, label: 'Отменено' },
|
||||
scheduled: { color: 'text-accent-secondary', bg: 'bg-accent-secondary/10', icon: Calendar, label: 'Запланировано' },
|
||||
scheduled: { color: 'text-cyan-400', bg: 'bg-cyan-400/10', icon: Calendar, label: 'Запланировано' },
|
||||
paused: { color: 'text-warning', bg: 'bg-warning/10', icon: Pause, label: 'Пауза' },
|
||||
checkpoint: { color: 'text-accent', bg: 'bg-accent/10', icon: RefreshCw, label: 'Чекпоинт' },
|
||||
checkpoint: { color: 'text-gradient', bg: 'active-gradient', icon: RefreshCw, label: 'Чекпоинт' },
|
||||
};
|
||||
|
||||
const artifactIcons: Record<string, React.ElementType> = {
|
||||
@@ -294,7 +294,7 @@ export default function ComputerPage() {
|
||||
>
|
||||
<div className="p-4 border-b border-border/50 flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-primary flex items-center gap-2">
|
||||
<Cpu className="w-5 h-5 text-accent" />
|
||||
<Cpu className="w-5 h-5 text-gradient" />
|
||||
Задачи
|
||||
</h2>
|
||||
<button
|
||||
@@ -321,7 +321,7 @@ export default function ComputerPage() {
|
||||
<div className="hidden md:flex w-72 lg:w-80 border-r border-border/50 flex-col">
|
||||
<div className="p-4 border-b border-border/50">
|
||||
<h2 className="text-lg font-semibold text-primary flex items-center gap-2">
|
||||
<Cpu className="w-5 h-5 text-accent" />
|
||||
<Cpu className="w-5 h-5 text-gradient" />
|
||||
Computer
|
||||
</h2>
|
||||
<p className="text-xs text-muted mt-1">Автономные AI-задачи</p>
|
||||
@@ -398,12 +398,12 @@ export default function ComputerPage() {
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${selectedTask.progress}%` }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="h-full bg-gradient-to-r from-accent to-accent-hover rounded-full"
|
||||
className="h-full progress-gradient rounded-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between mt-1.5">
|
||||
<span className="text-xs text-muted">Прогресс</span>
|
||||
<span className="text-xs text-accent">{selectedTask.progress}%</span>
|
||||
<span className="text-xs text-gradient">{selectedTask.progress}%</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -484,11 +484,11 @@ export default function ComputerPage() {
|
||||
return (
|
||||
<div
|
||||
key={artifact.id}
|
||||
className="p-4 rounded-lg bg-elevated/40 border border-border/40 hover:border-accent/30 transition-colors"
|
||||
className="p-4 rounded-lg bg-elevated/40 border border-border/40 hover-gradient transition-colors"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="p-2 rounded-lg bg-surface/60 flex-shrink-0">
|
||||
<Icon className="w-5 h-5 text-accent" />
|
||||
<div className="p-2 rounded-lg icon-gradient flex-shrink-0">
|
||||
<Icon className="w-5 h-5 text-gradient" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm text-primary truncate">{artifact.name}</p>
|
||||
@@ -559,7 +559,7 @@ export default function ComputerPage() {
|
||||
}}
|
||||
placeholder="Опишите задачу..."
|
||||
rows={3}
|
||||
className="w-full px-4 py-4 pr-14 rounded-xl bg-elevated/60 border border-border text-primary placeholder-muted focus:outline-none focus:border-accent/50 focus:ring-2 focus:ring-accent/20 resize-none transition-all text-base"
|
||||
className="w-full px-4 py-4 pr-14 rounded-xl bg-elevated/60 border border-border text-primary placeholder-muted focus:outline-none input-gradient resize-none transition-all text-base"
|
||||
/>
|
||||
<button
|
||||
onClick={handleExecute}
|
||||
@@ -597,16 +597,16 @@ export default function ComputerPage() {
|
||||
<button
|
||||
key={mode.value}
|
||||
onClick={() => setDurationMode(mode.value)}
|
||||
className={`p-2 sm:p-3 rounded-xl border text-center transition-all ${
|
||||
className={`p-2 sm:p-3 rounded-xl text-center transition-all ${
|
||||
durationMode === mode.value
|
||||
? 'bg-accent/10 border-accent/30 text-primary'
|
||||
: 'bg-elevated/40 border-border/40 text-secondary hover:border-border'
|
||||
? 'active-gradient text-primary'
|
||||
: 'bg-elevated/40 border border-border/40 text-secondary hover:border-border'
|
||||
}`}
|
||||
>
|
||||
<mode.icon className={`w-4 sm:w-5 h-4 sm:h-5 mx-auto mb-1 ${
|
||||
durationMode === mode.value ? 'text-accent' : 'text-muted'
|
||||
durationMode === mode.value ? 'text-gradient' : 'text-muted'
|
||||
}`} />
|
||||
<div className="text-xs sm:text-sm font-medium">{mode.label}</div>
|
||||
<div className={`text-xs sm:text-sm font-medium ${durationMode === mode.value ? 'text-gradient' : ''}`}>{mode.label}</div>
|
||||
<div className="text-xs text-muted mt-0.5 hidden sm:block">{mode.desc}</div>
|
||||
</button>
|
||||
))}
|
||||
@@ -633,9 +633,9 @@ export default function ComputerPage() {
|
||||
].map((conn) => (
|
||||
<button
|
||||
key={conn.label}
|
||||
className="p-3 rounded-lg bg-surface/40 border border-border/50 hover:border-accent/30 transition-colors text-left"
|
||||
className="p-3 rounded-lg bg-surface/40 border border-border/50 hover-gradient transition-colors text-left"
|
||||
>
|
||||
<conn.icon className="w-4 h-4 text-accent mb-1.5" />
|
||||
<conn.icon className="w-4 h-4 text-gradient mb-1.5" />
|
||||
<div className="text-xs text-primary">{conn.label}</div>
|
||||
<div className="text-xs text-muted hidden sm:block">{conn.desc}</div>
|
||||
</button>
|
||||
@@ -657,16 +657,16 @@ export default function ComputerPage() {
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.05 }}
|
||||
onClick={() => handleExample(example.query)}
|
||||
className="group relative p-3 sm:p-4 rounded-xl bg-elevated/40 border border-border/40 hover:border-accent/30 text-left transition-all"
|
||||
className="group relative p-3 sm:p-4 rounded-xl bg-elevated/40 border border-border/40 hover-gradient text-left transition-all"
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className={`w-8 h-8 rounded-lg ${example.bgColor} flex items-center justify-center flex-shrink-0`}>
|
||||
<example.icon className={`w-4 h-4 ${example.color}`} />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-primary group-hover:text-accent transition-colors truncate">{example.title}</span>
|
||||
<span className="text-sm font-medium text-primary group-hover:text-gradient transition-colors truncate">{example.title}</span>
|
||||
</div>
|
||||
<p className="text-xs text-secondary line-clamp-2 leading-relaxed">{example.query}</p>
|
||||
<ArrowRight className="absolute bottom-3 right-3 w-4 h-4 text-transparent group-hover:text-accent/60 transition-all transform translate-x-2 group-hover:translate-x-0" />
|
||||
<ArrowRight className="absolute bottom-3 right-3 w-4 h-4 text-transparent group-hover:text-gradient opacity-60 transition-all transform translate-x-2 group-hover:translate-x-0" />
|
||||
</motion.button>
|
||||
))}
|
||||
</div>
|
||||
@@ -694,7 +694,7 @@ function TaskList({ tasks, selectedTask, isLoading, onSelect, isTaskActive, stat
|
||||
<div className="flex-1 overflow-y-auto p-3 space-y-2">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="w-6 h-6 text-accent animate-spin" />
|
||||
<Loader2 className="w-6 h-6 loader-gradient animate-spin" />
|
||||
</div>
|
||||
) : tasks.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted text-sm">
|
||||
@@ -710,10 +710,10 @@ function TaskList({ tasks, selectedTask, isLoading, onSelect, isTaskActive, stat
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
onClick={() => onSelect(task)}
|
||||
className={`w-full text-left p-3 rounded-xl border transition-all ${
|
||||
className={`w-full text-left p-3 rounded-xl transition-all ${
|
||||
selectedTask?.id === task.id
|
||||
? 'bg-elevated/60 border-accent/30'
|
||||
: 'bg-elevated/40 border-border/40 hover:border-border'
|
||||
? 'bg-elevated/60 active-gradient'
|
||||
: 'bg-elevated/40 border border-border/40 hover:border-border'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
@@ -737,7 +737,7 @@ function TaskList({ tasks, selectedTask, isLoading, onSelect, isTaskActive, stat
|
||||
<motion.div
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${task.progress}%` }}
|
||||
className="h-full bg-gradient-to-r from-accent to-accent-hover"
|
||||
className="h-full progress-gradient"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
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';
|
||||
@@ -14,10 +15,16 @@ const topics = [
|
||||
] as const;
|
||||
|
||||
export default function DiscoverPage() {
|
||||
const router = useRouter();
|
||||
const [items, setItems] = useState<DiscoverItem[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [topic, setTopic] = useState<string>('tech');
|
||||
|
||||
const handleSummarize = useCallback((url: string) => {
|
||||
const query = encodeURIComponent(`Summary: ${url}`);
|
||||
router.push(`/?q=${query}`);
|
||||
}, [router]);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
@@ -46,7 +53,7 @@ export default function DiscoverPage() {
|
||||
<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"
|
||||
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-gradient rounded-xl transition-all w-fit"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />
|
||||
<span className="hidden sm:inline">Обновить</span>
|
||||
@@ -60,7 +67,7 @@ export default function DiscoverPage() {
|
||||
<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"
|
||||
className="flex items-center gap-2 px-4 py-2.5 text-sm rounded-xl border transition-all whitespace-nowrap data-[state=active]:active-gradient 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}
|
||||
@@ -73,8 +80,8 @@ export default function DiscoverPage() {
|
||||
{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" />
|
||||
<Loader2 className="w-8 h-8 animate-spin loader-gradient" />
|
||||
<div className="absolute inset-0 blur-xl progress-gradient opacity-20" />
|
||||
</div>
|
||||
<p className="text-sm text-muted mt-4">Загрузка новостей...</p>
|
||||
</div>
|
||||
@@ -91,7 +98,7 @@ export default function DiscoverPage() {
|
||||
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"
|
||||
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-gradient hover:bg-elevated/60 transition-all duration-200"
|
||||
>
|
||||
{/* Thumbnail */}
|
||||
{item.thumbnail && (
|
||||
@@ -113,7 +120,7 @@ export default function DiscoverPage() {
|
||||
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"
|
||||
className="text-sm font-medium text-primary line-clamp-2 group-hover:text-gradient transition-colors mb-2"
|
||||
>
|
||||
{item.title}
|
||||
</a>
|
||||
@@ -132,7 +139,10 @@ export default function DiscoverPage() {
|
||||
<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">
|
||||
<button
|
||||
onClick={() => handleSummarize(item.url)}
|
||||
className="flex items-center gap-1.5 text-xs text-gradient hover:opacity-80 transition-colors"
|
||||
>
|
||||
<Sparkles className="w-3.5 h-3.5" />
|
||||
AI Саммари
|
||||
</button>
|
||||
|
||||
@@ -47,7 +47,7 @@ function StockRow({ stock, market, delay }: { stock: FinanceStock; market: strin
|
||||
<span className="text-[10px] sm:text-xs font-semibold text-secondary">{stock.symbol.slice(0, 4)}</span>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-primary truncate group-hover:text-accent transition-colors">
|
||||
<p className="text-sm font-medium text-primary truncate group-hover:text-gradient transition-colors">
|
||||
{stock.name}
|
||||
</p>
|
||||
{stock.sector && (
|
||||
@@ -150,7 +150,7 @@ export default function FinancePage() {
|
||||
<button
|
||||
onClick={loadData}
|
||||
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"
|
||||
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-gradient rounded-xl transition-all w-fit"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />
|
||||
<span className="hidden sm:inline">Обновить</span>
|
||||
@@ -164,7 +164,7 @@ export default function FinancePage() {
|
||||
<Tabs.Trigger
|
||||
key={m.id}
|
||||
value={m.id}
|
||||
className="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"
|
||||
className="px-4 py-2.5 text-sm rounded-xl border transition-all whitespace-nowrap data-[state=active]:active-gradient data-[state=active]:text-primary bg-surface/30 border-border/30 text-muted hover:text-secondary hover:border-border"
|
||||
>
|
||||
{m.name}
|
||||
</Tabs.Trigger>
|
||||
@@ -180,7 +180,7 @@ export default function FinancePage() {
|
||||
onClick={() => setTimeRange(range.id)}
|
||||
className={`px-3 py-1.5 text-xs rounded-lg transition-all whitespace-nowrap ${
|
||||
timeRange === range.id
|
||||
? 'bg-accent/15 text-accent border border-accent/30'
|
||||
? 'active-gradient text-gradient'
|
||||
: 'text-muted hover:text-secondary'
|
||||
}`}
|
||||
>
|
||||
@@ -192,7 +192,7 @@ export default function FinancePage() {
|
||||
{/* Content */}
|
||||
{isLoading ? (
|
||||
<div className="flex flex-col items-center justify-center py-16 sm:py-20">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-accent/60" />
|
||||
<Loader2 className="w-8 h-8 animate-spin loader-gradient" />
|
||||
<p className="text-sm text-muted mt-4">Загрузка данных...</p>
|
||||
</div>
|
||||
) : (
|
||||
@@ -218,7 +218,7 @@ export default function FinancePage() {
|
||||
stocks={moversData.mostActive}
|
||||
market={currentMarket}
|
||||
icon={Activity}
|
||||
color="bg-accent/10 text-accent"
|
||||
color="active-gradient text-gradient"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -129,7 +129,7 @@ export default function HistoryPage() {
|
||||
<button
|
||||
onClick={load}
|
||||
disabled={isLoading}
|
||||
className="flex items-center gap-2 px-3 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"
|
||||
className="flex items-center gap-2 px-3 py-2.5 text-sm text-secondary hover:text-primary bg-surface/40 border border-border/50 hover-gradient rounded-xl transition-all"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
@@ -152,14 +152,14 @@ export default function HistoryPage() {
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="Поиск в истории..."
|
||||
className="w-full pl-11 pr-4 py-3 text-sm bg-elevated/40 border border-border/50 rounded-xl text-primary placeholder:text-muted focus:outline-none focus:border-accent/40 transition-colors"
|
||||
className="w-full pl-11 pr-4 py-3 text-sm bg-elevated/40 border border-border/50 rounded-xl text-primary placeholder:text-muted focus:outline-none input-gradient transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
{isLoading ? (
|
||||
<div className="flex flex-col items-center justify-center py-16 sm:py-20">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-accent/60" />
|
||||
<Loader2 className="w-8 h-8 animate-spin loader-gradient" />
|
||||
<p className="text-sm text-muted mt-4">Загрузка истории...</p>
|
||||
</div>
|
||||
) : Object.keys(grouped).length > 0 ? (
|
||||
@@ -177,13 +177,13 @@ export default function HistoryPage() {
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.03 }}
|
||||
onClick={() => openThread(thread.id)}
|
||||
className="group flex items-center gap-3 sm:gap-4 p-3 sm:p-4 rounded-xl bg-elevated/40 border border-border/40 hover:bg-elevated/60 hover:border-accent/25 cursor-pointer transition-all duration-200"
|
||||
className="group flex items-center gap-3 sm:gap-4 p-3 sm:p-4 rounded-xl bg-elevated/40 border border-border/40 hover:bg-elevated/60 hover-gradient cursor-pointer transition-all duration-200"
|
||||
>
|
||||
<div className="w-9 h-9 sm:w-10 sm:h-10 rounded-xl bg-surface/60 flex items-center justify-center flex-shrink-0">
|
||||
<MessageSquare className="w-4 h-4 text-secondary" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm text-primary truncate group-hover:text-accent transition-colors">
|
||||
<p className="text-sm text-primary truncate group-hover:text-gradient transition-colors">
|
||||
{thread.title}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
|
||||
@@ -92,7 +92,7 @@ export default function NewLessonPage() {
|
||||
value={formData.topic}
|
||||
onChange={(e) => setFormData((f) => ({ ...f, topic: e.target.value }))}
|
||||
placeholder="Например: Основы Python"
|
||||
className="w-full px-4 py-3 text-sm bg-elevated/60 border border-border rounded-xl text-primary placeholder:text-muted focus:outline-none focus:border-accent/40 transition-colors"
|
||||
className="w-full px-4 py-3 text-sm bg-elevated/60 border border-border rounded-xl text-primary placeholder:text-muted focus:outline-none input-gradient transition-colors"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
@@ -109,11 +109,11 @@ export default function NewLessonPage() {
|
||||
onClick={() => setFormData((f) => ({ ...f, difficulty: d.value }))}
|
||||
className={`flex items-center gap-3 p-3 rounded-xl border transition-all ${
|
||||
formData.difficulty === d.value
|
||||
? 'bg-accent/10 border-accent/30 text-primary'
|
||||
? 'active-gradient text-primary'
|
||||
: 'bg-elevated/40 border-border/50 text-muted hover:border-border hover:text-secondary'
|
||||
}`}
|
||||
>
|
||||
<d.icon className={`w-4 h-4 ${formData.difficulty === d.value ? 'text-accent' : ''}`} />
|
||||
<d.icon className={`w-4 h-4 ${formData.difficulty === d.value ? 'text-gradient' : ''}`} />
|
||||
<span className="text-sm">{d.label}</span>
|
||||
</button>
|
||||
))}
|
||||
@@ -127,7 +127,7 @@ export default function NewLessonPage() {
|
||||
<select
|
||||
value={formData.mode}
|
||||
onChange={(e) => setFormData((f) => ({ ...f, mode: e.target.value }))}
|
||||
className="w-full px-4 py-3 text-sm bg-elevated/60 border border-border rounded-xl text-primary focus:outline-none focus:border-accent/40 transition-colors"
|
||||
className="w-full px-4 py-3 text-sm bg-elevated/60 border border-border rounded-xl text-primary focus:outline-none input-gradient transition-colors"
|
||||
>
|
||||
{modes.map((m) => (
|
||||
<option key={m.value} value={m.value}>
|
||||
@@ -145,7 +145,7 @@ export default function NewLessonPage() {
|
||||
<label
|
||||
className={`flex-1 flex items-center gap-3 p-4 rounded-xl border cursor-pointer transition-all ${
|
||||
formData.includeCode
|
||||
? 'bg-accent/10 border-accent/30 text-primary'
|
||||
? 'active-gradient text-primary'
|
||||
: 'bg-elevated/40 border-border/50 text-muted hover:border-border'
|
||||
}`}
|
||||
>
|
||||
@@ -155,13 +155,13 @@ export default function NewLessonPage() {
|
||||
onChange={(e) => setFormData((f) => ({ ...f, includeCode: e.target.checked }))}
|
||||
className="sr-only"
|
||||
/>
|
||||
<Code2 className={`w-5 h-5 ${formData.includeCode ? 'text-accent' : ''}`} />
|
||||
<Code2 className={`w-5 h-5 ${formData.includeCode ? 'text-gradient' : ''}`} />
|
||||
<span className="text-sm">Примеры кода</span>
|
||||
</label>
|
||||
<label
|
||||
className={`flex-1 flex items-center gap-3 p-4 rounded-xl border cursor-pointer transition-all ${
|
||||
formData.includeQuiz
|
||||
? 'bg-accent/10 border-accent/30 text-primary'
|
||||
? 'active-gradient text-primary'
|
||||
: 'bg-elevated/40 border-border/50 text-muted hover:border-border'
|
||||
}`}
|
||||
>
|
||||
@@ -171,7 +171,7 @@ export default function NewLessonPage() {
|
||||
onChange={(e) => setFormData((f) => ({ ...f, includeQuiz: e.target.checked }))}
|
||||
className="sr-only"
|
||||
/>
|
||||
<CheckCircle2 className={`w-5 h-5 ${formData.includeQuiz ? 'text-accent' : ''}`} />
|
||||
<CheckCircle2 className={`w-5 h-5 ${formData.includeQuiz ? 'text-gradient' : ''}`} />
|
||||
<span className="text-sm">Тесты</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -76,7 +76,7 @@ export default function LearningPage() {
|
||||
<button
|
||||
onClick={load}
|
||||
disabled={isLoading}
|
||||
className="flex items-center gap-2 px-3 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"
|
||||
className="flex items-center gap-2 px-3 py-2.5 text-sm text-secondary hover:text-primary bg-surface/40 border border-border/50 hover-gradient rounded-xl transition-all"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
@@ -96,8 +96,8 @@ export default function LearningPage() {
|
||||
<p className="text-xl sm:text-2xl font-semibold text-success">{stats.completed}</p>
|
||||
<p className="text-xs text-muted mt-1">Завершено</p>
|
||||
</div>
|
||||
<div className="p-3 sm:p-4 bg-accent/5 border border-accent/20 rounded-xl text-center">
|
||||
<p className="text-xl sm:text-2xl font-semibold text-accent">{stats.inProgress}</p>
|
||||
<div className="p-3 sm:p-4 active-gradient rounded-xl text-center">
|
||||
<p className="text-xl sm:text-2xl font-semibold text-gradient">{stats.inProgress}</p>
|
||||
<p className="text-xs text-muted mt-1">В процессе</p>
|
||||
</div>
|
||||
<div className="p-3 sm:p-4 bg-surface/40 border border-border/40 rounded-xl text-center">
|
||||
@@ -109,7 +109,7 @@ export default function LearningPage() {
|
||||
{/* Content */}
|
||||
{isLoading ? (
|
||||
<div className="flex flex-col items-center justify-center py-16 sm:py-20">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-accent/60" />
|
||||
<Loader2 className="w-8 h-8 animate-spin loader-gradient" />
|
||||
<p className="text-sm text-muted mt-4">Загрузка уроков...</p>
|
||||
</div>
|
||||
) : lessons.length > 0 ? (
|
||||
@@ -125,7 +125,7 @@ export default function LearningPage() {
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.05 }}
|
||||
className="group p-3 sm:p-4 bg-elevated/40 border border-border/40 rounded-xl hover:bg-elevated/60 hover:border-accent/25 cursor-pointer transition-all duration-200"
|
||||
className="group p-3 sm:p-4 bg-elevated/40 border border-border/40 rounded-xl hover:bg-elevated/60 hover-gradient cursor-pointer transition-all duration-200"
|
||||
>
|
||||
<div className="flex items-start gap-3 sm:gap-4">
|
||||
<div
|
||||
@@ -133,21 +133,21 @@ export default function LearningPage() {
|
||||
done
|
||||
? 'bg-success/10 border border-success/25'
|
||||
: inProgress
|
||||
? 'bg-accent/10 border border-accent/25'
|
||||
? 'icon-gradient'
|
||||
: 'bg-surface/60 border border-border/50'
|
||||
}`}
|
||||
>
|
||||
{done ? (
|
||||
<CheckCircle2 className="w-4 h-4 sm:w-5 sm:h-5 text-success" />
|
||||
) : inProgress ? (
|
||||
<Play className="w-4 h-4 sm:w-5 sm:h-5 text-accent" />
|
||||
<Play className="w-4 h-4 sm:w-5 sm:h-5 text-gradient" />
|
||||
) : (
|
||||
<BookOpen className="w-4 h-4 sm:w-5 sm:h-5 text-secondary" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<p className="text-sm font-medium text-primary group-hover:text-accent transition-colors truncate">
|
||||
<p className="text-sm font-medium text-primary group-hover:text-gradient transition-colors truncate">
|
||||
{lesson.title || lesson.topic}
|
||||
</p>
|
||||
<button
|
||||
@@ -182,7 +182,7 @@ export default function LearningPage() {
|
||||
animate={{ width: `${pct}%` }}
|
||||
transition={{ duration: 0.5, ease: 'easeOut' }}
|
||||
className={`h-full rounded-full ${
|
||||
done ? 'bg-success' : 'bg-accent'
|
||||
done ? 'bg-success' : 'progress-gradient'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import { useRef, useEffect, useState } from 'react';
|
||||
import { useRef, useEffect, useState, Suspense } from 'react';
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { ChatInput } from '@/components/ChatInput';
|
||||
import { ChatMessage } from '@/components/ChatMessage';
|
||||
import { useChat } from '@/lib/hooks/useChat';
|
||||
|
||||
export default function HomePage() {
|
||||
function HomePageContent() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { messages, isLoading, sendMessage, stopGeneration } = useChat();
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const [showWelcome, setShowWelcome] = useState(true);
|
||||
const initialQueryProcessed = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (messages.length > 0) {
|
||||
@@ -17,6 +21,15 @@ export default function HomePage() {
|
||||
}
|
||||
}, [messages.length]);
|
||||
|
||||
useEffect(() => {
|
||||
const query = searchParams.get('q');
|
||||
if (query && !initialQueryProcessed.current && !isLoading) {
|
||||
initialQueryProcessed.current = true;
|
||||
router.replace('/', { scroll: false });
|
||||
sendMessage(decodeURIComponent(query));
|
||||
}
|
||||
}, [searchParams, isLoading, sendMessage, router]);
|
||||
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, [messages]);
|
||||
@@ -106,3 +119,11 @@ export default function HomePage() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<Suspense fallback={<div className="flex-1" />}>
|
||||
<HomePageContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -35,11 +35,11 @@ export default function SettingsPage() {
|
||||
onClick={() => setMode(m.id)}
|
||||
className={`flex flex-col items-center gap-1 sm:gap-2 p-3 sm:p-4 rounded-xl border transition-all ${
|
||||
mode === m.id
|
||||
? 'bg-accent/10 border-accent/30 text-primary'
|
||||
? 'active-gradient text-primary'
|
||||
: 'bg-surface/30 border-border/30 text-muted hover:border-border hover:text-secondary'
|
||||
}`}
|
||||
>
|
||||
<m.icon className={`w-4 h-4 sm:w-5 sm:h-5 ${mode === m.id ? 'text-accent' : ''}`} />
|
||||
<m.icon className={`w-4 h-4 sm:w-5 sm:h-5 ${mode === m.id ? 'text-gradient' : ''}`} />
|
||||
<span className="text-xs sm:text-sm font-medium">{m.label}</span>
|
||||
<span className="text-[10px] sm:text-xs text-faint">{m.desc}</span>
|
||||
</button>
|
||||
@@ -86,7 +86,7 @@ export default function SettingsPage() {
|
||||
onClick={() => setLanguage(lang.id)}
|
||||
className={`flex items-center gap-3 p-3 sm:p-4 rounded-xl border transition-all ${
|
||||
language === lang.id
|
||||
? 'bg-accent/10 border-accent/30 text-primary'
|
||||
? 'active-gradient text-primary'
|
||||
: 'bg-surface/30 border-border/30 text-muted hover:border-border hover:text-secondary'
|
||||
}`}
|
||||
>
|
||||
@@ -163,9 +163,9 @@ function ToggleRow({ icon: Icon, label, description, checked, onChange }: Toggle
|
||||
<Switch.Root
|
||||
checked={checked}
|
||||
onCheckedChange={onChange}
|
||||
className="w-10 h-[22px] sm:w-11 sm:h-6 bg-surface/80 rounded-full relative transition-colors data-[state=checked]:bg-accent/20 border border-border data-[state=checked]:border-accent/30 flex-shrink-0"
|
||||
className="w-10 h-[22px] sm:w-11 sm:h-6 bg-surface/80 rounded-full relative transition-colors data-[state=checked]:active-gradient border border-border flex-shrink-0"
|
||||
>
|
||||
<Switch.Thumb className="block w-[18px] h-[18px] sm:w-5 sm:h-5 bg-secondary rounded-full transition-transform translate-x-0.5 data-[state=checked]:translate-x-[18px] sm:data-[state=checked]:translate-x-[22px] data-[state=checked]:bg-accent" />
|
||||
<Switch.Thumb className="block w-[18px] h-[18px] sm:w-5 sm:h-5 bg-secondary rounded-full transition-transform translate-x-0.5 data-[state=checked]:translate-x-[18px] sm:data-[state=checked]:translate-x-[22px] data-[state=checked]:progress-gradient" />
|
||||
</Switch.Root>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -103,7 +103,7 @@ export default function EditSpacePage() {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-accent/60" />
|
||||
<Loader2 className="w-8 h-8 animate-spin loader-gradient" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -155,7 +155,7 @@ export default function EditSpacePage() {
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData((f) => ({ ...f, name: e.target.value }))}
|
||||
placeholder="Название пространства"
|
||||
className="w-full px-4 py-3 text-sm bg-elevated/60 border border-border rounded-xl text-primary placeholder:text-muted focus:outline-none focus:border-accent/40 transition-colors"
|
||||
className="w-full px-4 py-3 text-sm bg-elevated/60 border border-border rounded-xl text-primary placeholder:text-muted focus:outline-none input-gradient transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -168,7 +168,7 @@ export default function EditSpacePage() {
|
||||
onChange={(e) => setFormData((f) => ({ ...f, description: e.target.value }))}
|
||||
placeholder="Краткое описание"
|
||||
rows={2}
|
||||
className="w-full px-4 py-3 text-sm bg-elevated/60 border border-border rounded-xl text-primary placeholder:text-muted resize-none focus:outline-none focus:border-accent/40 transition-colors"
|
||||
className="w-full px-4 py-3 text-sm bg-elevated/60 border border-border rounded-xl text-primary placeholder:text-muted resize-none focus:outline-none input-gradient transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -181,7 +181,7 @@ export default function EditSpacePage() {
|
||||
onChange={(e) => setFormData((f) => ({ ...f, instructions: e.target.value }))}
|
||||
placeholder="Специальные инструкции для AI"
|
||||
rows={3}
|
||||
className="w-full px-4 py-3 text-sm bg-elevated/60 border border-border rounded-xl text-primary placeholder:text-muted resize-none focus:outline-none focus:border-accent/40 transition-colors"
|
||||
className="w-full px-4 py-3 text-sm bg-elevated/60 border border-border rounded-xl text-primary placeholder:text-muted resize-none focus:outline-none input-gradient transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -197,11 +197,11 @@ export default function EditSpacePage() {
|
||||
onClick={() => setFormData((f) => ({ ...f, focusMode: mode.value }))}
|
||||
className={`flex flex-col items-center gap-2 p-3 rounded-xl border transition-all ${
|
||||
formData.focusMode === mode.value
|
||||
? 'bg-accent/10 border-accent/30 text-primary'
|
||||
? 'active-gradient text-primary'
|
||||
: 'bg-elevated/40 border-border/50 text-muted hover:border-border hover:text-secondary'
|
||||
}`}
|
||||
>
|
||||
<mode.icon className={`w-4 h-4 ${formData.focusMode === mode.value ? 'text-accent' : ''}`} />
|
||||
<mode.icon className={`w-4 h-4 ${formData.focusMode === mode.value ? 'text-gradient' : ''}`} />
|
||||
<span className="text-xs text-center">{mode.label}</span>
|
||||
</button>
|
||||
))}
|
||||
|
||||
@@ -84,7 +84,7 @@ export default function NewSpacePage() {
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData((f) => ({ ...f, name: e.target.value }))}
|
||||
placeholder="Например: Исследование рынка"
|
||||
className="w-full px-4 py-3 text-sm bg-elevated/60 border border-border rounded-xl text-primary placeholder:text-muted focus:outline-none focus:border-accent/40 transition-colors"
|
||||
className="w-full px-4 py-3 text-sm bg-elevated/60 border border-border rounded-xl text-primary placeholder:text-muted focus:outline-none input-gradient transition-colors"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
@@ -98,7 +98,7 @@ export default function NewSpacePage() {
|
||||
onChange={(e) => setFormData((f) => ({ ...f, description: e.target.value }))}
|
||||
placeholder="Краткое описание пространства"
|
||||
rows={2}
|
||||
className="w-full px-4 py-3 text-sm bg-elevated/60 border border-border rounded-xl text-primary placeholder:text-muted resize-none focus:outline-none focus:border-accent/40 transition-colors"
|
||||
className="w-full px-4 py-3 text-sm bg-elevated/60 border border-border rounded-xl text-primary placeholder:text-muted resize-none focus:outline-none input-gradient transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -111,7 +111,7 @@ export default function NewSpacePage() {
|
||||
onChange={(e) => setFormData((f) => ({ ...f, instructions: e.target.value }))}
|
||||
placeholder="Специальные инструкции для AI в этом пространстве"
|
||||
rows={3}
|
||||
className="w-full px-4 py-3 text-sm bg-elevated/60 border border-border rounded-xl text-primary placeholder:text-muted resize-none focus:outline-none focus:border-accent/40 transition-colors"
|
||||
className="w-full px-4 py-3 text-sm bg-elevated/60 border border-border rounded-xl text-primary placeholder:text-muted resize-none focus:outline-none input-gradient transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -127,11 +127,11 @@ export default function NewSpacePage() {
|
||||
onClick={() => setFormData((f) => ({ ...f, focusMode: mode.value }))}
|
||||
className={`flex flex-col items-center gap-2 p-3 rounded-xl border transition-all ${
|
||||
formData.focusMode === mode.value
|
||||
? 'bg-accent/10 border-accent/30 text-primary'
|
||||
? 'active-gradient text-primary'
|
||||
: 'bg-elevated/40 border-border/50 text-muted hover:border-border hover:text-secondary'
|
||||
}`}
|
||||
>
|
||||
<mode.icon className={`w-4 h-4 ${formData.focusMode === mode.value ? 'text-accent' : ''}`} />
|
||||
<mode.icon className={`w-4 h-4 ${formData.focusMode === mode.value ? 'text-gradient' : ''}`} />
|
||||
<span className="text-xs text-center">{mode.label}</span>
|
||||
</button>
|
||||
))}
|
||||
|
||||
@@ -82,7 +82,7 @@ export default function SpacesPage() {
|
||||
<button
|
||||
onClick={load}
|
||||
disabled={isLoading}
|
||||
className="flex items-center gap-2 px-3 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"
|
||||
className="flex items-center gap-2 px-3 py-2.5 text-sm text-secondary hover:text-primary bg-surface/40 border border-border/50 hover-gradient rounded-xl transition-all"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
@@ -103,14 +103,14 @@ export default function SpacesPage() {
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="Поиск пространств..."
|
||||
className="w-full pl-11 pr-4 py-3 text-sm bg-elevated/40 border border-border/50 rounded-xl text-primary placeholder:text-muted focus:outline-none focus:border-accent/40 transition-colors"
|
||||
className="w-full pl-11 pr-4 py-3 text-sm bg-elevated/40 border border-border/50 rounded-xl text-primary placeholder:text-muted focus:outline-none input-gradient transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
{isLoading ? (
|
||||
<div className="flex flex-col items-center justify-center py-16 sm:py-20">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-accent/60" />
|
||||
<Loader2 className="w-8 h-8 animate-spin loader-gradient" />
|
||||
<p className="text-sm text-muted mt-4">Загрузка пространств...</p>
|
||||
</div>
|
||||
) : filtered.length > 0 ? (
|
||||
@@ -124,13 +124,13 @@ export default function SpacesPage() {
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.05 }}
|
||||
onClick={() => openSpace(space.id)}
|
||||
className="group flex items-center gap-3 sm:gap-4 p-3 sm:p-4 bg-elevated/40 border border-border/40 rounded-xl hover:bg-elevated/60 hover:border-accent/25 cursor-pointer transition-all duration-200"
|
||||
className="group flex items-center gap-3 sm:gap-4 p-3 sm:p-4 bg-elevated/40 border border-border/40 rounded-xl hover:bg-elevated/60 hover-gradient cursor-pointer transition-all duration-200"
|
||||
>
|
||||
<div className="w-10 h-10 sm:w-12 sm:h-12 rounded-xl bg-accent/10 border border-accent/20 flex items-center justify-center flex-shrink-0">
|
||||
<FolderOpen className="w-4 h-4 sm:w-5 sm:h-5 text-accent/70" />
|
||||
<div className="w-10 h-10 sm:w-12 sm:h-12 rounded-xl icon-gradient flex items-center justify-center flex-shrink-0">
|
||||
<FolderOpen className="w-4 h-4 sm:w-5 sm:h-5 text-gradient" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-sm font-medium text-primary group-hover:text-accent transition-colors truncate">
|
||||
<h3 className="text-sm font-medium text-primary group-hover:text-gradient transition-colors truncate">
|
||||
{space.name}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
|
||||
Reference in New Issue
Block a user