From 120fbbaafb701a0bb2a536a20afd7a3f5b66fc40 Mon Sep 17 00:00:00 2001 From: home Date: Fri, 27 Feb 2026 05:17:42 +0300 Subject: [PATCH] =?UTF-8?q?fix(computer-svc):=20stream=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=B7=D0=B0=D0=B2=D0=B5=D1=80=D1=88=D0=B5=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D1=85=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=20+=20Timeweb=20e?= =?UTF-8?q?nv=20vars?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Исправлен Stream() в computer.go: для completed/failed/cancelled задач сразу отправляется финальное событие и канал закрывается (ранее соединение зависало с socket hang up) - Добавлены TIMEWEB_* переменные в docker-compose.yml для computer-svc (LLM через Timeweb Cloud AI для России) - UI компоненты webui обновлены Made-with: Cursor --- backend/deploy/docker/docker-compose.yml | 3 + backend/internal/computer/computer.go | 25 +++ .../webui/src/app/(main)/computer/page.tsx | 50 +++--- .../webui/src/app/(main)/discover/page.tsx | 24 ++- backend/webui/src/app/(main)/finance/page.tsx | 12 +- backend/webui/src/app/(main)/history/page.tsx | 10 +- .../src/app/(main)/learning/new/page.tsx | 16 +- .../webui/src/app/(main)/learning/page.tsx | 18 +- backend/webui/src/app/(main)/page.tsx | 25 ++- .../webui/src/app/(main)/settings/page.tsx | 10 +- .../src/app/(main)/spaces/[id]/edit/page.tsx | 12 +- .../webui/src/app/(main)/spaces/new/page.tsx | 10 +- backend/webui/src/app/(main)/spaces/page.tsx | 14 +- backend/webui/src/app/globals.css | 158 ++++++++++++++++++ backend/webui/src/components/ChatInput.tsx | 8 +- backend/webui/src/components/ChatMessage.tsx | 24 +-- backend/webui/src/components/Sidebar.tsx | 29 +--- backend/webui/src/lib/hooks/useChat.ts | 57 +++++++ backend/webui/src/lib/types.ts | 12 ++ 19 files changed, 391 insertions(+), 126 deletions(-) diff --git a/backend/deploy/docker/docker-compose.yml b/backend/deploy/docker/docker-compose.yml index 41dc7ae..2bf15d3 100644 --- a/backend/deploy/docker/docker-compose.yml +++ b/backend/deploy/docker/docker-compose.yml @@ -200,6 +200,9 @@ services: - OPENAI_API_KEY=${OPENAI_API_KEY} - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} - GEMINI_API_KEY=${GEMINI_API_KEY} + - TIMEWEB_API_BASE_URL=${TIMEWEB_API_BASE_URL} + - TIMEWEB_AGENT_ACCESS_ID=${TIMEWEB_AGENT_ACCESS_ID} + - TIMEWEB_API_KEY=${TIMEWEB_API_KEY} - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN} - SANDBOX_IMAGE=python:3.11-slim - BROWSER_SVC_URL=http://browser-svc:3050 diff --git a/backend/internal/computer/computer.go b/backend/internal/computer/computer.go index 925a165..1df73bd 100644 --- a/backend/internal/computer/computer.go +++ b/backend/internal/computer/computer.go @@ -645,6 +645,31 @@ func (c *Computer) GetUserTasks(ctx context.Context, userID string, limit, offse } func (c *Computer) Stream(ctx context.Context, taskID string) (<-chan TaskEvent, error) { + task, err := c.taskRepo.GetByID(ctx, taskID) + if err != nil { + return nil, fmt.Errorf("task not found: %w", err) + } + + if task.Status == StatusCompleted || task.Status == StatusFailed || task.Status == StatusCancelled { + ch := make(chan TaskEvent, 1) + go func() { + eventType := EventTaskCompleted + if task.Status == StatusFailed { + eventType = EventTaskFailed + } + ch <- TaskEvent{ + TaskID: taskID, + Type: eventType, + Status: task.Status, + Progress: task.Progress, + Message: task.Message, + Timestamp: time.Now(), + } + close(ch) + }() + return ch, nil + } + return c.eventBus.Subscribe(taskID), nil } diff --git a/backend/webui/src/app/(main)/computer/page.tsx b/backend/webui/src/app/(main)/computer/page.tsx index c9ec83a..90b59ba 100644 --- a/backend/webui/src/app/(main)/computer/page.tsx +++ b/backend/webui/src/app/(main)/computer/page.tsx @@ -104,9 +104,9 @@ const statusConfig: Record = { @@ -294,7 +294,7 @@ export default function ComputerPage() { >

- + Задачи

)} @@ -484,11 +484,11 @@ export default function ComputerPage() { return (
-
- +
+

{artifact.name}

@@ -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" /> ))} @@ -633,9 +633,9 @@ export default function ComputerPage() { ].map((conn) => ( @@ -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" >
- {example.title} + {example.title}

{example.query}

- + ))}
@@ -694,7 +694,7 @@ function TaskList({ tasks, selectedTask, isLoading, onSelect, isTaskActive, stat
{isLoading ? (
- +
) : tasks.length === 0 ? (
@@ -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' }`} >
@@ -737,7 +737,7 @@ function TaskList({ tasks, selectedTask, isLoading, onSelect, isTaskActive, stat
)} diff --git a/backend/webui/src/app/(main)/discover/page.tsx b/backend/webui/src/app/(main)/discover/page.tsx index f78262a..29f4ef1 100644 --- a/backend/webui/src/app/(main)/discover/page.tsx +++ b/backend/webui/src/app/(main)/discover/page.tsx @@ -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([]); const [isLoading, setIsLoading] = useState(true); const [topic, setTopic] = useState('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() { diff --git a/backend/webui/src/app/(main)/finance/page.tsx b/backend/webui/src/app/(main)/finance/page.tsx index 9436f17..680a5d0 100644 --- a/backend/webui/src/app/(main)/finance/page.tsx +++ b/backend/webui/src/app/(main)/finance/page.tsx @@ -47,7 +47,7 @@ function StockRow({ stock, market, delay }: { stock: FinanceStock; market: strin {stock.symbol.slice(0, 4)}
-

+

{stock.name}

{stock.sector && ( @@ -150,7 +150,7 @@ export default function FinancePage() {
)} diff --git a/backend/webui/src/app/(main)/history/page.tsx b/backend/webui/src/app/(main)/history/page.tsx index 48d5750..6a0c744 100644 --- a/backend/webui/src/app/(main)/history/page.tsx +++ b/backend/webui/src/app/(main)/history/page.tsx @@ -129,7 +129,7 @@ export default function HistoryPage() { @@ -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" />
{/* Content */} {isLoading ? (
- +

Загрузка истории...

) : 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" >
-

+

{thread.title}

diff --git a/backend/webui/src/app/(main)/learning/new/page.tsx b/backend/webui/src/app/(main)/learning/new/page.tsx index 5cf89da..1b6d2a4 100644 --- a/backend/webui/src/app/(main)/learning/new/page.tsx +++ b/backend/webui/src/app/(main)/learning/new/page.tsx @@ -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 />
@@ -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.label} ))} @@ -127,7 +127,7 @@ export default function NewLessonPage() {