Merge PR #1616: share manual Pixel Office tasks across Web, CLI and agent
This commit is contained in:
commit
1387fc1d0e
42 files changed
+3745
-83
No files matched your search
@@ -177,3 +177,6 @@ agent/tests/artifacts/
|
|||||||
.idea/*
|
.idea/*
|
||||||
|
|
||||||
/custom-llm-providers.json
|
/custom-llm-providers.json
|
||||||
|
|
||||||
|
/pixel-office.sqlite3
|
||||||
|
/pixel-office.sqlite3-*
|
||||||
@@ -403,45 +403,14 @@ vibe-trading --upload trades_export.csv
|
|||||||
vibe-trading run -p "Analyze my trading behavior, extract my shadow strategy, and compare it with my actual trades"
|
vibe-trading run -p "Analyze my trading behavior, extract my shadow strategy, and compare it with my actual trades"
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
### Pixel Office management
|
||||||
|
|
||||||
## 👥 Shadow Account
|
Open **Pixel Office** from the feature menu to plan and assign research tasks.
|
||||||
|
The Web page, `vibe-trading office` CLI and `pixel_office` agent tool share one
|
||||||
Shadow Account starts from your own trading records instead of a generic strategy template.
|
SQLite task board under your runtime directory. Assignment does not start
|
||||||
|
execution; organizational ranks grant no trading permissions. Record the actual
|
||||||
Upload a broker export, let the agent summarize your behavior, then compare the actual trading path with a rule-based shadow strategy.
|
result when completing a task. See [Pixel Office](pixel-office/README.md) for CLI
|
||||||
|
and API examples.
|
||||||
| Step | Agent output |
|
|
||||||
|------|--------------|
|
|
||||||
| **1. Read your journal** | Parses broker exports from 同花顺, 东方财富, 富途, and generic CSV formats. |
|
|
||||||
| **2. Profile your behavior** | Holding days, win rate, PnL ratio, drawdown, disposition effect, overtrading, momentum chasing, and anchoring checks. |
|
|
||||||
| **3. Extract your rules** | Turns recurring entries/exits into an explicit strategy profile instead of a hand-wavy summary. |
|
|
||||||
| **4. Run the shadow** | Backtests the extracted rules and highlights rule breaks, early exits, missed signals, and alternative trade paths. |
|
|
||||||
| **5. Deliver the report** | Produces an HTML/PDF report that can be inspected, archived, or refined in a later session. |
|
|
||||||
|
|
||||||
```bash
|
|
||||||
vibe-trading --upload trades_export.csv
|
|
||||||
vibe-trading run -p "Analyze my trading behavior, extract my shadow strategy, and compare it with my actual trades"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 💼 Local Multi-Broker Portfolio
|
|
||||||
|
|
||||||
The Web UI adds a read-only **Portfolio** page that aggregates holdings across the broker connections you pick. Sources are connection instances of read-only profiles declaring `account.read` and `positions.read` — set them up under **Broker Connectors** in [Detailed Capabilities](#-detailed-capabilities). Eligibility is only the safety gate; the connection center also shows how far each connector has been verified for portfolio valuation.
|
|
||||||
|
|
||||||
| Behavior | What you get |
|
|
||||||
|----------|--------------|
|
|
||||||
| **Per-source provenance** | Every holding names the connection it came from, valued in USD with a CNY conversion. |
|
|
||||||
| **Failed sources excluded** | A source that errors is reported as an error and left out of the totals — never carried forward — and the snapshot is marked incomplete. |
|
|
||||||
| **Immutable snapshots** | Each refresh is stored in `~/.vibe-trading/portfolio/portfolio.sqlite3`; credential-free settings live in `~/.vibe-trading/portfolio.json` and `connections.json`. |
|
|
||||||
| **Export & analysis** | CSV export, plus a sanitized `portfolio_summary` agent tool whose `risk_xray_args` pass straight into `portfolio_risk_xray`. The same snapshot prints in the terminal with `vibe-trading portfolio show` (`refresh` / `sources` alongside). Risk x-ray uses only observed adjacent returns; it does not bridge missing prices into artificial one-bar returns. |
|
|
||||||
|
|
||||||
Broker-reported source currencies are preserved during valuation: HKD account totals and positions, including Futu `HK.*` holdings, are converted with the snapshot USD/HKD rate before USD and CNY values are displayed. Older snapshots remain stored, but value history compares only snapshots produced by the current valuation methodology to avoid false gains or losses after a valuation fix.
|
|
||||||
|
|
||||||
For `portfolio_risk_xray`, weekly (`1W`) and monthly (`1M`) inputs annualize
|
|
||||||
volatility with 52 and 12 bars per year respectively; daily bars retain the
|
|
||||||
252-bar convention. Monthly `1M` is distinct from minute `1m`.
|
|
||||||
|
|
||||||
### Portfolio connector compatibility
|
### Portfolio connector compatibility
|
||||||
|
|
||||||
@@ -2155,7 +2124,7 @@ Vibe-Trading/
|
|||||||
│ │ ├── memory/ # Cross-session persistent memory
|
│ │ ├── memory/ # Cross-session persistent memory
|
||||||
│ │ │ └── persistent.py # file-based memory (~/.vibe-trading/memory/)
|
│ │ │ └── persistent.py # file-based memory (~/.vibe-trading/memory/)
|
||||||
│ │ │
|
│ │ │
|
||||||
│ │ ├── tools/ # 110 auto-discovered agent tools
|
│ │ ├── tools/ # 111 auto-discovered agent tools
|
||||||
│ │ │ ├── backtest_tool.py # run backtests
|
│ │ │ ├── backtest_tool.py # run backtests
|
||||||
│ │ │ ├── remember_tool.py # cross-session memory (save/recall/forget)
|
│ │ │ ├── remember_tool.py # cross-session memory (save/recall/forget)
|
||||||
│ │ │ ├── skill_writer_tool.py # skill CRUD (save/patch/delete/file)
|
│ │ │ ├── skill_writer_tool.py # skill CRUD (save/patch/delete/file)
|
||||||
|
|||||||
+1
-1
@@ -1722,7 +1722,7 @@ Vibe-Trading/
|
|||||||
│ │ ├── memory/ # Cross-session persistent memory
|
│ │ ├── memory/ # Cross-session persistent memory
|
||||||
│ │ │ └── persistent.py # file-based memory (~/.vibe-trading/memory/)
|
│ │ │ └── persistent.py # file-based memory (~/.vibe-trading/memory/)
|
||||||
│ │ │
|
│ │ │
|
||||||
│ │ ├── tools/ # 110 auto-discovered agent tools
|
│ │ ├── tools/ # 111 auto-discovered agent tools
|
||||||
│ │ │ ├── backtest_tool.py # run backtests
|
│ │ │ ├── backtest_tool.py # run backtests
|
||||||
│ │ │ ├── remember_tool.py # cross-session memory (save/recall/forget)
|
│ │ │ ├── remember_tool.py # cross-session memory (save/recall/forget)
|
||||||
│ │ │ ├── skill_writer_tool.py # skill CRUD (save/patch/delete/file)
|
│ │ │ ├── skill_writer_tool.py # skill CRUD (save/patch/delete/file)
|
||||||
|
|||||||
+1
-1
@@ -1810,7 +1810,7 @@ Vibe-Trading/
|
|||||||
│ │ ├── memory/ # Memoria persistente entre sesiones
|
│ │ ├── memory/ # Memoria persistente entre sesiones
|
||||||
│ │ │ └── persistent.py # memoria basada en archivos (~/.vibe-trading/memory/)
|
│ │ │ └── persistent.py # memoria basada en archivos (~/.vibe-trading/memory/)
|
||||||
│ │ │
|
│ │ │
|
||||||
│ │ ├── tools/ # 110 herramientas de agente autodescubiertas
|
│ │ ├── tools/ # 111 herramientas de agente autodescubiertas
|
||||||
│ │ │ ├── backtest_tool.py # ejecuta backtests
|
│ │ │ ├── backtest_tool.py # ejecuta backtests
|
||||||
│ │ │ ├── remember_tool.py # memoria entre sesiones (save/recall/forget)
|
│ │ │ ├── remember_tool.py # memoria entre sesiones (save/recall/forget)
|
||||||
│ │ │ ├── skill_writer_tool.py # CRUD de skills (save/patch/delete/file)
|
│ │ │ ├── skill_writer_tool.py # CRUD de skills (save/patch/delete/file)
|
||||||
|
|||||||
+1
-1
@@ -2055,7 +2055,7 @@ Vibe-Trading/
|
|||||||
│ │ ├── memory/ # Cross-session persistent memory
|
│ │ ├── memory/ # Cross-session persistent memory
|
||||||
│ │ │ └── persistent.py # file-based memory (~/.vibe-trading/memory/)
|
│ │ │ └── persistent.py # file-based memory (~/.vibe-trading/memory/)
|
||||||
│ │ │
|
│ │ │
|
||||||
│ │ ├── tools/ # 110 auto-discovered agent tools
|
│ │ ├── tools/ # 111 auto-discovered agent tools
|
||||||
│ │ │ ├── backtest_tool.py # run backtests
|
│ │ │ ├── backtest_tool.py # run backtests
|
||||||
│ │ │ ├── remember_tool.py # cross-session memory (save/recall/forget)
|
│ │ │ ├── remember_tool.py # cross-session memory (save/recall/forget)
|
||||||
│ │ │ ├── skill_writer_tool.py # skill CRUD (save/patch/delete/file)
|
│ │ │ ├── skill_writer_tool.py # skill CRUD (save/patch/delete/file)
|
||||||
|
|||||||
+1
-1
@@ -1730,7 +1730,7 @@ Vibe-Trading/
|
|||||||
│ │ ├── memory/ # クロスセッション永続メモリ
|
│ │ ├── memory/ # クロスセッション永続メモリ
|
||||||
│ │ │ └── persistent.py # ファイルベースメモリ (~/.vibe-trading/memory/)
|
│ │ │ └── persistent.py # ファイルベースメモリ (~/.vibe-trading/memory/)
|
||||||
│ │ │
|
│ │ │
|
||||||
│ │ ├── tools/ # 110 個の自動検出エージェントツール
|
│ │ ├── tools/ # 111 個の自動検出エージェントツール
|
||||||
│ │ │ ├── backtest_tool.py # バックテスト実行
|
│ │ │ ├── backtest_tool.py # バックテスト実行
|
||||||
│ │ │ ├── remember_tool.py # クロスセッションメモリ (save/recall/forget)
|
│ │ │ ├── remember_tool.py # クロスセッションメモリ (save/recall/forget)
|
||||||
│ │ │ ├── skill_writer_tool.py # skill CRUD (save/patch/delete/file)
|
│ │ │ ├── skill_writer_tool.py # skill CRUD (save/patch/delete/file)
|
||||||
|
|||||||
+1
-1
@@ -1727,7 +1727,7 @@ Vibe-Trading/
|
|||||||
│ │ ├── memory/ # Cross-session persistent memory
|
│ │ ├── memory/ # Cross-session persistent memory
|
||||||
│ │ │ └── persistent.py # file-based memory (~/.vibe-trading/memory/)
|
│ │ │ └── persistent.py # file-based memory (~/.vibe-trading/memory/)
|
||||||
│ │ │
|
│ │ │
|
||||||
│ │ ├── tools/ # 110 auto-discovered agent tools
|
│ │ ├── tools/ # 111 auto-discovered agent tools
|
||||||
│ │ │ ├── backtest_tool.py # run backtests
|
│ │ │ ├── backtest_tool.py # run backtests
|
||||||
│ │ │ ├── remember_tool.py # cross-session memory (save/recall/forget)
|
│ │ │ ├── remember_tool.py # cross-session memory (save/recall/forget)
|
||||||
│ │ │ ├── skill_writer_tool.py # skill CRUD (save/patch/delete/file)
|
│ │ │ ├── skill_writer_tool.py # skill CRUD (save/patch/delete/file)
|
||||||
|
|||||||
+1
-1
@@ -1725,7 +1725,7 @@ Vibe-Trading/
|
|||||||
│ │ ├── memory/ # 跨 session 持久记忆
|
│ │ ├── memory/ # 跨 session 持久记忆
|
||||||
│ │ │ └── persistent.py # 基于文件的记忆(~/.vibe-trading/memory/)
|
│ │ │ └── persistent.py # 基于文件的记忆(~/.vibe-trading/memory/)
|
||||||
│ │ │
|
│ │ │
|
||||||
│ │ ├── tools/ # 110 个自动发现的 agent 工具
|
│ │ ├── tools/ # 111 个自动发现的 agent 工具
|
||||||
│ │ │ ├── backtest_tool.py # 运行回测
|
│ │ │ ├── backtest_tool.py # 运行回测
|
||||||
│ │ │ ├── remember_tool.py # 跨 session 记忆(save/recall/forget)
|
│ │ │ ├── remember_tool.py # 跨 session 记忆(save/recall/forget)
|
||||||
│ │ │ ├── skill_writer_tool.py # skill CRUD(save/patch/delete/file)
|
│ │ │ ├── skill_writer_tool.py # skill CRUD(save/patch/delete/file)
|
||||||
|
|||||||
@@ -218,6 +218,9 @@ from src.api.settings_routes import ( # noqa: F401, E402
|
|||||||
from src.api.custom_provider_routes import register_custom_provider_routes # noqa: E402
|
from src.api.custom_provider_routes import register_custom_provider_routes # noqa: E402
|
||||||
register_custom_provider_routes(app, require_local_or_auth, require_settings_write_auth)
|
register_custom_provider_routes(app, require_local_or_auth, require_settings_write_auth)
|
||||||
|
|
||||||
|
from src.api.pixel_office_routes import register_pixel_office_routes # noqa: E402
|
||||||
|
register_pixel_office_routes(app, require_local_or_auth, require_settings_write_auth)
|
||||||
|
|
||||||
# --- Uploads ---
|
# --- Uploads ---
|
||||||
from src.api.uploads_routes import register_uploads_routes # noqa: E402
|
from src.api.uploads_routes import register_uploads_routes # noqa: E402
|
||||||
register_uploads_routes(app)
|
register_uploads_routes(app)
|
||||||
|
|||||||
@@ -5428,6 +5428,9 @@ def _build_parser() -> argparse.ArgumentParser:
|
|||||||
serve_parser.add_argument("--port", type=int, default=8000, help="Listen port")
|
serve_parser.add_argument("--port", type=int, default=8000, help="Listen port")
|
||||||
serve_parser.add_argument("--dev", action="store_true", help="Start the Vite dev server")
|
serve_parser.add_argument("--dev", action="store_true", help="Start the Vite dev server")
|
||||||
|
|
||||||
|
office_parser = subparsers.add_parser("office", help="Manage the shared research task board")
|
||||||
|
office_parser.add_argument("office_args", nargs=argparse.REMAINDER)
|
||||||
|
|
||||||
provider_parser = subparsers.add_parser("provider", help="Manage OAuth providers")
|
provider_parser = subparsers.add_parser("provider", help="Manage OAuth providers")
|
||||||
provider_subparsers = provider_parser.add_subparsers(dest="provider_command")
|
provider_subparsers = provider_parser.add_subparsers(dest="provider_command")
|
||||||
login_parser = provider_subparsers.add_parser("login", help="Authenticate with an OAuth provider")
|
login_parser = provider_subparsers.add_parser("login", help="Authenticate with an OAuth provider")
|
||||||
@@ -6562,6 +6565,10 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
)
|
)
|
||||||
if args.command == "serve":
|
if args.command == "serve":
|
||||||
return serve_main(raw_argv[1:])
|
return serve_main(raw_argv[1:])
|
||||||
|
if args.command == "office":
|
||||||
|
from src.pixel_agents.cli import main as office_main
|
||||||
|
office_main(args.office_args)
|
||||||
|
return 0
|
||||||
if args.command == "provider":
|
if args.command == "provider":
|
||||||
if args.provider_command == "login":
|
if args.provider_command == "login":
|
||||||
return cmd_provider_login(args.provider)
|
return cmd_provider_login(args.provider)
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
"""Authenticated routes for the shared manual task board."""
|
||||||
|
|
||||||
|
from typing import Any, Literal
|
||||||
|
from fastapi import Depends, FastAPI, HTTPException
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from src.pixel_agents.office import OfficeManager, office_action
|
||||||
|
|
||||||
|
|
||||||
|
class OfficeActionRequest(BaseModel):
|
||||||
|
action: Literal["create", "assign", "complete", "rank"]
|
||||||
|
arguments: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
def register_pixel_office_routes(app: FastAPI, read_auth, write_auth) -> None:
|
||||||
|
"""Mount read and mutation routes with the normal local/settings guards."""
|
||||||
|
|
||||||
|
@app.get("/api/pixel-office", dependencies=[Depends(read_auth)])
|
||||||
|
def dashboard():
|
||||||
|
return OfficeManager().dashboard()
|
||||||
|
|
||||||
|
@app.post("/api/pixel-office/actions", dependencies=[Depends(write_auth)])
|
||||||
|
def update(payload: OfficeActionRequest):
|
||||||
|
try:
|
||||||
|
return office_action(OfficeManager(), payload.action, payload.arguments)
|
||||||
|
except (ValueError, TypeError) as exc:
|
||||||
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
"""Shared manual research planning; no task dispatch or trading authority."""
|
||||||
|
|
||||||
|
from .office import Agent, OfficeManager, Task
|
||||||
|
|
||||||
|
__all__ = ["Agent", "OfficeManager", "Task"]
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from src.pixel_agents.office import default_office_path, OfficeManager
|
||||||
|
|
||||||
|
|
||||||
|
def _build_parser() -> argparse.ArgumentParser:
|
||||||
|
storage_parent = argparse.ArgumentParser(add_help=False)
|
||||||
|
storage_parent.add_argument(
|
||||||
|
"--storage", type=str, default=argparse.SUPPRESS, help="Path to the SQLite office database"
|
||||||
|
)
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="Pixel Agents office manager", parents=[storage_parent])
|
||||||
|
subparsers = parser.add_subparsers(dest="command")
|
||||||
|
|
||||||
|
init_cmd = subparsers.add_parser("init", help="Initialize office state", parents=[storage_parent])
|
||||||
|
init_cmd.set_defaults(action="init")
|
||||||
|
|
||||||
|
add_agent_cmd = subparsers.add_parser("agent", help="Add an agent to the office", parents=[storage_parent])
|
||||||
|
add_agent_cmd.add_argument("--id", required=True)
|
||||||
|
add_agent_cmd.add_argument("--name", required=True)
|
||||||
|
add_agent_cmd.add_argument("--role", required=True)
|
||||||
|
add_agent_cmd.add_argument("--specialties", nargs="*", default=[])
|
||||||
|
add_agent_cmd.add_argument("--office", default="trading-floor")
|
||||||
|
add_agent_cmd.set_defaults(action="agent")
|
||||||
|
|
||||||
|
task_cmd = subparsers.add_parser("task", help="Create a task for the office", parents=[storage_parent])
|
||||||
|
task_cmd.add_argument("--id", required=True)
|
||||||
|
task_cmd.add_argument("--title", required=True)
|
||||||
|
task_cmd.add_argument("--description", required=True)
|
||||||
|
task_cmd.add_argument("--workstream", default="research")
|
||||||
|
task_cmd.add_argument("--priority", default="normal")
|
||||||
|
task_cmd.add_argument("--owner", default="ops")
|
||||||
|
task_cmd.set_defaults(action="task")
|
||||||
|
|
||||||
|
assign_cmd = subparsers.add_parser("assign", help="Assign a task to an agent", parents=[storage_parent])
|
||||||
|
assign_cmd.add_argument("--task-id", required=True)
|
||||||
|
assign_cmd.add_argument("--agent-id", default=None)
|
||||||
|
assign_cmd.set_defaults(action="assign")
|
||||||
|
|
||||||
|
complete_cmd = subparsers.add_parser("complete", help="Complete a task", parents=[storage_parent])
|
||||||
|
complete_cmd.add_argument("--task-id", required=True)
|
||||||
|
complete_cmd.add_argument("--result", required=True)
|
||||||
|
complete_cmd.set_defaults(action="complete")
|
||||||
|
|
||||||
|
rank_cmd = subparsers.add_parser("rank", help="Change an organizational rank", parents=[storage_parent])
|
||||||
|
rank_cmd.add_argument("--agent-id", required=True)
|
||||||
|
rank_cmd.add_argument("--rank", required=True, choices=["Operator", "Senior", "Lead"])
|
||||||
|
rank_cmd.set_defaults(action="rank")
|
||||||
|
|
||||||
|
list_cmd = subparsers.add_parser("list", help="List tasks and agents", parents=[storage_parent])
|
||||||
|
list_cmd.set_defaults(action="list")
|
||||||
|
|
||||||
|
dashboard_cmd = subparsers.add_parser("dashboard", help="Show the office dashboard", parents=[storage_parent])
|
||||||
|
dashboard_cmd.set_defaults(action="dashboard")
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None) -> None:
|
||||||
|
parser = _build_parser()
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
try:
|
||||||
|
return _run(parser, args)
|
||||||
|
except ValueError as exc:
|
||||||
|
parser.error(str(exc))
|
||||||
|
|
||||||
|
|
||||||
|
def _run(parser, args):
|
||||||
|
storage = Path(args.storage) if hasattr(args, "storage") else default_office_path()
|
||||||
|
manager = OfficeManager(storage)
|
||||||
|
|
||||||
|
if args.command is None:
|
||||||
|
parser.print_help()
|
||||||
|
return
|
||||||
|
|
||||||
|
if args.action == "init":
|
||||||
|
print(f"Initialized Pixel office at {storage}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if args.action == "agent":
|
||||||
|
agent = manager.add_agent(
|
||||||
|
agent_id=args.id,
|
||||||
|
name=args.name,
|
||||||
|
role=args.role,
|
||||||
|
specialties=args.specialties,
|
||||||
|
office=args.office,
|
||||||
|
)
|
||||||
|
print(json.dumps({"agent": agent.to_dict()}, indent=2, ensure_ascii=False))
|
||||||
|
return
|
||||||
|
|
||||||
|
if args.action == "task":
|
||||||
|
task = manager.create_task(
|
||||||
|
task_id=args.id,
|
||||||
|
title=args.title,
|
||||||
|
description=args.description,
|
||||||
|
workstream=args.workstream,
|
||||||
|
priority=args.priority,
|
||||||
|
owner=args.owner,
|
||||||
|
)
|
||||||
|
print(json.dumps({"task": task.to_dict()}, indent=2, ensure_ascii=False))
|
||||||
|
return
|
||||||
|
|
||||||
|
if args.action == "assign":
|
||||||
|
task = manager.assign_task(args.task_id, agent_id=args.agent_id)
|
||||||
|
if task is None:
|
||||||
|
print(f"No eligible agent available for task {args.task_id}")
|
||||||
|
return
|
||||||
|
print(json.dumps({"task": task.to_dict()}, indent=2, ensure_ascii=False))
|
||||||
|
return
|
||||||
|
|
||||||
|
if args.action == "complete":
|
||||||
|
task = manager.complete_task(args.task_id, args.result)
|
||||||
|
if task is None:
|
||||||
|
print(f"Task not found: {args.task_id}")
|
||||||
|
return
|
||||||
|
print(json.dumps({"task": task.to_dict()}, indent=2, ensure_ascii=False))
|
||||||
|
return
|
||||||
|
|
||||||
|
if args.action == "rank":
|
||||||
|
print(json.dumps(manager.set_rank(args.agent_id, args.rank).to_dict(), indent=2))
|
||||||
|
return
|
||||||
|
|
||||||
|
if args.action == "list":
|
||||||
|
print(
|
||||||
|
json.dumps({"agents": manager.list_agents(), "tasks": manager.list_tasks()}, indent=2, ensure_ascii=False)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if args.action == "dashboard":
|
||||||
|
print(json.dumps(manager.dashboard(), indent=2, ensure_ascii=False))
|
||||||
|
return
|
||||||
|
|
||||||
|
parser.print_help()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"office_name": "Pixel Trading Office",
|
||||||
|
"agents": [
|
||||||
|
{
|
||||||
|
"id": "research-analyst",
|
||||||
|
"name": "Research Analyst",
|
||||||
|
"role": "research",
|
||||||
|
"specialities": [
|
||||||
|
"macro",
|
||||||
|
"equity",
|
||||||
|
"crypto",
|
||||||
|
"news"
|
||||||
|
],
|
||||||
|
"office": "trading-floor",
|
||||||
|
"rank": "Operator",
|
||||||
|
"color": "#c59259"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "risk-guard",
|
||||||
|
"name": "Risk Guard",
|
||||||
|
"role": "risk",
|
||||||
|
"specialities": [
|
||||||
|
"risk",
|
||||||
|
"drawdown",
|
||||||
|
"execution"
|
||||||
|
],
|
||||||
|
"office": "trade-ops",
|
||||||
|
"rank": "Operator",
|
||||||
|
"color": "#73a4a4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "execution-agent",
|
||||||
|
"name": "Execution Agent",
|
||||||
|
"role": "execution",
|
||||||
|
"specialities": [
|
||||||
|
"broker",
|
||||||
|
"orders",
|
||||||
|
"routing"
|
||||||
|
],
|
||||||
|
"office": "live-trading",
|
||||||
|
"rank": "Operator",
|
||||||
|
"color": "#a38bae"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
"""Shared manual research-task board. It never dispatches broker operations."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sqlite3
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from dataclasses import asdict, dataclass, field
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from src.config.paths import get_runtime_root
|
||||||
|
|
||||||
|
DB_FILENAME = "pixel-office.sqlite3"
|
||||||
|
|
||||||
|
|
||||||
|
def default_office_path() -> Path:
|
||||||
|
"""Resolve storage at use time, respecting the selected runtime root."""
|
||||||
|
return get_runtime_root() / DB_FILENAME
|
||||||
|
|
||||||
|
|
||||||
|
def _utc_now() -> str:
|
||||||
|
return datetime.now(timezone.utc).isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Agent:
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
role: str
|
||||||
|
rank: str = "Operator"
|
||||||
|
color: str = "#c59259"
|
||||||
|
office: str = "research"
|
||||||
|
specialities: list[str] = field(default_factory=list)
|
||||||
|
status: str = "idle"
|
||||||
|
load: int = 0
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
return asdict(self)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Task:
|
||||||
|
id: str
|
||||||
|
title: str
|
||||||
|
description: str
|
||||||
|
workstream: str = "research"
|
||||||
|
priority: str = "normal"
|
||||||
|
owner: str = "user"
|
||||||
|
status: str = "queued"
|
||||||
|
assigned_to: str | None = None
|
||||||
|
created_at: str = field(default_factory=_utc_now)
|
||||||
|
updated_at: str = field(default_factory=_utc_now)
|
||||||
|
result: str = ""
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
return asdict(self)
|
||||||
|
|
||||||
|
|
||||||
|
def _text(value: str, field_name: str, *, maximum: int = 2000, empty: bool = False) -> str:
|
||||||
|
if not isinstance(value, str) or len(value) > maximum or (not empty and not value.strip()):
|
||||||
|
raise ValueError(f"{field_name} must be {'0' if empty else '1'} to {maximum} characters")
|
||||||
|
return value.strip()
|
||||||
|
|
||||||
|
|
||||||
|
class OfficeManager:
|
||||||
|
"""Transactional task storage shared by Web, CLI and the agent tool.
|
||||||
|
|
||||||
|
Assignment and ranks are organizational metadata, not execution state or
|
||||||
|
trading authority. Completion is an explicit user/agent-recorded result.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, storage_path: str | Path | None = None):
|
||||||
|
self.storage_path = Path(storage_path) if storage_path else default_office_path()
|
||||||
|
self.storage_path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
||||||
|
defaults = json.loads(Path(__file__).with_name("defaults.json").read_text())
|
||||||
|
self.office_name = defaults["office_name"]
|
||||||
|
with self._db() as db:
|
||||||
|
db.execute("CREATE TABLE IF NOT EXISTS agents (id TEXT PRIMARY KEY, payload TEXT NOT NULL)")
|
||||||
|
db.execute("CREATE TABLE IF NOT EXISTS tasks (id TEXT PRIMARY KEY, payload TEXT NOT NULL)")
|
||||||
|
for item in defaults["agents"]:
|
||||||
|
db.execute(
|
||||||
|
"INSERT OR IGNORE INTO agents VALUES (?, ?)", (item["id"], json.dumps(Agent(**item).to_dict()))
|
||||||
|
)
|
||||||
|
self.storage_path.chmod(0o600)
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def _db(self):
|
||||||
|
db = sqlite3.connect(self.storage_path, timeout=10)
|
||||||
|
try:
|
||||||
|
with db:
|
||||||
|
db.execute("BEGIN IMMEDIATE")
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
def add_agent(
|
||||||
|
self, *, agent_id: str, name: str, role: str, specialties: list[str] | None = None, office: str = "research"
|
||||||
|
) -> Agent:
|
||||||
|
"""Add a distinct planning role; an existing role is never overwritten."""
|
||||||
|
agent = Agent(
|
||||||
|
id=_text(agent_id, "id", maximum=100),
|
||||||
|
name=_text(name, "name", maximum=100),
|
||||||
|
role=_text(role, "role", maximum=100),
|
||||||
|
specialities=specialties or [],
|
||||||
|
office=office,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with self._db() as db:
|
||||||
|
db.execute("INSERT INTO agents VALUES (?, ?)", (agent.id, json.dumps(agent.to_dict())))
|
||||||
|
except sqlite3.IntegrityError as exc:
|
||||||
|
raise ValueError("agent id already exists") from exc
|
||||||
|
return agent
|
||||||
|
|
||||||
|
def create_task(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
task_id: str,
|
||||||
|
title: str,
|
||||||
|
description: str = "",
|
||||||
|
workstream: str = "research",
|
||||||
|
priority: str = "normal",
|
||||||
|
owner: str = "user",
|
||||||
|
assigned_to: str | None = None,
|
||||||
|
) -> Task:
|
||||||
|
"""Create a task, optionally assigned, in one transaction."""
|
||||||
|
if priority not in {"low", "normal", "high"}:
|
||||||
|
raise ValueError("priority must be low, normal or high")
|
||||||
|
task = Task(
|
||||||
|
id=_text(task_id, "task_id", maximum=100),
|
||||||
|
title=_text(title, "title", maximum=200),
|
||||||
|
description=_text(description, "description", empty=True),
|
||||||
|
workstream=_text(workstream, "workstream", maximum=100),
|
||||||
|
priority=priority,
|
||||||
|
owner=_text(owner, "owner", maximum=100),
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with self._db() as db:
|
||||||
|
if assigned_to is not None:
|
||||||
|
if not db.execute("SELECT id FROM agents WHERE id=?", (assigned_to,)).fetchone():
|
||||||
|
raise ValueError("agent not found")
|
||||||
|
task.assigned_to, task.status = assigned_to, "assigned"
|
||||||
|
db.execute("INSERT INTO tasks VALUES (?, ?)", (task.id, json.dumps(task.to_dict())))
|
||||||
|
except sqlite3.IntegrityError as exc:
|
||||||
|
raise ValueError("task id already exists") from exc
|
||||||
|
return task
|
||||||
|
|
||||||
|
def assign_task(self, task_id: str, agent_id: str | None = None) -> Task:
|
||||||
|
"""Assign/reassign one unfinished task without changing any other task."""
|
||||||
|
with self._db() as db:
|
||||||
|
row = db.execute("SELECT payload FROM tasks WHERE id=?", (task_id,)).fetchone()
|
||||||
|
if row is None:
|
||||||
|
raise ValueError("task not found")
|
||||||
|
task = Task(**json.loads(row[0]))
|
||||||
|
if task.status == "completed":
|
||||||
|
raise ValueError("completed tasks cannot be assigned")
|
||||||
|
candidates = [json.loads(r[0]) for r in db.execute("SELECT payload FROM agents ORDER BY id")]
|
||||||
|
if agent_id is None:
|
||||||
|
agent_id = next((a["id"] for a in candidates if a["role"] == task.workstream), None)
|
||||||
|
if not any(a["id"] == agent_id for a in candidates):
|
||||||
|
raise ValueError("no matching agent; choose an agent explicitly")
|
||||||
|
task.assigned_to, task.status, task.updated_at = agent_id, "assigned", _utc_now()
|
||||||
|
db.execute("UPDATE tasks SET payload=? WHERE id=?", (json.dumps(task.to_dict()), task_id))
|
||||||
|
return task
|
||||||
|
|
||||||
|
def complete_task(self, task_id: str, result: str) -> Task:
|
||||||
|
"""Record completion, preserving an already-completed task's result."""
|
||||||
|
result = _text(result, "result")
|
||||||
|
with self._db() as db:
|
||||||
|
row = db.execute("SELECT payload FROM tasks WHERE id=?", (task_id,)).fetchone()
|
||||||
|
if row is None:
|
||||||
|
raise ValueError("task not found")
|
||||||
|
task = Task(**json.loads(row[0]))
|
||||||
|
if task.status != "completed":
|
||||||
|
task.status, task.result, task.updated_at = "completed", result, _utc_now()
|
||||||
|
db.execute("UPDATE tasks SET payload=? WHERE id=?", (json.dumps(task.to_dict()), task_id))
|
||||||
|
return task
|
||||||
|
|
||||||
|
def set_rank(self, agent_id: str, rank: str) -> Agent:
|
||||||
|
"""Change an organizational label; it grants no permissions."""
|
||||||
|
if rank not in {"Operator", "Senior", "Lead"}:
|
||||||
|
raise ValueError("rank must be Operator, Senior or Lead")
|
||||||
|
with self._db() as db:
|
||||||
|
row = db.execute("SELECT payload FROM agents WHERE id=?", (agent_id,)).fetchone()
|
||||||
|
if row is None:
|
||||||
|
raise ValueError("agent not found")
|
||||||
|
agent = Agent(**json.loads(row[0]))
|
||||||
|
agent.rank = rank
|
||||||
|
db.execute("UPDATE agents SET payload=? WHERE id=?", (json.dumps(agent.to_dict()), agent_id))
|
||||||
|
return agent
|
||||||
|
|
||||||
|
def dashboard(self) -> dict[str, Any]:
|
||||||
|
"""Read one coherent snapshot; derive each agent's load from its tasks."""
|
||||||
|
with self._db() as db:
|
||||||
|
agents = [json.loads(r[0]) for r in db.execute("SELECT payload FROM agents ORDER BY rowid")]
|
||||||
|
tasks = [json.loads(r[0]) for r in db.execute("SELECT payload FROM tasks ORDER BY rowid")]
|
||||||
|
for agent in agents:
|
||||||
|
agent["load"] = sum(t["assigned_to"] == agent["id"] and t["status"] != "completed" for t in tasks)
|
||||||
|
agent["status"] = "busy" if agent["load"] else "idle"
|
||||||
|
return {
|
||||||
|
"office_name": self.office_name,
|
||||||
|
"agents": agents,
|
||||||
|
"tasks": tasks,
|
||||||
|
"agent_count": len(agents),
|
||||||
|
"task_count": len(tasks),
|
||||||
|
"completed": sum(t["status"] == "completed" for t in tasks),
|
||||||
|
"pending": sum(t["status"] != "completed" for t in tasks),
|
||||||
|
"mode": "manual_planning",
|
||||||
|
}
|
||||||
|
|
||||||
|
def list_agents(self) -> list[dict[str, Any]]:
|
||||||
|
return self.dashboard()["agents"]
|
||||||
|
|
||||||
|
def list_tasks(self) -> list[dict[str, Any]]:
|
||||||
|
return self.dashboard()["tasks"]
|
||||||
|
|
||||||
|
|
||||||
|
def office_action(manager: OfficeManager, action: str, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Dispatch the bounded action vocabulary used on every surface."""
|
||||||
|
methods = {
|
||||||
|
"create": manager.create_task,
|
||||||
|
"assign": manager.assign_task,
|
||||||
|
"complete": manager.complete_task,
|
||||||
|
"rank": manager.set_rank,
|
||||||
|
}
|
||||||
|
if action == "list":
|
||||||
|
return manager.dashboard()
|
||||||
|
if action not in methods:
|
||||||
|
raise ValueError("unknown office action")
|
||||||
|
methods[action](**arguments)
|
||||||
|
return manager.dashboard()
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"""Agent surface for shared research task planning."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from typing import Any
|
||||||
|
from src.agent.tools import BaseTool
|
||||||
|
from src.pixel_agents.office import OfficeManager, office_action
|
||||||
|
|
||||||
|
|
||||||
|
class PixelOfficeTool(BaseTool):
|
||||||
|
name = "pixel_office"
|
||||||
|
description = (
|
||||||
|
"Manage the shared manual research-task board. list reads agents/tasks; create takes task_id, title, description, "
|
||||||
|
"optional assigned_to and priority (low/normal/high); assign takes task_id, agent_id; complete takes task_id and "
|
||||||
|
"a truthful result; rank takes agent_id and rank (Operator/Senior/Lead). Assignment does not execute a task. "
|
||||||
|
"Ranks do not grant trading permissions. Never mark a task complete without a real result."
|
||||||
|
)
|
||||||
|
parameters = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"action": {"type": "string", "enum": ["list", "create", "assign", "complete", "rank"]},
|
||||||
|
"arguments": {"type": "object"},
|
||||||
|
},
|
||||||
|
"required": ["action"],
|
||||||
|
}
|
||||||
|
repeatable = True
|
||||||
|
is_readonly = False
|
||||||
|
|
||||||
|
def execute(self, **kwargs: Any) -> str:
|
||||||
|
return json.dumps(
|
||||||
|
office_action(OfficeManager(), kwargs["action"], kwargs.get("arguments") or {}), ensure_ascii=False
|
||||||
|
)
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
"""Exercise shared state through CLI, REST and tools without executing tasks."""
|
||||||
|
import json
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi import FastAPI, HTTPException
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from src.api.pixel_office_routes import register_pixel_office_routes
|
||||||
|
from src.pixel_agents.cli import main
|
||||||
|
from src.pixel_agents.office import OfficeManager
|
||||||
|
from src.tools.pixel_office_tool import PixelOfficeTool
|
||||||
|
|
||||||
|
|
||||||
|
def test_shared_surfaces_and_result_preservation(tmp_path, monkeypatch, capsys):
|
||||||
|
monkeypatch.setenv("VIBE_TRADING_HOME", str(tmp_path))
|
||||||
|
main(["task", "--id", "brief", "--title", "Review", "--description", "Use observed sources"])
|
||||||
|
capsys.readouterr()
|
||||||
|
app = FastAPI()
|
||||||
|
register_pixel_office_routes(app, lambda: None, lambda: None)
|
||||||
|
with TestClient(app) as client:
|
||||||
|
assert client.get("/api/pixel-office").json()["tasks"][0]["title"] == "Review"
|
||||||
|
response = client.post("/api/pixel-office/actions", json={"action": "assign", "arguments": {"task_id": "brief", "agent_id": "research-analyst"}})
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["mode"] == "manual_planning"
|
||||||
|
assert response.json()["tasks"][0]["status"] == "assigned"
|
||||||
|
assert client.post("/api/pixel-office/actions", json={"action": "complete", "arguments": {"task_id": "brief", "result": " "}}).status_code == 400
|
||||||
|
result = json.loads(PixelOfficeTool().execute(action="complete", arguments={"task_id": "brief", "result": "Observed result"}))
|
||||||
|
assert result["completed"] == 1
|
||||||
|
assert all(a["load"] == 0 for a in result["agents"])
|
||||||
|
main(["complete", "--task-id", "brief", "--result", "replacement"])
|
||||||
|
assert OfficeManager().list_tasks()[0]["result"] == "Observed result"
|
||||||
|
with pytest.raises(ValueError, match="completed"):
|
||||||
|
OfficeManager().assign_task("brief", "risk-guard")
|
||||||
|
|
||||||
|
|
||||||
|
def test_multiple_assignments_reassignment_and_partial_completion(tmp_path):
|
||||||
|
board = OfficeManager(tmp_path / "board.db")
|
||||||
|
board.create_task(task_id="a", title="A", assigned_to="research-analyst")
|
||||||
|
board.create_task(task_id="b", title="B", assigned_to="research-analyst")
|
||||||
|
board.assign_task("a", "research-analyst")
|
||||||
|
assert board.list_agents()[0]["load"] == 2
|
||||||
|
board.complete_task("a", "Done A")
|
||||||
|
assert board.list_agents()[0]["load"] == 1
|
||||||
|
board.assign_task("b", "risk-guard")
|
||||||
|
agents = {a["id"]: a for a in board.list_agents()}
|
||||||
|
assert agents["research-analyst"]["status"] == "idle"
|
||||||
|
assert agents["risk-guard"]["load"] == 1
|
||||||
|
before = board.dashboard()
|
||||||
|
with pytest.raises(ValueError, match="no matching agent"):
|
||||||
|
board.assign_task("b", "missing")
|
||||||
|
assert board.dashboard() == before
|
||||||
|
|
||||||
|
|
||||||
|
def test_concurrent_process_like_managers_do_not_lose_updates(tmp_path):
|
||||||
|
path = tmp_path / "office.db"
|
||||||
|
def write(index):
|
||||||
|
OfficeManager(path).create_task(task_id=str(index), title=f"Task {index}")
|
||||||
|
with ThreadPoolExecutor(max_workers=8) as workers:
|
||||||
|
list(workers.map(write, range(24)))
|
||||||
|
board = OfficeManager(path)
|
||||||
|
assert len(board.list_tasks()) == 24
|
||||||
|
with pytest.raises(ValueError, match="already exists"):
|
||||||
|
board.create_task(task_id="0", title="Overwrite")
|
||||||
|
assert board.list_tasks()[0]["title"].startswith("Task ")
|
||||||
|
|
||||||
|
|
||||||
|
def test_corrupt_store_is_never_replaced(tmp_path):
|
||||||
|
path = tmp_path / "bad.db"
|
||||||
|
path.write_bytes(b"not a database")
|
||||||
|
with pytest.raises(Exception, match="not a database"):
|
||||||
|
OfficeManager(path)
|
||||||
|
assert path.read_bytes() == b"not a database"
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_guards_and_invalid_action(tmp_path, monkeypatch):
|
||||||
|
monkeypatch.setenv("VIBE_TRADING_HOME", str(tmp_path))
|
||||||
|
app = FastAPI()
|
||||||
|
def deny():
|
||||||
|
raise HTTPException(status_code=403)
|
||||||
|
register_pixel_office_routes(app, lambda: None, deny)
|
||||||
|
with TestClient(app) as client:
|
||||||
|
assert client.get("/api/pixel-office").status_code == 200
|
||||||
|
assert client.post("/api/pixel-office/actions", json={"action": "rank", "arguments": {"agent_id": "risk-guard", "rank": "Lead"}}).status_code == 403
|
||||||
|
assert OfficeManager().list_agents()[1]["rank"] == "Operator"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("placement", ["before", "after"])
|
||||||
|
def test_cli_storage_option_and_errors(tmp_path, placement, capsys):
|
||||||
|
path = tmp_path / "custom.sqlite3"
|
||||||
|
args = ["rank", "--agent-id", "risk-guard", "--rank", "Senior"]
|
||||||
|
option = ["--storage", str(path)]
|
||||||
|
main(option + args if placement == "before" else args + option)
|
||||||
|
assert json.loads(capsys.readouterr().out)["rank"] == "Senior"
|
||||||
|
assert OfficeManager(path).list_agents()[1]["rank"] == "Senior"
|
||||||
|
with pytest.raises(SystemExit) as failure:
|
||||||
|
main(["assign", "--task-id", "missing", *option])
|
||||||
|
assert failure.value.code == 2
|
||||||
@@ -58,6 +58,7 @@ npm run build
|
|||||||
|
|
||||||
cd ..\desktop\electron
|
cd ..\desktop\electron
|
||||||
npm ci
|
npm ci
|
||||||
|
npm run runtime:win -- -Clean
|
||||||
npm run prepare:electron
|
npm run prepare:electron
|
||||||
npm start
|
npm start
|
||||||
```
|
```
|
||||||
@@ -68,6 +69,10 @@ the source lifecycle tests. The packaging command additionally requires a
|
|||||||
current `7z.exe`; it treats the pinned GTK NSIS asset as an archive and never
|
current `7z.exe`; it treats the pinned GTK NSIS asset as an archive and never
|
||||||
executes its legacy self-extractor.
|
executes its legacy self-extractor.
|
||||||
|
|
||||||
|
The installer command builds the isolated Python backend before Electron
|
||||||
|
packaging. It produces an unsigned local review installer and does not publish
|
||||||
|
or auto-update anything.
|
||||||
|
|
||||||
The Windows lifecycle suite used by CI can be run from the same directory:
|
The Windows lifecycle suite used by CI can be run from the same directory:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ npm run smoke:lifecycle
|
|||||||
npm run installer:win:review
|
npm run installer:win:review
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`installer:win:review` also runs the backend runtime build automatically, so
|
||||||
|
running `npm run runtime:win` separately is optional when using that command.
|
||||||
|
|
||||||
The installer and checksum are written under `desktop/electron/release/`.
|
The installer and checksum are written under `desktop/electron/release/`.
|
||||||
The `installer:win:review` artifact is deliberately unsigned and must never be
|
The `installer:win:review` artifact is deliberately unsigned and must never be
|
||||||
published to users. The command strips all supported signing credentials,
|
published to users. The command strips all supported signing credentials,
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"smoke:credentials": "npm run build && npm run prepare:electron && electron scripts/smoke-credentials.cjs",
|
"smoke:credentials": "npm run build && npm run prepare:electron && electron scripts/smoke-credentials.cjs",
|
||||||
"smoke:lifecycle": "npm run build && npm run prepare:electron && node scripts/test-packaging-preparation.mjs && node scripts/test-locales.mjs && node scripts/test-backend-resolution.mjs && node scripts/smoke-lifecycle.mjs && node scripts/smoke-parent-death.mjs",
|
"smoke:lifecycle": "npm run build && npm run prepare:electron && node scripts/test-packaging-preparation.mjs && node scripts/test-locales.mjs && node scripts/test-backend-resolution.mjs && node scripts/smoke-lifecycle.mjs && node scripts/smoke-parent-death.mjs",
|
||||||
"pack:win": "npm run build && npm run prepare:electron && electron-builder --publish never --win dir",
|
"pack:win": "npm run build && npm run prepare:electron && electron-builder --publish never --win dir",
|
||||||
"installer:win:review": "npm run build && npm run prepare:electron && node scripts/build-review-installer.mjs",
|
"installer:win:review": "npm run runtime:win -- -Clean && npm run build && npm run prepare:electron && node scripts/build-review-installer.mjs",
|
||||||
"installer:win:signed": "node scripts/build-signed-installer.mjs",
|
"installer:win:signed": "node scripts/build-signed-installer.mjs",
|
||||||
"sbom": "npm sbom --sbom-format cyclonedx"
|
"sbom": "npm sbom --sbom-format cyclonedx"
|
||||||
},
|
},
|
||||||
@@ -48,6 +48,7 @@
|
|||||||
"directories": {
|
"directories": {
|
||||||
"output": "release"
|
"output": "release"
|
||||||
},
|
},
|
||||||
|
"publish": null,
|
||||||
"files": [
|
"files": [
|
||||||
"dist/**/*",
|
"dist/**/*",
|
||||||
"package.json"
|
"package.json"
|
||||||
|
|||||||
@@ -95,11 +95,12 @@ function Resolve-SevenZip {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not $BuildPython) {
|
if (-not $BuildPython) {
|
||||||
$repoVenvPython = Join-Path $repoRoot '.venv\Scripts\python.exe'
|
$pythonCandidates = @(
|
||||||
if (Test-Path -LiteralPath $repoVenvPython) {
|
(Join-Path $repoRoot '.packaging-venv\Scripts\python.exe'),
|
||||||
$BuildPython = $repoVenvPython
|
(Join-Path $repoRoot '.venv\Scripts\python.exe')
|
||||||
}
|
)
|
||||||
else {
|
$BuildPython = $pythonCandidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
|
||||||
|
if (-not $BuildPython) {
|
||||||
$BuildPython = (Get-Command python.exe -ErrorAction Stop).Source
|
$BuildPython = (Get-Command python.exe -ErrorAction Stop).Source
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Activity, BarChart3, Bot, CalendarClock, CandlestickChart, FileText, Layers, Settings, WalletCards } from "lucide-react";
|
import { Building2, Activity, BarChart3, Bot, CalendarClock, CandlestickChart, FileText, Layers, Settings, WalletCards } from "lucide-react";
|
||||||
|
|
||||||
export const NAVIGATION_GROUPS = [
|
export const NAVIGATION_GROUPS = [
|
||||||
{ id: "research", items: [
|
{ id: "research", items: [
|
||||||
@@ -12,6 +12,7 @@ export const NAVIGATION_GROUPS = [
|
|||||||
{ id: "correlation", to: "/correlation", icon: BarChart3 },
|
{ id: "correlation", to: "/correlation", icon: BarChart3 },
|
||||||
] },
|
] },
|
||||||
{ id: "automation", items: [
|
{ id: "automation", items: [
|
||||||
|
{ id: "pixelOffice", to: "/pixel-office", icon: Building2 },
|
||||||
{ id: "scheduled", to: "/scheduled", icon: CalendarClock },
|
{ id: "scheduled", to: "/scheduled", icon: CalendarClock },
|
||||||
{ id: "runtime", to: "/runtime", icon: Activity },
|
{ id: "runtime", to: "/runtime", icon: Activity },
|
||||||
{ id: "settings", to: "/settings", icon: Settings },
|
{ id: "settings", to: "/settings", icon: Settings },
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ describe("i18n utilities", () => {
|
|||||||
// the order the switcher renders, and that "en" stays first so the
|
// the order the switcher renders, and that "en" stays first so the
|
||||||
// primary-match fallback in Layout resolves regional codes to it.
|
// primary-match fallback in Layout resolves regional codes to it.
|
||||||
const codes = SUPPORTED_LANGUAGES.map((l) => l.code);
|
const codes = SUPPORTED_LANGUAGES.map((l) => l.code);
|
||||||
expect(codes).toEqual(["en", "zh-CN", "ja", "ko", "ar", "fa", "es", "de", "pt-BR", "id"]);
|
expect(codes).toEqual(["en", "zh-CN", "ja", "ko", "ar", "fa", "es", "de", "pt-BR", "id", "tr"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("accepts zh-CN as an explicit supported language", async () => {
|
it("accepts zh-CN as an explicit supported language", async () => {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export const SUPPORTED_LANGUAGES = [
|
|||||||
{ code: "de", label: "Deutsch", dir: "ltr" as const },
|
{ code: "de", label: "Deutsch", dir: "ltr" as const },
|
||||||
{ code: "pt-BR", label: "Português (Brasil)", dir: "ltr" as const },
|
{ code: "pt-BR", label: "Português (Brasil)", dir: "ltr" as const },
|
||||||
{ code: "id", label: "Bahasa Indonesia", dir: "ltr" as const },
|
{ code: "id", label: "Bahasa Indonesia", dir: "ltr" as const },
|
||||||
|
{ code: "tr", label: "Türkçe", dir: "ltr" as const },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type SupportedLanguageCode = (typeof SUPPORTED_LANGUAGES)[number]["code"];
|
export type SupportedLanguageCode = (typeof SUPPORTED_LANGUAGES)[number]["code"];
|
||||||
@@ -33,6 +34,7 @@ const localeLoaders = {
|
|||||||
de: () => import("./locales/de.json"),
|
de: () => import("./locales/de.json"),
|
||||||
"pt-BR": () => import("./locales/pt-BR.json"),
|
"pt-BR": () => import("./locales/pt-BR.json"),
|
||||||
id: () => import("./locales/id.json"),
|
id: () => import("./locales/id.json"),
|
||||||
|
tr: () => import("./locales/tr.json"),
|
||||||
} satisfies Record<LazyLanguageCode, () => Promise<{ default: typeof en }>>;
|
} satisfies Record<LazyLanguageCode, () => Promise<{ default: typeof en }>>;
|
||||||
|
|
||||||
const LANGUAGE_STORAGE_KEY = "i18nextLng";
|
const LANGUAGE_STORAGE_KEY = "i18nextLng";
|
||||||
|
|||||||
@@ -21,7 +21,8 @@
|
|||||||
"correlation": "قارن العلاقة بين تحركات الأصول.",
|
"correlation": "قارن العلاقة بين تحركات الأصول.",
|
||||||
"scheduled": "كرر مهمة بحثية وفق جدول زمني.",
|
"scheduled": "كرر مهمة بحثية وفق جدول زمني.",
|
||||||
"runtime": "تحقق من اتصالات الوسطاء وحالة تشغيل التداول.",
|
"runtime": "تحقق من اتصالات الوسطاء وحالة تشغيل التداول.",
|
||||||
"settings": "اضبط النماذج ومصادر البيانات واتصالات الوسطاء."
|
"settings": "اضبط النماذج ومصادر البيانات واتصالات الوسطاء.",
|
||||||
|
"pixelOffice": "خطط لمهام البحث ووزعها. إسناد المهمة لا يبدأ تنفيذها؛ الرتب مجرد تسميات ولا تمنح صلاحيات تداول."
|
||||||
},
|
},
|
||||||
"instructions": {
|
"instructions": {
|
||||||
"reports": "اطلب أولاً من المساعد اختبار استراتيجية تاريخياً، ثم افتح نتائجها هنا. نزّل ملفات PDF والمستندات المنشأة في الدردشة من المحادثة الأصلية.",
|
"reports": "اطلب أولاً من المساعد اختبار استراتيجية تاريخياً، ثم افتح نتائجها هنا. نزّل ملفات PDF والمستندات المنشأة في الدردشة من المحادثة الأصلية.",
|
||||||
@@ -31,7 +32,8 @@
|
|||||||
"correlation": "أدخل رمزين على الأقل للأصول، واختر الفترة، ثم اضغط حساب.",
|
"correlation": "أدخل رمزين على الأقل للأصول، واختر الفترة، ثم اضغط حساب.",
|
||||||
"scheduled": "أدخل تعليمات البحث وتكراره واحفظها. يلزم تفعيل المجدول المحلي لتشغيل المهام، وإعداد قناة لإرسال النتائج.",
|
"scheduled": "أدخل تعليمات البحث وتكراره واحفظها. يلزم تفعيل المجدول المحلي لتشغيل المهام، وإعداد قناة لإرسال النتائج.",
|
||||||
"runtime": "اضبط اتصال الوسيط أولاً في الإعدادات أو صفحة المحفظة. تعتمد عمليات التداول المتاحة على قدرات الوسيط والتفويض الذي تمنحه.",
|
"runtime": "اضبط اتصال الوسيط أولاً في الإعدادات أو صفحة المحفظة. تعتمد عمليات التداول المتاحة على قدرات الوسيط والتفويض الذي تمنحه.",
|
||||||
"settings": "اضبط نموذجاً للبدء. أضف مصادر بيانات أو اتصالات وسطاء حسب المهمة؛ لا يحتاج بحث بيانات السوق العامة إلى حساب وسيط."
|
"settings": "اضبط نموذجاً للبدء. أضف مصادر بيانات أو اتصالات وسطاء حسب المهمة؛ لا يحتاج بحث بيانات السوق العامة إلى حساب وسيط.",
|
||||||
|
"pixelOffice": "خطط لمهام البحث ووزعها. إسناد المهمة لا يبدأ تنفيذها؛ الرتب مجرد تسميات ولا تمنح صلاحيات تداول."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"layout": {
|
"layout": {
|
||||||
@@ -61,7 +63,8 @@
|
|||||||
"sidebar": "الشريط الجانبي لـ Vibe-Trading",
|
"sidebar": "الشريط الجانبي لـ Vibe-Trading",
|
||||||
"mainNavigation": "التنقل الرئيسي",
|
"mainNavigation": "التنقل الرئيسي",
|
||||||
"about": "حول",
|
"about": "حول",
|
||||||
"sessionsLoadFailed": "تعذر تحميل المحادثات."
|
"sessionsLoadFailed": "تعذر تحميل المحادثات.",
|
||||||
|
"pixelOffice": "المكتب البكسلي"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"monitorBadge": "مراقبة التداول",
|
"monitorBadge": "مراقبة التداول",
|
||||||
@@ -2380,5 +2383,40 @@
|
|||||||
"preview": "Test passed: {{preview}}",
|
"preview": "Test passed: {{preview}}",
|
||||||
"error": "Provider test failed: {{error}}",
|
"error": "Provider test failed: {{error}}",
|
||||||
"waiting": "The provider is taking longer than usual ({{seconds}}s). The request will continue until the server timeout."
|
"waiting": "The provider is taking longer than usual ({{seconds}}s). The request will continue until the server timeout."
|
||||||
|
},
|
||||||
|
"pixelOffice": {
|
||||||
|
"title": "المكتب البكسلي",
|
||||||
|
"description": "خطط لمهام البحث ووزعها. إسناد المهمة لا يبدأ تنفيذها؛ الرتب مجرد تسميات ولا تمنح صلاحيات تداول.",
|
||||||
|
"refresh": "تحديث",
|
||||||
|
"loading": "جارٍ تحميل المكتب…",
|
||||||
|
"unavailable": "المكتب غير متاح. حاول التحديث.",
|
||||||
|
"floor": "مساحة المكتب",
|
||||||
|
"taskCount": "المهام المفتوحة: {{count}}",
|
||||||
|
"board": "لوحة المهام",
|
||||||
|
"empty": "لا توجد مهام بعد.",
|
||||||
|
"assignee": "المسؤول",
|
||||||
|
"unassigned": "غير مسندة",
|
||||||
|
"result": "نتيجة الإنجاز",
|
||||||
|
"complete": "تسجيل الإنجاز",
|
||||||
|
"create": "إنشاء مهمة",
|
||||||
|
"taskTitle": "عنوان المهمة",
|
||||||
|
"instructions": "التعليمات",
|
||||||
|
"priority": "الأولوية",
|
||||||
|
"ranksTitle": "الرتب التنظيمية",
|
||||||
|
"priorities": {
|
||||||
|
"low": "منخفضة",
|
||||||
|
"normal": "عادية",
|
||||||
|
"high": "عالية"
|
||||||
|
},
|
||||||
|
"statuses": {
|
||||||
|
"queued": "في الانتظار",
|
||||||
|
"assigned": "مسندة",
|
||||||
|
"completed": "مكتملة"
|
||||||
|
},
|
||||||
|
"ranks": {
|
||||||
|
"Operator": "عضو",
|
||||||
|
"Senior": "عضو خبير",
|
||||||
|
"Lead": "قائد"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,8 @@
|
|||||||
"correlation": "Vergleichen, wie sich Vermögenswerte gemeinsam bewegen.",
|
"correlation": "Vergleichen, wie sich Vermögenswerte gemeinsam bewegen.",
|
||||||
"scheduled": "Eine Recherche nach Zeitplan wiederholen.",
|
"scheduled": "Eine Recherche nach Zeitplan wiederholen.",
|
||||||
"runtime": "Brokerverbindungen und den Betriebsstatus des Handels prüfen.",
|
"runtime": "Brokerverbindungen und den Betriebsstatus des Handels prüfen.",
|
||||||
"settings": "Modelle, Datenquellen und Brokerverbindungen konfigurieren."
|
"settings": "Modelle, Datenquellen und Brokerverbindungen konfigurieren.",
|
||||||
|
"pixelOffice": "Plane und verteile Rechercheaufgaben. Eine Zuweisung startet keine Ausführung. Ränge sind Bezeichnungen und erteilen keine Handelsrechte."
|
||||||
},
|
},
|
||||||
"instructions": {
|
"instructions": {
|
||||||
"reports": "Bitte den Assistenten zuerst um einen Strategie-Backtest und öffne hier die Ergebnisse. Im Chat erstellte PDFs und andere Dokumente lädst du in der ursprünglichen Unterhaltung herunter.",
|
"reports": "Bitte den Assistenten zuerst um einen Strategie-Backtest und öffne hier die Ergebnisse. Im Chat erstellte PDFs und andere Dokumente lädst du in der ursprünglichen Unterhaltung herunter.",
|
||||||
@@ -36,7 +37,8 @@
|
|||||||
"correlation": "Gib mindestens zwei Wertpapiercodes ein, wähle den Zeitraum und klicke auf Berechnen.",
|
"correlation": "Gib mindestens zwei Wertpapiercodes ein, wähle den Zeitraum und klicke auf Berechnen.",
|
||||||
"scheduled": "Gib einen Rechercheauftrag ein, wähle die Häufigkeit und speichere. Für die Ausführung muss der lokale Planer aktiviert sein; zur Zustellung ist ein eingerichteter Kanal nötig.",
|
"scheduled": "Gib einen Rechercheauftrag ein, wähle die Häufigkeit und speichere. Für die Ausführung muss der lokale Planer aktiviert sein; zur Zustellung ist ein eingerichteter Kanal nötig.",
|
||||||
"runtime": "Richte zuerst unter Einstellungen oder Portfolio eine Brokerverbindung ein. Verfügbare Handelsaktionen hängen von den Brokerfunktionen und deiner Autorisierung ab.",
|
"runtime": "Richte zuerst unter Einstellungen oder Portfolio eine Brokerverbindung ein. Verfügbare Handelsaktionen hängen von den Brokerfunktionen und deiner Autorisierung ab.",
|
||||||
"settings": "Richte zunächst ein Modell ein. Ergänze Datenquellen oder Brokerverbindungen nach Bedarf; für öffentliche Marktrecherche ist kein Brokerkonto nötig."
|
"settings": "Richte zunächst ein Modell ein. Ergänze Datenquellen oder Brokerverbindungen nach Bedarf; für öffentliche Marktrecherche ist kein Brokerkonto nötig.",
|
||||||
|
"pixelOffice": "Plane und verteile Rechercheaufgaben. Eine Zuweisung startet keine Ausführung. Ränge sind Bezeichnungen und erteilen keine Handelsrechte."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"layout": {
|
"layout": {
|
||||||
@@ -66,7 +68,8 @@
|
|||||||
"mainNavigation": "Hauptnavigation",
|
"mainNavigation": "Hauptnavigation",
|
||||||
"about": "Über",
|
"about": "Über",
|
||||||
"portfolio": "Kontopositionen",
|
"portfolio": "Kontopositionen",
|
||||||
"sessionsLoadFailed": "Gespräche können nicht geladen werden."
|
"sessionsLoadFailed": "Gespräche können nicht geladen werden.",
|
||||||
|
"pixelOffice": "Pixelbüro"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"monitorBadge": "Runtime-Monitor",
|
"monitorBadge": "Runtime-Monitor",
|
||||||
@@ -2380,5 +2383,40 @@
|
|||||||
"preview": "Test passed: {{preview}}",
|
"preview": "Test passed: {{preview}}",
|
||||||
"error": "Provider test failed: {{error}}",
|
"error": "Provider test failed: {{error}}",
|
||||||
"waiting": "The provider is taking longer than usual ({{seconds}}s). The request will continue until the server timeout."
|
"waiting": "The provider is taking longer than usual ({{seconds}}s). The request will continue until the server timeout."
|
||||||
|
},
|
||||||
|
"pixelOffice": {
|
||||||
|
"title": "Pixelbüro",
|
||||||
|
"description": "Plane und verteile Rechercheaufgaben. Eine Zuweisung startet keine Ausführung. Ränge sind Bezeichnungen und erteilen keine Handelsrechte.",
|
||||||
|
"refresh": "Aktualisieren",
|
||||||
|
"loading": "Büro wird geladen…",
|
||||||
|
"unavailable": "Büro nicht verfügbar. Bitte aktualisieren.",
|
||||||
|
"floor": "Bürofläche",
|
||||||
|
"taskCount": "Offene Aufgaben: {{count}}",
|
||||||
|
"board": "Aufgabenübersicht",
|
||||||
|
"empty": "Noch keine Aufgaben.",
|
||||||
|
"assignee": "Zuständig",
|
||||||
|
"unassigned": "Nicht zugewiesen",
|
||||||
|
"result": "Ergebnis",
|
||||||
|
"complete": "Abschluss erfassen",
|
||||||
|
"create": "Aufgabe erstellen",
|
||||||
|
"taskTitle": "Aufgabentitel",
|
||||||
|
"instructions": "Anweisungen",
|
||||||
|
"priority": "Priorität",
|
||||||
|
"ranksTitle": "Organisatorische Ränge",
|
||||||
|
"priorities": {
|
||||||
|
"low": "Niedrig",
|
||||||
|
"normal": "Normal",
|
||||||
|
"high": "Hoch"
|
||||||
|
},
|
||||||
|
"statuses": {
|
||||||
|
"queued": "Wartend",
|
||||||
|
"assigned": "Zugewiesen",
|
||||||
|
"completed": "Abgeschlossen"
|
||||||
|
},
|
||||||
|
"ranks": {
|
||||||
|
"Operator": "Mitglied",
|
||||||
|
"Senior": "Erfahren",
|
||||||
|
"Lead": "Leitung"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,8 @@
|
|||||||
"correlation": "Compare how assets move together.",
|
"correlation": "Compare how assets move together.",
|
||||||
"scheduled": "Repeat a research task on a schedule.",
|
"scheduled": "Repeat a research task on a schedule.",
|
||||||
"runtime": "Check broker connections and trading runtime status.",
|
"runtime": "Check broker connections and trading runtime status.",
|
||||||
"settings": "Configure models, data sources, and broker connections."
|
"settings": "Configure models, data sources, and broker connections.",
|
||||||
|
"pixelOffice": "Plan and assign research tasks. Assignment does not start execution; ranks are labels and grant no trading permissions."
|
||||||
},
|
},
|
||||||
"instructions": {
|
"instructions": {
|
||||||
"reports": "Ask the research assistant to backtest a strategy first, then open its results here. Download PDFs and other chat-generated documents from the original conversation.",
|
"reports": "Ask the research assistant to backtest a strategy first, then open its results here. Download PDFs and other chat-generated documents from the original conversation.",
|
||||||
@@ -36,7 +37,8 @@
|
|||||||
"correlation": "Enter at least two asset symbols, choose a lookback window, then select Compute.",
|
"correlation": "Enter at least two asset symbols, choose a lookback window, then select Compute.",
|
||||||
"scheduled": "Enter a research instruction, choose its cadence, and save. The local scheduler must be enabled for jobs to run; delivery also needs a configured channel.",
|
"scheduled": "Enter a research instruction, choose its cadence, and save. The local scheduler must be enabled for jobs to run; delivery also needs a configured channel.",
|
||||||
"runtime": "Configure a broker connection in Settings or Portfolio first. Available trading actions depend on that broker's capabilities and your authorization.",
|
"runtime": "Configure a broker connection in Settings or Portfolio first. Available trading actions depend on that broker's capabilities and your authorization.",
|
||||||
"settings": "Configure a model to start. Add data sources or broker connections as your task needs them; public-market research does not require a broker account."
|
"settings": "Configure a model to start. Add data sources or broker connections as your task needs them; public-market research does not require a broker account.",
|
||||||
|
"pixelOffice": "Plan and assign research tasks. Assignment does not start execution; ranks are labels and grant no trading permissions."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"layout": {
|
"layout": {
|
||||||
@@ -66,7 +68,8 @@
|
|||||||
"sidebar": "Vibe-Trading sidebar",
|
"sidebar": "Vibe-Trading sidebar",
|
||||||
"mainNavigation": "Main navigation",
|
"mainNavigation": "Main navigation",
|
||||||
"about": "About",
|
"about": "About",
|
||||||
"sessionsLoadFailed": "Cannot load conversations."
|
"sessionsLoadFailed": "Cannot load conversations.",
|
||||||
|
"pixelOffice": "Pixel Office"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"monitorBadge": "Runtime Monitor",
|
"monitorBadge": "Runtime Monitor",
|
||||||
@@ -2380,5 +2383,40 @@
|
|||||||
"preview": "Test passed: {{preview}}",
|
"preview": "Test passed: {{preview}}",
|
||||||
"error": "Provider test failed: {{error}}",
|
"error": "Provider test failed: {{error}}",
|
||||||
"waiting": "The provider is taking longer than usual ({{seconds}}s). The request will continue until the server timeout."
|
"waiting": "The provider is taking longer than usual ({{seconds}}s). The request will continue until the server timeout."
|
||||||
|
},
|
||||||
|
"pixelOffice": {
|
||||||
|
"title": "Pixel Office",
|
||||||
|
"description": "Plan and assign research tasks. Assignment does not start execution; ranks are labels and grant no trading permissions.",
|
||||||
|
"refresh": "Refresh",
|
||||||
|
"loading": "Loading office…",
|
||||||
|
"unavailable": "Office unavailable. Try refreshing.",
|
||||||
|
"floor": "Office floor",
|
||||||
|
"taskCount": "Open tasks: {{count}}",
|
||||||
|
"board": "Task board",
|
||||||
|
"empty": "No tasks yet.",
|
||||||
|
"assignee": "Assignee",
|
||||||
|
"unassigned": "Unassigned",
|
||||||
|
"result": "Completion result",
|
||||||
|
"complete": "Record completion",
|
||||||
|
"create": "Create task",
|
||||||
|
"taskTitle": "Task title",
|
||||||
|
"instructions": "Instructions",
|
||||||
|
"priority": "Priority",
|
||||||
|
"ranksTitle": "Organizational ranks",
|
||||||
|
"priorities": {
|
||||||
|
"low": "Low",
|
||||||
|
"normal": "Normal",
|
||||||
|
"high": "High"
|
||||||
|
},
|
||||||
|
"statuses": {
|
||||||
|
"queued": "Queued",
|
||||||
|
"assigned": "Assigned",
|
||||||
|
"completed": "Completed"
|
||||||
|
},
|
||||||
|
"ranks": {
|
||||||
|
"Operator": "Operator",
|
||||||
|
"Senior": "Senior",
|
||||||
|
"Lead": "Lead"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,8 @@
|
|||||||
"correlation": "Compara cómo se mueven los activos entre sí.",
|
"correlation": "Compara cómo se mueven los activos entre sí.",
|
||||||
"scheduled": "Repite una investigación según un horario.",
|
"scheduled": "Repite una investigación según un horario.",
|
||||||
"runtime": "Comprueba conexiones de brókeres y el estado de ejecución de operaciones.",
|
"runtime": "Comprueba conexiones de brókeres y el estado de ejecución de operaciones.",
|
||||||
"settings": "Configura modelos, fuentes de datos y conexiones de brókeres."
|
"settings": "Configura modelos, fuentes de datos y conexiones de brókeres.",
|
||||||
|
"pixelOffice": "Planifica y asigna tareas de investigación. La asignación no inicia la ejecución; los rangos son etiquetas y no conceden permisos de negociación."
|
||||||
},
|
},
|
||||||
"instructions": {
|
"instructions": {
|
||||||
"reports": "Primero pide al asistente un backtest de una estrategia y abre aquí sus resultados. Descarga los PDF y documentos generados en el chat desde la conversación original.",
|
"reports": "Primero pide al asistente un backtest de una estrategia y abre aquí sus resultados. Descarga los PDF y documentos generados en el chat desde la conversación original.",
|
||||||
@@ -36,7 +37,8 @@
|
|||||||
"correlation": "Introduce al menos dos símbolos de activos, elige el período y pulsa Calcular.",
|
"correlation": "Introduce al menos dos símbolos de activos, elige el período y pulsa Calcular.",
|
||||||
"scheduled": "Introduce la instrucción de investigación, elige la frecuencia y guarda. El programador local debe estar activado para ejecutar tareas; el envío requiere un canal configurado.",
|
"scheduled": "Introduce la instrucción de investigación, elige la frecuencia y guarda. El programador local debe estar activado para ejecutar tareas; el envío requiere un canal configurado.",
|
||||||
"runtime": "Primero configura una conexión de bróker en Ajustes o Cartera. Las operaciones disponibles dependen de sus capacidades y de tu autorización.",
|
"runtime": "Primero configura una conexión de bróker en Ajustes o Cartera. Las operaciones disponibles dependen de sus capacidades y de tu autorización.",
|
||||||
"settings": "Configura un modelo para empezar. Añade datos o conexiones de brókeres según lo necesites; investigar datos públicos de mercado no requiere una cuenta de bróker."
|
"settings": "Configura un modelo para empezar. Añade datos o conexiones de brókeres según lo necesites; investigar datos públicos de mercado no requiere una cuenta de bróker.",
|
||||||
|
"pixelOffice": "Planifica y asigna tareas de investigación. La asignación no inicia la ejecución; los rangos son etiquetas y no conceden permisos de negociación."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"layout": {
|
"layout": {
|
||||||
@@ -66,7 +68,8 @@
|
|||||||
"sidebar": "Barra lateral de Vibe-Trading",
|
"sidebar": "Barra lateral de Vibe-Trading",
|
||||||
"mainNavigation": "Navegación principal",
|
"mainNavigation": "Navegación principal",
|
||||||
"about": "Acerca de",
|
"about": "Acerca de",
|
||||||
"sessionsLoadFailed": "No se pueden cargar las conversaciones."
|
"sessionsLoadFailed": "No se pueden cargar las conversaciones.",
|
||||||
|
"pixelOffice": "Oficina de píxeles"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"monitorBadge": "Monitor de Runtime",
|
"monitorBadge": "Monitor de Runtime",
|
||||||
@@ -2380,5 +2383,40 @@
|
|||||||
"preview": "Test passed: {{preview}}",
|
"preview": "Test passed: {{preview}}",
|
||||||
"error": "Provider test failed: {{error}}",
|
"error": "Provider test failed: {{error}}",
|
||||||
"waiting": "The provider is taking longer than usual ({{seconds}}s). The request will continue until the server timeout."
|
"waiting": "The provider is taking longer than usual ({{seconds}}s). The request will continue until the server timeout."
|
||||||
|
},
|
||||||
|
"pixelOffice": {
|
||||||
|
"title": "Oficina de píxeles",
|
||||||
|
"description": "Planifica y asigna tareas de investigación. La asignación no inicia la ejecución; los rangos son etiquetas y no conceden permisos de negociación.",
|
||||||
|
"refresh": "Actualizar",
|
||||||
|
"loading": "Cargando oficina…",
|
||||||
|
"unavailable": "Oficina no disponible. Intenta actualizar.",
|
||||||
|
"floor": "Área de oficinas",
|
||||||
|
"taskCount": "Tareas pendientes: {{count}}",
|
||||||
|
"board": "Tablero de tareas",
|
||||||
|
"empty": "Todavía no hay tareas.",
|
||||||
|
"assignee": "Responsable",
|
||||||
|
"unassigned": "Sin asignar",
|
||||||
|
"result": "Resultado de la tarea",
|
||||||
|
"complete": "Registrar finalización",
|
||||||
|
"create": "Crear tarea",
|
||||||
|
"taskTitle": "Título de la tarea",
|
||||||
|
"instructions": "Instrucciones",
|
||||||
|
"priority": "Prioridad",
|
||||||
|
"ranksTitle": "Rangos organizativos",
|
||||||
|
"priorities": {
|
||||||
|
"low": "Baja",
|
||||||
|
"normal": "Normal",
|
||||||
|
"high": "Alta"
|
||||||
|
},
|
||||||
|
"statuses": {
|
||||||
|
"queued": "En espera",
|
||||||
|
"assigned": "Asignada",
|
||||||
|
"completed": "Completada"
|
||||||
|
},
|
||||||
|
"ranks": {
|
||||||
|
"Operator": "Miembro",
|
||||||
|
"Senior": "Sénior",
|
||||||
|
"Lead": "Responsable"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,8 @@
|
|||||||
"correlation": "Compare how assets move together.",
|
"correlation": "Compare how assets move together.",
|
||||||
"scheduled": "Repeat a research task on a schedule.",
|
"scheduled": "Repeat a research task on a schedule.",
|
||||||
"runtime": "Check broker connections and trading runtime status.",
|
"runtime": "Check broker connections and trading runtime status.",
|
||||||
"settings": "Configure models, data sources, and broker connections."
|
"settings": "Configure models, data sources, and broker connections.",
|
||||||
|
"pixelOffice": "کارهای پژوهشی را برنامهریزی و واگذار کنید. واگذاری باعث شروع اجرا نمیشود؛ رتبهها فقط برچسب هستند و مجوز معامله نمیدهند."
|
||||||
},
|
},
|
||||||
"instructions": {
|
"instructions": {
|
||||||
"reports": "Ask the research assistant to backtest a strategy first, then open its results here. Download PDFs and other chat-generated documents from the original conversation.",
|
"reports": "Ask the research assistant to backtest a strategy first, then open its results here. Download PDFs and other chat-generated documents from the original conversation.",
|
||||||
@@ -36,7 +37,8 @@
|
|||||||
"correlation": "Enter at least two asset symbols, choose a lookback window, then select Compute.",
|
"correlation": "Enter at least two asset symbols, choose a lookback window, then select Compute.",
|
||||||
"scheduled": "Enter a research instruction, choose its cadence, and save. The local scheduler must be enabled for jobs to run; delivery also needs a configured channel.",
|
"scheduled": "Enter a research instruction, choose its cadence, and save. The local scheduler must be enabled for jobs to run; delivery also needs a configured channel.",
|
||||||
"runtime": "Configure a broker connection in Settings or Portfolio first. Available trading actions depend on that broker's capabilities and your authorization.",
|
"runtime": "Configure a broker connection in Settings or Portfolio first. Available trading actions depend on that broker's capabilities and your authorization.",
|
||||||
"settings": "Configure a model to start. Add data sources or broker connections as your task needs them; public-market research does not require a broker account."
|
"settings": "Configure a model to start. Add data sources or broker connections as your task needs them; public-market research does not require a broker account.",
|
||||||
|
"pixelOffice": "کارهای پژوهشی را برنامهریزی و واگذار کنید. واگذاری باعث شروع اجرا نمیشود؛ رتبهها فقط برچسب هستند و مجوز معامله نمیدهند."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"layout": {
|
"layout": {
|
||||||
@@ -66,7 +68,8 @@
|
|||||||
"sidebar": "نوار کناری Vibe-Trading",
|
"sidebar": "نوار کناری Vibe-Trading",
|
||||||
"mainNavigation": "ناوبری اصلی",
|
"mainNavigation": "ناوبری اصلی",
|
||||||
"about": "درباره",
|
"about": "درباره",
|
||||||
"sessionsLoadFailed": "Cannot load conversations."
|
"sessionsLoadFailed": "Cannot load conversations.",
|
||||||
|
"pixelOffice": "دفتر پیکسلی"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"monitorBadge": "مانیتور رانتایم",
|
"monitorBadge": "مانیتور رانتایم",
|
||||||
@@ -2380,5 +2383,40 @@
|
|||||||
"preview": "Test passed: {{preview}}",
|
"preview": "Test passed: {{preview}}",
|
||||||
"error": "Provider test failed: {{error}}",
|
"error": "Provider test failed: {{error}}",
|
||||||
"waiting": "The provider is taking longer than usual ({{seconds}}s). The request will continue until the server timeout."
|
"waiting": "The provider is taking longer than usual ({{seconds}}s). The request will continue until the server timeout."
|
||||||
|
},
|
||||||
|
"pixelOffice": {
|
||||||
|
"title": "دفتر پیکسلی",
|
||||||
|
"description": "کارهای پژوهشی را برنامهریزی و واگذار کنید. واگذاری باعث شروع اجرا نمیشود؛ رتبهها فقط برچسب هستند و مجوز معامله نمیدهند.",
|
||||||
|
"refresh": "تازهسازی",
|
||||||
|
"loading": "در حال بارگذاری دفتر…",
|
||||||
|
"unavailable": "دفتر در دسترس نیست. دوباره تازهسازی کنید.",
|
||||||
|
"floor": "فضای دفتر",
|
||||||
|
"taskCount": "کارهای باز: {{count}}",
|
||||||
|
"board": "تابلوی کارها",
|
||||||
|
"empty": "هنوز کاری وجود ندارد.",
|
||||||
|
"assignee": "مسئول",
|
||||||
|
"unassigned": "واگذار نشده",
|
||||||
|
"result": "نتیجه انجام کار",
|
||||||
|
"complete": "ثبت تکمیل",
|
||||||
|
"create": "ایجاد کار",
|
||||||
|
"taskTitle": "عنوان کار",
|
||||||
|
"instructions": "دستورالعمل",
|
||||||
|
"priority": "اولویت",
|
||||||
|
"ranksTitle": "رتبههای سازمانی",
|
||||||
|
"priorities": {
|
||||||
|
"low": "کم",
|
||||||
|
"normal": "عادی",
|
||||||
|
"high": "زیاد"
|
||||||
|
},
|
||||||
|
"statuses": {
|
||||||
|
"queued": "در انتظار",
|
||||||
|
"assigned": "واگذار شده",
|
||||||
|
"completed": "تکمیل شده"
|
||||||
|
},
|
||||||
|
"ranks": {
|
||||||
|
"Operator": "عضو",
|
||||||
|
"Senior": "عضو ارشد",
|
||||||
|
"Lead": "سرپرست"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,8 @@
|
|||||||
"correlation": "Bandingkan hubungan pergerakan aset.",
|
"correlation": "Bandingkan hubungan pergerakan aset.",
|
||||||
"scheduled": "Ulangi tugas riset sesuai jadwal.",
|
"scheduled": "Ulangi tugas riset sesuai jadwal.",
|
||||||
"runtime": "Periksa koneksi broker dan status pelaksanaan perdagangan.",
|
"runtime": "Periksa koneksi broker dan status pelaksanaan perdagangan.",
|
||||||
"settings": "Atur model, sumber data, dan koneksi broker."
|
"settings": "Atur model, sumber data, dan koneksi broker.",
|
||||||
|
"pixelOffice": "Rencanakan dan tugaskan riset. Penugasan tidak memulai eksekusi; pangkat hanya label dan tidak memberikan izin transaksi."
|
||||||
},
|
},
|
||||||
"instructions": {
|
"instructions": {
|
||||||
"reports": "Minta asisten melakukan backtest strategi terlebih dahulu, lalu buka hasilnya di sini. Unduh PDF dan dokumen yang dibuat di chat dari percakapan asal.",
|
"reports": "Minta asisten melakukan backtest strategi terlebih dahulu, lalu buka hasilnya di sini. Unduh PDF dan dokumen yang dibuat di chat dari percakapan asal.",
|
||||||
@@ -36,7 +37,8 @@
|
|||||||
"correlation": "Masukkan setidaknya dua simbol aset, pilih periode, lalu klik Hitung.",
|
"correlation": "Masukkan setidaknya dua simbol aset, pilih periode, lalu klik Hitung.",
|
||||||
"scheduled": "Isi instruksi riset, pilih frekuensi, dan simpan. Penjadwal lokal harus diaktifkan agar tugas berjalan; pengiriman juga memerlukan kanal yang telah diatur.",
|
"scheduled": "Isi instruksi riset, pilih frekuensi, dan simpan. Penjadwal lokal harus diaktifkan agar tugas berjalan; pengiriman juga memerlukan kanal yang telah diatur.",
|
||||||
"runtime": "Atur koneksi broker di Pengaturan atau Portofolio terlebih dahulu. Tindakan perdagangan yang tersedia bergantung pada kemampuan broker dan otorisasi Anda.",
|
"runtime": "Atur koneksi broker di Pengaturan atau Portofolio terlebih dahulu. Tindakan perdagangan yang tersedia bergantung pada kemampuan broker dan otorisasi Anda.",
|
||||||
"settings": "Atur model untuk memulai. Tambahkan data atau koneksi broker sesuai kebutuhan; riset data pasar publik tidak memerlukan akun broker."
|
"settings": "Atur model untuk memulai. Tambahkan data atau koneksi broker sesuai kebutuhan; riset data pasar publik tidak memerlukan akun broker.",
|
||||||
|
"pixelOffice": "Rencanakan dan tugaskan riset. Penugasan tidak memulai eksekusi; pangkat hanya label dan tidak memberikan izin transaksi."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"layout": {
|
"layout": {
|
||||||
@@ -66,7 +68,8 @@
|
|||||||
"sidebar": "Sidebar Vibe-Trading",
|
"sidebar": "Sidebar Vibe-Trading",
|
||||||
"mainNavigation": "Navigasi utama",
|
"mainNavigation": "Navigasi utama",
|
||||||
"about": "Tentang",
|
"about": "Tentang",
|
||||||
"sessionsLoadFailed": "Percakapan tidak dapat dimuat."
|
"sessionsLoadFailed": "Percakapan tidak dapat dimuat.",
|
||||||
|
"pixelOffice": "Kantor piksel"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"monitorBadge": "Monitor Runtime",
|
"monitorBadge": "Monitor Runtime",
|
||||||
@@ -2380,5 +2383,40 @@
|
|||||||
"preview": "Test passed: {{preview}}",
|
"preview": "Test passed: {{preview}}",
|
||||||
"error": "Provider test failed: {{error}}",
|
"error": "Provider test failed: {{error}}",
|
||||||
"waiting": "The provider is taking longer than usual ({{seconds}}s). The request will continue until the server timeout."
|
"waiting": "The provider is taking longer than usual ({{seconds}}s). The request will continue until the server timeout."
|
||||||
|
},
|
||||||
|
"pixelOffice": {
|
||||||
|
"title": "Kantor piksel",
|
||||||
|
"description": "Rencanakan dan tugaskan riset. Penugasan tidak memulai eksekusi; pangkat hanya label dan tidak memberikan izin transaksi.",
|
||||||
|
"refresh": "Muat ulang",
|
||||||
|
"loading": "Memuat kantor…",
|
||||||
|
"unavailable": "Kantor tidak tersedia. Coba muat ulang.",
|
||||||
|
"floor": "Area kantor",
|
||||||
|
"taskCount": "Tugas terbuka: {{count}}",
|
||||||
|
"board": "Papan tugas",
|
||||||
|
"empty": "Belum ada tugas.",
|
||||||
|
"assignee": "Penanggung jawab",
|
||||||
|
"unassigned": "Belum ditugaskan",
|
||||||
|
"result": "Hasil penyelesaian",
|
||||||
|
"complete": "Catat penyelesaian",
|
||||||
|
"create": "Buat tugas",
|
||||||
|
"taskTitle": "Judul tugas",
|
||||||
|
"instructions": "Instruksi",
|
||||||
|
"priority": "Prioritas",
|
||||||
|
"ranksTitle": "Pangkat organisasi",
|
||||||
|
"priorities": {
|
||||||
|
"low": "Rendah",
|
||||||
|
"normal": "Normal",
|
||||||
|
"high": "Tinggi"
|
||||||
|
},
|
||||||
|
"statuses": {
|
||||||
|
"queued": "Mengantre",
|
||||||
|
"assigned": "Ditugaskan",
|
||||||
|
"completed": "Selesai"
|
||||||
|
},
|
||||||
|
"ranks": {
|
||||||
|
"Operator": "Anggota",
|
||||||
|
"Senior": "Senior",
|
||||||
|
"Lead": "Pemimpin"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,8 @@
|
|||||||
"correlation": "複数の資産の値動きの関係を比較します。",
|
"correlation": "複数の資産の値動きの関係を比較します。",
|
||||||
"scheduled": "リサーチを指定した周期で繰り返します。",
|
"scheduled": "リサーチを指定した周期で繰り返します。",
|
||||||
"runtime": "ブローカーの接続と取引の稼働状況を確認します。",
|
"runtime": "ブローカーの接続と取引の稼働状況を確認します。",
|
||||||
"settings": "モデル、データソース、ブローカー接続を設定します。"
|
"settings": "モデル、データソース、ブローカー接続を設定します。",
|
||||||
|
"pixelOffice": "調査タスクを計画・割り当てます。割り当てでは実行は始まりません。役職はラベルであり、取引権限を付与しません。"
|
||||||
},
|
},
|
||||||
"instructions": {
|
"instructions": {
|
||||||
"reports": "まずアシスタントに戦略のバックテストを依頼し、ここで結果を開きます。会話で作成した PDF などは元の会話からダウンロードします。",
|
"reports": "まずアシスタントに戦略のバックテストを依頼し、ここで結果を開きます。会話で作成した PDF などは元の会話からダウンロードします。",
|
||||||
@@ -31,7 +32,8 @@
|
|||||||
"correlation": "資産コードを2つ以上入力し、対象期間を選んで計算します。",
|
"correlation": "資産コードを2つ以上入力し、対象期間を選んで計算します。",
|
||||||
"scheduled": "リサーチ指示と周期を入力して保存します。実行にはローカルのスケジューラーを有効にする必要があり、配信にはチャネル設定も必要です。",
|
"scheduled": "リサーチ指示と周期を入力して保存します。実行にはローカルのスケジューラーを有効にする必要があり、配信にはチャネル設定も必要です。",
|
||||||
"runtime": "先に設定または保有資産ページでブローカーを接続します。利用できる取引操作はブローカーの機能とあなたの許可に依存します。",
|
"runtime": "先に設定または保有資産ページでブローカーを接続します。利用できる取引操作はブローカーの機能とあなたの許可に依存します。",
|
||||||
"settings": "まずモデルを設定します。必要に応じてデータソースやブローカー接続を追加します。公開市場のリサーチにはブローカー口座は不要です。"
|
"settings": "まずモデルを設定します。必要に応じてデータソースやブローカー接続を追加します。公開市場のリサーチにはブローカー口座は不要です。",
|
||||||
|
"pixelOffice": "調査タスクを計画・割り当てます。割り当てでは実行は始まりません。役職はラベルであり、取引権限を付与しません。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"layout": {
|
"layout": {
|
||||||
@@ -61,7 +63,8 @@
|
|||||||
"sidebar": "Vibe-Trading サイドバー",
|
"sidebar": "Vibe-Trading サイドバー",
|
||||||
"mainNavigation": "メインナビゲーション",
|
"mainNavigation": "メインナビゲーション",
|
||||||
"about": "概要",
|
"about": "概要",
|
||||||
"sessionsLoadFailed": "会話を読み込めません。"
|
"sessionsLoadFailed": "会話を読み込めません。",
|
||||||
|
"pixelOffice": "ピクセルオフィス"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"monitorBadge": "取引モニター",
|
"monitorBadge": "取引モニター",
|
||||||
@@ -2380,5 +2383,40 @@
|
|||||||
"preview": "Test passed: {{preview}}",
|
"preview": "Test passed: {{preview}}",
|
||||||
"error": "Provider test failed: {{error}}",
|
"error": "Provider test failed: {{error}}",
|
||||||
"waiting": "The provider is taking longer than usual ({{seconds}}s). The request will continue until the server timeout."
|
"waiting": "The provider is taking longer than usual ({{seconds}}s). The request will continue until the server timeout."
|
||||||
|
},
|
||||||
|
"pixelOffice": {
|
||||||
|
"title": "ピクセルオフィス",
|
||||||
|
"description": "調査タスクを計画・割り当てます。割り当てでは実行は始まりません。役職はラベルであり、取引権限を付与しません。",
|
||||||
|
"refresh": "更新",
|
||||||
|
"loading": "オフィスを読み込み中…",
|
||||||
|
"unavailable": "オフィスを利用できません。再読み込みしてください。",
|
||||||
|
"floor": "オフィスフロア",
|
||||||
|
"taskCount": "未完了タスク:{{count}}",
|
||||||
|
"board": "タスクボード",
|
||||||
|
"empty": "タスクはまだありません。",
|
||||||
|
"assignee": "担当者",
|
||||||
|
"unassigned": "未割り当て",
|
||||||
|
"result": "完了結果",
|
||||||
|
"complete": "完了を記録",
|
||||||
|
"create": "タスクを作成",
|
||||||
|
"taskTitle": "タスク名",
|
||||||
|
"instructions": "指示",
|
||||||
|
"priority": "優先度",
|
||||||
|
"ranksTitle": "組織上の役職",
|
||||||
|
"priorities": {
|
||||||
|
"low": "低",
|
||||||
|
"normal": "通常",
|
||||||
|
"high": "高"
|
||||||
|
},
|
||||||
|
"statuses": {
|
||||||
|
"queued": "待機中",
|
||||||
|
"assigned": "割り当て済み",
|
||||||
|
"completed": "完了"
|
||||||
|
},
|
||||||
|
"ranks": {
|
||||||
|
"Operator": "担当者",
|
||||||
|
"Senior": "シニア",
|
||||||
|
"Lead": "リーダー"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,8 @@
|
|||||||
"correlation": "여러 자산의 가격 움직임 관계를 비교합니다.",
|
"correlation": "여러 자산의 가격 움직임 관계를 비교합니다.",
|
||||||
"scheduled": "지정한 주기로 리서치를 반복합니다.",
|
"scheduled": "지정한 주기로 리서치를 반복합니다.",
|
||||||
"runtime": "브로커 연결과 거래 실행 상태를 확인합니다.",
|
"runtime": "브로커 연결과 거래 실행 상태를 확인합니다.",
|
||||||
"settings": "모델, 데이터 소스, 브로커 연결을 설정합니다."
|
"settings": "모델, 데이터 소스, 브로커 연결을 설정합니다.",
|
||||||
|
"pixelOffice": "리서치 작업을 계획하고 배정합니다. 배정만으로 실행되지는 않으며, 직급은 거래 권한을 부여하지 않는 표시에 불과합니다."
|
||||||
},
|
},
|
||||||
"instructions": {
|
"instructions": {
|
||||||
"reports": "먼저 도우미에게 전략 백테스트를 요청한 후 여기에서 결과를 엽니다. 대화에서 생성한 PDF 등은 원래 대화에서 다운로드하세요.",
|
"reports": "먼저 도우미에게 전략 백테스트를 요청한 후 여기에서 결과를 엽니다. 대화에서 생성한 PDF 등은 원래 대화에서 다운로드하세요.",
|
||||||
@@ -31,7 +32,8 @@
|
|||||||
"correlation": "자산 코드를 두 개 이상 입력하고 기간을 선택한 다음 계산을 누르세요.",
|
"correlation": "자산 코드를 두 개 이상 입력하고 기간을 선택한 다음 계산을 누르세요.",
|
||||||
"scheduled": "리서치 지시와 실행 주기를 입력하고 저장하세요. 실행하려면 로컬 스케줄러가 활성화되어 있어야 하며, 결과 전달에는 채널 설정이 필요합니다.",
|
"scheduled": "리서치 지시와 실행 주기를 입력하고 저장하세요. 실행하려면 로컬 스케줄러가 활성화되어 있어야 하며, 결과 전달에는 채널 설정이 필요합니다.",
|
||||||
"runtime": "먼저 설정 또는 보유 자산 페이지에서 브로커를 연결하세요. 사용 가능한 거래 작업은 브로커 기능과 사용자의 승인에 따라 달라집니다.",
|
"runtime": "먼저 설정 또는 보유 자산 페이지에서 브로커를 연결하세요. 사용 가능한 거래 작업은 브로커 기능과 사용자의 승인에 따라 달라집니다.",
|
||||||
"settings": "먼저 모델을 설정하세요. 필요에 따라 데이터 소스나 브로커 연결을 추가하면 됩니다. 공개 시장 리서치에는 브로커 계좌가 필요하지 않습니다."
|
"settings": "먼저 모델을 설정하세요. 필요에 따라 데이터 소스나 브로커 연결을 추가하면 됩니다. 공개 시장 리서치에는 브로커 계좌가 필요하지 않습니다.",
|
||||||
|
"pixelOffice": "리서치 작업을 계획하고 배정합니다. 배정만으로 실행되지는 않으며, 직급은 거래 권한을 부여하지 않는 표시에 불과합니다."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"layout": {
|
"layout": {
|
||||||
@@ -61,7 +63,8 @@
|
|||||||
"sidebar": "Vibe-Trading 사이드바",
|
"sidebar": "Vibe-Trading 사이드바",
|
||||||
"mainNavigation": "주 내비게이션",
|
"mainNavigation": "주 내비게이션",
|
||||||
"about": "소개",
|
"about": "소개",
|
||||||
"sessionsLoadFailed": "대화를 불러올 수 없습니다."
|
"sessionsLoadFailed": "대화를 불러올 수 없습니다.",
|
||||||
|
"pixelOffice": "픽셀 오피스"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"monitorBadge": "거래 모니터",
|
"monitorBadge": "거래 모니터",
|
||||||
@@ -2380,5 +2383,40 @@
|
|||||||
"preview": "Test passed: {{preview}}",
|
"preview": "Test passed: {{preview}}",
|
||||||
"error": "Provider test failed: {{error}}",
|
"error": "Provider test failed: {{error}}",
|
||||||
"waiting": "The provider is taking longer than usual ({{seconds}}s). The request will continue until the server timeout."
|
"waiting": "The provider is taking longer than usual ({{seconds}}s). The request will continue until the server timeout."
|
||||||
|
},
|
||||||
|
"pixelOffice": {
|
||||||
|
"title": "픽셀 오피스",
|
||||||
|
"description": "리서치 작업을 계획하고 배정합니다. 배정만으로 실행되지는 않으며, 직급은 거래 권한을 부여하지 않는 표시에 불과합니다.",
|
||||||
|
"refresh": "새로고침",
|
||||||
|
"loading": "오피스 불러오는 중…",
|
||||||
|
"unavailable": "오피스를 이용할 수 없습니다. 새로고침해 주세요.",
|
||||||
|
"floor": "사무 공간",
|
||||||
|
"taskCount": "미완료 작업: {{count}}",
|
||||||
|
"board": "작업 보드",
|
||||||
|
"empty": "아직 작업이 없습니다.",
|
||||||
|
"assignee": "담당자",
|
||||||
|
"unassigned": "미배정",
|
||||||
|
"result": "완료 결과",
|
||||||
|
"complete": "완료 기록",
|
||||||
|
"create": "작업 만들기",
|
||||||
|
"taskTitle": "작업 제목",
|
||||||
|
"instructions": "작업 지침",
|
||||||
|
"priority": "우선순위",
|
||||||
|
"ranksTitle": "조직 직급",
|
||||||
|
"priorities": {
|
||||||
|
"low": "낮음",
|
||||||
|
"normal": "보통",
|
||||||
|
"high": "높음"
|
||||||
|
},
|
||||||
|
"statuses": {
|
||||||
|
"queued": "대기 중",
|
||||||
|
"assigned": "배정됨",
|
||||||
|
"completed": "완료됨"
|
||||||
|
},
|
||||||
|
"ranks": {
|
||||||
|
"Operator": "담당자",
|
||||||
|
"Senior": "선임",
|
||||||
|
"Lead": "책임자"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,8 @@
|
|||||||
"correlation": "Compare como os ativos se movimentam juntos.",
|
"correlation": "Compare como os ativos se movimentam juntos.",
|
||||||
"scheduled": "Repita uma pesquisa em horários definidos.",
|
"scheduled": "Repita uma pesquisa em horários definidos.",
|
||||||
"runtime": "Confira conexões de corretoras e o estado de execução das operações.",
|
"runtime": "Confira conexões de corretoras e o estado de execução das operações.",
|
||||||
"settings": "Configure modelos, fontes de dados e conexões de corretoras."
|
"settings": "Configure modelos, fontes de dados e conexões de corretoras.",
|
||||||
|
"pixelOffice": "Planeje e atribua tarefas de pesquisa. A atribuição não inicia a execução; os cargos são rótulos e não concedem permissões de negociação."
|
||||||
},
|
},
|
||||||
"instructions": {
|
"instructions": {
|
||||||
"reports": "Primeiro peça ao assistente um backtest de uma estratégia e abra os resultados aqui. Baixe PDFs e documentos gerados no chat na conversa original.",
|
"reports": "Primeiro peça ao assistente um backtest de uma estratégia e abra os resultados aqui. Baixe PDFs e documentos gerados no chat na conversa original.",
|
||||||
@@ -36,7 +37,8 @@
|
|||||||
"correlation": "Informe pelo menos dois códigos de ativos, escolha o período e clique em Calcular.",
|
"correlation": "Informe pelo menos dois códigos de ativos, escolha o período e clique em Calcular.",
|
||||||
"scheduled": "Informe a instrução de pesquisa, escolha a frequência e salve. O agendador local deve estar habilitado para executar tarefas; a entrega também requer um canal configurado.",
|
"scheduled": "Informe a instrução de pesquisa, escolha a frequência e salve. O agendador local deve estar habilitado para executar tarefas; a entrega também requer um canal configurado.",
|
||||||
"runtime": "Primeiro configure uma conexão de corretora em Configurações ou Carteira. As operações disponíveis dependem dos recursos da corretora e da sua autorização.",
|
"runtime": "Primeiro configure uma conexão de corretora em Configurações ou Carteira. As operações disponíveis dependem dos recursos da corretora e da sua autorização.",
|
||||||
"settings": "Configure um modelo para começar. Adicione dados ou conexões de corretoras conforme necessário; pesquisas com dados públicos de mercado não exigem conta em corretora."
|
"settings": "Configure um modelo para começar. Adicione dados ou conexões de corretoras conforme necessário; pesquisas com dados públicos de mercado não exigem conta em corretora.",
|
||||||
|
"pixelOffice": "Planeje e atribua tarefas de pesquisa. A atribuição não inicia a execução; os cargos são rótulos e não concedem permissões de negociação."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"layout": {
|
"layout": {
|
||||||
@@ -66,7 +68,8 @@
|
|||||||
"sidebar": "Barra lateral Vibe-Trading",
|
"sidebar": "Barra lateral Vibe-Trading",
|
||||||
"mainNavigation": "Navegação principal",
|
"mainNavigation": "Navegação principal",
|
||||||
"about": "Sobre",
|
"about": "Sobre",
|
||||||
"sessionsLoadFailed": "Não foi possível carregar as conversas."
|
"sessionsLoadFailed": "Não foi possível carregar as conversas.",
|
||||||
|
"pixelOffice": "Escritório pixel"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"monitorBadge": "Monitor de Runtime",
|
"monitorBadge": "Monitor de Runtime",
|
||||||
@@ -2380,5 +2383,40 @@
|
|||||||
"preview": "Test passed: {{preview}}",
|
"preview": "Test passed: {{preview}}",
|
||||||
"error": "Provider test failed: {{error}}",
|
"error": "Provider test failed: {{error}}",
|
||||||
"waiting": "The provider is taking longer than usual ({{seconds}}s). The request will continue until the server timeout."
|
"waiting": "The provider is taking longer than usual ({{seconds}}s). The request will continue until the server timeout."
|
||||||
|
},
|
||||||
|
"pixelOffice": {
|
||||||
|
"title": "Escritório pixel",
|
||||||
|
"description": "Planeje e atribua tarefas de pesquisa. A atribuição não inicia a execução; os cargos são rótulos e não concedem permissões de negociação.",
|
||||||
|
"refresh": "Atualizar",
|
||||||
|
"loading": "Carregando escritório…",
|
||||||
|
"unavailable": "Escritório indisponível. Tente atualizar.",
|
||||||
|
"floor": "Área do escritório",
|
||||||
|
"taskCount": "Tarefas pendentes: {{count}}",
|
||||||
|
"board": "Quadro de tarefas",
|
||||||
|
"empty": "Ainda não há tarefas.",
|
||||||
|
"assignee": "Responsável",
|
||||||
|
"unassigned": "Sem responsável",
|
||||||
|
"result": "Resultado da conclusão",
|
||||||
|
"complete": "Registrar conclusão",
|
||||||
|
"create": "Criar tarefa",
|
||||||
|
"taskTitle": "Título da tarefa",
|
||||||
|
"instructions": "Instruções",
|
||||||
|
"priority": "Prioridade",
|
||||||
|
"ranksTitle": "Cargos organizacionais",
|
||||||
|
"priorities": {
|
||||||
|
"low": "Baixa",
|
||||||
|
"normal": "Normal",
|
||||||
|
"high": "Alta"
|
||||||
|
},
|
||||||
|
"statuses": {
|
||||||
|
"queued": "Na fila",
|
||||||
|
"assigned": "Atribuída",
|
||||||
|
"completed": "Concluída"
|
||||||
|
},
|
||||||
|
"ranks": {
|
||||||
|
"Operator": "Membro",
|
||||||
|
"Senior": "Sênior",
|
||||||
|
"Lead": "Líder"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -21,7 +21,8 @@
|
|||||||
"correlation": "比较多只资产是否同涨同跌。",
|
"correlation": "比较多只资产是否同涨同跌。",
|
||||||
"scheduled": "按指定时间重复执行研究任务。",
|
"scheduled": "按指定时间重复执行研究任务。",
|
||||||
"runtime": "查看券商连接和交易运行状态。",
|
"runtime": "查看券商连接和交易运行状态。",
|
||||||
"settings": "配置模型、数据源和券商连接。"
|
"settings": "配置模型、数据源和券商连接。",
|
||||||
|
"pixelOffice": "规划和分配研究任务。分配任务不会启动执行;职级只是标签,不授予交易权限。"
|
||||||
},
|
},
|
||||||
"instructions": {
|
"instructions": {
|
||||||
"reports": "先在研究助手中描述策略并完成回测,再到这里打开结果。聊天生成的 PDF 等文档在原对话中下载。",
|
"reports": "先在研究助手中描述策略并完成回测,再到这里打开结果。聊天生成的 PDF 等文档在原对话中下载。",
|
||||||
@@ -31,7 +32,8 @@
|
|||||||
"correlation": "输入至少两个资产代码,选择时间窗口,再点击计算。",
|
"correlation": "输入至少两个资产代码,选择时间窗口,再点击计算。",
|
||||||
"scheduled": "填写研究指令、选择执行频率并保存。任务执行需要启用本地调度服务;推送结果还需配置投递渠道。",
|
"scheduled": "填写研究指令、选择执行频率并保存。任务执行需要启用本地调度服务;推送结果还需配置投递渠道。",
|
||||||
"runtime": "先在设置或持仓页配置券商连接。可用的交易操作取决于该券商的能力和你的授权。",
|
"runtime": "先在设置或持仓页配置券商连接。可用的交易操作取决于该券商的能力和你的授权。",
|
||||||
"settings": "初次使用先配置模型。按任务需要添加数据源或券商连接;研究公开行情无需连接券商账户。"
|
"settings": "初次使用先配置模型。按任务需要添加数据源或券商连接;研究公开行情无需连接券商账户。",
|
||||||
|
"pixelOffice": "规划和分配研究任务。分配任务不会启动执行;职级只是标签,不授予交易权限。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"layout": {
|
"layout": {
|
||||||
@@ -61,7 +63,8 @@
|
|||||||
"sidebar": "Vibe-Trading 侧边栏",
|
"sidebar": "Vibe-Trading 侧边栏",
|
||||||
"mainNavigation": "主导航",
|
"mainNavigation": "主导航",
|
||||||
"about": "关于",
|
"about": "关于",
|
||||||
"sessionsLoadFailed": "无法加载会话。"
|
"sessionsLoadFailed": "无法加载会话。",
|
||||||
|
"pixelOffice": "像素办公室"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"monitorBadge": "交易监控",
|
"monitorBadge": "交易监控",
|
||||||
@@ -2380,5 +2383,40 @@
|
|||||||
"preview": "Test passed: {{preview}}",
|
"preview": "Test passed: {{preview}}",
|
||||||
"error": "Provider test failed: {{error}}",
|
"error": "Provider test failed: {{error}}",
|
||||||
"waiting": "The provider is taking longer than usual ({{seconds}}s). The request will continue until the server timeout."
|
"waiting": "The provider is taking longer than usual ({{seconds}}s). The request will continue until the server timeout."
|
||||||
|
},
|
||||||
|
"pixelOffice": {
|
||||||
|
"title": "像素办公室",
|
||||||
|
"description": "规划和分配研究任务。分配任务不会启动执行;职级只是标签,不授予交易权限。",
|
||||||
|
"refresh": "刷新",
|
||||||
|
"loading": "正在加载办公室…",
|
||||||
|
"unavailable": "办公室不可用,请刷新重试。",
|
||||||
|
"floor": "办公区",
|
||||||
|
"taskCount": "未完成任务:{{count}}",
|
||||||
|
"board": "任务板",
|
||||||
|
"empty": "暂无任务。",
|
||||||
|
"assignee": "负责人",
|
||||||
|
"unassigned": "未分配",
|
||||||
|
"result": "完成结果",
|
||||||
|
"complete": "记录完成",
|
||||||
|
"create": "创建任务",
|
||||||
|
"taskTitle": "任务标题",
|
||||||
|
"instructions": "任务说明",
|
||||||
|
"priority": "优先级",
|
||||||
|
"ranksTitle": "组织职级",
|
||||||
|
"priorities": {
|
||||||
|
"low": "低",
|
||||||
|
"normal": "普通",
|
||||||
|
"high": "高"
|
||||||
|
},
|
||||||
|
"statuses": {
|
||||||
|
"queued": "待分配",
|
||||||
|
"assigned": "已分配",
|
||||||
|
"completed": "已完成"
|
||||||
|
},
|
||||||
|
"ranks": {
|
||||||
|
"Operator": "成员",
|
||||||
|
"Senior": "资深成员",
|
||||||
|
"Lead": "负责人"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,6 +109,84 @@
|
|||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Lightweight office sprites used by the native Pixel Office management view. */
|
||||||
|
.pixel-office-floor {
|
||||||
|
background-color: hsl(var(--card));
|
||||||
|
background-image:
|
||||||
|
linear-gradient(hsl(var(--border) / 0.22) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, hsl(var(--border) / 0.22) 1px, transparent 1px);
|
||||||
|
background-size: 28px 28px;
|
||||||
|
}
|
||||||
|
.pixel-desk {
|
||||||
|
background: hsl(var(--background) / 0.82);
|
||||||
|
box-shadow: inset 0 -10px 0 hsl(var(--muted) / 0.32);
|
||||||
|
}
|
||||||
|
.pixel-character {
|
||||||
|
position: relative;
|
||||||
|
width: 66px;
|
||||||
|
height: 70px;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
.pixel-character__shadow {
|
||||||
|
position: absolute;
|
||||||
|
left: 8px;
|
||||||
|
bottom: 3px;
|
||||||
|
width: 48px;
|
||||||
|
height: 8px;
|
||||||
|
background: hsl(var(--foreground) / 0.14);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.pixel-character__body {
|
||||||
|
position: absolute;
|
||||||
|
left: 16px;
|
||||||
|
top: 6px;
|
||||||
|
width: 34px;
|
||||||
|
height: 55px;
|
||||||
|
filter: drop-shadow(2px 3px 0 hsl(var(--foreground) / 0.2));
|
||||||
|
}
|
||||||
|
.pixel-character__head {
|
||||||
|
position: absolute;
|
||||||
|
left: 6px;
|
||||||
|
top: 0;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background: #f1b48b;
|
||||||
|
box-shadow: 0 -5px 0 #3b2a24, 5px -5px 0 #3b2a24, 10px -5px 0 #3b2a24, 0 5px 0 #f1b48b, 24px 5px 0 #f1b48b;
|
||||||
|
}
|
||||||
|
.pixel-character__torso {
|
||||||
|
position: absolute;
|
||||||
|
left: 5px;
|
||||||
|
top: 29px;
|
||||||
|
width: 25px;
|
||||||
|
height: 20px;
|
||||||
|
background: var(--pixel-color);
|
||||||
|
}
|
||||||
|
.pixel-character__arm,
|
||||||
|
.pixel-character__leg {
|
||||||
|
position: absolute;
|
||||||
|
width: 7px;
|
||||||
|
background: var(--pixel-color);
|
||||||
|
}
|
||||||
|
.pixel-character__arm { top: 30px; height: 18px; }
|
||||||
|
.pixel-character__arm--left { left: 0; }
|
||||||
|
.pixel-character__arm--right { right: 0; }
|
||||||
|
.pixel-character__leg { top: 49px; height: 12px; background: #334155; }
|
||||||
|
.pixel-character__leg--left { left: 7px; }
|
||||||
|
.pixel-character__leg--right { right: 7px; }
|
||||||
|
.pixel-character__typing {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 2px;
|
||||||
|
color: var(--pixel-color);
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
animation: pixel-typing 900ms steps(2, end) infinite;
|
||||||
|
}
|
||||||
|
@keyframes pixel-typing {
|
||||||
|
0%, 100% { opacity: 0.25; transform: translateY(2px); }
|
||||||
|
50% { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
/* Message/hero entrance — one subtle rise per mount; removed under reduced motion */
|
/* Message/hero entrance — one subtle rise per mount; removed under reduced motion */
|
||||||
@keyframes msg-enter {
|
@keyframes msg-enter {
|
||||||
from { opacity: 0; transform: translateY(10px); }
|
from { opacity: 0; transform: translateY(10px); }
|
||||||
|
|||||||
@@ -429,6 +429,9 @@ function appendQueryParam(url: string, key: string, value: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const api = {
|
export const api = {
|
||||||
|
getPixelOffice: () => request<OfficeSnapshot>("/api/pixel-office"),
|
||||||
|
updatePixelOffice: (action: "create" | "assign" | "complete" | "rank", arguments_: Record<string, unknown>) =>
|
||||||
|
request<OfficeSnapshot>("/api/pixel-office/actions", { method: "POST", body: JSON.stringify({ action, arguments: arguments_ }) }),
|
||||||
uploadFile,
|
uploadFile,
|
||||||
getCorrelationAnalysis: (codes: string, days: number, method: "pearson" | "spearman", includeRegime: boolean, signal?: AbortSignal) =>
|
getCorrelationAnalysis: (codes: string, days: number, method: "pearson" | "spearman", includeRegime: boolean, signal?: AbortSignal) =>
|
||||||
request<CorrelationAnalysisResponse>(`/correlation/analysis?${new URLSearchParams({ codes, days: String(days), method, include_regime: String(includeRegime) })}`, { signal }),
|
request<CorrelationAnalysisResponse>(`/correlation/analysis?${new URLSearchParams({ codes, days: String(days), method, include_regime: String(includeRegime) })}`, { signal }),
|
||||||
@@ -2035,3 +2038,9 @@ export interface ToolTrailItem {
|
|||||||
call_id?: string;
|
call_id?: string;
|
||||||
timestamp?: number;
|
timestamp?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OfficeSnapshot {
|
||||||
|
mode: "manual_planning";
|
||||||
|
agents: { id: string; name: string; role: string; color: string; rank: "Operator" | "Senior" | "Lead"; load: number }[];
|
||||||
|
tasks: { id: string; title: string; description: string; priority: "low" | "normal" | "high"; assigned_to: string | null; status: "queued" | "assigned" | "completed"; result: string }[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import { useCallback, useEffect, useState, type CSSProperties, type FormEvent } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { api, type OfficeSnapshot } from "@/lib/api";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export function PixelOffice() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [snapshot, setSnapshot] = useState<OfficeSnapshot | null>(null);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [pending, setPending] = useState(false);
|
||||||
|
const [agentId, setAgentId] = useState("");
|
||||||
|
const [title, setTitle] = useState("");
|
||||||
|
const [description, setDescription] = useState("");
|
||||||
|
const [priority, setPriority] = useState("normal");
|
||||||
|
const [results, setResults] = useState<Record<string, string>>({});
|
||||||
|
const refresh = useCallback(async () => {
|
||||||
|
setPending(true);
|
||||||
|
try {
|
||||||
|
const value = await api.getPixelOffice();
|
||||||
|
setSnapshot(value);
|
||||||
|
setAgentId((current) => current || value.agents[0]?.id || "");
|
||||||
|
setError("");
|
||||||
|
} catch (err) { setError(err instanceof Error ? err.message : String(err)); }
|
||||||
|
finally { setPending(false); }
|
||||||
|
}, []);
|
||||||
|
useEffect(() => { void refresh(); }, [refresh]);
|
||||||
|
|
||||||
|
async function mutate(action: "create" | "assign" | "complete" | "rank", arguments_: Record<string, unknown>) {
|
||||||
|
setPending(true);
|
||||||
|
try {
|
||||||
|
setSnapshot(await api.updatePixelOffice(action, arguments_));
|
||||||
|
setError("");
|
||||||
|
return true;
|
||||||
|
} catch (err) { setError(err instanceof Error ? err.message : String(err)); return false; }
|
||||||
|
finally { setPending(false); }
|
||||||
|
}
|
||||||
|
async function create(event: FormEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (await mutate("create", { task_id: crypto.randomUUID(), title, description, priority, assigned_to: agentId || null })) {
|
||||||
|
setTitle(""); setDescription("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const input = "mt-1 w-full rounded-md border border-border bg-background px-3 py-2 text-sm";
|
||||||
|
const agents = snapshot?.agents ?? [];
|
||||||
|
const agentName = (id: string | null) => agents.find((agent) => agent.id === id)?.name ?? t("pixelOffice.unassigned");
|
||||||
|
return (
|
||||||
|
<main className="h-full overflow-y-auto bg-background px-4 py-5 sm:px-6">
|
||||||
|
<div className="mx-auto max-w-7xl space-y-5">
|
||||||
|
<header className="flex items-start justify-between gap-4">
|
||||||
|
<div><h1 className="text-2xl font-semibold">{t("pixelOffice.title")}</h1>
|
||||||
|
<p className="mt-2 max-w-3xl text-sm text-muted-foreground">{t("pixelOffice.description")}</p></div>
|
||||||
|
<button type="button" disabled={pending} onClick={() => void refresh()} className="rounded-md border px-3 py-2 text-sm disabled:opacity-50">{t("pixelOffice.refresh")}</button>
|
||||||
|
</header>
|
||||||
|
{error && <p role="alert" className="rounded-lg border border-destructive/30 p-3 text-sm text-destructive">{error}</p>}
|
||||||
|
{!snapshot && <p role="status">{t(pending ? "pixelOffice.loading" : "pixelOffice.unavailable")}</p>}
|
||||||
|
{snapshot && <>
|
||||||
|
<section className="pixel-office-floor rounded-xl border border-border p-4 sm:p-6" aria-label={t("pixelOffice.floor")}>
|
||||||
|
<h2 className="mb-4 font-semibold">{t("pixelOffice.floor")}</h2>
|
||||||
|
<div className="grid gap-4 md:grid-cols-3">{agents.map((agent) => (
|
||||||
|
<button type="button" key={agent.id} onClick={() => setAgentId(agent.id)}
|
||||||
|
className={cn("pixel-desk rounded-lg border p-4 text-start", agentId === agent.id ? "border-primary ring-2 ring-primary/20" : "border-border/70")}>
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="pixel-character" style={{ "--pixel-color": agent.color } as CSSProperties} aria-hidden="true">
|
||||||
|
<div className="pixel-character__shadow" /><div className="pixel-character__body">
|
||||||
|
<span className="pixel-character__head" /><span className="pixel-character__torso" />
|
||||||
|
<span className="pixel-character__arm pixel-character__arm--left" /><span className="pixel-character__arm pixel-character__arm--right" />
|
||||||
|
<span className="pixel-character__leg pixel-character__leg--left" /><span className="pixel-character__leg pixel-character__leg--right" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="rounded-full bg-background/70 px-2 py-1 text-xs">{t("pixelOffice.taskCount", { count: agent.load })}</span>
|
||||||
|
</div>
|
||||||
|
<p className="mt-3 font-semibold">{agent.name}</p><p className="text-xs text-muted-foreground">{t(`pixelOffice.ranks.${agent.rank}`)}</p>
|
||||||
|
</button>
|
||||||
|
))}</div>
|
||||||
|
</section>
|
||||||
|
<div className="grid gap-5 lg:grid-cols-[minmax(0,1fr)_360px]">
|
||||||
|
<section className="rounded-xl border border-border bg-card p-4">
|
||||||
|
<h2 className="mb-4 font-semibold">{t("pixelOffice.board")}</h2>
|
||||||
|
{!snapshot.tasks.length && <p className="rounded-lg border border-dashed p-6 text-center text-sm text-muted-foreground">{t("pixelOffice.empty")}</p>}
|
||||||
|
<div className="space-y-3">{snapshot.tasks.map((task) => (
|
||||||
|
<article key={task.id} className="rounded-lg border border-border/70 p-3">
|
||||||
|
<div className="flex flex-wrap justify-between gap-2"><h3 className="break-words font-medium">{task.title}</h3><span className="text-xs text-muted-foreground">{t(`pixelOffice.priorities.${task.priority}`)} · {t(`pixelOffice.statuses.${task.status}`)}</span></div>
|
||||||
|
<p className="mt-1 whitespace-pre-wrap break-words text-sm text-muted-foreground">{task.description}</p>
|
||||||
|
<p className="mt-2 text-xs text-primary">{agentName(task.assigned_to)}</p>
|
||||||
|
{task.status === "completed" ? <p className="mt-2 whitespace-pre-wrap break-words text-sm">{task.result}</p> : (
|
||||||
|
<fieldset disabled={pending} className="mt-3 space-y-2 disabled:opacity-50">
|
||||||
|
<label className="block text-xs">{t("pixelOffice.assignee")}
|
||||||
|
<select className={input} value={task.assigned_to ?? ""} onChange={(event) => void mutate("assign", { task_id: task.id, agent_id: event.target.value })}>
|
||||||
|
<option value="" disabled>{t("pixelOffice.unassigned")}</option>{agents.map((agent) => <option value={agent.id} key={agent.id}>{agent.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<form onSubmit={(event) => { event.preventDefault(); void mutate("complete", { task_id: task.id, result: results[task.id] ?? "" }); }}>
|
||||||
|
<label className="block text-xs">{t("pixelOffice.result")}
|
||||||
|
<textarea className={input} rows={2} required maxLength={2000} value={results[task.id] ?? ""} onChange={(event) => setResults((current) => ({ ...current, [task.id]: event.target.value }))} />
|
||||||
|
</label>
|
||||||
|
<button type="submit" disabled={!results[task.id]?.trim()} className="mt-2 rounded-md border px-3 py-1.5 text-xs disabled:opacity-50">{t("pixelOffice.complete")}</button>
|
||||||
|
</form>
|
||||||
|
</fieldset>
|
||||||
|
)}
|
||||||
|
</article>
|
||||||
|
))}</div>
|
||||||
|
</section>
|
||||||
|
<section className="h-fit rounded-xl border border-border bg-card p-4">
|
||||||
|
<h2 className="mb-4 font-semibold">{t("pixelOffice.create")}</h2>
|
||||||
|
<form onSubmit={(event) => void create(event)}><fieldset disabled={pending} className="space-y-3 disabled:opacity-50">
|
||||||
|
<label className="block text-xs">{t("pixelOffice.assignee")}<select className={input} value={agentId} onChange={(event) => setAgentId(event.target.value)}><option value="">{t("pixelOffice.unassigned")}</option>{agents.map((agent) => <option key={agent.id} value={agent.id}>{agent.name}</option>)}</select></label>
|
||||||
|
<label className="block text-xs">{t("pixelOffice.taskTitle")}<input className={input} required maxLength={200} value={title} onChange={(event) => setTitle(event.target.value)} /></label>
|
||||||
|
<label className="block text-xs">{t("pixelOffice.instructions")}<textarea className={input} rows={3} maxLength={2000} value={description} onChange={(event) => setDescription(event.target.value)} /></label>
|
||||||
|
<label className="block text-xs">{t("pixelOffice.priority")}<select className={input} value={priority} onChange={(event) => setPriority(event.target.value)}>{(["low", "normal", "high"] as const).map((value) => <option key={value} value={value}>{t(`pixelOffice.priorities.${value}`)}</option>)}</select></label>
|
||||||
|
<button type="submit" disabled={!title.trim()} className="w-full rounded-md bg-primary px-3 py-2 text-sm text-primary-foreground disabled:opacity-50">{t("pixelOffice.create")}</button>
|
||||||
|
</fieldset></form>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<section className="rounded-xl border border-border bg-card p-4"><h2 className="mb-3 font-semibold">{t("pixelOffice.ranksTitle")}</h2>
|
||||||
|
<div className="grid gap-3 md:grid-cols-3">{agents.map((agent) => <label key={agent.id} className="block text-sm">{agent.name}
|
||||||
|
<select className={input} disabled={pending} value={agent.rank} onChange={(event) => void mutate("rank", { agent_id: agent.id, rank: event.target.value })}>{(["Operator", "Senior", "Lead"] as const).map((rank) => <option key={rank} value={rank}>{t(`pixelOffice.ranks.${rank}`)}</option>)}</select>
|
||||||
|
</label>)}</div>
|
||||||
|
</section>
|
||||||
|
</>}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { PixelOffice } from "../PixelOffice";
|
||||||
|
import { api, type OfficeSnapshot } from "@/lib/api";
|
||||||
|
import i18n from "@/i18n";
|
||||||
|
|
||||||
|
vi.mock("@/lib/api", () => ({ api: { getPixelOffice: vi.fn(), updatePixelOffice: vi.fn() } }));
|
||||||
|
const initial: OfficeSnapshot = { mode: "manual_planning", agents: [{ id: "research", name: "Research Analyst", role: "research", rank: "Operator", color: "#c59259", load: 0 }], tasks: [] };
|
||||||
|
beforeEach(async () => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
await i18n.changeLanguage("en");
|
||||||
|
vi.mocked(api.getPixelOffice).mockResolvedValue(initial);
|
||||||
|
});
|
||||||
|
describe("shared Pixel Office", () => {
|
||||||
|
it("uses persisted tasks, creates on the API and preserves failed completion input", async () => {
|
||||||
|
localStorage.setItem("vibe-pixel-office-tasks", JSON.stringify([{ title: "old local-only task" }]));
|
||||||
|
render(<PixelOffice />);
|
||||||
|
await screen.findByText("No tasks yet.");
|
||||||
|
expect(screen.queryByText("old local-only task")).not.toBeInTheDocument();
|
||||||
|
const created: OfficeSnapshot = { ...initial, tasks: [{ id: "a", title: "Read filing", description: "Observed sources only", assigned_to: "research", status: "assigned", priority: "normal", result: "" }] };
|
||||||
|
vi.mocked(api.updatePixelOffice).mockResolvedValueOnce(created);
|
||||||
|
fireEvent.change(screen.getByLabelText("Task title"), { target: { value: "Read filing" } });
|
||||||
|
fireEvent.change(screen.getByLabelText("Instructions"), { target: { value: "Observed sources only" } });
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Create task" }));
|
||||||
|
await screen.findByText("Read filing");
|
||||||
|
expect(api.updatePixelOffice).toHaveBeenCalledWith("create", expect.objectContaining({ title: "Read filing", assigned_to: "research" }));
|
||||||
|
expect(screen.getByRole("button", { name: "Record completion" })).toBeDisabled();
|
||||||
|
fireEvent.change(screen.getByLabelText("Completion result"), { target: { value: "Reviewed the reported cash flows" } });
|
||||||
|
vi.mocked(api.updatePixelOffice).mockRejectedValueOnce(new Error("Write failed"));
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Record completion" }));
|
||||||
|
await screen.findByRole("alert");
|
||||||
|
expect(screen.getByLabelText("Completion result")).toHaveValue("Reviewed the reported cash flows");
|
||||||
|
expect(screen.queryByText("Completed")).not.toBeInTheDocument();
|
||||||
|
vi.mocked(api.updatePixelOffice).mockResolvedValueOnce({ ...created, tasks: [{ ...created.tasks[0], status: "completed", result: "Reviewed the reported cash flows" }] });
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Record completion" }));
|
||||||
|
await waitFor(() => expect(screen.queryByLabelText("Completion result")).not.toBeInTheDocument());
|
||||||
|
expect(screen.getByText("Reviewed the reported cash flows")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not invent an office snapshot on read failure and supports retry", async () => {
|
||||||
|
vi.mocked(api.getPixelOffice).mockRejectedValueOnce(new Error("Unavailable"));
|
||||||
|
render(<PixelOffice />);
|
||||||
|
await screen.findByRole("alert");
|
||||||
|
expect(screen.queryByText("Research Analyst")).not.toBeInTheDocument();
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Refresh" }));
|
||||||
|
await screen.findByText("No tasks yet.");
|
||||||
|
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -34,6 +34,9 @@ const AlphaZoo = lazy(() =>
|
|||||||
const OptionsLab = lazy(() =>
|
const OptionsLab = lazy(() =>
|
||||||
import("@/pages/OptionsLab").then((m) => ({ default: m.OptionsLab })),
|
import("@/pages/OptionsLab").then((m) => ({ default: m.OptionsLab })),
|
||||||
);
|
);
|
||||||
|
const PixelOffice = lazy(() =>
|
||||||
|
import("@/pages/PixelOffice").then((m) => ({ default: m.PixelOffice })),
|
||||||
|
);
|
||||||
|
|
||||||
function PageLoader() {
|
function PageLoader() {
|
||||||
return (
|
return (
|
||||||
@@ -58,6 +61,7 @@ export const router = createBrowserRouter([
|
|||||||
{ path: "/", element: wrap(Agent) },
|
{ path: "/", element: wrap(Agent) },
|
||||||
{ path: "/about", element: wrap(Home) },
|
{ path: "/about", element: wrap(Home) },
|
||||||
{ path: "/agent", element: wrap(Agent) },
|
{ path: "/agent", element: wrap(Agent) },
|
||||||
|
{ path: "/pixel-office", element: wrap(PixelOffice) },
|
||||||
{ path: "/runtime", element: wrap(Runtime) },
|
{ path: "/runtime", element: wrap(Runtime) },
|
||||||
{ path: "/scheduled", element: wrap(Scheduled) },
|
{ path: "/scheduled", element: wrap(Scheduled) },
|
||||||
{ path: "/reports", element: wrap(Reports) },
|
{ path: "/reports", element: wrap(Reports) },
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Pixel Office
|
||||||
|
|
||||||
|
The `/pixel-office` page, `vibe-trading office` CLI and `pixel_office` agent tool
|
||||||
|
share a manual research-task board. Assigning a task does not launch an agent or
|
||||||
|
place an order. Organizational ranks grant no trading permissions. Completion
|
||||||
|
requires an explicit result; recorded results are not an execution audit.
|
||||||
|
|
||||||
|
State is stored transactionally in `pixel-office.sqlite3` under the runtime root
|
||||||
|
(default `~/.vibe-trading`), outside the repository. No sample task is treated as
|
||||||
|
real work. Separate CLI processes and the Web API see the same state.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
vibe-trading office list
|
||||||
|
vibe-trading office task --id morning-brief --title "Morning research brief" --description "Summarize today's sources and risks"
|
||||||
|
vibe-trading office assign --task-id morning-brief --agent-id research-analyst
|
||||||
|
vibe-trading office complete --task-id morning-brief --result "Reviewed the brief; findings are in the research session."
|
||||||
|
vibe-trading office rank --agent-id research-analyst --rank Senior
|
||||||
|
```
|
||||||
|
|
||||||
|
For an isolated board, pass `--storage /absolute/path/office.sqlite3` before or
|
||||||
|
after the office subcommand. REST clients use authenticated `GET /api/pixel-office`
|
||||||
|
and `POST /api/pixel-office/actions` with `{ "action": "create|assign|complete|rank",
|
||||||
|
"arguments": { ... } }`. Writes use the settings authentication guard.
|
||||||
@@ -110,6 +110,7 @@ include = ["src*", "backtest*", "cli*", "evals*"]
|
|||||||
"skills/**/*.json",
|
"skills/**/*.json",
|
||||||
"skills/**/*.py",
|
"skills/**/*.py",
|
||||||
"providers/*.json",
|
"providers/*.json",
|
||||||
|
"pixel_agents/*.json",
|
||||||
"scheduled_research/playbooks/*.md",
|
"scheduled_research/playbooks/*.md",
|
||||||
"swarm/presets/*.yaml",
|
"swarm/presets/*.yaml",
|
||||||
"shadow_account/templates/*.j2",
|
"shadow_account/templates/*.j2",
|
||||||
|
|||||||
Reference in new issue
Block a user