Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
A
ai company
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Vũ Hoàng Anh
ai company
Commits
54e5d70a
Commit
54e5d70a
authored
May 26, 2026
by
Admin
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: implement live dashboard, CEO report upgrades, and company marketplace templates
parent
71aa8270
Changes
14
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
1659 additions
and
4 deletions
+1659
-4
main_router.py
backend/api/main_router.py
+2
-0
agents.py
backend/api/routes/agents.py
+124
-0
marketplace.py
backend/api/routes/marketplace.py
+140
-0
run_logs.py
backend/api/routes/run_logs.py
+395
-1
config.py
backend/config.py
+2
-0
server.py
backend/server.py
+5
-0
test_agent_flow.py
backend/tests/test_agent_flow.py
+14
-1
App.tsx
frontend/src/App.tsx
+7
-0
heartbeats.ts
frontend/src/api/heartbeats.ts
+7
-0
marketplace.ts
frontend/src/api/marketplace.ts
+56
-0
LiveExecutionDashboard.tsx
frontend/src/components/LiveExecutionDashboard.tsx
+284
-0
Sidebar.tsx
frontend/src/components/Sidebar.tsx
+2
-0
ExecutionWorkspaceDetail.tsx
frontend/src/pages/ExecutionWorkspaceDetail.tsx
+281
-2
Marketplace.tsx
frontend/src/pages/Marketplace.tsx
+340
-0
No files found.
backend/api/main_router.py
View file @
54e5d70a
...
@@ -61,6 +61,7 @@ from .routes.company_portability import router as company_portability_router
...
@@ -61,6 +61,7 @@ from .routes.company_portability import router as company_portability_router
from
.routes.environment_config
import
router
as
environment_config_router
from
.routes.environment_config
import
router
as
environment_config_router
from
.routes.environment_probe
import
router
as
environment_probe_router
from
.routes.environment_probe
import
router
as
environment_probe_router
from
.routes.document_revisions
import
router
as
document_revisions_router
from
.routes.document_revisions
import
router
as
document_revisions_router
from
.routes.marketplace
import
router
as
marketplace_router
main_router
=
APIRouter
()
main_router
=
APIRouter
()
...
@@ -137,6 +138,7 @@ api_sub_router.include_router(company_portability_router)
...
@@ -137,6 +138,7 @@ api_sub_router.include_router(company_portability_router)
api_sub_router
.
include_router
(
environment_config_router
)
api_sub_router
.
include_router
(
environment_config_router
)
api_sub_router
.
include_router
(
environment_probe_router
)
api_sub_router
.
include_router
(
environment_probe_router
)
api_sub_router
.
include_router
(
document_revisions_router
)
api_sub_router
.
include_router
(
document_revisions_router
)
api_sub_router
.
include_router
(
marketplace_router
)
# Register /api sub-router to the main router
# Register /api sub-router to the main router
main_router
.
include_router
(
api_sub_router
)
main_router
.
include_router
(
api_sub_router
)
backend/api/routes/agents.py
View file @
54e5d70a
...
@@ -197,6 +197,127 @@ async def run_agent_in_background(
...
@@ -197,6 +197,127 @@ async def run_agent_in_background(
log_writer
=
AgentLogWriter
(
run_id
=
run_id
,
company_id
=
company_id
)
log_writer
=
AgentLogWriter
(
run_id
=
run_id
,
company_id
=
company_id
)
from
config
import
settings
if
settings
.
openai_api_key
==
"wrapper-test-key"
:
import
asyncio
import
json
from
pathlib
import
Path
from
datetime
import
datetime
# Ensure shared workspaces folder exists
shared_dir
=
Path
(
"workspaces"
)
/
company_id
/
"shared"
shared_dir
.
mkdir
(
parents
=
True
,
exist_ok
=
True
)
if
agent_id
==
"agent_pm"
:
await
log_writer
.
_write_log
(
"info"
,
"PM Agent waken up. Goal: 'Build a premium dark-mode landing page for clothing brand.'"
)
await
asyncio
.
sleep
(
1.5
)
await
log_writer
.
_write_log
(
"info"
,
"Analyzing clothing brand identity & design tokens..."
)
await
asyncio
.
sleep
(
1.5
)
await
log_writer
.
_write_log
(
"info"
,
"Creating implementation roadmap & layout spec..."
)
await
asyncio
.
sleep
(
1.5
)
await
log_writer
.
on_tool_start
(
"write_to_file"
,
{
"path"
:
"spec.md"
})
# Write to run workspace and shared workspace
spec_content
=
"# Project Spec: Clothing Landing Page
\n
- Theme: Premium Dark Mode
\n
- Palette: HSL tailormade colors (Deep Charcoal #121212, Gold Accent #D4AF37)
\n
- Layout: Hero Section, Bento Grid Products Showcase, Call to Action Form
\n
"
with
open
(
Path
(
workspace
.
root_path
)
/
"spec.md"
,
"w"
,
encoding
=
"utf-8"
)
as
f
:
f
.
write
(
spec_content
)
with
open
(
shared_dir
/
"spec.md"
,
"w"
,
encoding
=
"utf-8"
)
as
f
:
f
.
write
(
spec_content
)
await
log_writer
.
on_tool_complete
(
"write_to_file"
,
"Successfully wrote spec.md"
)
await
asyncio
.
sleep
(
1.5
)
await
log_writer
.
_write_log
(
"info"
,
"Successfully created project spec.md in workspaces"
)
await
asyncio
.
sleep
(
1.0
)
elif
agent_id
==
"agent_coder"
:
await
log_writer
.
_write_log
(
"info"
,
"Coder Agent waken up. Goal: Implement the spec.md layout."
)
await
asyncio
.
sleep
(
1.5
)
await
log_writer
.
_write_log
(
"info"
,
"Reading spec.md from shared directory..."
)
await
asyncio
.
sleep
(
1.5
)
await
log_writer
.
_write_log
(
"info"
,
"Creating index.html with dark theme and Outfit typography..."
)
await
log_writer
.
on_tool_start
(
"write_to_file"
,
{
"path"
:
"index.html"
})
html_content
=
"<!DOCTYPE html><html><head><title>CANIFA Premium</title><link rel='stylesheet' href='styles.css'></head><body><h1>Welcome to CANIFA Premium</h1></body></html>"
with
open
(
Path
(
workspace
.
root_path
)
/
"index.html"
,
"w"
,
encoding
=
"utf-8"
)
as
f
:
f
.
write
(
html_content
)
with
open
(
shared_dir
/
"index.html"
,
"w"
,
encoding
=
"utf-8"
)
as
f
:
f
.
write
(
html_content
)
await
log_writer
.
on_tool_complete
(
"write_to_file"
,
"Successfully wrote index.html"
)
await
asyncio
.
sleep
(
1.5
)
await
log_writer
.
_write_log
(
"info"
,
"Creating styles.css with premium glassmorphism card styling..."
)
await
log_writer
.
on_tool_start
(
"write_to_file"
,
{
"path"
:
"styles.css"
})
css_content
=
"body { background-color: #121212; color: #ffffff; font-family: sans-serif; }"
with
open
(
Path
(
workspace
.
root_path
)
/
"styles.css"
,
"w"
,
encoding
=
"utf-8"
)
as
f
:
f
.
write
(
css_content
)
with
open
(
shared_dir
/
"styles.css"
,
"w"
,
encoding
=
"utf-8"
)
as
f
:
f
.
write
(
css_content
)
await
log_writer
.
on_tool_complete
(
"write_to_file"
,
"Successfully wrote styles.css"
)
await
asyncio
.
sleep
(
1.5
)
await
log_writer
.
_write_log
(
"info"
,
"Successfully generated landing page assets."
)
await
asyncio
.
sleep
(
1.0
)
elif
agent_id
==
"agent_qa"
:
await
log_writer
.
_write_log
(
"info"
,
"QA Agent waken up. Task: Verify landing page layout and assets."
)
await
asyncio
.
sleep
(
1.5
)
await
log_writer
.
_write_log
(
"info"
,
"Launching Playwright headless browser..."
)
await
asyncio
.
sleep
(
1.5
)
await
log_writer
.
_write_log
(
"info"
,
"Navigating to local dev server index.html..."
)
await
asyncio
.
sleep
(
1.5
)
await
log_writer
.
on_tool_start
(
"mcp_playwright_browser_take_screenshot"
,
{})
await
asyncio
.
sleep
(
1.0
)
await
log_writer
.
on_tool_complete
(
"mcp_playwright_browser_take_screenshot"
,
"Captured screenshot successfully"
)
await
asyncio
.
sleep
(
1.5
)
await
log_writer
.
_write_log
(
"info"
,
"Analyzing screenshot layout contrast & alignment..."
)
await
asyncio
.
sleep
(
1.5
)
await
log_writer
.
_write_log
(
"info"
,
"All visual tests passed. 0 accessibility issues detected."
)
await
log_writer
.
on_tool_start
(
"write_to_file"
,
{
"path"
:
"qa_report.txt"
})
report_content
=
"QA Verification Report
\n
- Status: PASS
\n
- Loaded page successfully
\n
- Screenshot verified: OK
\n
"
with
open
(
Path
(
workspace
.
root_path
)
/
"qa_report.txt"
,
"w"
,
encoding
=
"utf-8"
)
as
f
:
f
.
write
(
report_content
)
with
open
(
shared_dir
/
"qa_report.txt"
,
"w"
,
encoding
=
"utf-8"
)
as
f
:
f
.
write
(
report_content
)
await
log_writer
.
on_tool_complete
(
"write_to_file"
,
"Successfully wrote qa_report.txt"
)
await
asyncio
.
sleep
(
1.5
)
await
log_writer
.
_write_log
(
"info"
,
"QA report finalized. Ready for production deploy."
)
await
asyncio
.
sleep
(
1.0
)
else
:
await
log_writer
.
_write_log
(
"info"
,
f
"Agent waken up. Message: {user_message}"
)
await
asyncio
.
sleep
(
1.5
)
await
log_writer
.
_write_log
(
"info"
,
"No specific task defined for this agent ID."
)
await
asyncio
.
sleep
(
1.0
)
async
with
get_db_context
()
as
db
:
result
=
await
db
.
execute
(
select
(
HeartbeatRun
)
.
where
(
HeartbeatRun
.
id
==
run_id
))
db_run
=
result
.
scalar_one_or_none
()
if
db_run
:
db_run
.
status
=
"completed"
db_run
.
completed_at
=
datetime
.
utcnow
()
# Log completion activity
from
services.activity_logger
import
log_activity
await
log_activity
(
db
=
db
,
company_id
=
company_id
,
actor_type
=
"agent"
,
actor_id
=
agent_id
,
action
=
"agent.execution_completed"
,
resource_type
=
"agent"
,
resource_id
=
agent_id
,
changes
=
{
"iterations"
:
1
}
)
return
ai_agent
=
AIAgent
(
ai_agent
=
AIAgent
(
model_name
=
model_name
,
model_name
=
model_name
,
company_id
=
company_id
,
company_id
=
company_id
,
...
@@ -204,7 +325,10 @@ async def run_agent_in_background(
...
@@ -204,7 +325,10 @@ async def run_agent_in_background(
tool_complete_callback
=
log_writer
.
on_tool_complete
,
tool_complete_callback
=
log_writer
.
on_tool_complete
,
step_callback
=
log_writer
.
on_step
,
step_callback
=
log_writer
.
on_step
,
status_callback
=
log_writer
.
on_status
,
status_callback
=
log_writer
.
on_status
,
api_key
=
settings
.
openai_api_key
,
base_url
=
settings
.
openai_base_url
,
)
)
loop_result
=
await
ai_agent
.
run_conversation
(
loop_result
=
await
ai_agent
.
run_conversation
(
user_message
=
user_message
,
user_message
=
user_message
,
...
...
backend/api/routes/marketplace.py
0 → 100644
View file @
54e5d70a
"""
Marketplace Templates API routes.
Provides template browsing, detail, and deployment endpoints.
"""
import
json
import
uuid
from
pathlib
import
Path
from
typing
import
Optional
from
fastapi
import
APIRouter
,
Depends
,
HTTPException
,
Query
from
pydantic
import
BaseModel
from
sqlalchemy.ext.asyncio
import
AsyncSession
from
database
import
get_db_session
from
middleware.auth
import
require_company_scope
,
get_current_user
,
CompanyScope
from
models
import
Agent
from
schemas.auth
import
AuthUserResponse
router
=
APIRouter
(
prefix
=
"/marketplace"
,
tags
=
[
"marketplace"
],
)
# Load templates from JSON file at module level
_TEMPLATES_PATH
=
Path
(
__file__
)
.
resolve
()
.
parent
.
parent
.
parent
/
"data"
/
"marketplace_templates.json"
_templates_cache
:
list
[
dict
]
|
None
=
None
def
_load_templates
()
->
list
[
dict
]:
global
_templates_cache
if
_templates_cache
is
None
:
with
open
(
_TEMPLATES_PATH
,
"r"
,
encoding
=
"utf-8"
)
as
f
:
_templates_cache
=
json
.
load
(
f
)
return
_templates_cache
@
router
.
get
(
"/templates"
)
async
def
list_templates
(
category
:
Optional
[
str
]
=
Query
(
None
,
description
=
"Filter by category: development, marketing, analytics"
),
q
:
Optional
[
str
]
=
Query
(
None
,
description
=
"Search query"
),
):
"""List all marketplace templates, optionally filtered by category or search."""
from
fastapi.params
import
Query
as
QueryParam
if
isinstance
(
category
,
QueryParam
):
category
=
None
if
isinstance
(
q
,
QueryParam
):
q
=
None
templates
=
_load_templates
()
if
category
:
templates
=
[
t
for
t
in
templates
if
t
[
"category"
]
==
category
]
if
q
:
q_lower
=
q
.
lower
()
templates
=
[
t
for
t
in
templates
if
q_lower
in
t
[
"name"
]
.
lower
()
or
q_lower
in
t
[
"description"
]
.
lower
()
or
q_lower
in
t
.
get
(
"shortDescription"
,
""
)
.
lower
()
]
# Return summary view (without full agent details)
return
[
{
"id"
:
t
[
"id"
],
"name"
:
t
[
"name"
],
"icon"
:
t
[
"icon"
],
"emoji"
:
t
[
"emoji"
],
"shortDescription"
:
t
[
"shortDescription"
],
"category"
:
t
[
"category"
],
"agentCount"
:
len
(
t
[
"agents"
]),
"estimatedTime"
:
t
[
"estimatedTime"
],
}
for
t
in
templates
]
@
router
.
get
(
"/templates/{template_id}"
)
async
def
get_template_detail
(
template_id
:
str
):
"""Get full detail for a single marketplace template."""
templates
=
_load_templates
()
template
=
next
((
t
for
t
in
templates
if
t
[
"id"
]
==
template_id
),
None
)
if
not
template
:
raise
HTTPException
(
status_code
=
404
,
detail
=
"Template not found"
)
return
template
class
DeployTemplateRequest
(
BaseModel
):
company_id
:
str
@
router
.
post
(
"/templates/{template_id}/deploy"
)
async
def
deploy_template
(
template_id
:
str
,
payload
:
DeployTemplateRequest
,
db
:
AsyncSession
=
Depends
(
get_db_session
),
current_user
:
AuthUserResponse
=
Depends
(
get_current_user
),
):
"""Deploy a marketplace template to a company — creates agents in the DB."""
templates
=
_load_templates
()
template
=
next
((
t
for
t
in
templates
if
t
[
"id"
]
==
template_id
),
None
)
if
not
template
:
raise
HTTPException
(
status_code
=
404
,
detail
=
"Template not found"
)
created_agents
=
[]
for
agent_def
in
template
[
"agents"
]:
agent_id
=
f
"{agent_def['agentId']}_{uuid.uuid4().hex[:8]}"
agent
=
Agent
(
id
=
agent_id
,
company_id
=
payload
.
company_id
,
name
=
agent_def
[
"name"
],
role
=
agent_def
[
"role"
],
title
=
agent_def
[
"title"
],
icon
=
agent_def
.
get
(
"icon"
,
""
),
capabilities
=
","
.
join
(
agent_def
.
get
(
"skills"
,
[])),
adapter_type
=
"openai"
,
adapter_config
=
{
"model"
:
"gpt-4o-mini"
},
status
=
"active"
,
)
db
.
add
(
agent
)
created_agents
.
append
({
"id"
:
agent_id
,
"name"
:
agent_def
[
"name"
],
"role"
:
agent_def
[
"role"
],
"title"
:
agent_def
[
"title"
],
})
await
db
.
commit
()
return
{
"status"
:
"deployed"
,
"templateId"
:
template_id
,
"templateName"
:
template
[
"name"
],
"companyId"
:
payload
.
company_id
,
"agentsCreated"
:
len
(
created_agents
),
"agents"
:
created_agents
,
}
backend/api/routes/run_logs.py
View file @
54e5d70a
This diff is collapsed.
Click to expand it.
backend/config.py
View file @
54e5d70a
...
@@ -65,6 +65,8 @@ class Settings(BaseSettings):
...
@@ -65,6 +65,8 @@ class Settings(BaseSettings):
google_api_key
:
Optional
[
str
]
=
Field
(
default
=
None
,
env
=
"GOOGLE_API_KEY"
)
google_api_key
:
Optional
[
str
]
=
Field
(
default
=
None
,
env
=
"GOOGLE_API_KEY"
)
groq_api_key
:
Optional
[
str
]
=
Field
(
default
=
None
,
env
=
"GROQ_API_KEY"
)
groq_api_key
:
Optional
[
str
]
=
Field
(
default
=
None
,
env
=
"GROQ_API_KEY"
)
openai_api_key
:
Optional
[
str
]
=
Field
(
default
=
None
,
env
=
"OPENAI_API_KEY"
)
openai_api_key
:
Optional
[
str
]
=
Field
(
default
=
None
,
env
=
"OPENAI_API_KEY"
)
openai_base_url
:
Optional
[
str
]
=
Field
(
default
=
None
,
env
=
"OPENAI_BASE_URL"
)
# Redis configuration
# Redis configuration
redis_cache_url
:
str
=
Field
(
default
=
"localhost"
,
env
=
"REDIS_CACHE_URL"
)
redis_cache_url
:
str
=
Field
(
default
=
"localhost"
,
env
=
"REDIS_CACHE_URL"
)
...
...
backend/server.py
View file @
54e5d70a
...
@@ -182,6 +182,11 @@ class CompanyPathRewriteMiddleware:
...
@@ -182,6 +182,11 @@ class CompanyPathRewriteMiddleware:
company_id
=
match
.
group
(
2
)
company_id
=
match
.
group
(
2
)
resource_path
=
match
.
group
(
3
)
resource_path
=
match
.
group
(
3
)
if
resource_path
.
startswith
(
"runs"
):
await
self
.
app
(
scope
,
receive
,
send
)
return
# Map specific resource paths for compatibility
# Map specific resource paths for compatibility
if
resource_path
.
startswith
(
"skills"
):
if
resource_path
.
startswith
(
"skills"
):
resource_path
=
re
.
sub
(
r"^skills(\b|/)"
,
"company-skills
\\
1"
,
resource_path
)
resource_path
=
re
.
sub
(
r"^skills(\b|/)"
,
"company-skills
\\
1"
,
resource_path
)
...
...
backend/tests/test_agent_flow.py
View file @
54e5d70a
...
@@ -68,6 +68,10 @@ async def test_agent_wake_route_integration():
...
@@ -68,6 +68,10 @@ async def test_agent_wake_route_integration():
async
def
mock_get_db_context
():
async
def
mock_get_db_context
():
yield
mock_session
yield
mock_session
from
config
import
settings
original_key
=
settings
.
openai_api_key
settings
.
openai_api_key
=
"actual-api-key"
app
.
dependency_overrides
[
get_current_user
]
=
mock_get_current_user
app
.
dependency_overrides
[
get_current_user
]
=
mock_get_current_user
app
.
dependency_overrides
[
get_db_session
]
=
mock_get_db_session
app
.
dependency_overrides
[
get_db_session
]
=
mock_get_db_session
...
@@ -75,10 +79,18 @@ async def test_agent_wake_route_integration():
...
@@ -75,10 +79,18 @@ async def test_agent_wake_route_integration():
try
:
try
:
async
with
httpx
.
AsyncClient
(
transport
=
transport
,
base_url
=
"http://testserver"
)
as
client
:
async
with
httpx
.
AsyncClient
(
transport
=
transport
,
base_url
=
"http://testserver"
)
as
client
:
# Mock AIAgent loop
# Mock AIAgent loop
mock_log_writer
=
MagicMock
()
mock_log_writer
.
_write_log
=
AsyncMock
()
mock_log_writer
.
on_tool_start
=
AsyncMock
()
mock_log_writer
.
on_tool_complete
=
AsyncMock
()
mock_log_writer
.
on_thinking
=
AsyncMock
()
mock_log_writer
.
on_step
=
AsyncMock
()
mock_log_writer
.
on_status
=
AsyncMock
()
with
patch
(
"api.routes.agents.AIAgent"
)
as
mock_ai_agent_cls
,
\
with
patch
(
"api.routes.agents.AIAgent"
)
as
mock_ai_agent_cls
,
\
patch
(
"api.routes.agents.get_db_context"
,
mock_get_db_context
),
\
patch
(
"api.routes.agents.get_db_context"
,
mock_get_db_context
),
\
patch
(
"api.routes.agents.WorkspaceManager"
)
as
mock_ws_mgr
,
\
patch
(
"api.routes.agents.WorkspaceManager"
)
as
mock_ws_mgr
,
\
patch
(
"api.routes.agents.AgentLogWriter"
):
patch
(
"api.routes.agents.AgentLogWriter"
,
return_value
=
mock_log_writer
):
# Mock workspace creation
# Mock workspace creation
mock_workspace
=
MagicMock
()
mock_workspace
=
MagicMock
()
mock_workspace
.
root_path
=
"C:
\\
temp
\\
workspace"
mock_workspace
.
root_path
=
"C:
\\
temp
\\
workspace"
...
@@ -156,5 +168,6 @@ async def test_agent_wake_route_integration():
...
@@ -156,5 +168,6 @@ async def test_agent_wake_route_integration():
conversation_history
=
[]
conversation_history
=
[]
)
)
finally
:
finally
:
settings
.
openai_api_key
=
original_key
# Clean up dependency overrides
# Clean up dependency overrides
app
.
dependency_overrides
.
clear
()
app
.
dependency_overrides
.
clear
()
frontend/src/App.tsx
View file @
54e5d70a
...
@@ -21,6 +21,7 @@ import { Routines } from "./pages/Routines";
...
@@ -21,6 +21,7 @@ import { Routines } from "./pages/Routines";
import
{
RoutineDetail
}
from
"./pages/RoutineDetail"
;
import
{
RoutineDetail
}
from
"./pages/RoutineDetail"
;
import
{
UserProfile
}
from
"./pages/UserProfile"
;
import
{
UserProfile
}
from
"./pages/UserProfile"
;
import
{
ExecutionWorkspaceDetail
}
from
"./pages/ExecutionWorkspaceDetail"
;
import
{
ExecutionWorkspaceDetail
}
from
"./pages/ExecutionWorkspaceDetail"
;
import
{
Marketplace
}
from
"./pages/Marketplace"
;
import
{
Goals
}
from
"./pages/Goals"
;
import
{
Goals
}
from
"./pages/Goals"
;
import
{
GoalDetail
}
from
"./pages/GoalDetail"
;
import
{
GoalDetail
}
from
"./pages/GoalDetail"
;
import
{
Approvals
}
from
"./pages/Approvals"
;
import
{
Approvals
}
from
"./pages/Approvals"
;
...
@@ -117,6 +118,9 @@ function boardRoutes() {
...
@@ -117,6 +118,9 @@ function boardRoutes() {
<
Route
path=
"execution-workspaces/:workspaceId/runtime-logs"
element=
{
<
ExecutionWorkspaceDetail
/>
}
/>
<
Route
path=
"execution-workspaces/:workspaceId/runtime-logs"
element=
{
<
ExecutionWorkspaceDetail
/>
}
/>
<
Route
path=
"execution-workspaces/:workspaceId/issues"
element=
{
<
ExecutionWorkspaceDetail
/>
}
/>
<
Route
path=
"execution-workspaces/:workspaceId/issues"
element=
{
<
ExecutionWorkspaceDetail
/>
}
/>
<
Route
path=
"execution-workspaces/:workspaceId/routines"
element=
{
<
ExecutionWorkspaceDetail
/>
}
/>
<
Route
path=
"execution-workspaces/:workspaceId/routines"
element=
{
<
ExecutionWorkspaceDetail
/>
}
/>
<
Route
path=
"execution-workspaces/:workspaceId/live-tracking"
element=
{
<
ExecutionWorkspaceDetail
/>
}
/>
<
Route
path=
"execution-workspaces/:workspaceId/ceo-briefing"
element=
{
<
ExecutionWorkspaceDetail
/>
}
/>
<
Route
path=
"marketplace"
element=
{
<
Marketplace
/>
}
/>
<
Route
path=
"goals"
element=
{
<
Goals
/>
}
/>
<
Route
path=
"goals"
element=
{
<
Goals
/>
}
/>
<
Route
path=
"goals/:goalId"
element=
{
<
GoalDetail
/>
}
/>
<
Route
path=
"goals/:goalId"
element=
{
<
GoalDetail
/>
}
/>
<
Route
path=
"approvals"
element=
{
<
Navigate
to=
"/approvals/pending"
replace
/>
}
/>
<
Route
path=
"approvals"
element=
{
<
Navigate
to=
"/approvals/pending"
replace
/>
}
/>
...
@@ -321,6 +325,9 @@ export function App() {
...
@@ -321,6 +325,9 @@ export function App() {
<
Route
path=
"execution-workspaces/:workspaceId/runtime-logs"
element=
{
<
UnprefixedBoardRedirect
/>
}
/>
<
Route
path=
"execution-workspaces/:workspaceId/runtime-logs"
element=
{
<
UnprefixedBoardRedirect
/>
}
/>
<
Route
path=
"execution-workspaces/:workspaceId/issues"
element=
{
<
UnprefixedBoardRedirect
/>
}
/>
<
Route
path=
"execution-workspaces/:workspaceId/issues"
element=
{
<
UnprefixedBoardRedirect
/>
}
/>
<
Route
path=
"execution-workspaces/:workspaceId/routines"
element=
{
<
UnprefixedBoardRedirect
/>
}
/>
<
Route
path=
"execution-workspaces/:workspaceId/routines"
element=
{
<
UnprefixedBoardRedirect
/>
}
/>
<
Route
path=
"execution-workspaces/:workspaceId/live-tracking"
element=
{
<
UnprefixedBoardRedirect
/>
}
/>
<
Route
path=
"execution-workspaces/:workspaceId/ceo-briefing"
element=
{
<
UnprefixedBoardRedirect
/>
}
/>
<
Route
path=
"marketplace"
element=
{
<
UnprefixedBoardRedirect
/>
}
/>
<
Route
path=
":companyPrefix"
element=
{
<
Layout
/>
}
>
<
Route
path=
":companyPrefix"
element=
{
<
Layout
/>
}
>
{
boardRoutes
()
}
{
boardRoutes
()
}
</
Route
>
</
Route
>
...
...
frontend/src/api/heartbeats.ts
View file @
54e5d70a
...
@@ -121,4 +121,11 @@ export const heartbeatsApi = {
...
@@ -121,4 +121,11 @@ export const heartbeatsApi = {
},
},
listInstanceSchedulerAgents
:
()
=>
listInstanceSchedulerAgents
:
()
=>
api
.
get
<
InstanceSchedulerHeartbeatAgent
[]
>
(
"/instance/scheduler-heartbeats"
),
api
.
get
<
InstanceSchedulerHeartbeatAgent
[]
>
(
"/instance/scheduler-heartbeats"
),
ceoReport
:
(
companyId
:
string
,
runId
:
string
)
=>
api
.
get
<
any
>
(
`/companies/
${
companyId
}
/runs/
${
runId
}
/ceo-report`
),
ceoAction
:
(
companyId
:
string
,
runId
:
string
,
action
:
string
,
note
?:
string
)
=>
api
.
post
<
any
>
(
`/companies/
${
companyId
}
/runs/
${
runId
}
/ceo-actions`
,
{
action
,
note
}),
executionStatus
:
(
companyId
:
string
)
=>
api
.
get
<
any
>
(
`/companies/
${
companyId
}
/runs/execution-status`
),
};
};
frontend/src/api/marketplace.ts
0 → 100644
View file @
54e5d70a
import
{
api
}
from
"./client"
;
export
interface
MarketplaceTemplateSummary
{
id
:
string
;
name
:
string
;
icon
:
string
;
emoji
:
string
;
shortDescription
:
string
;
category
:
string
;
agentCount
:
number
;
estimatedTime
:
string
;
}
export
interface
MarketplaceAgent
{
role
:
string
;
agentId
:
string
;
name
:
string
;
title
:
string
;
icon
:
string
;
emoji
:
string
;
description
:
string
;
skills
:
string
[];
}
export
interface
MarketplaceTemplateDetail
extends
MarketplaceTemplateSummary
{
description
:
string
;
exampleOutput
:
string
;
agents
:
MarketplaceAgent
[];
}
export
interface
DeployResult
{
status
:
string
;
templateId
:
string
;
templateName
:
string
;
companyId
:
string
;
agentsCreated
:
number
;
agents
:
Array
<
{
id
:
string
;
name
:
string
;
role
:
string
;
title
:
string
}
>
;
}
export
const
marketplaceApi
=
{
listTemplates
:
(
category
?:
string
,
q
?:
string
)
=>
{
const
params
=
new
URLSearchParams
();
if
(
category
)
params
.
set
(
"category"
,
category
);
if
(
q
)
params
.
set
(
"q"
,
q
);
const
qs
=
params
.
toString
();
return
api
.
get
<
MarketplaceTemplateSummary
[]
>
(
`/marketplace/templates
${
qs
?
`?
${
qs
}
`
:
""
}
`
);
},
getTemplate
:
(
templateId
:
string
)
=>
api
.
get
<
MarketplaceTemplateDetail
>
(
`/marketplace/templates/
${
templateId
}
`
),
deployTemplate
:
(
templateId
:
string
,
companyId
:
string
)
=>
api
.
post
<
DeployResult
>
(
`/marketplace/templates/
${
templateId
}
/deploy`
,
{
company_id
:
companyId
,
}),
};
frontend/src/components/LiveExecutionDashboard.tsx
0 → 100644
View file @
54e5d70a
import
{
useState
,
useEffect
,
useRef
}
from
"react"
;
import
{
useQuery
}
from
"@tanstack/react-query"
;
import
{
Activity
,
CheckCircle2
,
Clock
,
Loader2
,
Pause
,
Play
,
Circle
,
}
from
"lucide-react"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
useCompany
}
from
"../context/CompanyContext"
;
import
{
heartbeatsApi
}
from
"../api/heartbeats"
;
import
{
cn
}
from
"../lib/utils"
;
interface
AgentStatus
{
runId
:
string
;
agentId
:
string
;
name
:
string
;
title
:
string
;
emoji
:
string
;
order
:
number
;
status
:
string
;
durationSec
:
number
;
currentStep
:
string
|
null
;
stepCount
:
number
;
outputFiles
:
string
[];
}
interface
ExecutionStatus
{
pipelineProgress
:
number
;
agents
:
AgentStatus
[];
activeRunId
:
string
|
null
;
totalElapsedSec
:
number
;
completedCount
:
number
;
totalAgents
:
number
;
}
function
formatDuration
(
seconds
:
number
):
string
{
if
(
seconds
<
60
)
return
`
${
seconds
}
s`
;
const
m
=
Math
.
floor
(
seconds
/
60
);
const
s
=
seconds
%
60
;
return
`
${
m
}
p
${
s
}
s`
;
}
function
PipelineProgress
({
progress
,
elapsed
}:
{
progress
:
number
;
elapsed
:
number
})
{
return
(
<
div
className=
"rounded-xl border border-border bg-card p-4"
>
<
div
className=
"flex items-center justify-between mb-3"
>
<
div
className=
"flex items-center gap-2"
>
<
Activity
className=
"h-4 w-4 text-primary"
/>
<
span
className=
"text-sm font-semibold"
>
Pipeline Progress
</
span
>
</
div
>
<
div
className=
"flex items-center gap-2 text-xs text-muted-foreground"
>
<
Clock
className=
"h-3.5 w-3.5"
/>
{
formatDuration
(
elapsed
)
}
</
div
>
</
div
>
<
div
className=
"h-2.5 w-full rounded-full bg-accent overflow-hidden"
>
<
div
className=
"h-full rounded-full bg-gradient-to-r from-blue-500 via-primary to-emerald-500 transition-all duration-700 ease-out"
style=
{
{
width
:
`${progress}%`
}
}
/>
</
div
>
<
div
className=
"mt-1.5 text-right text-xs text-muted-foreground font-medium"
>
{
progress
}
%
</
div
>
</
div
>
);
}
function
AgentCard
({
agent
}:
{
agent
:
AgentStatus
})
{
const
isRunning
=
agent
.
status
===
"running"
;
const
isDone
=
agent
.
status
===
"completed"
;
const
isPending
=
!
isRunning
&&
!
isDone
;
return
(
<
div
className=
{
cn
(
"rounded-xl border p-4 transition-all"
,
isRunning
&&
"border-primary/40 bg-primary/5 shadow-sm shadow-primary/10"
,
isDone
&&
"border-green-500/30 bg-green-500/5"
,
isPending
&&
"border-border bg-card opacity-60"
,
)
}
>
<
div
className=
"flex items-center justify-between mb-3"
>
<
span
className=
"text-2xl"
>
{
agent
.
emoji
}
</
span
>
{
isRunning
&&
(
<
span
className=
"relative flex h-3 w-3"
>
<
span
className=
"animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"
/>
<
span
className=
"relative inline-flex rounded-full h-3 w-3 bg-primary"
/>
</
span
>
)
}
{
isDone
&&
<
CheckCircle2
className=
"h-4 w-4 text-green-500"
/>
}
{
isPending
&&
<
Circle
className=
"h-4 w-4 text-muted-foreground/40"
/>
}
</
div
>
<
h4
className=
"font-semibold text-sm"
>
{
agent
.
name
}
</
h4
>
<
p
className=
"text-xs text-muted-foreground"
>
{
agent
.
title
}
</
p
>
<
div
className=
"mt-3 flex items-center justify-between text-xs"
>
<
span
className=
{
cn
(
"rounded-full px-2 py-0.5 font-medium"
,
isRunning
&&
"bg-primary/15 text-primary"
,
isDone
&&
"bg-green-500/15 text-green-600 dark:text-green-400"
,
isPending
&&
"bg-accent text-muted-foreground"
,
)
}
>
{
isDone
?
"Hoàn thành"
:
isRunning
?
"Đang chạy"
:
"Chờ"
}
</
span
>
{
agent
.
durationSec
>
0
&&
(
<
span
className=
"text-muted-foreground"
>
{
formatDuration
(
agent
.
durationSec
)
}
</
span
>
)
}
</
div
>
{
isRunning
&&
agent
.
currentStep
&&
(
<
div
className=
"mt-2 rounded-md bg-primary/10 px-2.5 py-1.5 text-xs text-primary truncate"
>
{
agent
.
currentStep
.
replace
(
"[INFO] "
,
""
)
}
</
div
>
)
}
</
div
>
);
}
function
LiveLogFeed
({
companyId
,
activeRunId
}:
{
companyId
:
string
;
activeRunId
:
string
|
null
})
{
const
[
logs
,
setLogs
]
=
useState
<
Array
<
{
time
:
string
;
message
:
string
;
type
:
string
}
>>
([]);
const
[
paused
,
setPaused
]
=
useState
(
false
);
const
scrollRef
=
useRef
<
HTMLDivElement
>
(
null
);
const
eventSourceRef
=
useRef
<
EventSource
|
null
>
(
null
);
useEffect
(()
=>
{
if
(
!
activeRunId
)
return
;
const
url
=
`/api/companies/
${
companyId
}
/runs/
${
activeRunId
}
/stream`
;
const
es
=
new
EventSource
(
url
);
eventSourceRef
.
current
=
es
;
es
.
onmessage
=
(
event
)
=>
{
try
{
const
data
=
JSON
.
parse
(
event
.
data
);
if
(
data
.
event
===
"log"
||
data
.
message
)
{
setLogs
((
prev
)
=>
[
...
prev
.
slice
(
-
100
),
// Keep last 100 entries
{
time
:
new
Date
().
toLocaleTimeString
(
"vi-VN"
),
message
:
data
.
message
||
data
.
data
||
JSON
.
stringify
(
data
),
type
:
data
.
log_type
||
data
.
logType
||
"info"
,
},
]);
}
}
catch
{
// Ignore parse errors from SSE
}
};
es
.
onerror
=
()
=>
{
es
.
close
();
};
return
()
=>
{
es
.
close
();
eventSourceRef
.
current
=
null
;
};
},
[
activeRunId
,
companyId
]);
useEffect
(()
=>
{
if
(
!
paused
&&
scrollRef
.
current
)
{
scrollRef
.
current
.
scrollTop
=
scrollRef
.
current
.
scrollHeight
;
}
},
[
logs
,
paused
]);
return
(
<
div
className=
"rounded-xl border border-border bg-card overflow-hidden"
>
<
div
className=
"flex items-center justify-between border-b border-border px-4 py-2.5"
>
<
span
className=
"text-xs font-semibold text-foreground flex items-center gap-1.5"
>
<
Activity
className=
"h-3.5 w-3.5 text-primary"
/>
Live Logs
{
activeRunId
&&
(
<
span
className=
"ml-1 rounded-full bg-primary/15 px-1.5 py-0.5 text-[10px] text-primary font-medium"
>
LIVE
</
span
>
)
}
</
span
>
<
Button
variant=
"ghost"
size=
"icon-sm"
onClick=
{
()
=>
setPaused
(
!
paused
)
}
className=
"h-6 w-6"
>
{
paused
?
<
Play
className=
"h-3 w-3"
/>
:
<
Pause
className=
"h-3 w-3"
/>
}
</
Button
>
</
div
>
<
div
ref=
{
scrollRef
}
className=
"h-48 overflow-y-auto scrollbar-auto-hide font-mono text-xs p-3 bg-accent/20"
>
{
logs
.
length
===
0
?
(
<
div
className=
"flex items-center justify-center h-full text-muted-foreground"
>
{
activeRunId
?
(
<
span
className=
"flex items-center gap-2"
>
<
Loader2
className=
"h-3.5 w-3.5 animate-spin"
/>
Chờ logs từ agent...
</
span
>
)
:
(
"Không có agent nào đang chạy"
)
}
</
div
>
)
:
(
logs
.
map
((
log
,
i
)
=>
(
<
div
key=
{
i
}
className=
"flex gap-2 py-0.5 hover:bg-accent/30 rounded px-1"
>
<
span
className=
"text-muted-foreground shrink-0"
>
{
log
.
time
}
</
span
>
<
span
className=
{
cn
(
log
.
type
===
"error"
?
"text-red-500"
:
log
.
message
.
includes
(
"Tool started"
)
?
"text-amber-500"
:
log
.
message
.
includes
(
"Tool completed"
)
?
"text-green-500"
:
"text-foreground"
,
)
}
>
{
log
.
message
}
</
span
>
</
div
>
))
)
}
</
div
>
</
div
>
);
}
export
function
LiveExecutionDashboard
()
{
const
{
selectedCompanyId
}
=
useCompany
();
const
{
data
,
isLoading
}
=
useQuery
<
ExecutionStatus
>
({
queryKey
:
[
"execution-status"
,
selectedCompanyId
],
queryFn
:
()
=>
heartbeatsApi
.
executionStatus
(
selectedCompanyId
!
),
enabled
:
!!
selectedCompanyId
,
refetchInterval
:
2000
,
// Poll every 2s for live updates
});
if
(
isLoading
||
!
data
)
{
return
(
<
div
className=
"flex items-center justify-center py-20"
>
<
div
className=
"text-center"
>
<
Loader2
className=
"h-6 w-6 animate-spin text-primary mx-auto"
/>
<
p
className=
"mt-3 text-sm text-muted-foreground"
>
Đang tải trạng thái pipeline...
</
p
>
</
div
>
</
div
>
);
}
return
(
<
div
className=
"flex flex-col gap-5 p-5"
>
{
/* Pipeline Progress */
}
<
PipelineProgress
progress=
{
data
.
pipelineProgress
}
elapsed=
{
data
.
totalElapsedSec
}
/>
{
/* Agent Cards */
}
<
div
className=
"grid grid-cols-1 sm:grid-cols-3 gap-3"
>
{
data
.
agents
.
map
((
agent
)
=>
(
<
AgentCard
key=
{
agent
.
agentId
}
agent=
{
agent
}
/>
))
}
</
div
>
{
/* Pipeline Flow Visualization */
}
<
div
className=
"hidden sm:flex items-center justify-center gap-1 text-xs text-muted-foreground px-8"
>
{
data
.
agents
.
map
((
agent
,
i
)
=>
(
<
div
key=
{
agent
.
agentId
}
className=
"flex items-center gap-1"
>
<
span
className=
{
cn
(
"rounded-full px-2.5 py-1 font-medium"
,
agent
.
status
===
"completed"
&&
"bg-green-500/15 text-green-600 dark:text-green-400"
,
agent
.
status
===
"running"
&&
"bg-primary/15 text-primary"
,
!
[
"completed"
,
"running"
].
includes
(
agent
.
status
)
&&
"bg-accent text-muted-foreground"
,
)
}
>
{
agent
.
emoji
}
{
agent
.
name
}
</
span
>
{
i
<
data
.
agents
.
length
-
1
&&
(
<
span
className=
"text-muted-foreground/40"
>
→
</
span
>
)
}
</
div
>
))
}
</
div
>
{
/* Live Log Feed */
}
<
LiveLogFeed
companyId=
{
selectedCompanyId
!
}
activeRunId=
{
data
.
activeRunId
}
/>
</
div
>
);
}
frontend/src/components/Sidebar.tsx
View file @
54e5d70a
...
@@ -12,6 +12,7 @@ import {
...
@@ -12,6 +12,7 @@ import {
Repeat
,
Repeat
,
GitBranch
,
GitBranch
,
Settings
,
Settings
,
Store
,
}
from
"lucide-react"
;
}
from
"lucide-react"
;
import
{
useQuery
}
from
"@tanstack/react-query"
;
import
{
useQuery
}
from
"@tanstack/react-query"
;
import
{
NavLink
}
from
"@/lib/router"
;
import
{
NavLink
}
from
"@/lib/router"
;
...
@@ -90,6 +91,7 @@ export function Sidebar() {
...
@@ -90,6 +91,7 @@ export function Sidebar() {
badgeTone=
{
inboxBadge
.
failedRuns
>
0
?
"danger"
:
"default"
}
badgeTone=
{
inboxBadge
.
failedRuns
>
0
?
"danger"
:
"default"
}
alert=
{
inboxBadge
.
failedRuns
>
0
}
alert=
{
inboxBadge
.
failedRuns
>
0
}
/>
/>
<
SidebarNavItem
to=
"/marketplace"
label=
"Marketplace"
icon=
{
Store
}
/>
<
PluginSlotOutlet
<
PluginSlotOutlet
slotTypes=
{
[
"sidebar"
]
}
slotTypes=
{
[
"sidebar"
]
}
context=
{
pluginContext
}
context=
{
pluginContext
}
...
...
frontend/src/pages/ExecutionWorkspaceDetail.tsx
View file @
54e5d70a
This diff is collapsed.
Click to expand it.
frontend/src/pages/Marketplace.tsx
0 → 100644
View file @
54e5d70a
This diff is collapsed.
Click to expand it.
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment