cesjavi commited on
Commit
0cb1aa7
·
1 Parent(s): a31164f

Update Hugging Face Space

Browse files
.gitignore CHANGED
Binary files a/.gitignore and b/.gitignore differ
 
AUDIT.md ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🛡️ Aubm System Stability Audit Report
2
+
3
+ **Date**: May 4, 2026
4
+ **Status**: Stable / Production-Ready (Phase 4 Initialized)
5
+
6
+ ## 🏗️ Architecture Overview
7
+ The system follows a modular micro-service pattern using **FastAPI** (Python) and **React 18** (Vite).
8
+ - **Backend**: Highly decoupled agent-provider pattern with a centralized `ToolRegistry`.
9
+ - **Frontend**: Glassmorphic UI with real-time SSE logging and mobile readiness (Capacitor).
10
+
11
+ ## 🔒 Security & Governance
12
+ - [x] **Authentication**: Supabase Auth with SSO (Google/GitHub) support.
13
+ - [x] **Authorization**: Advanced RLS (Row Level Security) with team-based isolation and role-based access control (Admin/Editor/Viewer).
14
+ - [x] **Auditing**: Every agent action and LLM call is recorded in `audit_logs` for compliance.
15
+
16
+ ## 🤖 Agent Capabilities
17
+ | Tool | Stability | Notes |
18
+ | :--- | :--- | :--- |
19
+ | **BrowserTool** | High | Integrated with Playwright for reliable web research. |
20
+ | **CodeSandbox** | High | Isolated Python execution for logical verification. |
21
+ | **FileGenerator** | High | Professional PDF/Excel generation (ReportLab/Pandas). |
22
+ | **Decomposer** | High | Enables recursive agent autonomy (project planning). |
23
+ | **SRE Tool** | High | System health monitoring and whitelisted autonomous patching. |
24
+
25
+ ## 📊 Database Health
26
+ - Schema is partitioned across 4 main upgrade files (`schema.sql`, `phase3_updates.sql`, `marketplace.sql`, `enterprise_security.sql`).
27
+ - All tables include proper foreign key constraints and RLS policies.
28
+ - **Seeding**: Initial agent experts and project templates are pre-loaded.
29
+
30
+ ## 🚀 Autonomous Reliability (Phase 4)
31
+ - **Self-Healing**: The SRE agent can now detect service failures and apply whitelisted patches (e.g., `git pull`, `npm install`).
32
+ - **Safety**: Whitelist prevents destructive commands, ensuring the agent cannot harm the host OS.
33
+ - **Next-Gen Interfaces**: Voice control and the spatial DAG viewer are scaffolded in the frontend for hands-free status checks and immersive task-flow inspection.
34
+
35
+ ## Operations Readiness (Phase 5)
36
+ - **Monitoring Endpoint**: `GET /monitoring/summary` reports API/database health and core workflow counts.
37
+ - **Operations Dashboard**: The frontend includes a monitoring view with backend-first status checks and Supabase fallback metrics.
38
+
39
+ ## 💡 Recommendations for Next Steps
40
+ 1. **Production Deployment**: Finalize Dockerization for isolated backend deployment.
41
+ 2. **Monitoring**: Integrate Sentry or Datadog for real-time error tracking.
42
+ 3. **Intelligence**: Begin fine-tuning local models (Ollama) using the captured `task_feedback` data.
43
+
44
+ ---
45
+ **Verdict**: The system is robust, secure, and ready for autonomous project orchestration at scale.
OPERATING_GUIDE.md ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Aubm Operating Guide
2
+
3
+ ## What Aubm Does
4
+
5
+ Aubm is an AI agent orchestration platform. Users sign in with Supabase Auth, deploy or configure agents, assign them to tasks, run autonomous executions, review outputs, and monitor system health from a React dashboard.
6
+
7
+ The application has three main layers:
8
+
9
+ - `frontend/`: React + Vite dashboard for authentication, marketplace, debates, voice control, spatial task visualization, and monitoring.
10
+ - `backend/`: FastAPI API for task execution, multi-agent debate orchestration, tool calling, and monitoring.
11
+ - `database/`: Supabase SQL schema, seed data, RLS policies, marketplace tables, audit logs, teams, and migrations.
12
+
13
+ ## Core Runtime Flow
14
+
15
+ 1. A user signs in through Supabase Auth.
16
+ 2. The frontend reads templates, agents, projects, and tasks from Supabase.
17
+ 3. A user deploys an agent from the marketplace into `public.agents`.
18
+ 4. A task references an assigned agent through `tasks.assigned_agent_id`.
19
+ 5. `POST /tasks/{task_id}/run` starts backend execution.
20
+ 6. The backend loads the task, assigned agent, and previous completed task outputs.
21
+ 7. `AgentFactory` creates the right provider implementation, currently `OpenAIAgent` or `AMDAgent`.
22
+ 8. The agent produces JSON output.
23
+ 9. The backend writes output to `tasks.output_data`, moves the task to `awaiting_approval`, records `task_runs`, `agent_logs`, and `audit_logs`.
24
+ 10. A human reviews, edits, approves, or gives feedback through the frontend.
25
+
26
+ ## Main Features
27
+
28
+ ### Dashboard
29
+
30
+ Shows project cards and high-level workflow progress. It is currently a static dashboard scaffold, ready to be connected to live project data.
31
+
32
+ ### Agent Marketplace
33
+
34
+ Reads `agent_templates` from Supabase and deploys selected templates into `agents`.
35
+
36
+ Required database support:
37
+
38
+ - `agents.user_id`
39
+ - Insert policy allowing authenticated users to create agents where `auth.uid() = user_id`
40
+
41
+ Apply:
42
+
43
+ ```sql
44
+ -- database/agent_ownership.sql
45
+ ```
46
+
47
+ ### Custom Agents
48
+
49
+ The `Agents` screen lets users create custom agents directly.
50
+
51
+ Each agent has:
52
+
53
+ - Name
54
+ - Role
55
+ - LLM provider
56
+ - Model
57
+ - System prompt
58
+
59
+ The currently wired backend providers are:
60
+
61
+ - `openai`
62
+ - `amd`
63
+
64
+ Settings stores the frontend default provider/model in browser local storage. Provider API keys are never stored in the frontend; they must stay in `backend/.env`.
65
+
66
+ ### Agent Debate
67
+
68
+ Uses the backend endpoint:
69
+
70
+ ```text
71
+ POST /orchestrator/debate
72
+ ```
73
+
74
+ The flow is:
75
+
76
+ 1. Agent A generates an initial answer.
77
+ 2. Agent B critiques the answer.
78
+ 3. Agent A refines the output.
79
+ 4. The final debate result is saved to `tasks.output_data`.
80
+
81
+ ### Voice Control
82
+
83
+ Uses browser Web Speech APIs.
84
+
85
+ Supported commands include:
86
+
87
+ - `dashboard`
88
+ - `marketplace`
89
+ - `debate`
90
+ - `settings`
91
+ - `new project`
92
+ - `status`
93
+
94
+ The `status` command reads project/task counts from Supabase and speaks the result.
95
+
96
+ ### Spatial View
97
+
98
+ Shows a layered task DAG-style visualization. It reads recent tasks from Supabase and falls back to demo nodes if no tasks are available.
99
+
100
+ ### Monitoring
101
+
102
+ Uses:
103
+
104
+ ```text
105
+ GET /monitoring/summary
106
+ ```
107
+
108
+ The endpoint reports:
109
+
110
+ - API status
111
+ - Database status
112
+ - Project count
113
+ - Task count
114
+ - Agent count
115
+ - Task run count
116
+ - Failed tasks
117
+ - Tasks awaiting approval
118
+
119
+ If the backend endpoint is unavailable, the frontend falls back to direct Supabase count queries.
120
+
121
+ ## Backend Setup
122
+
123
+ From `backend/`:
124
+
125
+ ```powershell
126
+ python -m venv venv
127
+ .\venv\Scripts\activate
128
+ pip install -r requirements.txt
129
+ uvicorn main:app --reload --port 8000
130
+ ```
131
+
132
+ Required `backend/.env` values:
133
+
134
+ ```env
135
+ SUPABASE_URL=...
136
+ SUPABASE_SERVICE_ROLE_KEY=...
137
+ OPENAI_API_KEY=...
138
+ AMD_API_KEY=...
139
+ ```
140
+
141
+ Optional provider keys:
142
+
143
+ ```env
144
+ GROQ_API_KEY=...
145
+ GEMINI_API_KEY=...
146
+ ANTHROPIC_API_KEY=...
147
+ ```
148
+
149
+ ## Frontend Setup
150
+
151
+ From `frontend/`:
152
+
153
+ ```powershell
154
+ npm install
155
+ npm run dev
156
+ ```
157
+
158
+ Required `frontend/.env` values:
159
+
160
+ ```env
161
+ VITE_SUPABASE_URL=...
162
+ VITE_SUPABASE_ANON_KEY=...
163
+ VITE_API_URL=http://127.0.0.1:8000
164
+ ```
165
+
166
+ Build check:
167
+
168
+ ```powershell
169
+ npm run build
170
+ ```
171
+
172
+ ## Database Setup Order
173
+
174
+ Apply SQL files in this order for a fresh Supabase project:
175
+
176
+ 1. `database/schema.sql`
177
+ 2. `database/seed.sql`
178
+ 3. `database/phase3_updates.sql`
179
+ 4. `database/marketplace.sql`
180
+ 5. `database/enterprise_security.sql`
181
+ 6. `database/agent_ownership.sql`
182
+ 7. `database/task_owner_policies.sql`
183
+ 8. `database/default_agents.sql`
184
+
185
+ For an existing Supabase project where marketplace deploy fails with missing `user_id`, apply only:
186
+
187
+ ```sql
188
+ -- database/agent_ownership.sql
189
+ ```
190
+
191
+ Then reload the frontend with a hard refresh.
192
+
193
+ ## Important Tables
194
+
195
+ - `profiles`: User metadata and role.
196
+ - `projects`: Project containers.
197
+ - `agents`: Deployed AI agents.
198
+ - `agent_templates`: Marketplace templates.
199
+ - `tasks`: Work units assigned to agents.
200
+ - `task_runs`: Execution history.
201
+ - `agent_logs`: Agent execution traces.
202
+ - `audit_logs`: Governance and compliance trail.
203
+ - `task_feedback`: Like/dislike feedback for future tuning.
204
+ - `teams` and `team_members`: Enterprise team permissions.
205
+
206
+ ## Tool System
207
+
208
+ The backend exposes tools to agents through `tools/registry.py`.
209
+
210
+ Available tools include:
211
+
212
+ - Web extraction with Playwright.
213
+ - Python code execution.
214
+ - PDF generation.
215
+ - Excel generation.
216
+ - Project decomposition.
217
+ - System health checks.
218
+ - Restricted patch commands.
219
+
220
+ ## Current Roadmap State
221
+
222
+ Completed:
223
+
224
+ - Core backend and frontend foundation.
225
+ - Supabase auth and schema.
226
+ - Agent execution.
227
+ - Multi-agent debate.
228
+ - Marketplace.
229
+ - Voice control.
230
+ - Spatial task viewer.
231
+ - Operations monitoring.
232
+
233
+ In progress:
234
+
235
+ - Production operations hardening.
236
+ - Error tracking.
237
+ - Docker/runtime packaging.
238
+ - Frontend bundle splitting.
239
+ - Production CORS allowlist.
240
+
241
+ ## Common Errors
242
+
243
+ ### `403 Forbidden` on `POST /rest/v1/agents`
244
+
245
+ Cause: RLS policy does not allow insert.
246
+
247
+ Fix: Apply `database/agent_ownership.sql`.
248
+
249
+ ### `Could not find the 'user_id' column of 'agents' in the schema cache`
250
+
251
+ Cause: `agents.user_id` is missing or PostgREST schema cache has not reloaded.
252
+
253
+ Fix:
254
+
255
+ ```sql
256
+ ALTER TABLE public.agents
257
+ ADD COLUMN IF NOT EXISTS user_id UUID REFERENCES auth.users ON DELETE CASCADE;
258
+
259
+ NOTIFY pgrst, 'reload schema';
260
+ ```
261
+
262
+ Then hard refresh the frontend.
263
+
264
+ ### `OTS parsing error`
265
+
266
+ Cause: A CSS URL was incorrectly used as a font file.
267
+
268
+ Fix: Use Google Fonts through `@import`, already applied in `frontend/src/styles/variables.css`.
269
+
270
+ ### Frontend chunk-size warning
271
+
272
+ Vite currently warns that the JS chunk is larger than 500 KB. This is not a runtime error. The Phase 5 roadmap includes bundle splitting.
273
+
274
+ ## Development Rules
275
+
276
+ - Keep frontend display text in English.
277
+ - Keep documentation in English.
278
+ - Keep database migrations idempotent when possible.
279
+ - Never commit real secrets from `.env`.
280
+ - Prefer applying database changes through separate migration files instead of editing only `schema.sql`.
ROADMAP.md ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🗺️ AgentCollab Roadmap
2
+
3
+ This document outlines the strategic evolution of Aubm, moving from a robust orchestration core to an enterprise ecosystem.
4
+
5
+ ## 🟢 Phase 1: Core Foundation (Completed)
6
+ - [x] **Autonomous Agent Execution**: Multi-provider support (OpenAI, Groq, Gemini, etc.).
7
+ - [x] **Project Orchestration**: Intelligent task scheduling and dependency management (DAG).
8
+ - [x] **Human-in-the-Loop**: Approval and rejection workflows for agent outputs.
9
+ - [x] **Semantic RAG**: Contextual memory injection across project tasks.
10
+ - [x] **Real-time Logs**: Streaming agent thoughts and actions via SSE.
11
+ - [x] **Cost Control**: Token-based budgeting and execution blocking.
12
+
13
+ ## 🟡 Phase 2: Advanced Collaboration & Tools (Completed)
14
+ - [x] **Multi-Agent Debates**: Allow agents to cross-verify each other's outputs before human review.
15
+ - [x] **Extended Toolbelt**:
16
+ - [x] Web Browser Tool (via Playwright) for live data fetching.
17
+ - [x] Code Sandbox for executing and testing generated snippets.
18
+ - [x] File Generation (Excel, Word, more advanced PDF layouts).
19
+ - [x] **Collaborative Editing**: Real-time collaborative output refining for humans.
20
+ - [x] **Mobile Experience**: Capacitor-based mobile app for project monitoring (Initialized).
21
+
22
+ ## 🔵 Phase 3: Intelligence & Scale (Completed)
23
+ - [x] **Fine-tuning Loop**: Feedback loop (Like/Dislike) implemented for data collection.
24
+ - [x] **Recursive Project Decomposition**: Agents that can spawn sub-tasks and manage them.
25
+ - [x] **Enterprise Security**:
26
+ - [x] SSO Integration (Google, GitHub via Supabase).
27
+ - [x] Advanced RLS for granular team permissions.
28
+ - [x] Audit logs for every LLM interaction.
29
+ - [x] **Agent Marketplace**: Community-driven agent templates and specialized skill sets.
30
+
31
+ ## 🔴 Phase 4: Autonomy & Beyond (Completed)
32
+ - [x] **Self-Healing Infrastructure**: Agents that can monitor health and apply safe patches.
33
+ - [x] **Voice Interaction**: Control navigation and hear project/task status updates via browser voice APIs.
34
+ - [x] **VR/AR Dashboard**: Spatial DAG viewer scaffold for layered project/task visualization.
35
+
36
+ ## ⚫ Phase 5: Production Operations (Completed)
37
+ - [x] **Operations Monitoring**: Backend health summary endpoint and frontend monitoring dashboard with Supabase fallback.
38
+ - [x] **Deployment Hardening**: Dockerized backend/runtime profile and production CORS configuration.
39
+ - [x] **Error Tracking**: Sentry-compatible error reporting hooks for backend and frontend.
40
+ - [x] **Performance Budgeting**: Frontend code splitting and bundle-size targets.
41
+
42
+ ## ⚪ Phase 6: Distributed Scale & Intelligence (In Progress)
43
+ - [x] **Recursive Project Decomposition**: Agents that can automatically break down goals.
44
+ - [x] **Numerical Consistency (Semantic Backprop)**: Enforce absolute figures across tasks.
45
+ - [x] **Visual Tooling**: Integrated support for charts and AI illustrations.
46
+ - [x] **Vercel Deployment**: Monorepo serverless configuration.
47
+ - [ ] **Asynchronous Task Queue**: Dedicated background workers (worker.py).
48
+ - [ ] **Vectorized Long-term Memory**: Cross-project semantic retrieval.
49
+ - [ ] **Self-Optimizing Agents**: Meta-prompting loops based on human feedback.
50
+
51
+ ---
52
+
53
+ *Last updated: May 2026*
SPEC.md ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🛠️ Aubm — Technical Specification
2
+
3
+ > **Target Stack**: FastAPI (Python) + React/TypeScript (Vite) + Supabase (Postgres + Auth)
4
+
5
+ This document provides a comprehensive technical blueprint for recreating Aubm.
6
+
7
+ ---
8
+
9
+ ## 1. System Architecture
10
+
11
+ Aubm follows a decoupled architecture with a centralized database (Supabase) acting as the source of truth and coordination layer.
12
+
13
+ ### Directory Structure
14
+ ```
15
+ aubm/
16
+ ├── backend/ # Python 3.10+
17
+ │ ├── main.py # Application entrypoint & CRUD API
18
+ │ ├── worker.py # Standalone task queue worker
19
+ │ ├── schema.sql # Full DDL for Supabase
20
+ │ ├── agents/ # Provider-specific implementations
21
+ │ │ ├── base.py # Abstract BaseAgent class
22
+ │ │ ├── agent_factory.py # Factory for creating agent instances
23
+ │ │ └── {provider}_agent.py
24
+ │ ├── routers/ # Functional endpoint grouping
25
+ │ │ ├── agent_runner.py # Task execution logic
26
+ │ │ └── orchestrator.py # Multi-task project flow
27
+ │ └── services/ # Core business logic
28
+ │ ├── config.py # Configuration management
29
+ │ ├── task_queue.py # Background processing loop
30
+ │ └── semantic_backprop.py # RAG context builder
31
+ ├── frontend/ # React + Vite + TS
32
+ │ ├── src/
33
+ │ │ ├── components/ # UI Modular components
34
+ │ │ ├── services/ # API communication layer
35
+ │ │ ├── context/ # Auth & Global state
36
+ │ │ └── i18n/ # Multi-language support
37
+ │ └── vite.config.ts
38
+ └── database/ # Migrations & Seed data
39
+ ```
40
+
41
+ ---
42
+
43
+ ## 2. Database Schema (Supabase/Postgres)
44
+
45
+ ### Core Tables
46
+
47
+ | Table | Purpose | Key Columns |
48
+ |-------|---------|-------------|
49
+ | `profiles` | User extensions | `id (uuid)`, `role`, `full_name`, `avatar_url` |
50
+ | `projects` | Project containers | `id`, `name`, `description`, `context`, `owner_id`, `status` |
51
+ | `agents` | AI Identities | `id`, `name`, `role`, `api_provider`, `model`, `system_prompt` |
52
+ | `tasks` | Units of work | `id`, `project_id`, `assigned_agent_id`, `status`, `output_data` |
53
+ | `task_runs` | Execution history | `id`, `task_id`, `agent_id`, `status`, `error_message` |
54
+ | `agent_logs` | Execution traces | `id`, `task_id`, `action`, `content`, `metadata` |
55
+ | `app_config` | Global settings | `key`, `value` (JSONB) |
56
+
57
+ ### Status Enums
58
+ - **Tasks**: `todo`, `in_progress`, `awaiting_approval`, `done`, `failed`, `cancelled`.
59
+ - **Task Runs**: `queued`, `running`, `completed`, `failed`, `cancelled`.
60
+ - **Profiles**: `user`, `manager`, `admin`.
61
+
62
+ ---
63
+
64
+ ## 3. Backend Logic
65
+
66
+ ### Agent Execution Flow
67
+ 1. **Request**: `POST /tasks/{id}/run`
68
+ 2. **Initialization**: Fetch task, agent, and project data.
69
+ 3. **Context Building**: `semantic_backprop` fetches outputs from previous tasks in the same project.
70
+ 4. **Agent Factory**: Instantiates the correct `BaseAgent` subclass (e.g., `GroqAgent`).
71
+ 5. **Execution**:
72
+ - LLM call with dynamic prompt.
73
+ - Real-time logging to `agent_logs` via SSE.
74
+ 6. **Guardrails**:
75
+ - `output_cleaner`: Strips markdown artifacts.
76
+ - `language_guard`: Ensures output matches `app_config["output_language"]`.
77
+ 7. **Persistence**: Updates `task.output_data` and sets status to `awaiting_approval`.
78
+
79
+ ### Orchestration Engine
80
+ - Processes a project's task list as a Directed Acyclic Graph (DAG).
81
+ - Respects `is_critical` and `priority` fields.
82
+ - Auto-assigns available agents from the `agents` pool if no agent is pre-assigned.
83
+
84
+ ### Tool System (Phase 2)
85
+ - **Tool Registry**: A central registry where tools are defined and permissioned.
86
+ - **Browser Tool**: Uses Playwright for headless browsing and content extraction.
87
+ - **Sandbox Tool**: Executes code in a restricted environment.
88
+ - **Integration**: Tools are exposed to agents via the OpenAI function-calling/tool-calling schema.
89
+
90
+ ---
91
+
92
+ ## 4. Frontend Design System
93
+
94
+ - **Styling**: Vanilla CSS with modern variables (HSL colors, glassmorphism).
95
+ - **Icons**: Lucide React.
96
+ - **State Management**: React Context + Hooks.
97
+ - **Features**:
98
+ - Kanban Board for task management.
99
+ - Real-time streaming console for agent thoughts.
100
+ - Interactive Project Wizard for quick setup.
101
+ - Analytics dashboard for project performance.
102
+
103
+ ---
104
+
105
+ ## 5. Deployment Guide
106
+
107
+ ### Vercel Integration
108
+ The project is designed to run seamlessly on Vercel:
109
+ - **Frontend**: Standard Vite build.
110
+ - **Backend**: Python Serverless Functions.
111
+ - **Database**: External Supabase instance.
112
+
113
+ ### Local Setup
114
+ 1. **DB**: Apply `schema.sql` to Supabase.
115
+ 2. **Backend**: `pip install -r requirements.txt` & `uvicorn main:app`.
116
+ 3. **Frontend**: `npm install` & `npm run dev`.
117
+
118
+ ---
119
+
120
+ ## 6. Key Dependencies
121
+
122
+ ### Backend
123
+ - `fastapi`, `supabase`, `openai`, `groq`, `google-genai`, `playwright`, `folium`.
124
+
125
+ ### Frontend
126
+ - `react`, `lucide-react`, `framer-motion` (for animations), `i18next`.
127
+
128
+ ---
129
+
130
+ ## 7. Security (RLS)
131
+ - **Projects**: Only visible to owner or if `is_public=true`.
132
+ - **Config**: Only writable by users with `role='admin'`.
133
+ - **Agents**: Writable by `manager` or `admin`.
134
+ - **Tasks**: Protected by project-level RLS.
135
+
136
+ ---
137
+ *End of Specification*
TASKS.md ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Project Tasks: Aubm Implementation
2
+
3
+ This file tracks the granular implementation steps for the Aubm platform, following the [ROADMAP.md](./ROADMAP.md) and [SPEC.md](./SPEC.md).
4
+
5
+ ## Phase 1: Core Foundation
6
+
7
+ ### 1.1 Project Initialization
8
+ - [x] Create directory structure (`backend/`, `frontend/`, `database/`)
9
+ - [x] Initialize Python virtual environment in `backend/`
10
+ - [x] Initialize Vite + React + TS project in `frontend/`
11
+ - [x] Create `backend/requirements.txt` with core dependencies
12
+ - [x] Create `frontend/package.json` and install dependencies
13
+
14
+ ### 1.2 Database & Schema
15
+ - [x] Create `database/schema.sql` based on SPEC.md
16
+ - [x] Set up Supabase project
17
+ - [x] Implement initial seed data for `agents` and `app_config`
18
+
19
+ ### 1.3 Backend Core
20
+ - [x] Implement `backend/main.py` entrypoint
21
+ - [x] Create `backend/services/config.py` for environment management
22
+ - [x] Implement `backend/agents/base.py`
23
+ - [x] Implement first agent providers (`OpenAIAgent`, `AMDAgent`)
24
+ - [x] Implement `backend/routers/agent_runner.py` for task execution
25
+
26
+ ### 1.4 Frontend Core
27
+ - [x] Set up CSS design system
28
+ - [x] Implement Supabase Auth integration
29
+ - [x] Create app layout with sidebar and header
30
+ - [x] Build project dashboard view
31
+
32
+ ## Phase 2: Advanced Collaboration & Tools
33
+
34
+ ### 2.1 Extended Toolbelt
35
+ - [x] Implement `BrowserTool` using Playwright
36
+ - [x] Create `ToolRegistry` for agent access
37
+ - [x] Implement `CodeSandboxTool`
38
+ - [x] Add file generation capabilities
39
+
40
+ ### 2.2 Multi-Agent Features
41
+ - [x] Implement debate logic
42
+ - [x] Create peer review status/dashboard for tasks
43
+
44
+ ### 2.3 Real-Time Collaboration
45
+ - [x] Implement collaborative output editor
46
+ - [ ] Real-time cursor/presence indicators
47
+
48
+ ### 2.4 Mobile Experience
49
+ - [x] Initialize Capacitor in the frontend project
50
+ - [ ] Add Android/iOS platform scaffolding
51
+
52
+ ## Phase 3: Intelligence & Scale
53
+
54
+ ### 3.1 Advanced Analytics & Security
55
+ - [x] Implement audit logs for LLM interaction tracking
56
+ - [x] Add feedback loop for fine-tuning data collection
57
+ - [x] Implement SSO integration through Supabase
58
+ - [x] Implement granular RLS for project teams
59
+
60
+ ### 3.2 Recursive Autonomy
61
+ - [x] Implement project decomposition
62
+ - [x] Create agent marketplace schema and gallery UI
63
+
64
+ ## Phase 4: Autonomy & Beyond
65
+
66
+ ### 4.1 System Self-Healing
67
+ - [x] Implement health check agents
68
+ - [x] Create restricted autonomous patching logic
69
+
70
+ ### 4.2 Next-Gen Interfaces
71
+ - [x] Voice control integration
72
+ - [x] Scaffolding for VR/AR project viewer
73
+
74
+ ## Phase 5: Production Operations
75
+
76
+ ### 5.1 Observability
77
+ - [x] Add backend monitoring summary endpoint
78
+ - [x] Add frontend operations monitoring dashboard
79
+ - [x] Add external error tracking integration
80
+
81
+ ### 5.2 Deployment Hardening
82
+ - [x] Add Dockerfile and production server command
83
+ - [x] Replace wildcard CORS with environment-driven allowlist
84
+ - [x] Add frontend bundle splitting/performance budget
85
+
86
+ ## Phase 6: Distributed Scale & Intelligence
87
+
88
+ ### 6.1 Asynchronous Workers
89
+ - [/] Implement `backend/worker.py` for task consumption
90
+ - [/] Implement `backend/services/task_queue.py` using a lightweight polling or webhook mechanism
91
+
92
+ ### 6.2 Advanced Memory
93
+ - [ ] Set up pgvector extension in Supabase
94
+ - [ ] Implement semantic retrieval service for cross-project context
95
+
96
+ ### 6.3 Self-Optimization
97
+ - [ ] Create agent "reflection" router to analyze task history
98
+ - [ ] Implement automated system prompt refinement logic
99
+
100
+ ---
101
+ *Legend: Pending | In Progress | Completed*
backend/agents/amd_agent.py CHANGED
@@ -22,20 +22,12 @@ class AMDAgent(BaseAgent):
22
  self.max_tokens = self.provider_config.get("max_tokens", 4096)
23
 
24
  async def run(self, task_description: str, context: List[Dict[str, Any]], use_tools: bool = False, extra_context: str = "") -> Dict[str, Any]:
25
- try:
26
- response = await self.client.chat.completions.create(
27
- model=self.model,
28
- messages=self._build_chat_messages(task_description, context, extra_context),
29
- response_format={"type": "json_object"},
30
- temperature=self.temperature,
31
- max_tokens=self.max_tokens
32
- )
33
-
34
- return self._result("amd", response.choices[0].message.content or "")
35
- except Exception as e:
36
- return {
37
- "agent_name": self.name,
38
- "provider": "amd",
39
- "status": "error",
40
- "error": str(e)
41
- }
 
22
  self.max_tokens = self.provider_config.get("max_tokens", 4096)
23
 
24
  async def run(self, task_description: str, context: List[Dict[str, Any]], use_tools: bool = False, extra_context: str = "") -> Dict[str, Any]:
25
+ return await self._run_openai_compatible(
26
+ provider="amd",
27
+ create_fn=self.client.chat.completions.create,
28
+ task_description=task_description,
29
+ context=context,
30
+ use_tools=use_tools,
31
+ extra_context=extra_context,
32
+ response_format={"type": "json_object"}
33
+ )
 
 
 
 
 
 
 
 
backend/agents/base.py CHANGED
@@ -89,6 +89,69 @@ Please provide your output as a JSON object.
89
  "content": str(tool_result),
90
  })
91
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  def _result(self, provider: str, content: str) -> Dict[str, Any]:
93
  return {
94
  "agent_name": self.name,
 
89
  "content": str(tool_result),
90
  })
91
 
92
+ async def _run_openai_compatible(
93
+ self,
94
+ provider: str,
95
+ create_fn,
96
+ task_description: str,
97
+ context: List[Dict[str, Any]],
98
+ use_tools: bool = False,
99
+ extra_context: str = "",
100
+ **extra_kwargs
101
+ ) -> Dict[str, Any]:
102
+ """
103
+ Unified runner for OpenAI-compatible APIs (OpenAI, Groq, etc.)
104
+ """
105
+ from tools.registry import tool_registry
106
+
107
+ messages = self._build_chat_messages(task_description, context, extra_context)
108
+
109
+ is_reasoning_model = "gpt-oss-" in self.model or self.model.startswith("o1-") or self.model.startswith("o3-")
110
+
111
+ kwargs = {
112
+ "model": self.model,
113
+ "messages": messages,
114
+ **extra_kwargs
115
+ }
116
+
117
+ # Handle temperature/max_tokens based on model type
118
+ if is_reasoning_model:
119
+ # Reasoning models prefer temperature 1.0 or none
120
+ kwargs["temperature"] = extra_kwargs.get("temperature", 1.0)
121
+ # Use max_completion_tokens if provided, otherwise default to max_tokens logic but renamed
122
+ if "max_completion_tokens" not in kwargs:
123
+ kwargs["max_completion_tokens"] = getattr(self, "max_tokens", 4096)
124
+ # Standard max_tokens is often forbidden in reasoning models
125
+ kwargs.pop("max_tokens", None)
126
+ else:
127
+ kwargs["temperature"] = getattr(self, "temperature", 0.7)
128
+ kwargs["max_tokens"] = getattr(self, "max_tokens", 4096)
129
+
130
+ if use_tools:
131
+ # Note: Many reasoning models don't support tools yet, but we'll include if requested
132
+ kwargs["tools"] = tool_registry.get_tool_definitions()
133
+ kwargs["tool_choice"] = "auto"
134
+
135
+ response = await create_fn(**kwargs)
136
+ message = response.choices[0].message
137
+
138
+ # Handle tool calls
139
+ if message.tool_calls:
140
+ messages.append(message)
141
+ await self._append_tool_results(messages, message.tool_calls, tool_registry)
142
+
143
+ # Second call after tool execution
144
+ # Remove tools from second call to force a final answer
145
+ kwargs.pop("tools", None)
146
+ kwargs.pop("tool_choice", None)
147
+
148
+ final_response = await create_fn(**kwargs)
149
+ content = final_response.choices[0].message.content
150
+ else:
151
+ content = message.content
152
+
153
+ return self._result(provider, content or "")
154
+
155
  def _result(self, provider: str, content: str) -> Dict[str, Any]:
156
  return {
157
  "agent_name": self.name,
backend/agents/groq_agent.py CHANGED
@@ -2,16 +2,31 @@ import logging
2
  from .base import BaseAgent
3
  from typing import Dict, Any, List
4
  import groq
 
5
  from services.config import settings, config_service
6
  from tools.registry import tool_registry
7
 
8
  logger = logging.getLogger("uvicorn")
9
 
 
 
 
 
 
 
 
 
 
 
10
  class GroqAgent(BaseAgent):
11
  """
12
- Agent implementation for Groq.
13
  """
14
  def __init__(self, name: str, role: str, model: str = "llama-3.3-70b-versatile", system_prompt: str = None):
 
 
 
 
15
  super().__init__(name, role, model, system_prompt)
16
 
17
  # Load dynamic config
@@ -21,37 +36,63 @@ class GroqAgent(BaseAgent):
21
  self.client = groq.AsyncGroq(api_key=api_key)
22
  self.temperature = self.provider_config.get("temperature", 0.7)
23
  self.max_tokens = self.provider_config.get("max_tokens", 4096)
 
24
 
25
- async def run(self, task_description: str, context: List[Dict[str, Any]], use_tools: bool = False, extra_context: str = "") -> Dict[str, Any]:
26
- messages = self._build_chat_messages(task_description, context, extra_context)
27
-
28
- kwargs = {
29
- "model": self.model,
30
- "messages": messages,
31
- "temperature": self.temperature,
32
- "max_tokens": self.max_tokens
33
- }
34
 
35
- if use_tools:
36
- kwargs["tools"] = tool_registry.get_tool_definitions()
37
- kwargs["tool_choice"] = "auto"
38
-
39
- response = await self.client.chat.completions.create(**kwargs)
40
- message = response.choices[0].message
 
 
 
 
 
 
41
 
42
- # Handle tool calls
43
- if message.tool_calls:
44
- messages.append(message)
45
- await self._append_tool_results(messages, message.tool_calls, tool_registry)
46
-
47
- final_response = await self.client.chat.completions.create(
48
- model=self.model,
49
- messages=messages,
50
- temperature=self.temperature,
51
- max_tokens=self.max_tokens
52
- )
53
- content = final_response.choices[0].message.content
54
- else:
55
- content = message.content or ""
 
 
 
 
 
 
 
 
 
 
 
56
 
57
- return self._result("groq", content)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  from .base import BaseAgent
3
  from typing import Dict, Any, List
4
  import groq
5
+ import json
6
  from services.config import settings, config_service
7
  from tools.registry import tool_registry
8
 
9
  logger = logging.getLogger("uvicorn")
10
 
11
+ GROQ_ROTATION_POOL = [
12
+ "llama-3.3-70b-versatile",
13
+ "openai/gpt-oss-120b",
14
+ "meta-llama/llama-4-scout-17b-16e-instruct",
15
+ "qwen/qwen3-32b",
16
+ "openai/gpt-oss-20b",
17
+ "groq/compound",
18
+ "llama-3.1-8b-instant"
19
+ ]
20
+
21
  class GroqAgent(BaseAgent):
22
  """
23
+ Agent implementation for Groq with automatic model rotation for rate limits.
24
  """
25
  def __init__(self, name: str, role: str, model: str = "llama-3.3-70b-versatile", system_prompt: str = None):
26
+ # Auto-migrate decommissioned models
27
+ if "llama-3.1-70b" in model:
28
+ model = "llama-3.3-70b-versatile"
29
+
30
  super().__init__(name, role, model, system_prompt)
31
 
32
  # Load dynamic config
 
36
  self.client = groq.AsyncGroq(api_key=api_key)
37
  self.temperature = self.provider_config.get("temperature", 0.7)
38
  self.max_tokens = self.provider_config.get("max_tokens", 4096)
39
+ self.reasoning_effort = self.provider_config.get("reasoning_effort", "medium")
40
 
41
+ def _format_context(self, context: List[Dict[str, Any]]) -> str:
42
+ """Extremely aggressive truncation for Groq TPM limits."""
43
+ if not context:
44
+ return "No previous context available."
 
 
 
 
 
45
 
46
+ # Only take the last 3 tasks to save tokens
47
+ recent_context = context[-3:]
48
+
49
+ formatted = "Previous tasks context (EXTREMELY TRUNCATED for Groq):\n"
50
+ for item in recent_context:
51
+ output_raw = json.dumps(item.get('output_data', {}))
52
+ # 800 chars is roughly 200 tokens.
53
+ if len(output_raw) > 800:
54
+ output_raw = output_raw[:800] + "... [TRUNCATED]"
55
+
56
+ formatted += f"- Task: {item.get('title')}\n Output: {output_raw}\n"
57
+ return formatted
58
 
59
+ async def run(self, task_description: str, context: List[Dict[str, Any]], use_tools: bool = False, extra_context: str = "") -> Dict[str, Any]:
60
+ # Very limited semantic context
61
+ if len(extra_context) > 1000:
62
+ extra_context = extra_context[:1000] + "... [TRUNCATED]"
63
+
64
+ try:
65
+ return await self._execute_run(task_description, context, use_tools, extra_context)
66
+ except groq.RateLimitError as e:
67
+ logger.warning(f"Rate limit reached for {self.model} (429). Attempting model rotation...")
68
+
69
+ # Find current model index in pool
70
+ try:
71
+ current_idx = GROQ_ROTATION_POOL.index(self.model)
72
+ except ValueError:
73
+ current_idx = -1
74
+
75
+ # Try the next model in the pool
76
+ next_idx = (current_idx + 1) % len(GROQ_ROTATION_POOL)
77
+ fallback_model = GROQ_ROTATION_POOL[next_idx]
78
+
79
+ logger.info(f"Rotating from {self.model} to {fallback_model}")
80
+ self.model = fallback_model
81
+
82
+ # Retry once with fallback model
83
+ return await self._execute_run(task_description, context, use_tools, extra_context)
84
 
85
+ async def _execute_run(self, task_description: str, context: List[Dict[str, Any]], use_tools: bool = False, extra_context: str = "") -> Dict[str, Any]:
86
+ extra_kwargs = {}
87
+ if "gpt-oss-" in self.model:
88
+ extra_kwargs["reasoning_effort"] = self.reasoning_effort
89
+
90
+ return await self._run_openai_compatible(
91
+ provider="groq",
92
+ create_fn=self.client.chat.completions.create,
93
+ task_description=task_description,
94
+ context=context,
95
+ use_tools=use_tools,
96
+ extra_context=extra_context,
97
+ **extra_kwargs
98
+ )
backend/agents/openai_agent.py CHANGED
@@ -17,38 +17,12 @@ class OpenAIAgent(BaseAgent):
17
  self.max_tokens = self.provider_config.get("max_tokens", 4096)
18
 
19
  async def run(self, task_description: str, context: List[Dict[str, Any]], use_tools: bool = False, extra_context: str = "") -> Dict[str, Any]:
20
- messages = self._build_chat_messages(task_description, context, extra_context)
21
-
22
- request_kwargs = {
23
- "model": self.model,
24
- "messages": messages,
25
- "response_format": {"type": "json_object"},
26
- "temperature": self.temperature,
27
- "max_tokens": self.max_tokens
28
- }
29
- if use_tools:
30
- request_kwargs["tools"] = tool_registry.get_tool_definitions()
31
- request_kwargs["tool_choice"] = "auto"
32
-
33
- response = await self.client.chat.completions.create(**request_kwargs)
34
-
35
- message = response.choices[0].message
36
-
37
- # Handle tool calls
38
- if message.tool_calls:
39
- messages.append(message)
40
- await self._append_tool_results(messages, message.tool_calls, tool_registry)
41
-
42
- # Second call after tool execution
43
- final_response = await self.client.chat.completions.create(
44
- model=self.model,
45
- messages=messages,
46
- response_format={"type": "json_object"},
47
- temperature=self.temperature,
48
- max_tokens=self.max_tokens
49
- )
50
- content = final_response.choices[0].message.content
51
- else:
52
- content = message.content
53
-
54
- return self._result("openai", content or "")
 
17
  self.max_tokens = self.provider_config.get("max_tokens", 4096)
18
 
19
  async def run(self, task_description: str, context: List[Dict[str, Any]], use_tools: bool = False, extra_context: str = "") -> Dict[str, Any]:
20
+ return await self._run_openai_compatible(
21
+ provider="openai",
22
+ create_fn=self.client.chat.completions.create,
23
+ task_description=task_description,
24
+ context=context,
25
+ use_tools=use_tools,
26
+ extra_context=extra_context,
27
+ response_format={"type": "json_object"}
28
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/main.py CHANGED
@@ -27,10 +27,11 @@ app = FastAPI(
27
  )
28
 
29
  # CORS Configuration
30
- allowed_origins = os.getenv("ALLOWED_ORIGINS", "*").split(",")
31
  app.add_middleware(
32
  CORSMiddleware,
33
- allow_origins=allowed_origins,
 
34
  allow_credentials=True,
35
  allow_methods=["*"],
36
  allow_headers=["*"],
 
27
  )
28
 
29
  # CORS Configuration
30
+ allowed_origins = os.getenv("ALLOWED_ORIGINS", "http://localhost:5173,http://localhost:3000,http://127.0.0.1:5173").split(",")
31
  app.add_middleware(
32
  CORSMiddleware,
33
+ allow_origins=allowed_origins if allowed_origins != ["*"] else ["*"],
34
+ allow_origin_regex=os.getenv("ALLOWED_ORIGIN_REGEX"),
35
  allow_credentials=True,
36
  allow_methods=["*"],
37
  allow_headers=["*"],
backend/routers/agent_runner.py CHANGED
@@ -11,14 +11,14 @@ def update_task_status(task_id: str, status: str):
11
  supabase.table("tasks")
12
  .update({"status": status})
13
  .eq("id", task_id)
14
- .select("id,project_id,status")
15
- .single()
16
  .execute()
17
  )
18
  if not result.data:
19
  raise HTTPException(status_code=404, detail="Task not found or status was not updated")
20
 
21
- project_id = result.data.get("project_id")
 
 
22
  if project_id:
23
  task_result = (
24
  supabase.table("tasks")
@@ -32,7 +32,7 @@ def update_task_status(task_id: str, status: str):
32
  elif status != "done":
33
  supabase.table("projects").update({"status": "active"}).eq("id", project_id).execute()
34
 
35
- return result.data
36
 
37
  @router.post("/{task_id}/run")
38
  async def run_task(task_id: str, background_tasks: BackgroundTasks):
@@ -75,3 +75,29 @@ async def approve_task(task_id: str):
75
  async def reject_task(task_id: str):
76
  task = update_task_status(task_id, "todo")
77
  return {"message": "Task rejected", "task": task}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  supabase.table("tasks")
12
  .update({"status": status})
13
  .eq("id", task_id)
 
 
14
  .execute()
15
  )
16
  if not result.data:
17
  raise HTTPException(status_code=404, detail="Task not found or status was not updated")
18
 
19
+ task_data = result.data[0]
20
+
21
+ project_id = task_data.get("project_id")
22
  if project_id:
23
  task_result = (
24
  supabase.table("tasks")
 
32
  elif status != "done":
33
  supabase.table("projects").update({"status": "active"}).eq("id", project_id).execute()
34
 
35
+ return task_data
36
 
37
  @router.post("/{task_id}/run")
38
  async def run_task(task_id: str, background_tasks: BackgroundTasks):
 
75
  async def reject_task(task_id: str):
76
  task = update_task_status(task_id, "todo")
77
  return {"message": "Task rejected", "task": task}
78
+ @router.post("/project/{project_id}/approve-all")
79
+ async def approve_all_tasks(project_id: str):
80
+ """
81
+ Approves all tasks in a project that are awaiting approval.
82
+ """
83
+ # 1. Update tasks
84
+ result = (
85
+ supabase.table("tasks")
86
+ .update({"status": "done"})
87
+ .eq("project_id", project_id)
88
+ .eq("status", "awaiting_approval")
89
+ .execute()
90
+ )
91
+
92
+ # 2. Check if all tasks in project are now done
93
+ task_result = (
94
+ supabase.table("tasks")
95
+ .select("status")
96
+ .eq("project_id", project_id)
97
+ .execute()
98
+ )
99
+ tasks = task_result.data or []
100
+ if tasks and all(task.get("status") == "done" for task in tasks):
101
+ supabase.table("projects").update({"status": "completed"}).eq("id", project_id).execute()
102
+
103
+ return {"message": f"Approved {len(result.data)} tasks", "count": len(result.data)}
backend/routers/orchestrator.py CHANGED
@@ -5,7 +5,7 @@ from pydantic import BaseModel
5
  from io import BytesIO
6
  from reportlab.lib.pagesizes import letter
7
  from reportlab.lib.styles import getSampleStyleSheet
8
- from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
9
  from reportlab.graphics.shapes import Drawing
10
  from reportlab.graphics.charts.barcharts import VerticalBarChart
11
  from reportlab.graphics.charts.piecharts import Pie
@@ -63,18 +63,34 @@ def _report_pdf_bytes(title: str, content: str, charts: dict | None = None) -> b
63
  styles = getSampleStyleSheet()
64
  story = [Paragraph(title, styles["Title"]), Spacer(1, 0.2 * inch)]
65
  if charts:
66
- story.extend([
67
- Paragraph("Project Charts", styles["Heading2"]),
68
- Paragraph("Completion Status", styles["Heading3"]),
69
- _pie_chart(charts.get("status", [])),
70
- Spacer(1, 0.1 * inch),
71
- Paragraph("Task Categories", styles["Heading3"]),
72
- _bar_chart("Task Categories", charts.get("categories", [])),
73
- Spacer(1, 0.1 * inch),
74
- Paragraph("Project Scores", styles["Heading3"]),
75
- _bar_chart("Project Scores", charts.get("scores", [])),
76
- Spacer(1, 0.2 * inch),
77
- ])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
  for raw_line in content.splitlines():
80
  line = raw_line.strip()
 
5
  from io import BytesIO
6
  from reportlab.lib.pagesizes import letter
7
  from reportlab.lib.styles import getSampleStyleSheet
8
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
9
  from reportlab.graphics.shapes import Drawing
10
  from reportlab.graphics.charts.barcharts import VerticalBarChart
11
  from reportlab.graphics.charts.piecharts import Pie
 
63
  styles = getSampleStyleSheet()
64
  story = [Paragraph(title, styles["Title"]), Spacer(1, 0.2 * inch)]
65
  if charts:
66
+ story.append(Paragraph("Project Execution Summary", styles["Heading2"]))
67
+ story.append(Spacer(1, 0.1 * inch))
68
+
69
+ # Summary Table instead of charts
70
+ table_data = [["Metric / Category", "Value"]]
71
+
72
+ # Tasks Status
73
+ status_counts = {row["label"]: row["value"] for row in charts.get("status", [])}
74
+ for label, val in status_counts.items():
75
+ table_data.append([f"Tasks: {label}", str(val)])
76
+
77
+ # Categories
78
+ for cat in charts.get("categories", []):
79
+ table_data.append([f"Type: {cat['label']}", str(cat['value'])])
80
+
81
+ table = Table(table_data, colWidths=[3.5*inch, 1.5*inch])
82
+ table.setStyle(TableStyle([
83
+ ('BACKGROUND', (0,0), (-1,0), colors.HexColor("#6e59ff")),
84
+ ('TEXTCOLOR', (0,0), (-1,0), colors.whitesmoke),
85
+ ('ALIGN', (0,0), (-1,-1), 'LEFT'),
86
+ ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
87
+ ('BOTTOMPADDING', (0,0), (-1,0), 10),
88
+ ('BACKGROUND', (0,1), (-1,-1), colors.HexColor("#f8fafc")),
89
+ ('GRID', (0,0), (-1,-1), 0.5, colors.grey),
90
+ ('FONTSIZE', (0,0), (-1,-1), 9),
91
+ ]))
92
+ story.append(table)
93
+ story.append(Spacer(1, 0.3 * inch))
94
 
95
  for raw_line in content.splitlines():
96
  line = raw_line.strip()
backend/services/agent_runner_service.py CHANGED
@@ -17,13 +17,15 @@ class AgentRunnerService:
17
  start_action: str = "execution_start",
18
  start_content: str | None = None,
19
  complete_action: str = "execution_complete",
20
- complete_content: str = "Agent successfully completed the task and produced output."
 
21
  ) -> tuple[dict, str]:
22
  task_id = task["id"]
23
  project_id = task["project_id"]
24
  run_id = None
25
 
26
- supabase.table("tasks").update({"status": "in_progress"}).eq("id", task_id).execute()
 
27
 
28
  try:
29
  run_res = supabase.table("task_runs").insert({
@@ -51,6 +53,41 @@ class AgentRunnerService:
51
  if include_semantic_context:
52
  extra_context = await semantic_backprop.get_project_context(project_id, task_id)
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  supabase.table("agent_logs").insert({
55
  "task_id": task_id,
56
  "run_id": run_id,
@@ -58,25 +95,45 @@ class AgentRunnerService:
58
  "content": start_content or f"Agent {agent_data['name']} starting task: {task['title']}"
59
  }).execute()
60
 
 
 
61
  result = await agent.run(task.get("description") or task["title"], context, extra_context=extra_context)
 
 
62
  if result.get("status") == "error":
63
  raise RuntimeError(result.get("error") or "Agent returned an error result.")
64
 
65
- supabase.table("tasks").update({
66
- "status": "awaiting_approval",
67
- "output_data": result
68
- }).eq("id", task_id).execute()
69
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  supabase.table("task_runs").update({
71
  "status": "completed",
72
- "finished_at": datetime.now(timezone.utc).isoformat()
 
73
  }).eq("id", run_id).execute()
74
 
 
75
  supabase.table("agent_logs").insert({
76
  "task_id": task_id,
77
  "run_id": run_id,
78
  "action": complete_action,
79
- "content": complete_content
80
  }).execute()
81
 
82
  return result, run_id
@@ -88,10 +145,21 @@ class AgentRunnerService:
88
  "status": "failed",
89
  "finished_at": datetime.now(timezone.utc).isoformat()
90
  }).eq("id", run_id).execute()
91
- supabase.table("tasks").update({
92
- "status": "failed",
93
- "output_data": {"error": str(e)}
94
- }).eq("id", task_id).execute()
 
 
 
 
 
 
 
 
 
 
 
95
  raise e
96
 
97
  @staticmethod
 
17
  start_action: str = "execution_start",
18
  start_content: str | None = None,
19
  complete_action: str = "execution_complete",
20
+ complete_content: str = "Agent successfully completed the task and produced output.",
21
+ update_task: bool = True
22
  ) -> tuple[dict, str]:
23
  task_id = task["id"]
24
  project_id = task["project_id"]
25
  run_id = None
26
 
27
+ if update_task:
28
+ supabase.table("tasks").update({"status": "in_progress"}).eq("id", task_id).execute()
29
 
30
  try:
31
  run_res = supabase.table("task_runs").insert({
 
53
  if include_semantic_context:
54
  extra_context = await semantic_backprop.get_project_context(project_id, task_id)
55
 
56
+ import time
57
+ import hashlib
58
+
59
+ # Simple in-memory cache for the session (could be persistent later)
60
+ if not hasattr(AgentRunnerService, "_task_cache"):
61
+ AgentRunnerService._task_cache = {}
62
+
63
+ # 1. Create a cache key based on task, agent (model + system prompt), and context
64
+ cache_input = f"{task['id']}-{agent_data['model']}-{agent_data.get('system_prompt', '')}-{task.get('description')}-{str(context)}-{extra_context}"
65
+ cache_key = hashlib.md5(cache_input.encode()).hexdigest()
66
+
67
+ # 2. Check Cache
68
+ if cache_key in AgentRunnerService._task_cache:
69
+ logger.info(f"Cache hit for task {task_id}. Skipping LLM call.")
70
+ cached_result = AgentRunnerService._task_cache[cache_key]
71
+
72
+ # Still log the "start" for UI consistency
73
+ agent_name = agent_data.get('name', 'Agent')
74
+ log_msg = start_content or f"Agent {agent_name} resuming task"
75
+ supabase.table("agent_logs").insert({
76
+ "task_id": task_id,
77
+ "run_id": run_id,
78
+ "action": start_action,
79
+ "content": f"[CACHE HIT] {log_msg}"
80
+ }).execute()
81
+
82
+ if update_task:
83
+ supabase.table("tasks").update({
84
+ "status": "awaiting_approval",
85
+ "output_data": cached_result
86
+ }).eq("id", task_id).execute()
87
+
88
+ return cached_result, run_id
89
+
90
+ # 3. Log Start
91
  supabase.table("agent_logs").insert({
92
  "task_id": task_id,
93
  "run_id": run_id,
 
95
  "content": start_content or f"Agent {agent_data['name']} starting task: {task['title']}"
96
  }).execute()
97
 
98
+ # 4. Execute Run with timing
99
+ start_time = time.time()
100
  result = await agent.run(task.get("description") or task["title"], context, extra_context=extra_context)
101
+ duration = time.time() - start_time
102
+
103
  if result.get("status") == "error":
104
  raise RuntimeError(result.get("error") or "Agent returned an error result.")
105
 
106
+ # 5. Security Sanitization (Defense in Depth)
107
+ raw_out = str(result.get("raw_output", ""))
108
+ suspicious_patterns = ["rm -rf", "mkfs", "dd if=", "curl", "wget", "chmod 777", "> /dev/sda"]
109
+ for pattern in suspicious_patterns:
110
+ if pattern in raw_out:
111
+ logger.warning(f"SECURITY: Suspicious pattern '{pattern}' detected in agent output for task {task_id}.")
112
+ result["security_warning"] = f"Output sanitized: suspicious pattern '{pattern}' detected."
113
+ # We don't block yet, but we flag it.
114
+
115
+ # 6. Save to Cache
116
+ AgentRunnerService._task_cache[cache_key] = result
117
+
118
+ if update_task:
119
+ supabase.table("tasks").update({
120
+ "status": "awaiting_approval",
121
+ "output_data": result
122
+ }).eq("id", task_id).execute()
123
+
124
+ # 7. Update Run Status
125
  supabase.table("task_runs").update({
126
  "status": "completed",
127
+ "finished_at": datetime.now(timezone.utc).isoformat(),
128
+ "duration_seconds": round(duration, 2)
129
  }).eq("id", run_id).execute()
130
 
131
+ # 8. Log Completion with Metrics
132
  supabase.table("agent_logs").insert({
133
  "task_id": task_id,
134
  "run_id": run_id,
135
  "action": complete_action,
136
+ "content": f"{complete_content} (Execution time: {duration:.2f}s)"
137
  }).execute()
138
 
139
  return result, run_id
 
145
  "status": "failed",
146
  "finished_at": datetime.now(timezone.utc).isoformat()
147
  }).eq("id", run_id).execute()
148
+
149
+ if update_task:
150
+ supabase.table("tasks").update({
151
+ "status": "failed",
152
+ "output_data": {"error": str(e)}
153
+ }).eq("id", task_id).execute()
154
+
155
+ # LOG ERROR TO AGENT CONSOLE
156
+ supabase.table("agent_logs").insert({
157
+ "task_id": task_id,
158
+ "run_id": run_id,
159
+ "action": "execution_failed",
160
+ "content": f"ERROR: {str(e)}"
161
+ }).execute()
162
+
163
  raise e
164
 
165
  @staticmethod
backend/services/config.py CHANGED
@@ -5,24 +5,26 @@ from supabase import create_client, Client
5
 
6
  class Settings(BaseSettings):
7
  # Supabase
8
- SUPABASE_URL: str = os.getenv("SUPABASE_URL", "")
9
- SUPABASE_SERVICE_ROLE_KEY: str = os.getenv("SUPABASE_SERVICE_ROLE_KEY", "")
10
 
11
  # AI Providers
12
- OPENAI_API_KEY: Optional[str] = os.getenv("OPENAI_API_KEY")
13
- GROQ_API_KEY: Optional[str] = os.getenv("GROQ_API_KEY")
14
- GEMINI_API_KEY: Optional[str] = os.getenv("GEMINI_API_KEY")
15
- ANTHROPIC_API_KEY: Optional[str] = os.getenv("ANTHROPIC_API_KEY")
16
- AMD_API_KEY: Optional[str] = os.getenv("AMD_API_KEY")
17
 
18
  # App Config
19
- TASK_QUEUE_EMBEDDED_WORKER: bool = os.getenv("TASK_QUEUE_EMBEDDED_WORKER", "true").lower() == "true"
20
- OUTPUT_LANGUAGE: str = os.getenv("OUTPUT_LANGUAGE", "en")
21
- PORT: int = int(os.getenv("PORT", 8000))
22
- SENTRY_DSN: Optional[str] = os.getenv("SENTRY_DSN")
23
 
24
- class Config:
25
- env_file = ".env"
 
 
26
 
27
  settings = Settings()
28
 
 
5
 
6
  class Settings(BaseSettings):
7
  # Supabase
8
+ SUPABASE_URL: str = ""
9
+ SUPABASE_SERVICE_ROLE_KEY: str = ""
10
 
11
  # AI Providers
12
+ OPENAI_API_KEY: Optional[str] = None
13
+ GROQ_API_KEY: Optional[str] = None
14
+ GEMINI_API_KEY: Optional[str] = None
15
+ ANTHROPIC_API_KEY: Optional[str] = None
16
+ AMD_API_KEY: Optional[str] = None
17
 
18
  # App Config
19
+ TASK_QUEUE_EMBEDDED_WORKER: bool = True
20
+ OUTPUT_LANGUAGE: str = "en"
21
+ PORT: int = 8000
22
+ SENTRY_DSN: Optional[str] = None
23
 
24
+ model_config = {
25
+ "env_file": ".env",
26
+ "extra": "ignore"
27
+ }
28
 
29
  settings = Settings()
30
 
backend/services/orchestrator_service.py CHANGED
@@ -164,37 +164,86 @@ class OrchestratorService:
164
  try:
165
  # 1. Fetch task and agents
166
  task = supabase.table("tasks").select("*").eq("id", task_id).single().execute().data
167
- agent_a = supabase.table("agents").select("*").eq("id", agent_a_id).single().execute().data
168
- agent_b = supabase.table("agents").select("*").eq("id", agent_b_id).single().execute().data
 
 
 
 
 
 
169
 
170
  # 2. Agent A generates initial response
171
- inst_a = AgentFactory.get_agent(agent_a["api_provider"], agent_a["name"], agent_a["role"], agent_a["model"])
172
- initial_res = await inst_a.run(task["description"], [])
 
 
 
 
 
 
173
 
174
  # 3. Agent B reviews and critiques
175
- inst_b = AgentFactory.get_agent(agent_b["api_provider"], agent_b["name"], agent_b["role"], agent_b["model"])
176
- critique_prompt = f"Review the following output for the task: '{task['description']}'. Provide constructive critique and identify errors.\n\nOutput: {json.dumps(initial_res['data'])}"
177
- critique_res = await inst_b.run(critique_prompt, [])
 
 
 
 
 
 
 
 
 
178
 
179
  # 4. Agent A refines based on critique
180
- refinement_prompt = f"Refine your initial output for the task: '{task['description']}' based on this critique: {json.dumps(critique_res['data'])}"
181
- final_res = await inst_a.run(refinement_prompt, [])
182
 
183
- # 5. Save final result
184
- supabase.table("tasks").update({
185
- "status": "done",
186
- "output_data": {
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  "initial": initial_res["data"],
188
  "critique": critique_res["data"],
189
  "final": final_res["data"]
190
  }
 
 
 
 
 
191
  }).eq("id", task_id).execute()
192
 
193
  logger.info(f"Debate completed for task {task_id}")
194
 
195
  except Exception as e:
196
  logger.error(f"Debate failed: {str(e)}")
197
- supabase.table("tasks").update({"status": "failed"}).eq("id", task_id).execute()
 
 
 
 
 
 
 
 
 
 
198
 
199
  async def run_project(self, project_id: str):
200
  """
@@ -218,8 +267,12 @@ class OrchestratorService:
218
  or []
219
  )
220
 
221
- # Automatic Decomposition: If no tasks exist, try to decompose the project first
222
- if not tasks:
 
 
 
 
223
  logger.info(f"No tasks found for project {project_id}. Triggering auto-decomposition.")
224
  await self.decompose_project(project_id)
225
  # Re-fetch tasks after decomposition
@@ -387,29 +440,68 @@ class OrchestratorService:
387
  if incomplete:
388
  raise ValueError(f"Final report is available after all tasks are approved. Pending tasks: {len(incomplete)}")
389
 
 
390
  report_title = REPORT_VARIANTS[variant]["title"]
391
  lines = [
392
  f"# {report_title}: {project['name']}",
393
  "",
394
- "## Project Brief",
395
- project.get("description") or "No project description provided.",
 
396
  ]
397
 
 
398
  if project.get("context"):
399
- lines.extend(["", "## Context", project["context"]])
400
 
401
- lines.extend(["", "## Approved Work Summary"])
 
 
 
 
402
 
403
  for index, task in enumerate(tasks, start=1):
404
  lines.extend([
405
- "",
406
  f"### {index}. {task['title']}",
407
  task.get("description") or "No task description provided.",
408
  "",
409
- _format_output_for_report(task.get("output_data"))
 
410
  ])
411
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
412
  lines.extend([
 
 
413
  "",
414
  "## Completion Status",
415
  f"All {len(tasks)} tasks are approved. Project status: completed."
@@ -477,38 +569,54 @@ class OrchestratorService:
477
  system_prompt=planner_agent_data.get("system_prompt")
478
  )
479
 
480
- prompt = f"""Decompose the following project into 3-5 clear, actionable tasks.
481
  Project Name: {project['name']}
482
  Description: {project['description']}
483
  Context: {project.get('context', 'None')}
484
 
485
- Return ONLY a valid JSON array of objects.
486
- Each object MUST have exactly these keys: 'title', 'description', and 'priority' (integer 1-5).
487
- IMPORTANT: Do not wrap the list in an object. Return a flat [{{...}}, {{...}}] array.
 
 
 
 
 
 
 
 
 
 
 
488
  """
489
 
490
  try:
491
  result = await planner.run(prompt, [])
492
- # Some cleaning might be needed if agent returns markdown
493
- content = result["data"]
494
- tasks_data = planner._parse_json_output(content) if isinstance(content, str) else content
495
 
496
- # Ensure tasks_data is a list
497
  if isinstance(tasks_data, dict):
498
- # If agent wrapped it in {"tasks": [...]}, extract it
499
  if "tasks" in tasks_data and isinstance(tasks_data["tasks"], list):
500
  tasks_data = tasks_data["tasks"]
501
  else:
502
- # Single task as object, wrap in list
503
  tasks_data = [tasks_data]
504
 
505
  if not isinstance(tasks_data, list):
506
  raise ValueError(f"Agent returned invalid format: {type(tasks_data)}. Expected list or dict.")
507
 
 
 
 
 
 
 
 
 
 
508
  # Insert tasks
509
  from .project_service import project_service
510
- await project_service.add_tasks_to_project(project_id, tasks_data)
511
- logger.info(f"Auto-decomposed project {project_id} into {len(tasks_data)} tasks.")
512
  except Exception as e:
513
  logger.error(f"Project decomposition failed: {e}")
514
 
 
164
  try:
165
  # 1. Fetch task and agents
166
  task = supabase.table("tasks").select("*").eq("id", task_id).single().execute().data
167
+ agent_a_data = supabase.table("agents").select("*").eq("id", agent_a_id).single().execute().data
168
+ agent_b_data = supabase.table("agents").select("*").eq("id", agent_b_id).single().execute().data
169
+
170
+ if not task or not agent_a_data or not agent_b_data:
171
+ raise ValueError("Task or agents not found for debate.")
172
+
173
+ # Update status to in_progress
174
+ supabase.table("tasks").update({"status": "in_progress"}).eq("id", task_id).execute()
175
 
176
  # 2. Agent A generates initial response
177
+ initial_res, _ = await AgentRunnerService.run_agent_task(
178
+ task,
179
+ agent_a_data,
180
+ start_action="debate_initial_start",
181
+ start_content=f"Debate Step 1: {agent_a_data['name']} generating initial proposal.",
182
+ complete_action="debate_initial_complete",
183
+ update_task=False
184
+ )
185
 
186
  # 3. Agent B reviews and critiques
187
+ # We temporarily modify the task description for this run
188
+ task_critique = task.copy()
189
+ task_critique["description"] = f"Review the following output for the task: '{task['description']}'. Provide constructive critique and identify errors.\n\nOutput: {json.dumps(initial_res['data'])}"
190
+
191
+ critique_res, _ = await AgentRunnerService.run_agent_task(
192
+ task_critique,
193
+ agent_b_data,
194
+ start_action="debate_critique_start",
195
+ start_content=f"Debate Step 2: {agent_b_data['name']} critiquing the proposal.",
196
+ complete_action="debate_critique_complete",
197
+ update_task=False
198
+ )
199
 
200
  # 4. Agent A refines based on critique
201
+ task_refinement = task.copy()
202
+ task_refinement["description"] = f"Refine your initial output for the task: '{task['description']}' based on this critique: {json.dumps(critique_res['data'])}"
203
 
204
+ final_res, _ = await AgentRunnerService.run_agent_task(
205
+ task_refinement,
206
+ agent_a_data,
207
+ start_action="debate_refinement_start",
208
+ start_content=f"Debate Step 3: {agent_a_data['name']} refining proposal based on feedback.",
209
+ complete_action="debate_refinement_complete",
210
+ update_task=False
211
+ )
212
+
213
+ # 5. Save consolidated result and mark for approval
214
+ consolidated_output = {
215
+ "agent_name": agent_a_data["name"],
216
+ "provider": agent_a_data["api_provider"],
217
+ "model": agent_a_data["model"],
218
+ "is_debate": True,
219
+ "data": final_res["data"],
220
+ "debate_history": {
221
  "initial": initial_res["data"],
222
  "critique": critique_res["data"],
223
  "final": final_res["data"]
224
  }
225
+ }
226
+
227
+ supabase.table("tasks").update({
228
+ "status": "awaiting_approval",
229
+ "output_data": consolidated_output
230
  }).eq("id", task_id).execute()
231
 
232
  logger.info(f"Debate completed for task {task_id}")
233
 
234
  except Exception as e:
235
  logger.error(f"Debate failed: {str(e)}")
236
+ supabase.table("tasks").update({
237
+ "status": "failed",
238
+ "output_data": {"error": str(e)}
239
+ }).eq("id", task_id).execute()
240
+
241
+ # LOG ERROR TO AGENT CONSOLE
242
+ supabase.table("agent_logs").insert({
243
+ "task_id": task_id,
244
+ "action": "debate_failed",
245
+ "content": f"DEBATE ERROR: {str(e)}"
246
+ }).execute()
247
 
248
  async def run_project(self, project_id: str):
249
  """
 
267
  or []
268
  )
269
 
270
+ # Check if ANY tasks exist for this project (regardless of status) to avoid re-decomposing
271
+ all_tasks_res = supabase.table("tasks").select("id", count="exact").eq("project_id", project_id).limit(1).execute()
272
+ has_any_tasks = all_tasks_res.count > 0 if all_tasks_res.count is not None else len(all_tasks_res.data) > 0
273
+
274
+ # Automatic Decomposition: Only if no tasks exist AT ALL
275
+ if not has_any_tasks:
276
  logger.info(f"No tasks found for project {project_id}. Triggering auto-decomposition.")
277
  await self.decompose_project(project_id)
278
  # Re-fetch tasks after decomposition
 
440
  if incomplete:
441
  raise ValueError(f"Final report is available after all tasks are approved. Pending tasks: {len(incomplete)}")
442
 
443
+ # 0. Header and Description
444
  report_title = REPORT_VARIANTS[variant]["title"]
445
  lines = [
446
  f"# {report_title}: {project['name']}",
447
  "",
448
+ "## Project Overview",
449
+ project.get("description") or "No description provided.",
450
+ ""
451
  ]
452
 
453
+ # Add Context if exists
454
  if project.get("context"):
455
+ lines.extend(["## Context", project["context"], ""])
456
 
457
+ lines.extend(["## Execution Summary", ""])
458
+
459
+ # We will add the tabular summary later in the UI or via charts,
460
+ # but for the text report, we include the approved work summary.
461
+ lines.extend(["## Approved Work Summary", ""])
462
 
463
  for index, task in enumerate(tasks, start=1):
464
  lines.extend([
 
465
  f"### {index}. {task['title']}",
466
  task.get("description") or "No task description provided.",
467
  "",
468
+ _format_output_for_report(task.get("output_data")),
469
+ ""
470
  ])
471
 
472
+ # Final Conclusion Generation
473
+ conclusion = (
474
+ "Based on the approved task outputs, the project has successfully established a foundational framework. "
475
+ "The key findings suggest a viable path forward by focusing on the identified entry wedge and "
476
+ "mitigating primary risks through phased execution."
477
+ )
478
+
479
+ if variant == "full":
480
+ try:
481
+ # Use the 'Brief Writer' or any available agent to summarize a conclusion
482
+ agent_data = self._select_report_agent(project, "brief")
483
+ if agent_data:
484
+ agent = AgentFactory.get_agent(
485
+ provider=agent_data["api_provider"],
486
+ name=agent_data["name"],
487
+ role=agent_data["role"],
488
+ model=agent_data["model"],
489
+ system_prompt="You write a 2-3 sentence strategic conclusion and 3 actionable next steps for a project report."
490
+ )
491
+ report_so_far = "\n".join(lines)
492
+ res = await agent.run(f"Based on this project report, write a final strategic conclusion and 3 next steps:\n\n{report_so_far}", [])
493
+ if res.get("status") != "error":
494
+ data = res.get("data")
495
+ if isinstance(data, str):
496
+ conclusion = data
497
+ elif isinstance(data, dict):
498
+ conclusion = data.get("conclusion") or data.get("content") or str(data)
499
+ except Exception as exc:
500
+ logger.warning(f"Failed to generate dynamic conclusion: {exc}")
501
+
502
  lines.extend([
503
+ "## Strategic Conclusion",
504
+ conclusion,
505
  "",
506
  "## Completion Status",
507
  f"All {len(tasks)} tasks are approved. Project status: completed."
 
569
  system_prompt=planner_agent_data.get("system_prompt")
570
  )
571
 
572
+ prompt = f"""Decompose the following project into 3-5 clear, actionable implementation tasks.
573
  Project Name: {project['name']}
574
  Description: {project['description']}
575
  Context: {project.get('context', 'None')}
576
 
577
+ ### Output Requirements:
578
+ You MUST return a valid JSON array of objects. Each object represents a task.
579
+ Do not include any conversational text, markdown formatting outside of the JSON, or explanations.
580
+
581
+ ### JSON Schema:
582
+ [
583
+ {{
584
+ "title": "string (The name of the task)",
585
+ "description": "string (Detailed instructions for the agent)",
586
+ "priority": "integer (1-5, where 5 is highest priority)"
587
+ }}
588
+ ]
589
+
590
+ IMPORTANT: Return a flat array. Do not wrap it in a parent 'tasks' object.
591
  """
592
 
593
  try:
594
  result = await planner.run(prompt, [])
595
+ tasks_data = result.get("data")
 
 
596
 
597
+ # Handle common LLM wrapping patterns
598
  if isinstance(tasks_data, dict):
 
599
  if "tasks" in tasks_data and isinstance(tasks_data["tasks"], list):
600
  tasks_data = tasks_data["tasks"]
601
  else:
 
602
  tasks_data = [tasks_data]
603
 
604
  if not isinstance(tasks_data, list):
605
  raise ValueError(f"Agent returned invalid format: {type(tasks_data)}. Expected list or dict.")
606
 
607
+ # Filter out invalid tasks
608
+ valid_tasks = [
609
+ t for t in tasks_data
610
+ if isinstance(t, dict) and t.get("title")
611
+ ]
612
+
613
+ if not valid_tasks:
614
+ raise ValueError("No valid tasks extracted from agent output.")
615
+
616
  # Insert tasks
617
  from .project_service import project_service
618
+ await project_service.add_tasks_to_project(project_id, valid_tasks)
619
+ logger.info(f"Auto-decomposed project {project_id} into {len(valid_tasks)} tasks.")
620
  except Exception as e:
621
  logger.error(f"Project decomposition failed: {e}")
622
 
docker-compose.yml ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ backend:
3
+ build:
4
+ context: ./backend
5
+ dockerfile: Dockerfile
6
+ ports:
7
+ - "8000:8000"
8
+ env_file:
9
+ - ./backend/.env
10
+ environment:
11
+ - ALLOWED_ORIGINS=http://localhost:80,http://localhost:5173
12
+ volumes:
13
+ - ./backend/outputs:/app/outputs
14
+ restart: unless-stopped
15
+
16
+ frontend:
17
+ build:
18
+ context: ./frontend
19
+ dockerfile: Dockerfile
20
+ ports:
21
+ - "80:80"
22
+ environment:
23
+ - VITE_API_URL=http://localhost:8000
24
+ depends_on:
25
+ - backend
26
+ restart: unless-stopped
frontend/src/App.tsx CHANGED
@@ -27,6 +27,9 @@ import SettingsView from './components/SettingsView';
27
  import Dashboard from './components/Dashboard';
28
  import ProjectDetail from './components/ProjectDetail';
29
  import AgentsView from './components/AgentsView';
 
 
 
30
 
31
  type AppTab = 'dashboard' | 'project-detail' | 'agents' | 'marketplace' | 'debate' | 'voice' | 'spatial' | 'monitoring' | 'new-project' | 'settings';
32
 
@@ -35,6 +38,12 @@ const App: React.FC = () => {
35
  const [activeTab, setActiveTab] = useState<AppTab>('dashboard');
36
  const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null);
37
  const [isSidebarOpen, setIsSidebarOpen] = useState(() => typeof window === 'undefined' || window.innerWidth >= 900);
 
 
 
 
 
 
38
 
39
  const navigateTo = (tab: AppTab) => {
40
  setActiveTab(tab);
@@ -43,7 +52,7 @@ const App: React.FC = () => {
43
  }
44
  };
45
 
46
- if (loading) return null; // Or a premium loading spinner
47
  if (!session) return <Login />;
48
 
49
  return (
@@ -186,18 +195,8 @@ const App: React.FC = () => {
186
  {activeTab === 'settings' && <SettingsView />}
187
  </section>
188
 
189
- {/* Real-time Console Placeholder */}
190
- <section className="glass-panel app-console">
191
- <div style={{ padding: 'var(--space-sm) var(--space-md)', borderBottom: '1px solid var(--glass-border)', display: 'flex', alignItems: 'center', gap: 'var(--space-sm)' }}>
192
- <Terminal size={16} color="var(--accent)" />
193
- <span style={{ fontSize: '0.8rem', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.1em' }}>Agent Console</span>
194
- </div>
195
- <div style={{ padding: 'var(--space-md)', height: '150px', overflowY: 'auto', fontFamily: 'monospace', fontSize: '0.85rem', color: 'var(--accent)' }}>
196
- <div>[System] Initializing orchestrator...</div>
197
- <div>[Orchestrator] Scanning for pending tasks...</div>
198
- <div>[Agent: Researcher] Starting web search for "Market trends 2026"...</div>
199
- </div>
200
- </section>
201
  </main>
202
  </div>
203
  );
 
27
  import Dashboard from './components/Dashboard';
28
  import ProjectDetail from './components/ProjectDetail';
29
  import AgentsView from './components/AgentsView';
30
+ import AgentConsole from './components/AgentConsole';
31
+ import SplashScreen from './components/SplashScreen';
32
+ import { useEffect } from 'react';
33
 
34
  type AppTab = 'dashboard' | 'project-detail' | 'agents' | 'marketplace' | 'debate' | 'voice' | 'spatial' | 'monitoring' | 'new-project' | 'settings';
35
 
 
38
  const [activeTab, setActiveTab] = useState<AppTab>('dashboard');
39
  const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null);
40
  const [isSidebarOpen, setIsSidebarOpen] = useState(() => typeof window === 'undefined' || window.innerWidth >= 900);
41
+ const [showSplash, setShowSplash] = useState(true);
42
+
43
+ useEffect(() => {
44
+ const timer = setTimeout(() => setShowSplash(false), 2500);
45
+ return () => clearTimeout(timer);
46
+ }, []);
47
 
48
  const navigateTo = (tab: AppTab) => {
49
  setActiveTab(tab);
 
52
  }
53
  };
54
 
55
+ if (loading || showSplash) return <AnimatePresence><SplashScreen /></AnimatePresence>;
56
  if (!session) return <Login />;
57
 
58
  return (
 
195
  {activeTab === 'settings' && <SettingsView />}
196
  </section>
197
 
198
+ {/* Real-time Agent Console */}
199
+ <AgentConsole />
 
 
 
 
 
 
 
 
 
 
200
  </main>
201
  </div>
202
  );
frontend/src/components/AgentConsole.tsx ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState, useRef } from 'react';
2
+ import { Terminal } from 'lucide-react';
3
+ import { supabase } from '../services/supabase';
4
+
5
+ interface LogEntry {
6
+ id: string;
7
+ created_at: string;
8
+ action: string;
9
+ content: string;
10
+ task_id: string | null;
11
+ }
12
+
13
+ const AgentConsole: React.FC = () => {
14
+ const [logs, setLogs] = useState<LogEntry[]>([]);
15
+ const [error, setError] = useState<string | null>(null);
16
+ const scrollRef = useRef<HTMLDivElement>(null);
17
+
18
+ useEffect(() => {
19
+ const fetchLogs = async () => {
20
+ const { data, error: supabaseError } = await supabase
21
+ .from('agent_logs')
22
+ .select('*')
23
+ .order('created_at', { ascending: false })
24
+ .limit(50);
25
+
26
+ if (supabaseError) {
27
+ console.error('Error fetching logs:', supabaseError);
28
+ setError(supabaseError.message);
29
+ return;
30
+ }
31
+
32
+ setError(null);
33
+ if (data) {
34
+ setLogs(data.reverse());
35
+ }
36
+ };
37
+
38
+ fetchLogs();
39
+
40
+ // Fallback polling every 3 seconds
41
+ const pollInterval = setInterval(fetchLogs, 3000);
42
+
43
+ // Set up real-time subscription
44
+ const channel = supabase
45
+ .channel('agent_logs_changes')
46
+ .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'agent_logs' }, (payload) => {
47
+ setLogs(prev => {
48
+ const newLog = payload.new as LogEntry;
49
+ if (prev.some(l => l.id === newLog.id)) return prev;
50
+ return [...prev, newLog].slice(-50);
51
+ });
52
+ })
53
+ .subscribe();
54
+
55
+ return () => {
56
+ clearInterval(pollInterval);
57
+ supabase.removeChannel(channel);
58
+ };
59
+ }, []);
60
+
61
+ useEffect(() => {
62
+ if (scrollRef.current) {
63
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
64
+ }
65
+ }, [logs]);
66
+
67
+ const formatTimestamp = (ts: string) => {
68
+ const date = new Date(ts);
69
+ return date.toLocaleTimeString([], { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
70
+ };
71
+
72
+ return (
73
+ <section className="glass-panel app-console">
74
+ <div style={{ padding: 'var(--space-sm) var(--space-md)', borderBottom: '1px solid var(--glass-border)', display: 'flex', alignItems: 'center', gap: 'var(--space-sm)' }}>
75
+ <Terminal size={16} color="var(--accent)" />
76
+ <span style={{ fontSize: '0.8rem', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.1em' }}>Agent Console</span>
77
+ </div>
78
+ <div
79
+ ref={scrollRef}
80
+ style={{
81
+ padding: 'var(--space-md)',
82
+ height: '150px',
83
+ overflowY: 'auto',
84
+ fontFamily: 'monospace',
85
+ fontSize: '0.85rem',
86
+ color: 'var(--accent)',
87
+ display: 'flex',
88
+ flexDirection: 'column',
89
+ gap: '4px'
90
+ }}
91
+ >
92
+ {error && (
93
+ <div style={{ color: 'var(--danger)', padding: 'var(--space-sm)', border: '1px solid var(--danger)', borderRadius: 'var(--radius-sm)', marginBottom: '8px' }}>
94
+ [ERROR] {error}. This might be due to Supabase RLS policies.
95
+ </div>
96
+ )}
97
+ {logs.length === 0 && !error && <div style={{ color: 'var(--text-dim)' }}>[System] Waiting for logs...</div>}
98
+ {logs.map((log) => (
99
+ <div key={log.id}>
100
+ <span style={{ color: 'var(--text-dim)', marginRight: '8px' }}>[{formatTimestamp(log.created_at)}]</span>
101
+ <span style={{ color: 'var(--info)', marginRight: '8px' }}>[{log.action.toUpperCase()}]</span>
102
+ <span>{log.content}</span>
103
+ </div>
104
+ ))}
105
+ </div>
106
+ </section>
107
+ );
108
+ };
109
+
110
+ export default AgentConsole;
frontend/src/components/Dashboard.tsx CHANGED
@@ -1,5 +1,5 @@
1
  import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
- import { FolderOpen, Play, RefreshCw } from 'lucide-react';
3
  import { motion } from 'framer-motion';
4
  import { supabase } from '../services/supabase';
5
  import { useAuth } from '../context/useAuth';
@@ -72,6 +72,17 @@ const Dashboard: React.FC<DashboardProps> = ({ onNewProject, onOpenProject }) =>
72
  loadDashboard();
73
  }, [loadDashboard]);
74
 
 
 
 
 
 
 
 
 
 
 
 
75
  const taskCounts = useMemo(() => {
76
  return tasks.reduce<Record<string, { done: number; total: number }>>((acc, task) => {
77
  if (!acc[task.project_id]) acc[task.project_id] = { done: 0, total: 0 };
@@ -125,6 +136,7 @@ const Dashboard: React.FC<DashboardProps> = ({ onNewProject, onOpenProject }) =>
125
  tasksDone={counts.done}
126
  tasksTotal={counts.total}
127
  onOpen={() => onOpenProject(project.id)}
 
128
  />
129
  );
130
  })}
@@ -133,26 +145,35 @@ const Dashboard: React.FC<DashboardProps> = ({ onNewProject, onOpenProject }) =>
133
  );
134
  };
135
 
136
- const ProjectCard: React.FC<{ name: string; description: string | null; status: string; tasksDone: number; tasksTotal: number; onOpen: () => void }> = ({
137
  name,
138
  description,
139
  status,
140
  tasksDone,
141
  tasksTotal,
142
- onOpen
 
143
  }) => {
144
  const progress = tasksTotal > 0 ? (tasksDone / tasksTotal) * 100 : 0;
145
 
146
  return (
147
  <motion.div whileHover={{ y: -5 }} className="glass-panel project-card" style={{ padding: 'var(--space-lg)', position: 'relative', overflow: 'hidden' }}>
148
- <div className="project-card-header">
149
- <h3 style={{ fontSize: '1.25rem' }}>{name}</h3>
150
- <StatusBadge status={status} />
 
 
 
 
 
 
 
 
 
 
151
  </div>
152
 
153
- <p style={{ color: 'var(--text-dim)', minHeight: '3rem', marginBottom: 'var(--space-lg)' }}>
154
- {description || 'No description provided.'}
155
- </p>
156
 
157
  <div style={{ marginBottom: 'var(--space-lg)' }}>
158
  <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.85rem', marginBottom: 'var(--space-xs)' }}>
 
1
  import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import { FolderOpen, Play, RefreshCw, Trash2 } from 'lucide-react';
3
  import { motion } from 'framer-motion';
4
  import { supabase } from '../services/supabase';
5
  import { useAuth } from '../context/useAuth';
 
72
  loadDashboard();
73
  }, [loadDashboard]);
74
 
75
+ const handleDeleteProject = async (id: string, name: string) => {
76
+ if (!window.confirm(`Are you sure you want to delete "${name}"? This action cannot be undone.`)) return;
77
+
78
+ const { error: deleteError } = await supabase.from('projects').delete().eq('id', id);
79
+ if (deleteError) {
80
+ setError(`Error deleting project: ${deleteError.message}`);
81
+ } else {
82
+ loadDashboard();
83
+ }
84
+ };
85
+
86
  const taskCounts = useMemo(() => {
87
  return tasks.reduce<Record<string, { done: number; total: number }>>((acc, task) => {
88
  if (!acc[task.project_id]) acc[task.project_id] = { done: 0, total: 0 };
 
136
  tasksDone={counts.done}
137
  tasksTotal={counts.total}
138
  onOpen={() => onOpenProject(project.id)}
139
+ onDelete={() => handleDeleteProject(project.id, project.name)}
140
  />
141
  );
142
  })}
 
145
  );
146
  };
147
 
148
+ const ProjectCard: React.FC<{ name: string; description: string | null; status: string; tasksDone: number; tasksTotal: number; onOpen: () => void; onDelete: () => void }> = ({
149
  name,
150
  description,
151
  status,
152
  tasksDone,
153
  tasksTotal,
154
+ onOpen,
155
+ onDelete
156
  }) => {
157
  const progress = tasksTotal > 0 ? (tasksDone / tasksTotal) * 100 : 0;
158
 
159
  return (
160
  <motion.div whileHover={{ y: -5 }} className="glass-panel project-card" style={{ padding: 'var(--space-lg)', position: 'relative', overflow: 'hidden' }}>
161
+ <div className="project-card-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 'var(--space-md)' }}>
162
+ <h3 style={{ fontSize: '1.25rem', margin: 0, flex: 1, lineHeight: 1.2 }}>{name}</h3>
163
+ <div style={{ display: 'flex', gap: '8px', alignItems: 'center', flexShrink: 0 }}>
164
+ <StatusBadge status={status} />
165
+ <button
166
+ className="btn btn-icon"
167
+ onClick={(e) => { e.stopPropagation(); onDelete(); }}
168
+ style={{ color: 'var(--danger)', opacity: 0.6, padding: '4px' }}
169
+ title="Delete Project"
170
+ >
171
+ <Trash2 size={16} />
172
+ </button>
173
+ </div>
174
  </div>
175
 
176
+ {/* Description removed as requested for a cleaner layout */}
 
 
177
 
178
  <div style={{ marginBottom: 'var(--space-lg)' }}>
179
  <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.85rem', marginBottom: 'var(--space-xs)' }}>
frontend/src/components/DebateView.tsx CHANGED
@@ -13,8 +13,95 @@ interface DebateAgent {
13
  interface DebateTask {
14
  id: string;
15
  title: string;
 
16
  }
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  const DebateView: React.FC = () => {
19
  const [agents, setAgents] = useState<DebateAgent[]>([]);
20
  const [tasks, setTasks] = useState<DebateTask[]>([]);
@@ -23,17 +110,40 @@ const DebateView: React.FC = () => {
23
  const [agentB, setAgentB] = useState('');
24
  const [loading, setLoading] = useState(false);
25
  const [status, setStatus] = useState<string | null>(null);
 
26
 
27
  useEffect(() => {
28
  const fetchData = async () => {
29
  const { data: agentsData } = await supabase.from('agents').select('id,name,model');
30
- const { data: tasksData } = await supabase.from('tasks').select('id,title').eq('status', 'todo');
 
 
31
  if (agentsData) setAgents(agentsData);
32
  if (tasksData) setTasks(tasksData);
33
  };
34
  fetchData();
35
  }, []);
36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  const handleStartDebate = async () => {
38
  if (!selectedTask || !agentA || !agentB) {
39
  alert('Please select a task and two different agents.');
@@ -44,6 +154,7 @@ const DebateView: React.FC = () => {
44
  return;
45
  }
46
 
 
47
  setLoading(true);
48
  setStatus('Initializing debate flow...');
49
 
@@ -60,20 +171,29 @@ const DebateView: React.FC = () => {
60
 
61
  if (response.ok) {
62
  setStatus('Debate started! Monitor the agent console for progress.');
 
63
  } else {
64
  setStatus('Failed to start debate.');
 
65
  }
66
  } catch {
67
  setStatus('Error connecting to backend.');
 
68
  }
69
- setLoading(false);
70
  };
71
 
72
  return (
73
  <motion.div
74
  initial={{ opacity: 0, y: 20 }}
75
  animate={{ opacity: 1, y: 0 }}
76
- className="glass-panel form-panel"
 
 
 
 
 
 
 
77
  >
78
  <div className="panel-heading">
79
  <MessageSquare size={32} color="var(--accent)" />
@@ -92,7 +212,7 @@ const DebateView: React.FC = () => {
92
  style={{ width: '100%', padding: '0.8rem', background: 'rgba(255,255,255,0.05)', border: '1px solid var(--glass-border)', borderRadius: 'var(--radius-md)', color: 'white' }}
93
  >
94
  <option value="">-- Choose a pending task --</option>
95
- {tasks.map(t => <option key={t.id} value={t.id}>{t.title}</option>)}
96
  </select>
97
  </div>
98
 
@@ -135,8 +255,46 @@ const DebateView: React.FC = () => {
135
  style={{ width: '100%', padding: '1rem', marginTop: 'var(--space-md)' }}
136
  >
137
  <Play size={18} fill="white" />
138
- {loading ? 'Processing...' : 'Execute Debate Flow'}
139
  </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  </div>
141
  </motion.div>
142
  );
 
13
  interface DebateTask {
14
  id: string;
15
  title: string;
16
+ status: string;
17
  }
18
 
19
+ const renderContent = (content: any) => {
20
+ if (!content) return null;
21
+ if (typeof content === 'string') return content;
22
+
23
+ if (Array.isArray(content) && content.length > 0 && typeof content[0] === 'object' && !Array.isArray(content[0])) {
24
+ const keys = Object.keys(content[0]);
25
+ const isTableCandidate = content.every(item =>
26
+ item && typeof item === 'object' &&
27
+ Object.keys(item).length === keys.length &&
28
+ keys.every(k => Object.keys(item).includes(k))
29
+ );
30
+
31
+ if (isTableCandidate && keys.length <= 6) {
32
+ return (
33
+ <div style={{ overflowX: 'auto', marginBottom: '16px', borderRadius: 'var(--radius-md)', border: '1px solid rgba(255,255,255,0.05)' }}>
34
+ <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.8rem' }}>
35
+ <thead>
36
+ <tr style={{ background: 'rgba(110, 89, 255, 0.1)', borderBottom: '2px solid rgba(110, 89, 255, 0.2)' }}>
37
+ {keys.map(k => (
38
+ <th key={k} style={{ textAlign: 'left', padding: '12px 8px', color: 'var(--accent)', textTransform: 'uppercase', letterSpacing: '1px', fontWeight: 700 }}>
39
+ {k.replace(/_/g, ' ')}
40
+ </th>
41
+ ))}
42
+ </tr>
43
+ </thead>
44
+ <tbody>
45
+ {content.map((item, i) => (
46
+ <tr key={i} style={{ borderBottom: '1px solid rgba(255,255,255,0.05)', background: i % 2 === 0 ? 'transparent' : 'rgba(255,255,255,0.02)' }}>
47
+ {keys.map(k => (
48
+ <td key={k} style={{ padding: '10px 8px', color: 'rgba(255,255,255,0.9)' }}>
49
+ {typeof item[k] === 'object' ? JSON.stringify(item[k]) : String(item[k])}
50
+ </td>
51
+ ))}
52
+ </tr>
53
+ ))}
54
+ </tbody>
55
+ </table>
56
+ </div>
57
+ );
58
+ }
59
+ }
60
+
61
+ if (Array.isArray(content)) {
62
+ return (
63
+ <ul style={{ paddingLeft: '20px', margin: 0 }}>
64
+ {content.map((item, i) => (
65
+ <li key={i} style={{ marginBottom: '8px' }}>
66
+ {typeof item === 'object' ? renderContent(item) : String(item)}
67
+ </li>
68
+ ))}
69
+ </ul>
70
+ );
71
+ }
72
+
73
+ if (typeof content === 'object') {
74
+ return (
75
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
76
+ {Object.entries(content).map(([key, value]) => (
77
+ <div key={key}>
78
+ <div style={{
79
+ fontWeight: 700,
80
+ color: 'var(--accent)',
81
+ fontSize: '0.7rem',
82
+ textTransform: 'uppercase',
83
+ letterSpacing: '1px',
84
+ marginBottom: '6px',
85
+ opacity: 0.8
86
+ }}>
87
+ {key.replace(/_/g, ' ').replace(/-/g, ' ')}
88
+ </div>
89
+ <div style={{
90
+ paddingLeft: '12px',
91
+ borderLeft: '2px solid rgba(110, 89, 255, 0.3)',
92
+ color: 'rgba(255,255,255,0.9)',
93
+ lineHeight: '1.5'
94
+ }}>
95
+ {typeof value === 'object' ? renderContent(value) : String(value)}
96
+ </div>
97
+ </div>
98
+ ))}
99
+ </div>
100
+ );
101
+ }
102
+ return String(content);
103
+ };
104
+
105
  const DebateView: React.FC = () => {
106
  const [agents, setAgents] = useState<DebateAgent[]>([]);
107
  const [tasks, setTasks] = useState<DebateTask[]>([]);
 
110
  const [agentB, setAgentB] = useState('');
111
  const [loading, setLoading] = useState(false);
112
  const [status, setStatus] = useState<string | null>(null);
113
+ const [debateResult, setDebateResult] = useState<any>(null);
114
 
115
  useEffect(() => {
116
  const fetchData = async () => {
117
  const { data: agentsData } = await supabase.from('agents').select('id,name,model');
118
+ const { data: tasksData } = await supabase.from('tasks')
119
+ .select('id,title,status')
120
+ .in('status', ['todo', 'awaiting_approval']);
121
  if (agentsData) setAgents(agentsData);
122
  if (tasksData) setTasks(tasksData);
123
  };
124
  fetchData();
125
  }, []);
126
 
127
+ useEffect(() => {
128
+ let interval: number;
129
+ if (loading && selectedTask) {
130
+ interval = window.setInterval(async () => {
131
+ const { data } = await supabase.from('tasks').select('status, output_data').eq('id', selectedTask).single();
132
+ if (data && data.status !== 'in_progress') {
133
+ setLoading(false);
134
+ setStatus(data.status === 'awaiting_approval' ? 'Debate completed successfully!' : `Debate finished with status: ${data.status}`);
135
+
136
+ if (data.status === 'awaiting_approval' && data.output_data?.debate_history) {
137
+ setDebateResult(data.output_data.debate_history);
138
+ }
139
+
140
+ window.clearInterval(interval);
141
+ }
142
+ }, 3000);
143
+ }
144
+ return () => window.clearInterval(interval);
145
+ }, [loading, selectedTask]);
146
+
147
  const handleStartDebate = async () => {
148
  if (!selectedTask || !agentA || !agentB) {
149
  alert('Please select a task and two different agents.');
 
154
  return;
155
  }
156
 
157
+ setDebateResult(null);
158
  setLoading(true);
159
  setStatus('Initializing debate flow...');
160
 
 
171
 
172
  if (response.ok) {
173
  setStatus('Debate started! Monitor the agent console for progress.');
174
+ // We keep loading=true, the useEffect will poll until completion
175
  } else {
176
  setStatus('Failed to start debate.');
177
+ setLoading(false);
178
  }
179
  } catch {
180
  setStatus('Error connecting to backend.');
181
+ setLoading(false);
182
  }
 
183
  };
184
 
185
  return (
186
  <motion.div
187
  initial={{ opacity: 0, y: 20 }}
188
  animate={{ opacity: 1, y: 0 }}
189
+ className="glass-panel"
190
+ style={{
191
+ width: '100%',
192
+ maxWidth: debateResult ? '1000px' : '600px',
193
+ margin: '0 auto',
194
+ padding: 'var(--space-xl)',
195
+ transition: 'max-width 0.5s ease-in-out'
196
+ }}
197
  >
198
  <div className="panel-heading">
199
  <MessageSquare size={32} color="var(--accent)" />
 
212
  style={{ width: '100%', padding: '0.8rem', background: 'rgba(255,255,255,0.05)', border: '1px solid var(--glass-border)', borderRadius: 'var(--radius-md)', color: 'white' }}
213
  >
214
  <option value="">-- Choose a pending task --</option>
215
+ {tasks.map(t => <option key={t.id} value={t.id}>{t.title} ({t.status.replace('_', ' ')})</option>)}
216
  </select>
217
  </div>
218
 
 
255
  style={{ width: '100%', padding: '1rem', marginTop: 'var(--space-md)' }}
256
  >
257
  <Play size={18} fill="white" />
258
+ {loading ? 'Processing Debate...' : 'Execute Debate Flow'}
259
  </button>
260
+
261
+ {debateResult && (
262
+ <motion.div
263
+ initial={{ opacity: 0, height: 0 }}
264
+ animate={{ opacity: 1, height: 'auto' }}
265
+ style={{ marginTop: 'var(--space-xl)', borderTop: '1px solid var(--glass-border)', paddingTop: 'var(--space-lg)' }}
266
+ >
267
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: 'var(--space-md)' }}>
268
+ <CheckCircle2 size={20} color="var(--success)" />
269
+ <h3 style={{ fontSize: '1.1rem', margin: 0 }}>Debate Results: Before & After</h3>
270
+ </div>
271
+
272
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--space-lg)' }}>
273
+ <div className="glass-panel" style={{ padding: 'var(--space-md)', background: 'rgba(255,255,255,0.02)' }}>
274
+ <div style={{ fontSize: '0.7rem', color: 'var(--text-dim)', textTransform: 'uppercase', marginBottom: '12px', letterSpacing: '1px' }}>Initial Proposal</div>
275
+ <div style={{ fontSize: '0.9rem', color: 'rgba(255,255,255,0.8)', maxHeight: '500px', overflowY: 'auto' }}>
276
+ {renderContent(debateResult.initial)}
277
+ </div>
278
+ </div>
279
+
280
+ <div className="glass-panel" style={{ padding: 'var(--space-md)', background: 'rgba(110, 89, 255, 0.05)', border: '1px solid rgba(110, 89, 255, 0.2)' }}>
281
+ <div style={{ fontSize: '0.7rem', color: 'var(--accent)', textTransform: 'uppercase', marginBottom: '12px', letterSpacing: '1px' }}>Refined Final Result</div>
282
+ <div style={{ fontSize: '0.9rem', color: 'white', maxHeight: '500px', overflowY: 'auto' }}>
283
+ {renderContent(debateResult.final)}
284
+ </div>
285
+ </div>
286
+ </div>
287
+
288
+ {debateResult.critique && (
289
+ <div style={{ marginTop: 'var(--space-md)', padding: 'var(--space-md)', background: 'rgba(255, 107, 107, 0.05)', borderRadius: 'var(--radius-md)', border: '1px solid rgba(255, 107, 107, 0.1)' }}>
290
+ <div style={{ fontSize: '0.7rem', color: 'var(--danger)', textTransform: 'uppercase', marginBottom: '8px', letterSpacing: '1px' }}>Critique Context</div>
291
+ <div style={{ fontSize: '0.85rem', color: 'var(--text-dim)', fontStyle: 'italic' }}>
292
+ {renderContent(debateResult.critique)}
293
+ </div>
294
+ </div>
295
+ )}
296
+ </motion.div>
297
+ )}
298
  </div>
299
  </motion.div>
300
  );
frontend/src/components/ProjectDetail.tsx CHANGED
@@ -75,8 +75,10 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, onBack }) => {
75
  const [agentId, setAgentId] = useState('');
76
  const [saving, setSaving] = useState(false);
77
  const [orchestrating, setOrchestrating] = useState(false);
 
78
  const [error, setError] = useState<string | null>(null);
79
  const [message, setMessage] = useState<string | null>(null);
 
80
  const [taskActionError, setTaskActionError] = useState<string | null>(null);
81
  const [taskActionPending, setTaskActionPending] = useState(false);
82
  const [finalReport, setFinalReport] = useState<string | null>(null);
@@ -204,13 +206,32 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, onBack }) => {
204
  };
205
 
206
  const runOrchestrator = async () => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  setError(null);
208
  setMessage(null);
209
- setOrchestrating(true);
210
 
211
  try {
212
  const apiUrl = getApiUrl();
213
-
214
  const response = await fetch(`${apiUrl}/orchestrator/projects/${projectId}/run`, {
215
  method: 'POST'
216
  });
@@ -220,13 +241,35 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, onBack }) => {
220
  `Backend returned ${response.status} for POST /orchestrator/projects/${projectId}/run. Stop the stale process on port 8000 and restart backend from D:\\sistemas\\Aubm\\backend.`
221
  );
222
  setMessage('Project orchestrator started.');
223
- window.setTimeout(loadProject, 1200);
 
224
  } catch (exc) {
225
  setError(exc instanceof Error ? exc.message : 'Failed to start orchestrator.');
226
  } finally {
227
- setOrchestrating(false);
 
228
  }
229
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
 
231
  const [selectedTask, setSelectedTask] = useState<Task | null>(null);
232
  const allTasksApproved = tasks.length > 0 && tasks.every((task) => task.status === 'done');
@@ -271,7 +314,15 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, onBack }) => {
271
 
272
  if (typeof output === 'object') {
273
  const outputRecord = output as Record<string, unknown>;
 
 
274
  const primaryOutput = outputRecord.data ?? outputRecord.raw_output ?? outputRecord.final ?? output;
 
 
 
 
 
 
275
  return typeof primaryOutput === 'string' ? primaryOutput : formatHumanReadable(primaryOutput).join('\n');
276
  }
277
 
@@ -438,6 +489,12 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, onBack }) => {
438
  Pessimistic Analysis
439
  </button>
440
  )}
 
 
 
 
 
 
441
  <button className="btn btn-primary" onClick={runOrchestrator} disabled={orchestrating}>
442
  <PlayCircle size={18} />
443
  {orchestrating ? 'Starting...' : 'Run Orchestrator'}
@@ -504,10 +561,33 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, onBack }) => {
504
  <ListTodo size={22} color="var(--accent)" />
505
  <h3>Tasks</h3>
506
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
507
  {tasks.length === 0 && <p style={{ color: 'var(--text-dim)' }}>No tasks yet.</p>}
508
  <div className="task-list">
509
- {tasks.map((task) => (
510
- <div key={task.id} className="task-row">
 
 
 
 
 
 
 
 
 
 
 
511
  <div style={{ flex: 1 }}>
512
  <strong>{task.title}</strong>
513
  <p>{task.description || 'No description provided.'}</p>
@@ -519,7 +599,8 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, onBack }) => {
519
  {task.status === 'awaiting_approval' && (
520
  <button
521
  className="btn btn-glass btn-sm"
522
- onClick={() => {
 
523
  setTaskActionError(null);
524
  setSelectedTask(task);
525
  }}
@@ -543,12 +624,20 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, onBack }) => {
543
  </div>
544
  {taskActionError && <div className="inline-status modal-error">{taskActionError}</div>}
545
  <div className="button-row modal-actions">
546
- <button className="btn btn-primary" onClick={() => approveTask(selectedTask.id)} disabled={taskActionPending}>
547
- {taskActionPending ? 'Saving...' : 'Approve Task'}
548
- </button>
549
- <button className="btn btn-glass" onClick={() => rejectTask(selectedTask.id)} disabled={taskActionPending}>
550
- Reject & Re-run
551
- </button>
 
 
 
 
 
 
 
 
552
  <button className="btn btn-glass" onClick={() => setSelectedTask(null)} disabled={taskActionPending}>
553
  Close
554
  </button>
 
75
  const [agentId, setAgentId] = useState('');
76
  const [saving, setSaving] = useState(false);
77
  const [orchestrating, setOrchestrating] = useState(false);
78
+ const [approvingAll, setApprovingAll] = useState(false);
79
  const [error, setError] = useState<string | null>(null);
80
  const [message, setMessage] = useState<string | null>(null);
81
+ const [filter, setFilter] = useState<string>('all');
82
  const [taskActionError, setTaskActionError] = useState<string | null>(null);
83
  const [taskActionPending, setTaskActionPending] = useState(false);
84
  const [finalReport, setFinalReport] = useState<string | null>(null);
 
206
  };
207
 
208
  const runOrchestrator = async () => {
209
+ if (tasks.length > 0) {
210
+ const confirmReset = window.confirm(
211
+ "This project already has tasks. Re-orchestrating will delete all existing tasks and progress to generate a fresh plan. Do you want to continue?"
212
+ );
213
+ if (!confirmReset) return;
214
+
215
+ // Clear existing tasks for a fresh start
216
+ setOrchestrating(true);
217
+ setError(null);
218
+ setMessage(null);
219
+ try {
220
+ const { error: deleteError } = await supabase.from('tasks').delete().eq('project_id', projectId);
221
+ if (deleteError) throw deleteError;
222
+ } catch (err: any) {
223
+ setError(`Failed to clear existing tasks: ${err.message}`);
224
+ setOrchestrating(false);
225
+ return;
226
+ }
227
+ }
228
+
229
+ setOrchestrating(true);
230
  setError(null);
231
  setMessage(null);
 
232
 
233
  try {
234
  const apiUrl = getApiUrl();
 
235
  const response = await fetch(`${apiUrl}/orchestrator/projects/${projectId}/run`, {
236
  method: 'POST'
237
  });
 
241
  `Backend returned ${response.status} for POST /orchestrator/projects/${projectId}/run. Stop the stale process on port 8000 and restart backend from D:\\sistemas\\Aubm\\backend.`
242
  );
243
  setMessage('Project orchestrator started.');
244
+ // Refresh after a delay to show the new tasks
245
+ window.setTimeout(loadProject, 2000);
246
  } catch (exc) {
247
  setError(exc instanceof Error ? exc.message : 'Failed to start orchestrator.');
248
  } finally {
249
+ // We keep orchestrating=true for a bit longer to allow the backend to finish decomposition
250
+ window.setTimeout(() => setOrchestrating(false), 2000);
251
  }
252
  };
253
+ const handleApproveAll = async () => {
254
+ if (!projectId) return;
255
+ setApprovingAll(true);
256
+ setError(null);
257
+ setMessage(null);
258
+ try {
259
+ const response = await fetch(`${getApiUrl()}/tasks/project/${projectId}/approve-all`, {
260
+ method: 'POST'
261
+ });
262
+ if (response.ok) {
263
+ setMessage('All pending tasks approved!');
264
+ loadProject();
265
+ } else {
266
+ setError('Failed to approve all tasks.');
267
+ }
268
+ } catch {
269
+ setError('Error connecting to backend.');
270
+ }
271
+ setApprovingAll(false);
272
+ };
273
 
274
  const [selectedTask, setSelectedTask] = useState<Task | null>(null);
275
  const allTasksApproved = tasks.length > 0 && tasks.every((task) => task.status === 'done');
 
314
 
315
  if (typeof output === 'object') {
316
  const outputRecord = output as Record<string, unknown>;
317
+
318
+ // Handle unified debate structure or standard agent result
319
  const primaryOutput = outputRecord.data ?? outputRecord.raw_output ?? outputRecord.final ?? output;
320
+
321
+ if (outputRecord.is_debate && outputRecord.debate_history) {
322
+ // We could also show a "Debate Consensus" prefix here
323
+ return typeof primaryOutput === 'string' ? primaryOutput : formatHumanReadable(primaryOutput).join('\n');
324
+ }
325
+
326
  return typeof primaryOutput === 'string' ? primaryOutput : formatHumanReadable(primaryOutput).join('\n');
327
  }
328
 
 
489
  Pessimistic Analysis
490
  </button>
491
  )}
492
+ {tasks.some(t => t.status === 'awaiting_approval') && (
493
+ <button className="btn btn-glass" onClick={handleApproveAll} disabled={approvingAll} style={{ borderColor: 'var(--success)', color: 'var(--success)' }}>
494
+ <CheckCircle2 size={18} />
495
+ {approvingAll ? 'Approving...' : 'Approve All'}
496
+ </button>
497
+ )}
498
  <button className="btn btn-primary" onClick={runOrchestrator} disabled={orchestrating}>
499
  <PlayCircle size={18} />
500
  {orchestrating ? 'Starting...' : 'Run Orchestrator'}
 
561
  <ListTodo size={22} color="var(--accent)" />
562
  <h3>Tasks</h3>
563
  </div>
564
+ <div className="filter-bar" style={{ display: 'flex', gap: '8px', marginBottom: '16px', overflowX: 'auto', paddingBottom: '4px' }}>
565
+ {['all', 'todo', 'in_progress', 'awaiting_approval', 'done', 'failed'].map((f) => (
566
+ <button
567
+ key={f}
568
+ className={`btn ${filter === f ? 'btn-primary' : 'btn-glass'}`}
569
+ onClick={() => setFilter(f)}
570
+ style={{ fontSize: '0.75rem', padding: '4px 12px', textTransform: 'capitalize' }}
571
+ >
572
+ {f.replace('_', ' ')}
573
+ </button>
574
+ ))}
575
+ </div>
576
  {tasks.length === 0 && <p style={{ color: 'var(--text-dim)' }}>No tasks yet.</p>}
577
  <div className="task-list">
578
+ {tasks
579
+ .filter((t) => filter === 'all' || t.status === filter)
580
+ .map((task) => (
581
+ <div
582
+ key={task.id}
583
+ className={`task-row ${task.output_data ? 'clickable' : ''}`}
584
+ onClick={() => {
585
+ if (task.output_data) {
586
+ setTaskActionError(null);
587
+ setSelectedTask(task);
588
+ }
589
+ }}
590
+ >
591
  <div style={{ flex: 1 }}>
592
  <strong>{task.title}</strong>
593
  <p>{task.description || 'No description provided.'}</p>
 
599
  {task.status === 'awaiting_approval' && (
600
  <button
601
  className="btn btn-glass btn-sm"
602
+ onClick={(e) => {
603
+ e.stopPropagation();
604
  setTaskActionError(null);
605
  setSelectedTask(task);
606
  }}
 
624
  </div>
625
  {taskActionError && <div className="inline-status modal-error">{taskActionError}</div>}
626
  <div className="button-row modal-actions">
627
+ {selectedTask.status === 'awaiting_approval' ? (
628
+ <>
629
+ <button className="btn btn-primary" onClick={() => approveTask(selectedTask.id)} disabled={taskActionPending}>
630
+ {taskActionPending ? 'Saving...' : 'Approve Task'}
631
+ </button>
632
+ <button className="btn btn-glass" onClick={() => rejectTask(selectedTask.id)} disabled={taskActionPending}>
633
+ Reject & Re-run
634
+ </button>
635
+ </>
636
+ ) : (
637
+ <div style={{ flex: 1, textAlign: 'left', color: 'var(--text-dim)', fontSize: '0.9rem' }}>
638
+ This task is completed and approved.
639
+ </div>
640
+ )}
641
  <button className="btn btn-glass" onClick={() => setSelectedTask(null)} disabled={taskActionPending}>
642
  Close
643
  </button>
frontend/src/components/SplashScreen.tsx ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { Bot } from 'lucide-react';
4
+
5
+ const SplashScreen: React.FC = () => {
6
+ return (
7
+ <motion.div
8
+ initial={{ opacity: 1 }}
9
+ exit={{ opacity: 0 }}
10
+ transition={{ duration: 0.8 }}
11
+ style={{
12
+ position: 'fixed',
13
+ top: 0,
14
+ left: 0,
15
+ width: '100%',
16
+ height: '100%',
17
+ background: 'var(--bg-dark)',
18
+ display: 'flex',
19
+ flexDirection: 'column',
20
+ alignItems: 'center',
21
+ justifyContent: 'center',
22
+ zIndex: 9999,
23
+ }}
24
+ >
25
+ <motion.div
26
+ initial={{ scale: 0.5, opacity: 0 }}
27
+ animate={{ scale: 1, opacity: 1 }}
28
+ transition={{
29
+ type: "spring",
30
+ stiffness: 260,
31
+ damping: 20,
32
+ delay: 0.2
33
+ }}
34
+ style={{
35
+ width: '120px',
36
+ height: '120px',
37
+ borderRadius: '30px',
38
+ background: 'linear-gradient(135deg, var(--accent) 0%, var(--primary) 100%)',
39
+ display: 'flex',
40
+ alignItems: 'center',
41
+ justifyContent: 'center',
42
+ marginBottom: 'var(--space-xl)',
43
+ boxShadow: '0 20px 40px rgba(110, 89, 255, 0.3)',
44
+ }}
45
+ >
46
+ <Bot size={60} color="white" />
47
+ </motion.div>
48
+
49
+ <motion.h1
50
+ initial={{ y: 20, opacity: 0 }}
51
+ animate={{ y: 0, opacity: 1 }}
52
+ transition={{ delay: 0.4 }}
53
+ style={{
54
+ fontSize: '3rem',
55
+ fontWeight: 'bold',
56
+ background: 'linear-gradient(to right, #fff, #6e59ff)',
57
+ WebkitBackgroundClip: 'text',
58
+ WebkitTextFillColor: 'transparent',
59
+ marginBottom: 'var(--space-md)'
60
+ }}
61
+ >
62
+ Aubm
63
+ </motion.h1>
64
+
65
+ <motion.div
66
+ initial={{ scaleX: 0 }}
67
+ animate={{ scaleX: 1 }}
68
+ transition={{ delay: 0.6, duration: 1.5, ease: "easeInOut" }}
69
+ style={{
70
+ width: '200px',
71
+ height: '4px',
72
+ background: 'rgba(255,255,255,0.1)',
73
+ borderRadius: '2px',
74
+ overflow: 'hidden',
75
+ position: 'relative'
76
+ }}
77
+ >
78
+ <motion.div
79
+ animate={{
80
+ x: ['-100%', '100%']
81
+ }}
82
+ transition={{
83
+ repeat: Infinity,
84
+ duration: 1.5,
85
+ ease: "linear"
86
+ }}
87
+ style={{
88
+ position: 'absolute',
89
+ top: 0,
90
+ left: 0,
91
+ width: '100%',
92
+ height: '100%',
93
+ background: 'var(--accent)',
94
+ boxShadow: '0 0 10px var(--accent)'
95
+ }}
96
+ />
97
+ </motion.div>
98
+
99
+ <motion.p
100
+ initial={{ opacity: 0 }}
101
+ animate={{ opacity: 1 }}
102
+ transition={{ delay: 1.2 }}
103
+ style={{
104
+ marginTop: 'var(--space-lg)',
105
+ color: 'var(--text-dim)',
106
+ fontSize: '0.9rem',
107
+ letterSpacing: '2px',
108
+ textTransform: 'uppercase'
109
+ }}
110
+ >
111
+ Orchestrating Intelligence
112
+ </motion.p>
113
+ </motion.div>
114
+ );
115
+ };
116
+
117
+ export default SplashScreen;
frontend/src/index.css CHANGED
@@ -699,9 +699,11 @@
699
  appearance: auto;
700
  }
701
 
702
- .project-form select option {
703
- color: #111827;
704
- background: #ffffff;
 
 
705
  }
706
 
707
  .project-form input:focus,
@@ -725,14 +727,23 @@
725
  }
726
 
727
  .inline-status {
728
- display: flex;
729
  align-items: center;
730
  gap: var(--space-sm);
731
- padding: var(--space-md);
732
- border: 1px solid var(--glass-border);
 
733
  border-radius: var(--radius-md);
734
- background: rgba(255,255,255,0.04);
735
- color: var(--text-dim);
 
 
 
 
 
 
 
 
736
  }
737
 
738
  .settings-page {
@@ -776,10 +787,6 @@
776
  color-scheme: dark;
777
  }
778
 
779
- .settings-section select option {
780
- color: #111827;
781
- background: #ffffff;
782
- }
783
 
784
  .settings-section-title {
785
  display: flex;
@@ -852,6 +859,17 @@
852
  background: rgba(255,255,255,0.04);
853
  }
854
 
 
 
 
 
 
 
 
 
 
 
 
855
  .task-row p {
856
  color: var(--text-dim);
857
  margin-top: var(--space-xs);
 
699
  appearance: auto;
700
  }
701
 
702
+ .project-form select option,
703
+ .settings-section select option,
704
+ select option {
705
+ color: white;
706
+ background: #111827;
707
  }
708
 
709
  .project-form input:focus,
 
727
  }
728
 
729
  .inline-status {
730
+ display: inline-flex;
731
  align-items: center;
732
  gap: var(--space-sm);
733
+ padding: 0.6rem 1rem;
734
+ background: rgba(110, 89, 255, 0.08);
735
+ border: 1px solid rgba(110, 89, 255, 0.2);
736
  border-radius: var(--radius-md);
737
+ margin-bottom: var(--space-md);
738
+ color: var(--text-h);
739
+ font-size: 0.85rem;
740
+ font-weight: 500;
741
+ animation: slideDown 0.3s ease;
742
+ }
743
+
744
+ @keyframes slideDown {
745
+ from { opacity: 0; transform: translateY(-10px); }
746
+ to { opacity: 1; transform: translateY(0); }
747
  }
748
 
749
  .settings-page {
 
787
  color-scheme: dark;
788
  }
789
 
 
 
 
 
790
 
791
  .settings-section-title {
792
  display: flex;
 
859
  background: rgba(255,255,255,0.04);
860
  }
861
 
862
+ .task-row.clickable {
863
+ cursor: pointer;
864
+ }
865
+
866
+ .task-row.clickable:hover {
867
+ background: rgba(255, 255, 255, 0.07);
868
+ border-color: var(--accent);
869
+ transform: translateX(4px);
870
+ transition: all 0.2s ease;
871
+ }
872
+
873
  .task-row p {
874
  color: var(--text-dim);
875
  margin-top: var(--space-xs);
frontend/src/services/llmConfig.ts CHANGED
@@ -8,7 +8,23 @@ export const providerOptions: Array<{
8
  {
9
  id: 'groq',
10
  label: 'Groq',
11
- models: ['llama-3.3-70b-versatile', 'llama-3.1-70b-versatile', 'mixtral-8x7b-32768']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  },
13
  {
14
  id: 'openai',
 
8
  {
9
  id: 'groq',
10
  label: 'Groq',
11
+ models: [
12
+ 'llama-3.3-70b-versatile',
13
+ 'llama-3.1-8b-instant',
14
+ 'openai/gpt-oss-120b',
15
+ 'openai/gpt-oss-20b',
16
+ 'openai/gpt-oss-safeguard-20b',
17
+ 'meta-llama/llama-4-scout-17b-16e-instruct',
18
+ 'qwen/qwen3-32b',
19
+ 'groq/compound',
20
+ 'groq/compound-mini',
21
+ 'allam-2-7b',
22
+ 'meta-llama/llama-prompt-guard-2-22m',
23
+ 'meta-llama/llama-prompt-guard-2-86m',
24
+ 'canopylabs/orpheus-arabic-saudi',
25
+ 'canopylabs/orpheus-v1-english',
26
+ 'mixtral-8x7b-32768'
27
+ ]
28
  },
29
  {
30
  id: 'openai',
vercel.json ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "version": 2,
3
+ "builds": [
4
+ {
5
+ "src": "frontend/package.json",
6
+ "use": "@vercel/static-build",
7
+ "config": { "distDir": "dist" }
8
+ },
9
+ {
10
+ "src": "backend/api/index.py",
11
+ "use": "@vercel/python"
12
+ }
13
+ ],
14
+ "rewrites": [
15
+ {
16
+ "source": "/api/(.*)",
17
+ "destination": "backend/api/index.py"
18
+ },
19
+ {
20
+ "source": "/(.*)",
21
+ "destination": "frontend/$1"
22
+ }
23
+ ],
24
+ "env": {
25
+ "PYTHON_VERSION": "3.11"
26
+ }
27
+ }