mirror of
https://github.com/Pythagora-io/gpt-pilot.git
synced 2026-01-09 21:27:53 -05:00
Merge branch 'main' of https://github.com/Pythagora-io/gpt-pilot
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -21,3 +21,4 @@ config.json
|
|||||||
poetry.lock
|
poetry.lock
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.log
|
*.log
|
||||||
|
pythagora-vs-code.vsix
|
||||||
|
|||||||
168
Dockerfile
Normal file
168
Dockerfile
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
# Use Ubuntu 22.04 as the base image with multi-arch support
|
||||||
|
FROM ubuntu:22.04
|
||||||
|
|
||||||
|
# Set environment to prevent interactive prompts during builds
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
ENV TZ=Etc/UTC
|
||||||
|
|
||||||
|
# Use buildx args for multi-arch support
|
||||||
|
ARG TARGETPLATFORM
|
||||||
|
ARG BUILDPLATFORM
|
||||||
|
|
||||||
|
# Update package list and install prerequisites
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
software-properties-common \
|
||||||
|
build-essential \
|
||||||
|
curl \
|
||||||
|
git \
|
||||||
|
gnupg \
|
||||||
|
tzdata \
|
||||||
|
openssh-server \
|
||||||
|
inotify-tools \
|
||||||
|
vim \
|
||||||
|
nano \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Add deadsnakes PPA for Python 3.12 and install Python
|
||||||
|
RUN add-apt-repository ppa:deadsnakes/ppa -y && apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
python3.12 \
|
||||||
|
python3.12-venv \
|
||||||
|
python3.12-dev \
|
||||||
|
python3-pip \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
|
||||||
|
# Set Python 3.12 as the default python3 and python
|
||||||
|
RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.12 1 && \
|
||||||
|
update-alternatives --install /usr/bin/python python /usr/bin/python3 1 && \
|
||||||
|
python --version
|
||||||
|
|
||||||
|
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - && \
|
||||||
|
apt-get install -y nodejs && \
|
||||||
|
node --version && npm --version
|
||||||
|
|
||||||
|
# MongoDB installation with platform-specific approach
|
||||||
|
RUN case "$TARGETPLATFORM" in \
|
||||||
|
"linux/amd64") \
|
||||||
|
curl -fsSL https://www.mongodb.org/static/pgp/server-6.0.asc | gpg --dearmor -o /usr/share/keyrings/mongodb-archive-keyring.gpg && \
|
||||||
|
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/mongodb-archive-keyring.gpg] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/6.0 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-6.0.list && \
|
||||||
|
apt-get update && apt-get install -y mongodb-org \
|
||||||
|
;; \
|
||||||
|
"linux/arm64"|"linux/arm64/v8") \
|
||||||
|
curl -fsSL https://www.mongodb.org/static/pgp/server-6.0.asc | gpg --dearmor -o /usr/share/keyrings/mongodb-archive-keyring.gpg && \
|
||||||
|
echo "deb [arch=arm64 signed-by=/usr/share/keyrings/mongodb-archive-keyring.gpg] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/6.0 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-6.0.list && \
|
||||||
|
apt-get update && apt-get install -y mongodb-org \
|
||||||
|
;; \
|
||||||
|
*) \
|
||||||
|
echo "Unsupported platform: $TARGETPLATFORM" && exit 1 \
|
||||||
|
;; \
|
||||||
|
esac \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Configure SSH
|
||||||
|
RUN mkdir -p /run/sshd \
|
||||||
|
&& sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin no/' /etc/ssh/sshd_config \
|
||||||
|
&& sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config \
|
||||||
|
&& sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config \
|
||||||
|
&& sed -i 's/#ChallengeResponseAuthentication yes/ChallengeResponseAuthentication no/' /etc/ssh/sshd_config
|
||||||
|
|
||||||
|
ENV PYTH_INSTALL_DIR=/pythagora
|
||||||
|
|
||||||
|
# Set up work directory
|
||||||
|
WORKDIR ${PYTH_INSTALL_DIR}/pythagora-core
|
||||||
|
|
||||||
|
# Add Python requirements
|
||||||
|
ADD requirements.txt .
|
||||||
|
|
||||||
|
# Create and activate a virtual environment, then install dependencies
|
||||||
|
RUN python3 -m venv venv && \
|
||||||
|
. venv/bin/activate && \
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Copy application files
|
||||||
|
ADD main.py .
|
||||||
|
ADD core core
|
||||||
|
ADD config-docker.json config.json
|
||||||
|
|
||||||
|
# Set the virtual environment to be automatically activated
|
||||||
|
ENV VIRTUAL_ENV=${PYTH_INSTALL_DIR}/pythagora-core/venv
|
||||||
|
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||||
|
|
||||||
|
ENV PYTHAGORA_DATA_DIR=${PYTH_INSTALL_DIR}/pythagora-core/data/
|
||||||
|
RUN mkdir -p data
|
||||||
|
|
||||||
|
# Expose MongoDB and application ports
|
||||||
|
EXPOSE 27017 8000
|
||||||
|
|
||||||
|
# Create a group named "devusergroup" with a specific GID (1000, optional)
|
||||||
|
RUN groupadd -g 1000 devusergroup
|
||||||
|
|
||||||
|
ARG USERNAME=devuser
|
||||||
|
|
||||||
|
# Create a user named "devuser" with a specific UID (1000) and assign it to "devusergroup"
|
||||||
|
RUN useradd -m -u 1000 -g devusergroup -s /bin/bash $USERNAME
|
||||||
|
|
||||||
|
# Add the user to sudoers for admin privileges
|
||||||
|
RUN echo "$USERNAME ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
|
||||||
|
|
||||||
|
# Create an embedded entrypoint script
|
||||||
|
ADD entrypoint.sh /entrypoint.sh
|
||||||
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
|
RUN chown -R $USERNAME:devusergroup /pythagora
|
||||||
|
|
||||||
|
# Copy SSH public key from secret
|
||||||
|
#RUN --mount=type=secret,id=ssh_public_key \
|
||||||
|
# mkdir -p /home/${USERNAME}/.ssh \
|
||||||
|
# && cat /run/secrets/ssh_public_key > /home/${USERNAME}/.ssh/authorized_keys \
|
||||||
|
# && chown -R ${USERNAME}:devusergroup /home/${USERNAME}/.ssh \
|
||||||
|
# && chmod 700 /home/${USERNAME}/.ssh \
|
||||||
|
# && chmod 600 /home/${USERNAME}/.ssh/authorized_keys
|
||||||
|
|
||||||
|
USER $USERNAME
|
||||||
|
|
||||||
|
RUN npx @puppeteer/browsers install chrome@stable
|
||||||
|
|
||||||
|
# add this before vscode... better caching of layers
|
||||||
|
ADD pythagora-vs-code.vsix /var/init_data/pythagora-vs-code.vsix
|
||||||
|
|
||||||
|
RUN mkdir -p ~/.ssh && touch ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys
|
||||||
|
|
||||||
|
# ARG commitHash
|
||||||
|
# # VS Code server installation with platform-specific handling
|
||||||
|
# RUN case "$TARGETPLATFORM" in \
|
||||||
|
# "linux/amd64") \
|
||||||
|
# mkdir -p ~/.vscode-server/cli/servers/Stable-${commitHash} && \
|
||||||
|
# curl -fsSL https://update.code.visualstudio.com/commit:${commitHash}/server-linux-x64/stable -o server-linux-x64.tar.gz && \
|
||||||
|
# tar -xz -f server-linux-x64.tar.gz -C ~/.vscode-server/cli/servers/Stable-${commitHash} && \
|
||||||
|
# mv ~/.vscode-server/cli/servers/Stable-${commitHash}/vscode-server-linux-x64 ~/.vscode-server/cli/servers/Stable-${commitHash}/server \
|
||||||
|
# ;; \
|
||||||
|
# "linux/arm64"|"linux/arm64/v8") \
|
||||||
|
# mkdir -p ~/.vscode-server/cli/servers/Stable-${commitHash} && \
|
||||||
|
# curl -fsSL https://update.code.visualstudio.com/commit:${commitHash}/server-linux-arm64/stable -o server-linux-arm64.tar.gz && \
|
||||||
|
# tar -xz -f server-linux-arm64.tar.gz -C ~/.vscode-server/cli/servers/Stable-${commitHash} && \
|
||||||
|
# mv ~/.vscode-server/cli/servers/Stable-${commitHash}/vscode-server-linux-arm64 ~/.vscode-server/cli/servers/Stable-${commitHash}/server \
|
||||||
|
# ;; \
|
||||||
|
# *) \
|
||||||
|
# echo "Unsupported platform: $TARGETPLATFORM" && exit 1 \
|
||||||
|
# ;; \
|
||||||
|
# esac
|
||||||
|
|
||||||
|
|
||||||
|
# Install VS Code extension (platform-agnostic)
|
||||||
|
# RUN ~/.vscode-server/cli/servers/Stable-${commitHash}/server/bin/code-server --install-extension pythagora-vs-code.vsix
|
||||||
|
ADD on-event-extension-install.sh /var/init_data/on-event-extension-install.sh
|
||||||
|
|
||||||
|
# Create a workspace directory
|
||||||
|
RUN mkdir -p ${PYTH_INSTALL_DIR}/pythagora-core/workspace
|
||||||
|
|
||||||
|
RUN mkdir -p /home/$USERNAME/.vscode-server/cli/servers
|
||||||
|
|
||||||
|
USER root
|
||||||
|
|
||||||
|
RUN chmod +x /var/init_data/on-event-extension-install.sh
|
||||||
|
RUN chown -R devuser: /var/init_data/
|
||||||
|
|
||||||
|
# Set the entrypoint to the main application
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
35
README.md
35
README.md
@@ -107,37 +107,10 @@ After you have Python and (optionally) PostgreSQL installed, follow these steps:
|
|||||||
|
|
||||||
All generated code will be stored in the folder `workspace` inside the folder named after the app name you enter upon starting the pilot.
|
All generated code will be stored in the folder `workspace` inside the folder named after the app name you enter upon starting the pilot.
|
||||||
|
|
||||||
### If you're upgrading from GPT Pilot v0.1
|
|
||||||
|
|
||||||
Assuming you already have the git repository with an earlier version:
|
|
||||||
|
|
||||||
1. `git pull` (update the repo)
|
|
||||||
2. `source pilot-env/bin/activate` (or on Windows `pilot-env\Scripts\activate`) (activate the virtual environment)
|
|
||||||
3. `pip install -r requirements.txt` (install the new dependencies)
|
|
||||||
4. `python main.py --import-v0 pilot/gpt-pilot` (this should import your settings and existing projects)
|
|
||||||
|
|
||||||
This will create a new database `pythagora.db` and import all apps from the old database. For each app,
|
|
||||||
it will import the start of the latest task you were working on.
|
|
||||||
|
|
||||||
To verify that the import was successful, you can run `python main.py --list` to see all the apps you have created,
|
|
||||||
and check `config.json` to check the settings were correctly converted to the new config file format (and make
|
|
||||||
any adjustments if needed).
|
|
||||||
|
|
||||||
# 🔎 [Examples](https://github.com/Pythagora-io/gpt-pilot/wiki/Apps-created-with-GPT-Pilot)
|
# 🔎 [Examples](https://github.com/Pythagora-io/gpt-pilot/wiki/Apps-created-with-GPT-Pilot)
|
||||||
|
|
||||||
[Click here](https://github.com/Pythagora-io/gpt-pilot/wiki/Apps-created-with-GPT-Pilot) to see all example apps created with GPT Pilot.
|
[Click here](https://github.com/Pythagora-io/gpt-pilot/wiki/Apps-created-with-GPT-Pilot) to see all example apps created with GPT Pilot.
|
||||||
|
|
||||||
## 🐳 How to start gpt-pilot in docker?
|
|
||||||
1. `git clone https://github.com/Pythagora-io/gpt-pilot.git` (clone the repo)
|
|
||||||
2. Update the `docker-compose.yml` environment variables, which can be done via `docker compose config`. If you wish to use a local model, please go to [https://localai.io/basics/getting_started/](https://localai.io/basics/getting_started/).
|
|
||||||
3. By default, GPT Pilot will read & write to `~/gpt-pilot-workspace` on your machine, you can also edit this in `docker-compose.yml`
|
|
||||||
4. run `docker compose build`. this will build a gpt-pilot container for you.
|
|
||||||
5. run `docker compose up`.
|
|
||||||
6. access the web terminal on `port 7681`
|
|
||||||
7. `python main.py` (start GPT Pilot)
|
|
||||||
|
|
||||||
This will start two containers, one being a new image built by the `Dockerfile` and a Postgres database. The new image also has [ttyd](https://github.com/tsl0922/ttyd) installed so that you can easily interact with gpt-pilot. Node is also installed on the image and port 3000 is exposed.
|
|
||||||
|
|
||||||
### PostgreSQL support
|
### PostgreSQL support
|
||||||
|
|
||||||
GPT Pilot uses built-in SQLite database by default. If you want to use the PostgreSQL database, you need to additional install `asyncpg` and `psycopg2` packages:
|
GPT Pilot uses built-in SQLite database by default. If you want to use the PostgreSQL database, you need to additional install `asyncpg` and `psycopg2` packages:
|
||||||
@@ -180,14 +153,6 @@ python main.py --delete <app_id>
|
|||||||
|
|
||||||
Delete project with the specified `app_id`. Warning: this cannot be undone!
|
Delete project with the specified `app_id`. Warning: this cannot be undone!
|
||||||
|
|
||||||
### Import projects from v0.1
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python main.py --import-v0 <path>
|
|
||||||
```
|
|
||||||
|
|
||||||
This will import projects from the old GPT Pilot v0.1 database. The path should be the path to the old GPT Pilot v0.1 database. For each project, it will import the start of the latest task you were working on. If the project was already imported, the import procedure will skip it (won't overwrite the project in the database).
|
|
||||||
|
|
||||||
### Other command-line options
|
### Other command-line options
|
||||||
|
|
||||||
There are several other command-line options that mostly support calling GPT Pilot from our VSCode extension. To see all the available options, use the `--help` flag:
|
There are several other command-line options that mostly support calling GPT Pilot from our VSCode extension. To see all the available options, use the `--help` flag:
|
||||||
|
|||||||
136
config-docker.json
Normal file
136
config-docker.json
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
{
|
||||||
|
"llm": {
|
||||||
|
"openai": {
|
||||||
|
"base_url": null,
|
||||||
|
"api_key": null,
|
||||||
|
"connect_timeout": 60.0,
|
||||||
|
"read_timeout": 60.0,
|
||||||
|
"extra": null
|
||||||
|
},
|
||||||
|
"anthropic": {
|
||||||
|
"base_url": null,
|
||||||
|
"api_key": null,
|
||||||
|
"connect_timeout": 60.0,
|
||||||
|
"read_timeout": 60.0,
|
||||||
|
"extra": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"agent": {
|
||||||
|
"default": {
|
||||||
|
"provider": "openai",
|
||||||
|
"model": "gpt-4o-2024-05-13",
|
||||||
|
"temperature": 0.5
|
||||||
|
},
|
||||||
|
"BugHunter.check_logs": {
|
||||||
|
"provider": "anthropic",
|
||||||
|
"model": "claude-3-5-sonnet-20241022",
|
||||||
|
"temperature": 0.5
|
||||||
|
},
|
||||||
|
"CodeMonkey": {
|
||||||
|
"provider": "anthropic",
|
||||||
|
"model": "claude-3-5-sonnet-20241022",
|
||||||
|
"temperature": 0.0
|
||||||
|
},
|
||||||
|
"CodeMonkey.code_review": {
|
||||||
|
"provider": "anthropic",
|
||||||
|
"model": "claude-3-5-sonnet-20240620",
|
||||||
|
"temperature": 0.0
|
||||||
|
},
|
||||||
|
"CodeMonkey.describe_files": {
|
||||||
|
"provider": "openai",
|
||||||
|
"model": "gpt-4o-mini-2024-07-18",
|
||||||
|
"temperature": 0.0
|
||||||
|
},
|
||||||
|
"Frontend": {
|
||||||
|
"provider": "anthropic",
|
||||||
|
"model": "claude-3-5-sonnet-20241022",
|
||||||
|
"temperature": 0.0
|
||||||
|
},
|
||||||
|
"get_relevant_files": {
|
||||||
|
"provider": "openai",
|
||||||
|
"model": "gpt-4o-2024-05-13",
|
||||||
|
"temperature": 0.5
|
||||||
|
},
|
||||||
|
"Developer.parse_task": {
|
||||||
|
"provider": "anthropic",
|
||||||
|
"model": "claude-3-5-sonnet-20241022",
|
||||||
|
"temperature": 0.0
|
||||||
|
},
|
||||||
|
"SpecWriter": {
|
||||||
|
"provider": "openai",
|
||||||
|
"model": "gpt-4-0125-preview",
|
||||||
|
"temperature": 0.0
|
||||||
|
},
|
||||||
|
"Developer.breakdown_current_task": {
|
||||||
|
"provider": "anthropic",
|
||||||
|
"model": "claude-3-5-sonnet-20241022",
|
||||||
|
"temperature": 0.5
|
||||||
|
},
|
||||||
|
"TechLead.plan_epic": {
|
||||||
|
"provider": "anthropic",
|
||||||
|
"model": "claude-3-5-sonnet-20240620",
|
||||||
|
"temperature": 0.5
|
||||||
|
},
|
||||||
|
"TechLead.epic_breakdown": {
|
||||||
|
"provider": "anthropic",
|
||||||
|
"model": "claude-3-5-sonnet-20241022",
|
||||||
|
"temperature": 0.5
|
||||||
|
},
|
||||||
|
"Troubleshooter.generate_bug_report": {
|
||||||
|
"provider": "anthropic",
|
||||||
|
"model": "claude-3-5-sonnet-20240620",
|
||||||
|
"temperature": 0.5
|
||||||
|
},
|
||||||
|
"Troubleshooter.get_run_command": {
|
||||||
|
"provider": "anthropic",
|
||||||
|
"model": "claude-3-5-sonnet-20240620",
|
||||||
|
"temperature": 0.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"prompt": {
|
||||||
|
"paths": [
|
||||||
|
"/pythagora/pythagora-core/core/prompts"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"log": {
|
||||||
|
"level": "DEBUG",
|
||||||
|
"format": "%(asctime)s %(levelname)s [%(name)s] %(message)s",
|
||||||
|
"output": "pythagora.log"
|
||||||
|
},
|
||||||
|
"db": {
|
||||||
|
"url": "sqlite+aiosqlite:///data/database/pythagora.db",
|
||||||
|
"debug_sql": false
|
||||||
|
},
|
||||||
|
"ui": {
|
||||||
|
"type": "plain"
|
||||||
|
},
|
||||||
|
"fs": {
|
||||||
|
"type": "local",
|
||||||
|
"workspace_root": "/pythagora/pythagora-core/workspace",
|
||||||
|
"ignore_paths": [
|
||||||
|
".git",
|
||||||
|
".gpt-pilot",
|
||||||
|
".idea",
|
||||||
|
".vscode",
|
||||||
|
".next",
|
||||||
|
".DS_Store",
|
||||||
|
"__pycache__",
|
||||||
|
"site-packages",
|
||||||
|
"node_modules",
|
||||||
|
"package-lock.json",
|
||||||
|
"venv",
|
||||||
|
".venv",
|
||||||
|
"dist",
|
||||||
|
"build",
|
||||||
|
"target",
|
||||||
|
"*.min.js",
|
||||||
|
"*.min.css",
|
||||||
|
"*.svg",
|
||||||
|
"*.csv",
|
||||||
|
"*.log",
|
||||||
|
"go.sum",
|
||||||
|
"migration_lock.toml"
|
||||||
|
],
|
||||||
|
"ignore_size_threshold": 50000
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import json
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
@@ -16,7 +17,6 @@ from core.templates.registry import (
|
|||||||
PROJECT_TEMPLATES,
|
PROJECT_TEMPLATES,
|
||||||
ProjectTemplateEnum,
|
ProjectTemplateEnum,
|
||||||
)
|
)
|
||||||
from core.ui.base import ProjectStage
|
|
||||||
|
|
||||||
ARCHITECTURE_STEP_NAME = "Project architecture"
|
ARCHITECTURE_STEP_NAME = "Project architecture"
|
||||||
WARN_SYSTEM_DEPS = ["docker", "kubernetes", "microservices"]
|
WARN_SYSTEM_DEPS = ["docker", "kubernetes", "microservices"]
|
||||||
@@ -97,8 +97,6 @@ class Architect(BaseAgent):
|
|||||||
display_name = "Architect"
|
display_name = "Architect"
|
||||||
|
|
||||||
async def run(self) -> AgentResponse:
|
async def run(self) -> AgentResponse:
|
||||||
await self.ui.send_project_stage(ProjectStage.ARCHITECTURE)
|
|
||||||
|
|
||||||
spec = self.current_state.specification.clone()
|
spec = self.current_state.specification.clone()
|
||||||
|
|
||||||
if spec.example_project:
|
if spec.example_project:
|
||||||
@@ -137,28 +135,28 @@ class Architect(BaseAgent):
|
|||||||
)
|
)
|
||||||
tpl: TemplateSelection = await llm(convo, parser=JSONParser(TemplateSelection))
|
tpl: TemplateSelection = await llm(convo, parser=JSONParser(TemplateSelection))
|
||||||
templates = {}
|
templates = {}
|
||||||
if tpl.template:
|
# if tpl.template:
|
||||||
answer = await self.ask_question(
|
# answer = await self.ask_question(
|
||||||
f"Do you want to use the '{tpl.template.name}' template?",
|
# f"Do you want to use the '{tpl.template.name}' template?",
|
||||||
buttons={"yes": "Yes", "no": "No"},
|
# buttons={"yes": "Yes", "no": "No"},
|
||||||
default="yes",
|
# default="yes",
|
||||||
buttons_only=True,
|
# buttons_only=True,
|
||||||
hint="Project templates are here to speed up start of your app development and save tokens and time.\n"
|
# hint="Project templates are here to speed up start of your app development and save tokens and time.\n"
|
||||||
"Choose 'Yes' to use suggested template for your app.\n"
|
# "Choose 'Yes' to use suggested template for your app.\n"
|
||||||
"If you choose 'No', project will be created from scratch.",
|
# "If you choose 'No', project will be created from scratch.",
|
||||||
)
|
# )
|
||||||
|
#
|
||||||
if answer.button == "no":
|
# if answer.button == "no":
|
||||||
return tpl.architecture, templates
|
# return tpl.architecture, templates
|
||||||
|
#
|
||||||
template_class = PROJECT_TEMPLATES.get(tpl.template)
|
# template_class = PROJECT_TEMPLATES.get(tpl.template)
|
||||||
if template_class:
|
# if template_class:
|
||||||
options = await self.configure_template(spec, template_class)
|
# options = await self.configure_template(spec, template_class)
|
||||||
templates[tpl.template] = template_class(
|
# templates[tpl.template] = template_class(
|
||||||
options,
|
# options,
|
||||||
self.state_manager,
|
# self.state_manager,
|
||||||
self.process_manager,
|
# self.process_manager,
|
||||||
)
|
# )
|
||||||
|
|
||||||
return tpl.architecture, templates
|
return tpl.architecture, templates
|
||||||
|
|
||||||
@@ -186,6 +184,7 @@ class Architect(BaseAgent):
|
|||||||
spec.templates = {t.name: t.options_dict for t in templates.values()}
|
spec.templates = {t.name: t.options_dict for t in templates.values()}
|
||||||
spec.system_dependencies = [d.model_dump() for d in arch.system_dependencies]
|
spec.system_dependencies = [d.model_dump() for d in arch.system_dependencies]
|
||||||
spec.package_dependencies = [d.model_dump() for d in arch.package_dependencies]
|
spec.package_dependencies = [d.model_dump() for d in arch.package_dependencies]
|
||||||
|
telemetry.set("architecture", json.loads(arch.model_dump_json()))
|
||||||
|
|
||||||
async def check_compatibility(self, arch: Architecture) -> bool:
|
async def check_compatibility(self, arch: Architecture) -> bool:
|
||||||
warn_system_deps = [dep.name for dep in arch.system_dependencies if dep.name.lower() in WARN_SYSTEM_DEPS]
|
warn_system_deps = [dep.name for dep in arch.system_dependencies if dep.name.lower() in WARN_SYSTEM_DEPS]
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class BaseAgent:
|
|||||||
prev_response: Optional["AgentResponse"] = None,
|
prev_response: Optional["AgentResponse"] = None,
|
||||||
process_manager: Optional["ProcessManager"] = None,
|
process_manager: Optional["ProcessManager"] = None,
|
||||||
data: Optional[Any] = None,
|
data: Optional[Any] = None,
|
||||||
|
args: Optional[Any] = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Create a new agent.
|
Create a new agent.
|
||||||
@@ -40,6 +41,7 @@ class BaseAgent:
|
|||||||
self.prev_response = prev_response
|
self.prev_response = prev_response
|
||||||
self.step = step
|
self.step = step
|
||||||
self.data = data
|
self.data = data
|
||||||
|
self.args = args
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_state(self) -> ProjectState:
|
def current_state(self) -> ProjectState:
|
||||||
@@ -51,7 +53,7 @@ class BaseAgent:
|
|||||||
"""Next state of the project (write-only)."""
|
"""Next state of the project (write-only)."""
|
||||||
return self.state_manager.next_state
|
return self.state_manager.next_state
|
||||||
|
|
||||||
async def send_message(self, message: str):
|
async def send_message(self, message: str, extra_info: Optional[str] = None):
|
||||||
"""
|
"""
|
||||||
Send a message to the user.
|
Send a message to the user.
|
||||||
|
|
||||||
@@ -59,8 +61,11 @@ class BaseAgent:
|
|||||||
setting the correct source and project state ID.
|
setting the correct source and project state ID.
|
||||||
|
|
||||||
:param message: Message to send.
|
:param message: Message to send.
|
||||||
|
:param extra_info: Extra information to indicate special functionality in extension
|
||||||
"""
|
"""
|
||||||
await self.ui.send_message(message + "\n", source=self.ui_source, project_state_id=str(self.current_state.id))
|
await self.ui.send_message(
|
||||||
|
message + "\n", source=self.ui_source, project_state_id=str(self.current_state.id), extra_info=extra_info
|
||||||
|
)
|
||||||
|
|
||||||
async def ask_question(
|
async def ask_question(
|
||||||
self,
|
self,
|
||||||
@@ -69,9 +74,13 @@ class BaseAgent:
|
|||||||
buttons: Optional[dict[str, str]] = None,
|
buttons: Optional[dict[str, str]] = None,
|
||||||
default: Optional[str] = None,
|
default: Optional[str] = None,
|
||||||
buttons_only: bool = False,
|
buttons_only: bool = False,
|
||||||
initial_text: Optional[str] = None,
|
|
||||||
allow_empty: bool = False,
|
allow_empty: bool = False,
|
||||||
|
full_screen: Optional[bool] = False,
|
||||||
hint: Optional[str] = None,
|
hint: Optional[str] = None,
|
||||||
|
verbose: bool = True,
|
||||||
|
initial_text: Optional[str] = None,
|
||||||
|
extra_info: Optional[str] = None,
|
||||||
|
placeholder: Optional[str] = None,
|
||||||
) -> UserInput:
|
) -> UserInput:
|
||||||
"""
|
"""
|
||||||
Ask a question to the user and return the response.
|
Ask a question to the user and return the response.
|
||||||
@@ -85,8 +94,12 @@ class BaseAgent:
|
|||||||
:param default: Default button to select.
|
:param default: Default button to select.
|
||||||
:param buttons_only: Only display buttons, no text input.
|
:param buttons_only: Only display buttons, no text input.
|
||||||
:param allow_empty: Allow empty input.
|
:param allow_empty: Allow empty input.
|
||||||
|
:param full_screen: Show question full screen in extension.
|
||||||
:param hint: Text to display in a popup as a hint to the question.
|
:param hint: Text to display in a popup as a hint to the question.
|
||||||
|
:param verbose: Whether to log the question and response.
|
||||||
:param initial_text: Initial text input.
|
:param initial_text: Initial text input.
|
||||||
|
:param extra_info: Extra information to indicate special functionality in extension.
|
||||||
|
:param placeholder: Placeholder text for the input field.
|
||||||
:return: User response.
|
:return: User response.
|
||||||
"""
|
"""
|
||||||
response = await self.ui.ask_question(
|
response = await self.ui.ask_question(
|
||||||
@@ -95,10 +108,14 @@ class BaseAgent:
|
|||||||
default=default,
|
default=default,
|
||||||
buttons_only=buttons_only,
|
buttons_only=buttons_only,
|
||||||
allow_empty=allow_empty,
|
allow_empty=allow_empty,
|
||||||
|
full_screen=full_screen,
|
||||||
hint=hint,
|
hint=hint,
|
||||||
|
verbose=verbose,
|
||||||
initial_text=initial_text,
|
initial_text=initial_text,
|
||||||
source=self.ui_source,
|
source=self.ui_source,
|
||||||
project_state_id=str(self.current_state.id),
|
project_state_id=str(self.current_state.id),
|
||||||
|
extra_info=extra_info,
|
||||||
|
placeholder=placeholder,
|
||||||
)
|
)
|
||||||
await self.state_manager.log_user_input(question, response)
|
await self.state_manager.log_user_input(question, response)
|
||||||
return response
|
return response
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
|
import json
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from core.agents.base import BaseAgent
|
from core.agents.base import BaseAgent
|
||||||
from core.agents.convo import AgentConvo
|
from core.agents.convo import AgentConvo
|
||||||
|
from core.agents.mixins import ChatWithBreakdownMixin, TestSteps
|
||||||
from core.agents.response import AgentResponse
|
from core.agents.response import AgentResponse
|
||||||
from core.config import CHECK_LOGS_AGENT_NAME, magic_words
|
from core.config import CHECK_LOGS_AGENT_NAME, magic_words
|
||||||
from core.db.models.project_state import IterationStatus
|
from core.db.models.project_state import IterationStatus
|
||||||
from core.llm.parser import JSONParser
|
from core.llm.parser import JSONParser
|
||||||
from core.log import get_logger
|
from core.log import get_logger
|
||||||
from core.telemetry import telemetry
|
from core.telemetry import telemetry
|
||||||
|
from core.ui.base import ProjectStage, pythagora_source
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
@@ -40,7 +43,7 @@ class ImportantLogsForDebugging(BaseModel):
|
|||||||
logs: list[ImportantLog] = Field(description="Important logs that will help the human debug the current bug.")
|
logs: list[ImportantLog] = Field(description="Important logs that will help the human debug the current bug.")
|
||||||
|
|
||||||
|
|
||||||
class BugHunter(BaseAgent):
|
class BugHunter(ChatWithBreakdownMixin, BaseAgent):
|
||||||
agent_type = "bug-hunter"
|
agent_type = "bug-hunter"
|
||||||
display_name = "Bug Hunter"
|
display_name = "Bug Hunter"
|
||||||
|
|
||||||
@@ -63,23 +66,35 @@ class BugHunter(BaseAgent):
|
|||||||
return await self.start_pair_programming()
|
return await self.start_pair_programming()
|
||||||
|
|
||||||
async def get_bug_reproduction_instructions(self):
|
async def get_bug_reproduction_instructions(self):
|
||||||
llm = self.get_llm(stream_output=True)
|
await self.send_message("Finding a way to reproduce the bug ...")
|
||||||
convo = AgentConvo(self).template(
|
llm = self.get_llm()
|
||||||
"get_bug_reproduction_instructions",
|
convo = (
|
||||||
current_task=self.current_state.current_task,
|
AgentConvo(self)
|
||||||
user_feedback=self.current_state.current_iteration["user_feedback"],
|
.template(
|
||||||
user_feedback_qa=self.current_state.current_iteration["user_feedback_qa"],
|
"get_bug_reproduction_instructions",
|
||||||
docs=self.current_state.docs,
|
current_task=self.current_state.current_task,
|
||||||
next_solution_to_try=None,
|
user_feedback=self.current_state.current_iteration["user_feedback"],
|
||||||
|
user_feedback_qa=self.current_state.current_iteration["user_feedback_qa"],
|
||||||
|
docs=self.current_state.docs,
|
||||||
|
next_solution_to_try=None,
|
||||||
|
)
|
||||||
|
.require_schema(TestSteps)
|
||||||
|
)
|
||||||
|
bug_reproduction_instructions: TestSteps = await llm(convo, parser=JSONParser(TestSteps), temperature=0)
|
||||||
|
self.next_state.current_iteration["bug_reproduction_description"] = json.dumps(
|
||||||
|
[test.dict() for test in bug_reproduction_instructions.steps]
|
||||||
)
|
)
|
||||||
bug_reproduction_instructions = await llm(convo, temperature=0)
|
|
||||||
self.next_state.current_iteration["bug_reproduction_description"] = bug_reproduction_instructions
|
|
||||||
|
|
||||||
async def check_logs(self, logs_message: str = None):
|
async def check_logs(self, logs_message: str = None):
|
||||||
llm = self.get_llm(CHECK_LOGS_AGENT_NAME, stream_output=True)
|
llm = self.get_llm(CHECK_LOGS_AGENT_NAME, stream_output=True)
|
||||||
convo = self.generate_iteration_convo_so_far()
|
convo = self.generate_iteration_convo_so_far()
|
||||||
|
await self.ui.start_breakdown_stream()
|
||||||
human_readable_instructions = await llm(convo, temperature=0.5)
|
human_readable_instructions = await llm(convo, temperature=0.5)
|
||||||
|
|
||||||
|
convo.assistant(human_readable_instructions)
|
||||||
|
|
||||||
|
human_readable_instructions = await self.chat_with_breakdown(convo, human_readable_instructions)
|
||||||
|
|
||||||
convo = (
|
convo = (
|
||||||
AgentConvo(self)
|
AgentConvo(self)
|
||||||
.template(
|
.template(
|
||||||
@@ -110,12 +125,24 @@ class BugHunter(BaseAgent):
|
|||||||
async def ask_user_to_test(self, awaiting_bug_reproduction: bool = False, awaiting_user_test: bool = False):
|
async def ask_user_to_test(self, awaiting_bug_reproduction: bool = False, awaiting_user_test: bool = False):
|
||||||
await self.ui.stop_app()
|
await self.ui.stop_app()
|
||||||
test_instructions = self.current_state.current_iteration["bug_reproduction_description"]
|
test_instructions = self.current_state.current_iteration["bug_reproduction_description"]
|
||||||
await self.send_message("You can reproduce the bug like this:\n\n" + test_instructions)
|
await self.ui.send_message(
|
||||||
await self.ui.send_test_instructions(test_instructions)
|
"Start the app and test it by following these instructions:\n\n", source=pythagora_source
|
||||||
|
)
|
||||||
|
await self.send_message("")
|
||||||
|
await self.ui.send_test_instructions(test_instructions, project_state_id=str(self.current_state.id))
|
||||||
|
|
||||||
if self.current_state.run_command:
|
if self.current_state.run_command:
|
||||||
await self.ui.send_run_command(self.current_state.run_command)
|
await self.ui.send_run_command(self.current_state.run_command)
|
||||||
|
|
||||||
|
await self.ask_question(
|
||||||
|
"Please test the app again.",
|
||||||
|
buttons={"done": "I am done testing"},
|
||||||
|
buttons_only=True,
|
||||||
|
default="continue",
|
||||||
|
extra_info="restart_app",
|
||||||
|
hint="Instructions for testing:\n\n" + self.current_state.current_iteration["bug_reproduction_description"],
|
||||||
|
)
|
||||||
|
|
||||||
if awaiting_user_test:
|
if awaiting_user_test:
|
||||||
buttons = {"yes": "Yes, the issue is fixed", "no": "No", "start_pair_programming": "Start Pair Programming"}
|
buttons = {"yes": "Yes, the issue is fixed", "no": "No", "start_pair_programming": "Start Pair Programming"}
|
||||||
user_feedback = await self.ask_question(
|
user_feedback = await self.ask_question(
|
||||||
@@ -137,53 +164,38 @@ class BugHunter(BaseAgent):
|
|||||||
awaiting_bug_reproduction = True
|
awaiting_bug_reproduction = True
|
||||||
|
|
||||||
if awaiting_bug_reproduction:
|
if awaiting_bug_reproduction:
|
||||||
# TODO how can we get FE and BE logs automatically?
|
|
||||||
buttons = {
|
buttons = {
|
||||||
"copy_backend_logs": "Copy Backend Logs",
|
|
||||||
"continue": "Continue without logs",
|
|
||||||
"done": "Bug is fixed",
|
"done": "Bug is fixed",
|
||||||
|
"continue": "Continue without feedback", # DO NOT CHANGE THIS TEXT without changing it in the extension (it is hardcoded)
|
||||||
"start_pair_programming": "Start Pair Programming",
|
"start_pair_programming": "Start Pair Programming",
|
||||||
}
|
}
|
||||||
backend_logs = await self.ask_question(
|
await self.ui.send_project_stage(
|
||||||
"Please share the relevant Backend logs",
|
{
|
||||||
|
"stage": ProjectStage.ADDITIONAL_FEEDBACK,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
user_feedback = await self.ask_question(
|
||||||
|
"Please add any additional feedback that could help Pythagora solve this bug",
|
||||||
buttons=buttons,
|
buttons=buttons,
|
||||||
default="continue",
|
default="continue",
|
||||||
|
extra_info="collect_logs",
|
||||||
hint="Instructions for testing:\n\n"
|
hint="Instructions for testing:\n\n"
|
||||||
+ self.current_state.current_iteration["bug_reproduction_description"],
|
+ self.current_state.current_iteration["bug_reproduction_description"],
|
||||||
)
|
)
|
||||||
|
|
||||||
if backend_logs.button == "done":
|
if user_feedback.button == "done":
|
||||||
self.next_state.complete_iteration()
|
self.next_state.complete_iteration()
|
||||||
elif backend_logs.button == "start_pair_programming":
|
return AgentResponse.done(self)
|
||||||
|
elif user_feedback.button == "start_pair_programming":
|
||||||
self.next_state.current_iteration["status"] = IterationStatus.START_PAIR_PROGRAMMING
|
self.next_state.current_iteration["status"] = IterationStatus.START_PAIR_PROGRAMMING
|
||||||
self.next_state.flag_iterations_as_modified()
|
self.next_state.flag_iterations_as_modified()
|
||||||
else:
|
return AgentResponse.done(self)
|
||||||
buttons = {
|
|
||||||
"copy_frontend_logs": "Copy Frontend Logs",
|
|
||||||
"continue": "Continue without logs",
|
|
||||||
}
|
|
||||||
frontend_logs = await self.ask_question(
|
|
||||||
"Please share the relevant Frontend logs",
|
|
||||||
buttons=buttons,
|
|
||||||
default="continue",
|
|
||||||
hint="Instructions for testing:\n\n"
|
|
||||||
+ self.current_state.current_iteration["bug_reproduction_description"],
|
|
||||||
)
|
|
||||||
|
|
||||||
buttons = {"continue": "Continue without feedback"}
|
# TODO select only the logs that are new (with PYTHAGORA_DEBUGGING_LOG)
|
||||||
user_feedback = await self.ask_question(
|
self.next_state.current_iteration["bug_hunting_cycles"][-1]["backend_logs"] = None
|
||||||
"Please add any additional feedback that could help Pythagora solve this bug",
|
self.next_state.current_iteration["bug_hunting_cycles"][-1]["frontend_logs"] = None
|
||||||
buttons=buttons,
|
self.next_state.current_iteration["bug_hunting_cycles"][-1]["user_feedback"] = user_feedback.text
|
||||||
default="continue",
|
self.next_state.current_iteration["status"] = IterationStatus.HUNTING_FOR_BUG
|
||||||
hint="Instructions for testing:\n\n"
|
|
||||||
+ self.current_state.current_iteration["bug_reproduction_description"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# TODO select only the logs that are new (with PYTHAGORA_DEBUGGING_LOG)
|
|
||||||
self.next_state.current_iteration["bug_hunting_cycles"][-1]["backend_logs"] = backend_logs.text
|
|
||||||
self.next_state.current_iteration["bug_hunting_cycles"][-1]["frontend_logs"] = frontend_logs.text
|
|
||||||
self.next_state.current_iteration["bug_hunting_cycles"][-1]["user_feedback"] = user_feedback.text
|
|
||||||
self.next_state.current_iteration["status"] = IterationStatus.HUNTING_FOR_BUG
|
|
||||||
|
|
||||||
return AgentResponse.done(self)
|
return AgentResponse.done(self)
|
||||||
|
|
||||||
@@ -313,6 +325,7 @@ class BugHunter(BaseAgent):
|
|||||||
docs=self.current_state.docs,
|
docs=self.current_state.docs,
|
||||||
magic_words=magic_words,
|
magic_words=magic_words,
|
||||||
next_solution_to_try=None,
|
next_solution_to_try=None,
|
||||||
|
test_instructions=json.loads(self.current_state.current_task.get("test_instructions") or "[]"),
|
||||||
)
|
)
|
||||||
|
|
||||||
hunting_cycles = self.current_state.current_iteration.get("bug_hunting_cycles", [])[
|
hunting_cycles = self.current_state.current_iteration.get("bug_hunting_cycles", [])[
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from pydantic import BaseModel, Field
|
|||||||
|
|
||||||
from core.agents.base import BaseAgent
|
from core.agents.base import BaseAgent
|
||||||
from core.agents.convo import AgentConvo
|
from core.agents.convo import AgentConvo
|
||||||
|
from core.agents.mixins import FileDiffMixin
|
||||||
from core.agents.response import AgentResponse, ResponseType
|
from core.agents.response import AgentResponse, ResponseType
|
||||||
from core.config import CODE_MONKEY_AGENT_NAME, CODE_REVIEW_AGENT_NAME, DESCRIBE_FILES_AGENT_NAME
|
from core.config import CODE_MONKEY_AGENT_NAME, CODE_REVIEW_AGENT_NAME, DESCRIBE_FILES_AGENT_NAME
|
||||||
from core.llm.parser import JSONParser, OptionalCodeBlockParser
|
from core.llm.parser import JSONParser, OptionalCodeBlockParser
|
||||||
@@ -54,7 +55,7 @@ class FileDescription(BaseModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CodeMonkey(BaseAgent):
|
class CodeMonkey(FileDiffMixin, BaseAgent):
|
||||||
agent_type = "code-monkey"
|
agent_type = "code-monkey"
|
||||||
display_name = "Code Monkey"
|
display_name = "Code Monkey"
|
||||||
|
|
||||||
@@ -82,13 +83,15 @@ class CodeMonkey(BaseAgent):
|
|||||||
attempt = data["attempt"] + 1
|
attempt = data["attempt"] + 1
|
||||||
feedback = data["feedback"]
|
feedback = data["feedback"]
|
||||||
log.debug(f"Fixing file {file_name} after review feedback: {feedback} ({attempt}. attempt)")
|
log.debug(f"Fixing file {file_name} after review feedback: {feedback} ({attempt}. attempt)")
|
||||||
await self.ui.send_file_status(file_name, "reworking")
|
await self.ui.send_file_status(file_name, "reworking", source=self.ui_source)
|
||||||
else:
|
else:
|
||||||
log.debug(f"Implementing file {file_name}")
|
log.debug(f"Implementing file {file_name}")
|
||||||
if data is None:
|
if data is None:
|
||||||
await self.ui.send_file_status(file_name, "updating" if file_content else "creating")
|
await self.ui.send_file_status(
|
||||||
|
file_name, "updating" if file_content else "creating", source=self.ui_source
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await self.ui.send_file_status(file_name, "reworking")
|
await self.ui.send_file_status(file_name, "reworking", source=self.ui_source)
|
||||||
self.next_state.action = "Updating files"
|
self.next_state.action = "Updating files"
|
||||||
attempt = 1
|
attempt = 1
|
||||||
feedback = None
|
feedback = None
|
||||||
@@ -175,7 +178,7 @@ class CodeMonkey(BaseAgent):
|
|||||||
# ------------------------------
|
# ------------------------------
|
||||||
|
|
||||||
async def run_code_review(self, data: Optional[dict]) -> Union[AgentResponse, dict]:
|
async def run_code_review(self, data: Optional[dict]) -> Union[AgentResponse, dict]:
|
||||||
await self.ui.send_file_status(data["path"], "reviewing")
|
await self.ui.send_file_status(data["path"], "reviewing", source=self.ui_source)
|
||||||
if (
|
if (
|
||||||
data is not None
|
data is not None
|
||||||
and not data["old_content"]
|
and not data["old_content"]
|
||||||
@@ -202,15 +205,17 @@ class CodeMonkey(BaseAgent):
|
|||||||
return await self.accept_changes(data["path"], data["old_content"], approved_content)
|
return await self.accept_changes(data["path"], data["old_content"], approved_content)
|
||||||
|
|
||||||
async def accept_changes(self, file_path: str, old_content: str, new_content: str) -> AgentResponse:
|
async def accept_changes(self, file_path: str, old_content: str, new_content: str) -> AgentResponse:
|
||||||
await self.ui.send_file_status(file_path, "done")
|
await self.ui.send_file_status(file_path, "done", source=self.ui_source)
|
||||||
|
|
||||||
n_new_lines, n_del_lines = self.get_line_changes(old_content, new_content)
|
n_new_lines, n_del_lines = self.get_line_changes(old_content, new_content)
|
||||||
await self.ui.generate_diff(file_path, old_content, new_content, n_new_lines, n_del_lines)
|
await self.ui.generate_diff(
|
||||||
|
file_path, old_content, new_content, n_new_lines, n_del_lines, source=self.ui_source
|
||||||
|
)
|
||||||
|
|
||||||
await self.state_manager.save_file(file_path, new_content)
|
await self.state_manager.save_file(file_path, new_content)
|
||||||
self.next_state.complete_step()
|
self.next_state.complete_step("save_file")
|
||||||
|
|
||||||
input_required = self.state_manager.get_input_required(new_content)
|
input_required = self.state_manager.get_input_required(new_content, file_path)
|
||||||
if input_required:
|
if input_required:
|
||||||
return AgentResponse.input_required(
|
return AgentResponse.input_required(
|
||||||
self,
|
self,
|
||||||
@@ -229,6 +234,7 @@ class CodeMonkey(BaseAgent):
|
|||||||
task=task,
|
task=task,
|
||||||
iteration=None,
|
iteration=None,
|
||||||
current_task_index=current_task_index,
|
current_task_index=current_task_index,
|
||||||
|
related_api_endpoints=task.get("related_api_endpoints", []),
|
||||||
)
|
)
|
||||||
# TODO: We currently show last iteration to the code monkey; we might need to show the task
|
# TODO: We currently show last iteration to the code monkey; we might need to show the task
|
||||||
# breakdown and all the iterations instead? To think about when refactoring prompts
|
# breakdown and all the iterations instead? To think about when refactoring prompts
|
||||||
@@ -333,35 +339,6 @@ class CodeMonkey(BaseAgent):
|
|||||||
else:
|
else:
|
||||||
return new_content, None
|
return new_content, None
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_line_changes(old_content: str, new_content: str) -> tuple[int, int]:
|
|
||||||
"""
|
|
||||||
Get the number of added and deleted lines between two files.
|
|
||||||
|
|
||||||
This uses Python difflib to produce a unified diff, then counts
|
|
||||||
the number of added and deleted lines.
|
|
||||||
|
|
||||||
:param old_content: old file content
|
|
||||||
:param new_content: new file content
|
|
||||||
:return: a tuple (added_lines, deleted_lines)
|
|
||||||
"""
|
|
||||||
|
|
||||||
from_lines = old_content.splitlines(keepends=True)
|
|
||||||
to_lines = new_content.splitlines(keepends=True)
|
|
||||||
|
|
||||||
diff_gen = unified_diff(from_lines, to_lines)
|
|
||||||
|
|
||||||
added_lines = 0
|
|
||||||
deleted_lines = 0
|
|
||||||
|
|
||||||
for line in diff_gen:
|
|
||||||
if line.startswith("+") and not line.startswith("+++"): # Exclude the file headers
|
|
||||||
added_lines += 1
|
|
||||||
elif line.startswith("-") and not line.startswith("---"): # Exclude the file headers
|
|
||||||
deleted_lines += 1
|
|
||||||
|
|
||||||
return added_lines, deleted_lines
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_diff_hunks(file_name: str, old_content: str, new_content: str) -> list[str]:
|
def get_diff_hunks(file_name: str, old_content: str, new_content: str) -> list[str]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from pydantic import BaseModel, Field
|
|||||||
|
|
||||||
from core.agents.base import BaseAgent
|
from core.agents.base import BaseAgent
|
||||||
from core.agents.convo import AgentConvo
|
from core.agents.convo import AgentConvo
|
||||||
from core.agents.mixins import RelevantFilesMixin
|
from core.agents.mixins import ChatWithBreakdownMixin, RelevantFilesMixin
|
||||||
from core.agents.response import AgentResponse
|
from core.agents.response import AgentResponse
|
||||||
from core.config import PARSE_TASK_AGENT_NAME, TASK_BREAKDOWN_AGENT_NAME
|
from core.config import PARSE_TASK_AGENT_NAME, TASK_BREAKDOWN_AGENT_NAME
|
||||||
from core.db.models.project_state import IterationStatus, TaskStatus
|
from core.db.models.project_state import IterationStatus, TaskStatus
|
||||||
@@ -15,6 +15,7 @@ from core.db.models.specification import Complexity
|
|||||||
from core.llm.parser import JSONParser
|
from core.llm.parser import JSONParser
|
||||||
from core.log import get_logger
|
from core.log import get_logger
|
||||||
from core.telemetry import telemetry
|
from core.telemetry import telemetry
|
||||||
|
from core.ui.base import ProjectStage
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ class StepType(str, Enum):
|
|||||||
COMMAND = "command"
|
COMMAND = "command"
|
||||||
SAVE_FILE = "save_file"
|
SAVE_FILE = "save_file"
|
||||||
HUMAN_INTERVENTION = "human_intervention"
|
HUMAN_INTERVENTION = "human_intervention"
|
||||||
|
UTILITY_FUNCTION = "utility_function"
|
||||||
|
|
||||||
|
|
||||||
class CommandOptions(BaseModel):
|
class CommandOptions(BaseModel):
|
||||||
@@ -38,6 +40,7 @@ class SaveFileOptions(BaseModel):
|
|||||||
class SaveFileStep(BaseModel):
|
class SaveFileStep(BaseModel):
|
||||||
type: Literal[StepType.SAVE_FILE] = StepType.SAVE_FILE
|
type: Literal[StepType.SAVE_FILE] = StepType.SAVE_FILE
|
||||||
save_file: SaveFileOptions
|
save_file: SaveFileOptions
|
||||||
|
related_api_endpoints: list[str] = Field(description="API endpoints that are implemented in this file", default=[])
|
||||||
|
|
||||||
|
|
||||||
class CommandStep(BaseModel):
|
class CommandStep(BaseModel):
|
||||||
@@ -50,8 +53,18 @@ class HumanInterventionStep(BaseModel):
|
|||||||
human_intervention_description: str
|
human_intervention_description: str
|
||||||
|
|
||||||
|
|
||||||
|
class UtilityFunction(BaseModel):
|
||||||
|
type: Literal[StepType.UTILITY_FUNCTION] = StepType.UTILITY_FUNCTION
|
||||||
|
file: str
|
||||||
|
function_name: str
|
||||||
|
description: str
|
||||||
|
return_value: str
|
||||||
|
input_value: str
|
||||||
|
status: Literal["mocked", "implemented"]
|
||||||
|
|
||||||
|
|
||||||
Step = Annotated[
|
Step = Annotated[
|
||||||
Union[SaveFileStep, CommandStep, HumanInterventionStep],
|
Union[SaveFileStep, CommandStep, HumanInterventionStep, UtilityFunction],
|
||||||
Field(discriminator="type"),
|
Field(discriminator="type"),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -60,11 +73,14 @@ class TaskSteps(BaseModel):
|
|||||||
steps: list[Step]
|
steps: list[Step]
|
||||||
|
|
||||||
|
|
||||||
class Developer(RelevantFilesMixin, BaseAgent):
|
class Developer(ChatWithBreakdownMixin, RelevantFilesMixin, BaseAgent):
|
||||||
agent_type = "developer"
|
agent_type = "developer"
|
||||||
display_name = "Developer"
|
display_name = "Developer"
|
||||||
|
|
||||||
async def run(self) -> AgentResponse:
|
async def run(self) -> AgentResponse:
|
||||||
|
if self.current_state.current_step and self.current_state.current_step.get("type") == "utility_function":
|
||||||
|
return await self.update_knowledge_base()
|
||||||
|
|
||||||
if not self.current_state.unfinished_tasks:
|
if not self.current_state.unfinished_tasks:
|
||||||
log.warning("No unfinished tasks found, nothing to do (why am I called? is this a bug?)")
|
log.warning("No unfinished tasks found, nothing to do (why am I called? is this a bug?)")
|
||||||
return AgentResponse.done(self)
|
return AgentResponse.done(self)
|
||||||
@@ -119,9 +135,8 @@ class Developer(RelevantFilesMixin, BaseAgent):
|
|||||||
log.debug(f"Breaking down the iteration {description}")
|
log.debug(f"Breaking down the iteration {description}")
|
||||||
|
|
||||||
if self.current_state.files and self.current_state.relevant_files is None:
|
if self.current_state.files and self.current_state.relevant_files is None:
|
||||||
return await self.get_relevant_files(user_feedback, description)
|
await self.get_relevant_files(user_feedback, description)
|
||||||
|
|
||||||
await self.send_message("Breaking down the task into steps ...")
|
|
||||||
await self.ui.send_task_progress(
|
await self.ui.send_task_progress(
|
||||||
n_tasks, # iterations and reviews can be created only one at a time, so we are always on last one
|
n_tasks, # iterations and reviews can be created only one at a time, so we are always on last one
|
||||||
n_tasks,
|
n_tasks,
|
||||||
@@ -142,6 +157,7 @@ class Developer(RelevantFilesMixin, BaseAgent):
|
|||||||
user_feedback_qa=None,
|
user_feedback_qa=None,
|
||||||
next_solution_to_try=None,
|
next_solution_to_try=None,
|
||||||
docs=self.current_state.docs,
|
docs=self.current_state.docs,
|
||||||
|
test_instructions=json.loads(current_task.get("test_instructions") or "[]"),
|
||||||
)
|
)
|
||||||
.assistant(description)
|
.assistant(description)
|
||||||
.template("parse_task")
|
.template("parse_task")
|
||||||
@@ -195,13 +211,14 @@ class Developer(RelevantFilesMixin, BaseAgent):
|
|||||||
|
|
||||||
log.debug(f"Current state files: {len(self.current_state.files)}, relevant {self.current_state.relevant_files}")
|
log.debug(f"Current state files: {len(self.current_state.files)}, relevant {self.current_state.relevant_files}")
|
||||||
# Check which files are relevant to the current task
|
# Check which files are relevant to the current task
|
||||||
if self.current_state.files and self.current_state.relevant_files is None:
|
await self.get_relevant_files()
|
||||||
return await self.get_relevant_files()
|
|
||||||
|
|
||||||
current_task_index = self.current_state.tasks.index(current_task)
|
current_task_index = self.current_state.tasks.index(current_task)
|
||||||
|
|
||||||
await self.send_message("Thinking about how to implement this task ...")
|
await self.send_message("Thinking about how to implement this task ...")
|
||||||
|
|
||||||
|
await self.ui.start_breakdown_stream()
|
||||||
|
related_api_endpoints = current_task.get("related_api_endpoints", [])
|
||||||
llm = self.get_llm(TASK_BREAKDOWN_AGENT_NAME, stream_output=True)
|
llm = self.get_llm(TASK_BREAKDOWN_AGENT_NAME, stream_output=True)
|
||||||
convo = AgentConvo(self).template(
|
convo = AgentConvo(self).template(
|
||||||
"breakdown",
|
"breakdown",
|
||||||
@@ -209,10 +226,12 @@ class Developer(RelevantFilesMixin, BaseAgent):
|
|||||||
iteration=None,
|
iteration=None,
|
||||||
current_task_index=current_task_index,
|
current_task_index=current_task_index,
|
||||||
docs=self.current_state.docs,
|
docs=self.current_state.docs,
|
||||||
|
related_api_endpoints=related_api_endpoints,
|
||||||
)
|
)
|
||||||
response: str = await llm(convo)
|
response: str = await llm(convo)
|
||||||
|
convo.assistant(response)
|
||||||
|
|
||||||
await self.get_relevant_files(None, response)
|
response = await self.chat_with_breakdown(convo, response)
|
||||||
|
|
||||||
self.next_state.tasks[current_task_index] = {
|
self.next_state.tasks[current_task_index] = {
|
||||||
**current_task,
|
**current_task,
|
||||||
@@ -221,8 +240,7 @@ class Developer(RelevantFilesMixin, BaseAgent):
|
|||||||
self.next_state.flag_tasks_as_modified()
|
self.next_state.flag_tasks_as_modified()
|
||||||
|
|
||||||
llm = self.get_llm(PARSE_TASK_AGENT_NAME)
|
llm = self.get_llm(PARSE_TASK_AGENT_NAME)
|
||||||
await self.send_message("Breaking down the task into steps ...")
|
convo.template("parse_task").require_schema(TaskSteps)
|
||||||
convo.assistant(response).template("parse_task").require_schema(TaskSteps)
|
|
||||||
response: TaskSteps = await llm(convo, parser=JSONParser(TaskSteps), temperature=0)
|
response: TaskSteps = await llm(convo, parser=JSONParser(TaskSteps), temperature=0)
|
||||||
|
|
||||||
# There might be state leftovers from previous tasks that we need to clean here
|
# There might be state leftovers from previous tasks that we need to clean here
|
||||||
@@ -289,7 +307,16 @@ class Developer(RelevantFilesMixin, BaseAgent):
|
|||||||
buttons["skip"] = "Skip Task"
|
buttons["skip"] = "Skip Task"
|
||||||
|
|
||||||
description = self.current_state.current_task["description"]
|
description = self.current_state.current_task["description"]
|
||||||
await self.send_message("Starting new task with description:\n\n" + description)
|
task_index = self.current_state.tasks.index(self.current_state.current_task) + 1
|
||||||
|
await self.ui.send_project_stage(
|
||||||
|
{
|
||||||
|
"stage": ProjectStage.STARTING_TASK,
|
||||||
|
"task_index": task_index,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await self.send_message(f"Starting task #{task_index} with the description:\n\n" + description)
|
||||||
|
if self.current_state.run_command:
|
||||||
|
await self.ui.send_run_command(self.current_state.run_command)
|
||||||
user_response = await self.ask_question(
|
user_response = await self.ask_question(
|
||||||
"Do you want to execute the above task?",
|
"Do you want to execute the above task?",
|
||||||
buttons=buttons,
|
buttons=buttons,
|
||||||
@@ -329,3 +356,11 @@ class Developer(RelevantFilesMixin, BaseAgent):
|
|||||||
log.info(f"Task description updated to: {user_response.text}")
|
log.info(f"Task description updated to: {user_response.text}")
|
||||||
# Orchestrator will rerun us with the new task description
|
# Orchestrator will rerun us with the new task description
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
async def update_knowledge_base(self):
|
||||||
|
"""
|
||||||
|
Update the knowledge base with the current task and steps.
|
||||||
|
"""
|
||||||
|
await self.state_manager.update_utility_functions(self.current_state.current_step)
|
||||||
|
self.next_state.complete_step("utility_function")
|
||||||
|
return AgentResponse.done(self)
|
||||||
|
|||||||
@@ -86,7 +86,9 @@ class Executor(BaseAgent):
|
|||||||
q,
|
q,
|
||||||
buttons={"yes": "Yes", "no": "No"},
|
buttons={"yes": "Yes", "no": "No"},
|
||||||
default="yes",
|
default="yes",
|
||||||
buttons_only=True,
|
buttons_only=False,
|
||||||
|
initial_text=cmd,
|
||||||
|
extra_info="remove_button_yes",
|
||||||
)
|
)
|
||||||
if confirm.button == "no":
|
if confirm.button == "no":
|
||||||
log.info(f"Skipping command execution of `{cmd}` (requested by user)")
|
log.info(f"Skipping command execution of `{cmd}` (requested by user)")
|
||||||
@@ -95,6 +97,9 @@ class Executor(BaseAgent):
|
|||||||
self.next_state.action = f'Skip "{cmd_name}"'
|
self.next_state.action = f'Skip "{cmd_name}"'
|
||||||
return AgentResponse.done(self)
|
return AgentResponse.done(self)
|
||||||
|
|
||||||
|
if confirm.button != "yes":
|
||||||
|
cmd = confirm.text
|
||||||
|
|
||||||
started_at = datetime.now(timezone.utc)
|
started_at = datetime.now(timezone.utc)
|
||||||
|
|
||||||
log.info(f"Running command `{cmd}` with timeout {timeout}s")
|
log.info(f"Running command `{cmd}` with timeout {timeout}s")
|
||||||
@@ -172,4 +177,4 @@ class Executor(BaseAgent):
|
|||||||
information we give it.
|
information we give it.
|
||||||
"""
|
"""
|
||||||
self.step = None
|
self.step = None
|
||||||
self.next_state.complete_step()
|
self.next_state.complete_step("command")
|
||||||
|
|||||||
297
core/agents/frontend.py
Normal file
297
core/agents/frontend.py
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from core.agents.base import BaseAgent
|
||||||
|
from core.agents.convo import AgentConvo
|
||||||
|
from core.agents.mixins import FileDiffMixin
|
||||||
|
from core.agents.response import AgentResponse
|
||||||
|
from core.config import FRONTEND_AGENT_NAME
|
||||||
|
from core.llm.parser import DescriptiveCodeBlockParser
|
||||||
|
from core.log import get_logger
|
||||||
|
from core.telemetry import telemetry
|
||||||
|
from core.templates.registry import PROJECT_TEMPLATES
|
||||||
|
from core.ui.base import ProjectStage
|
||||||
|
|
||||||
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Frontend(FileDiffMixin, BaseAgent):
|
||||||
|
agent_type = "frontend"
|
||||||
|
display_name = "Frontend"
|
||||||
|
|
||||||
|
async def run(self) -> AgentResponse:
|
||||||
|
if not self.current_state.epics:
|
||||||
|
finished = await self.init_frontend()
|
||||||
|
elif not self.current_state.epics[0]["messages"]:
|
||||||
|
finished = await self.start_frontend()
|
||||||
|
elif not self.next_state.epics[-1].get("fe_iteration_done"):
|
||||||
|
finished = await self.continue_frontend()
|
||||||
|
else:
|
||||||
|
await self.set_app_details()
|
||||||
|
finished = await self.iterate_frontend()
|
||||||
|
|
||||||
|
return await self.end_frontend_iteration(finished)
|
||||||
|
|
||||||
|
async def init_frontend(self) -> bool:
|
||||||
|
"""
|
||||||
|
Builds frontend of the app.
|
||||||
|
|
||||||
|
:return: AgentResponse.done(self)
|
||||||
|
"""
|
||||||
|
await self.ui.send_project_stage({"stage": ProjectStage.PROJECT_DESCRIPTION})
|
||||||
|
|
||||||
|
description = await self.ask_question(
|
||||||
|
"Please describe the app you want to build.",
|
||||||
|
allow_empty=False,
|
||||||
|
full_screen=True,
|
||||||
|
)
|
||||||
|
description = description.text.strip()
|
||||||
|
|
||||||
|
auth_needed = await self.ask_question(
|
||||||
|
"Do you need authentication in your app (login, register, etc.)?",
|
||||||
|
buttons={
|
||||||
|
"yes": "Yes",
|
||||||
|
"no": "No",
|
||||||
|
},
|
||||||
|
buttons_only=True,
|
||||||
|
default="no",
|
||||||
|
)
|
||||||
|
options = {
|
||||||
|
"auth": auth_needed.button == "yes",
|
||||||
|
}
|
||||||
|
self.next_state.knowledge_base["user_options"] = options
|
||||||
|
self.state_manager.user_options = options
|
||||||
|
|
||||||
|
await self.send_message("Setting up the project...")
|
||||||
|
|
||||||
|
self.next_state.epics = [
|
||||||
|
{
|
||||||
|
"id": uuid4().hex,
|
||||||
|
"name": "Build frontend",
|
||||||
|
"source": "frontend",
|
||||||
|
"description": description,
|
||||||
|
"messages": [],
|
||||||
|
"summary": None,
|
||||||
|
"completed": False,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
await self.apply_template(options)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def start_frontend(self):
|
||||||
|
"""
|
||||||
|
Starts the frontend of the app.
|
||||||
|
"""
|
||||||
|
await self.send_message("Building the frontend... This may take a couple of minutes")
|
||||||
|
description = self.current_state.epics[0]["description"]
|
||||||
|
|
||||||
|
llm = self.get_llm(FRONTEND_AGENT_NAME)
|
||||||
|
convo = AgentConvo(self).template(
|
||||||
|
"build_frontend",
|
||||||
|
description=description,
|
||||||
|
user_feedback=None,
|
||||||
|
)
|
||||||
|
response = await llm(convo, parser=DescriptiveCodeBlockParser())
|
||||||
|
response_blocks = response.blocks
|
||||||
|
convo.assistant(response.original_response)
|
||||||
|
|
||||||
|
await self.process_response(response_blocks)
|
||||||
|
|
||||||
|
self.next_state.epics[-1]["messages"] = convo.messages
|
||||||
|
self.next_state.epics[-1]["fe_iteration_done"] = (
|
||||||
|
"done" in response.original_response[-20:].lower().strip() or len(convo.messages) > 11
|
||||||
|
)
|
||||||
|
self.next_state.flag_epics_as_modified()
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def continue_frontend(self):
|
||||||
|
"""
|
||||||
|
Continues building the frontend of the app after the initial user input.
|
||||||
|
"""
|
||||||
|
await self.ui.send_project_stage({"stage": ProjectStage.CONTINUE_FRONTEND})
|
||||||
|
await self.send_message("Continuing to build UI... This may take a couple of minutes")
|
||||||
|
|
||||||
|
llm = self.get_llm(FRONTEND_AGENT_NAME)
|
||||||
|
convo = AgentConvo(self)
|
||||||
|
convo.messages = self.current_state.epics[0]["messages"]
|
||||||
|
convo.user(
|
||||||
|
"Ok, now think carefully about your previous response. If the response ends by mentioning something about continuing with the implementation, continue but don't implement any files that have already been implemented. If your last response doesn't end by mentioning continuing, respond only with `DONE` and with nothing else."
|
||||||
|
)
|
||||||
|
response = await llm(convo, parser=DescriptiveCodeBlockParser())
|
||||||
|
response_blocks = response.blocks
|
||||||
|
convo.assistant(response.original_response)
|
||||||
|
|
||||||
|
await self.process_response(response_blocks)
|
||||||
|
|
||||||
|
self.next_state.epics[-1]["messages"] = convo.messages
|
||||||
|
self.next_state.epics[-1]["fe_iteration_done"] = (
|
||||||
|
"done" in response.original_response[-20:].lower().strip() or len(convo.messages) > 15
|
||||||
|
)
|
||||||
|
self.next_state.flag_epics_as_modified()
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def iterate_frontend(self) -> bool:
|
||||||
|
"""
|
||||||
|
Iterates over the frontend.
|
||||||
|
|
||||||
|
:return: True if the frontend is fully built, False otherwise.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# update the pages in the knowledge base
|
||||||
|
await self.state_manager.update_implemented_pages_and_apis()
|
||||||
|
|
||||||
|
await self.ui.send_project_stage({"stage": ProjectStage.ITERATE_FRONTEND})
|
||||||
|
|
||||||
|
answer = await self.ask_question(
|
||||||
|
"Do you want to change anything or report a bug? Keep in mind that currently ONLY frontend is implemented.",
|
||||||
|
buttons={
|
||||||
|
"yes": "I'm done building the UI",
|
||||||
|
},
|
||||||
|
default="yes",
|
||||||
|
extra_info="restart_app/collect_logs",
|
||||||
|
placeholder='For example, "I don\'t see anything when I open http://localhost:5173/" or "Nothing happens when I click on the NEW PROJECT button"',
|
||||||
|
)
|
||||||
|
|
||||||
|
if answer.button == "yes":
|
||||||
|
answer = await self.ask_question(
|
||||||
|
"Are you sure you're done building the UI and want to start building the backend functionality now?",
|
||||||
|
buttons={
|
||||||
|
"yes": "Yes, let's build the backend",
|
||||||
|
"no": "No, continue working on the UI",
|
||||||
|
},
|
||||||
|
buttons_only=True,
|
||||||
|
default="yes",
|
||||||
|
)
|
||||||
|
|
||||||
|
if answer.button == "yes":
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
await self.send_message("Implementing the changes you suggested...")
|
||||||
|
|
||||||
|
llm = self.get_llm(FRONTEND_AGENT_NAME, stream_output=True)
|
||||||
|
convo = AgentConvo(self).template(
|
||||||
|
"build_frontend",
|
||||||
|
description=self.current_state.epics[0]["description"],
|
||||||
|
user_feedback=answer.text,
|
||||||
|
)
|
||||||
|
response = await llm(convo, parser=DescriptiveCodeBlockParser())
|
||||||
|
|
||||||
|
await self.process_response(response.blocks)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def end_frontend_iteration(self, finished: bool) -> AgentResponse:
|
||||||
|
"""
|
||||||
|
Ends the frontend iteration.
|
||||||
|
|
||||||
|
:param finished: Whether the frontend is fully built.
|
||||||
|
:return: AgentResponse.done(self)
|
||||||
|
"""
|
||||||
|
if finished:
|
||||||
|
# TODO Add question if user app is fully finished
|
||||||
|
self.next_state.complete_epic()
|
||||||
|
await telemetry.trace_code_event(
|
||||||
|
"frontend-finished",
|
||||||
|
{
|
||||||
|
"description": self.current_state.epics[0]["description"],
|
||||||
|
"messages": self.current_state.epics[0]["messages"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
inputs = []
|
||||||
|
for file in self.current_state.files:
|
||||||
|
if not file.content:
|
||||||
|
continue
|
||||||
|
input_required = self.state_manager.get_input_required(file.content.content, file.path)
|
||||||
|
if input_required:
|
||||||
|
inputs += [{"file": file.path, "line": line} for line in input_required]
|
||||||
|
|
||||||
|
if inputs:
|
||||||
|
return AgentResponse.input_required(self, inputs)
|
||||||
|
|
||||||
|
return AgentResponse.done(self)
|
||||||
|
|
||||||
|
async def process_response(self, response_blocks: list) -> AgentResponse:
|
||||||
|
"""
|
||||||
|
Processes the response blocks from the LLM.
|
||||||
|
|
||||||
|
:param response_blocks: The response blocks from the LLM.
|
||||||
|
:return: AgentResponse.done(self)
|
||||||
|
"""
|
||||||
|
for block in response_blocks:
|
||||||
|
description = block.description.strip()
|
||||||
|
content = block.content.strip()
|
||||||
|
|
||||||
|
# Split description into lines and check the last line for file path
|
||||||
|
description_lines = description.split("\n")
|
||||||
|
last_line = description_lines[-1].strip()
|
||||||
|
|
||||||
|
if "file:" in last_line:
|
||||||
|
# Extract file path from the last line - get everything after "file:"
|
||||||
|
file_path = last_line[last_line.index("file:") + 5 :].strip()
|
||||||
|
file_path = file_path.strip("\"'`")
|
||||||
|
new_content = content
|
||||||
|
old_content = self.current_state.get_file_content_by_path(file_path)
|
||||||
|
n_new_lines, n_del_lines = self.get_line_changes(old_content, new_content)
|
||||||
|
await self.ui.send_file_status(file_path, "done", source=self.ui_source)
|
||||||
|
await self.ui.generate_diff(
|
||||||
|
file_path, old_content, new_content, n_new_lines, n_del_lines, source=self.ui_source
|
||||||
|
)
|
||||||
|
await self.state_manager.save_file(file_path, new_content)
|
||||||
|
|
||||||
|
elif "command:" in last_line:
|
||||||
|
# Split multiple commands and execute them sequentially
|
||||||
|
commands = content.strip().split("\n")
|
||||||
|
for command in commands:
|
||||||
|
command = command.strip()
|
||||||
|
if command:
|
||||||
|
# Add "cd client" prefix if not already present
|
||||||
|
if not command.startswith("cd "):
|
||||||
|
command = f"cd client && {command}"
|
||||||
|
await self.send_message(f"Running command: `{command}`...")
|
||||||
|
await self.process_manager.run_command(command)
|
||||||
|
else:
|
||||||
|
log.info(f"Unknown block description: {description}")
|
||||||
|
|
||||||
|
return AgentResponse.done(self)
|
||||||
|
|
||||||
|
async def apply_template(self, options: dict = {}):
|
||||||
|
"""
|
||||||
|
Applies a template to the frontend.
|
||||||
|
"""
|
||||||
|
template_name = "vite_react"
|
||||||
|
template_class = PROJECT_TEMPLATES.get(template_name)
|
||||||
|
if not template_class:
|
||||||
|
log.error(f"Project template not found: {template_name}")
|
||||||
|
return
|
||||||
|
|
||||||
|
template = template_class(
|
||||||
|
options,
|
||||||
|
self.state_manager,
|
||||||
|
self.process_manager,
|
||||||
|
)
|
||||||
|
|
||||||
|
log.info(f"Applying project template: {template.name}")
|
||||||
|
summary = await template.apply()
|
||||||
|
|
||||||
|
self.next_state.relevant_files = template.relevant_files
|
||||||
|
self.next_state.modified_files = {}
|
||||||
|
self.next_state.specification.template_summary = summary
|
||||||
|
|
||||||
|
async def set_app_details(self):
|
||||||
|
"""
|
||||||
|
Sets the app details.
|
||||||
|
"""
|
||||||
|
command = "npm run start"
|
||||||
|
app_link = "http://localhost:5173"
|
||||||
|
|
||||||
|
self.next_state.run_command = command
|
||||||
|
# todo store app link and send whenever we are sending run_command
|
||||||
|
# self.next_state.app_link = app_link
|
||||||
|
await self.ui.send_run_command(command)
|
||||||
|
await self.ui.send_app_link(app_link)
|
||||||
165
core/agents/git.py
Normal file
165
core/agents/git.py
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from core.agents.convo import AgentConvo
|
||||||
|
from core.config.magic_words import GITIGNORE_CONTENT
|
||||||
|
from core.ui.base import pythagora_source
|
||||||
|
|
||||||
|
|
||||||
|
class GitMixin:
|
||||||
|
"""
|
||||||
|
Mixin class for git commands
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def check_git_installed(self) -> bool:
|
||||||
|
"""Check if git is installed on the system."""
|
||||||
|
status_code, _, _ = await self.process_manager.run_command("git --version", show_output=False)
|
||||||
|
git_available = status_code == 0
|
||||||
|
self.state_manager.git_available = git_available
|
||||||
|
return git_available
|
||||||
|
|
||||||
|
async def is_git_initialized(self) -> bool:
|
||||||
|
"""Check if git is initialized in the workspace."""
|
||||||
|
workspace_path = self.state_manager.get_full_project_root()
|
||||||
|
|
||||||
|
status_code, _, _ = await self.process_manager.run_command(
|
||||||
|
"git rev-parse --git-dir --is-inside-git-dir",
|
||||||
|
cwd=workspace_path,
|
||||||
|
show_output=False,
|
||||||
|
)
|
||||||
|
# Will return status code 0 only if .git exists in the current directory
|
||||||
|
git_used = status_code == 0 and os.path.exists(os.path.join(workspace_path, ".git"))
|
||||||
|
self.state_manager.git_used = git_used
|
||||||
|
return git_used
|
||||||
|
|
||||||
|
async def init_git_if_needed(self) -> bool:
|
||||||
|
"""
|
||||||
|
Initialize git repository if it hasn't been initialized yet.
|
||||||
|
Returns True if initialization was needed and successful.
|
||||||
|
"""
|
||||||
|
|
||||||
|
workspace_path = self.state_manager.get_full_project_root()
|
||||||
|
if await self.is_git_initialized():
|
||||||
|
return False
|
||||||
|
|
||||||
|
answer = await self.ui.ask_question(
|
||||||
|
"Git is not initialized for this project. Do you want to initialize it now?",
|
||||||
|
buttons={"yes": "Yes", "no": "No"},
|
||||||
|
default="yes",
|
||||||
|
buttons_only=True,
|
||||||
|
source=pythagora_source,
|
||||||
|
)
|
||||||
|
|
||||||
|
if answer.button == "no":
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
status_code, _, stderr = await self.process_manager.run_command("git init", cwd=workspace_path)
|
||||||
|
if status_code != 0:
|
||||||
|
raise RuntimeError(f"Failed to initialize git repository: {stderr}")
|
||||||
|
|
||||||
|
gitignore_path = os.path.join(workspace_path, ".gitignore")
|
||||||
|
try:
|
||||||
|
with open(gitignore_path, "w") as f:
|
||||||
|
f.write(GITIGNORE_CONTENT)
|
||||||
|
except Exception as e:
|
||||||
|
raise RuntimeError(f"Failed to create .gitignore file: {str(e)}")
|
||||||
|
|
||||||
|
# First check if there are any changes to commit
|
||||||
|
status_code, stdout, stderr = await self.process_manager.run_command(
|
||||||
|
"git status --porcelain",
|
||||||
|
cwd=workspace_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
if status_code == 0 and stdout.strip(): # If there are changes (stdout is not empty)
|
||||||
|
# Stage all files
|
||||||
|
status_code, _, stderr = await self.process_manager.run_command(
|
||||||
|
"git add .",
|
||||||
|
cwd=workspace_path,
|
||||||
|
)
|
||||||
|
if status_code != 0:
|
||||||
|
raise RuntimeError(f"Failed to stage files: {stderr}")
|
||||||
|
|
||||||
|
# Create initial commit
|
||||||
|
status_code, _, stderr = await self.process_manager.run_command(
|
||||||
|
'git commit -m "initial commit"', cwd=workspace_path
|
||||||
|
)
|
||||||
|
if status_code != 0:
|
||||||
|
raise RuntimeError(f"Failed to create initial commit: {stderr}")
|
||||||
|
|
||||||
|
self.state_manager.git_used = True
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def git_commit(self) -> None:
|
||||||
|
"""
|
||||||
|
Create a git commit with the specified message.
|
||||||
|
Raises RuntimeError if the commit fails.
|
||||||
|
"""
|
||||||
|
workspace_path = self.state_manager.get_full_project_root()
|
||||||
|
|
||||||
|
# Check if there are any changes to commit
|
||||||
|
status_code, git_status, stderr = await self.process_manager.run_command(
|
||||||
|
"git status --porcelain",
|
||||||
|
cwd=workspace_path,
|
||||||
|
show_output=False,
|
||||||
|
)
|
||||||
|
if status_code != 0:
|
||||||
|
raise RuntimeError(f"Failed to get git status: {stderr}")
|
||||||
|
|
||||||
|
if not git_status.strip():
|
||||||
|
return
|
||||||
|
|
||||||
|
answer = await self.ui.ask_question(
|
||||||
|
"Do you want to create new git commit?",
|
||||||
|
buttons={"yes": "Yes", "no": "No"},
|
||||||
|
default="yes",
|
||||||
|
buttons_only=True,
|
||||||
|
source=pythagora_source,
|
||||||
|
)
|
||||||
|
|
||||||
|
if answer.button == "no":
|
||||||
|
return
|
||||||
|
|
||||||
|
# Stage all changes
|
||||||
|
status_code, _, stderr = await self.process_manager.run_command("git add .", cwd=workspace_path)
|
||||||
|
if status_code != 0:
|
||||||
|
raise RuntimeError(f"Failed to stage changes: {stderr}")
|
||||||
|
|
||||||
|
# Get git diff
|
||||||
|
status_code, git_diff, stderr = await self.process_manager.run_command(
|
||||||
|
"git diff --cached || git diff",
|
||||||
|
cwd=workspace_path,
|
||||||
|
show_output=False,
|
||||||
|
)
|
||||||
|
if status_code != 0:
|
||||||
|
raise RuntimeError(f"Failed to create initial commit: {stderr}")
|
||||||
|
|
||||||
|
llm = self.get_llm()
|
||||||
|
convo = AgentConvo(self).template(
|
||||||
|
"commit",
|
||||||
|
git_diff=git_diff,
|
||||||
|
)
|
||||||
|
commit_message: str = await llm(convo)
|
||||||
|
|
||||||
|
answer = await self.ui.ask_question(
|
||||||
|
f"Do you accept this 'git commit' message? Here is suggested message: '{commit_message}'",
|
||||||
|
buttons={"yes": "Yes", "edit": "Edit", "no": "No, I don't want to commit changes."},
|
||||||
|
default="yes",
|
||||||
|
buttons_only=True,
|
||||||
|
source=pythagora_source,
|
||||||
|
)
|
||||||
|
|
||||||
|
if answer.button == "no":
|
||||||
|
return
|
||||||
|
elif answer.button == "edit":
|
||||||
|
user_message = await self.ui.ask_question(
|
||||||
|
"Please enter the commit message",
|
||||||
|
source=pythagora_source,
|
||||||
|
initial_text=commit_message,
|
||||||
|
)
|
||||||
|
commit_message = user_message.text
|
||||||
|
|
||||||
|
# Create commit
|
||||||
|
status_code, _, stderr = await self.process_manager.run_command(
|
||||||
|
f'git commit -m "{commit_message}"', cwd=workspace_path
|
||||||
|
)
|
||||||
|
if status_code != 0:
|
||||||
|
raise RuntimeError(f"Failed to create commit: {stderr}")
|
||||||
@@ -21,7 +21,7 @@ class HumanInput(BaseAgent):
|
|||||||
default="continue",
|
default="continue",
|
||||||
buttons_only=True,
|
buttons_only=True,
|
||||||
)
|
)
|
||||||
self.next_state.complete_step()
|
self.next_state.complete_step("human_intervention")
|
||||||
return AgentResponse.done(self)
|
return AgentResponse.done(self)
|
||||||
|
|
||||||
async def input_required(self, files: list[dict]) -> AgentResponse:
|
async def input_required(self, files: list[dict]) -> AgentResponse:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class LegacyHandler(BaseAgent):
|
|||||||
|
|
||||||
async def run(self) -> AgentResponse:
|
async def run(self) -> AgentResponse:
|
||||||
if self.data["type"] == "review_task":
|
if self.data["type"] == "review_task":
|
||||||
self.next_state.complete_step()
|
self.next_state.complete_step("review_task")
|
||||||
return AgentResponse.done(self)
|
return AgentResponse.done(self)
|
||||||
|
|
||||||
raise ValueError(f"Unknown reason for calling Legacy Handler with data: {self.data}")
|
raise ValueError(f"Unknown reason for calling Legacy Handler with data: {self.data}")
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
|
import json
|
||||||
|
from difflib import unified_diff
|
||||||
from typing import List, Optional, Union
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from core.agents.convo import AgentConvo
|
from core.agents.convo import AgentConvo
|
||||||
from core.agents.response import AgentResponse
|
from core.agents.response import AgentResponse
|
||||||
from core.config import GET_RELEVANT_FILES_AGENT_NAME, TROUBLESHOOTER_BUG_REPORT
|
from core.config import GET_RELEVANT_FILES_AGENT_NAME, TASK_BREAKDOWN_AGENT_NAME, TROUBLESHOOTER_BUG_REPORT
|
||||||
from core.llm.parser import JSONParser
|
from core.llm.parser import JSONParser
|
||||||
from core.log import get_logger
|
from core.log import get_logger
|
||||||
|
from core.ui.base import ProjectStage
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
@@ -37,6 +40,58 @@ class RelevantFiles(BaseModel):
|
|||||||
action: Union[ReadFilesAction, AddFilesAction, RemoveFilesAction, DoneBooleanAction]
|
action: Union[ReadFilesAction, AddFilesAction, RemoveFilesAction, DoneBooleanAction]
|
||||||
|
|
||||||
|
|
||||||
|
class Test(BaseModel):
|
||||||
|
title: str = Field(description="Very short title of the test.")
|
||||||
|
action: str = Field(description="More detailed description of what actions have to be taken to test the app.")
|
||||||
|
result: str = Field(description="Expected result that verifies successful test.")
|
||||||
|
|
||||||
|
|
||||||
|
class TestSteps(BaseModel):
|
||||||
|
steps: List[Test]
|
||||||
|
|
||||||
|
|
||||||
|
class ChatWithBreakdownMixin:
|
||||||
|
"""
|
||||||
|
Provides a method to chat with the user and provide a breakdown of the conversation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def chat_with_breakdown(self, convo: AgentConvo, breakdown: str) -> AgentConvo:
|
||||||
|
"""
|
||||||
|
Chat with the user and provide a breakdown of the conversation.
|
||||||
|
|
||||||
|
:param convo: The conversation object.
|
||||||
|
:param breakdown: The breakdown of the conversation.
|
||||||
|
:return: The breakdown.
|
||||||
|
"""
|
||||||
|
|
||||||
|
llm = self.get_llm(TASK_BREAKDOWN_AGENT_NAME, stream_output=True)
|
||||||
|
while True:
|
||||||
|
await self.ui.send_project_stage(
|
||||||
|
{
|
||||||
|
"stage": ProjectStage.BREAKDOWN_CHAT,
|
||||||
|
"agent": self.agent_type,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
chat = await self.ask_question(
|
||||||
|
"Are you happy with the breakdown? Now is a good time to ask questions or suggest changes.",
|
||||||
|
buttons={"yes": "Yes, looks good!"},
|
||||||
|
default="yes",
|
||||||
|
verbose=False,
|
||||||
|
)
|
||||||
|
if chat.button == "yes":
|
||||||
|
break
|
||||||
|
|
||||||
|
if len(convo.messages) > 11:
|
||||||
|
convo.trim(3, 2)
|
||||||
|
|
||||||
|
convo.user(chat.text)
|
||||||
|
breakdown: str = await llm(convo)
|
||||||
|
convo.assistant(breakdown)
|
||||||
|
|
||||||
|
return breakdown
|
||||||
|
|
||||||
|
|
||||||
class IterationPromptMixin:
|
class IterationPromptMixin:
|
||||||
"""
|
"""
|
||||||
Provides a method to find a solution to a problem based on user feedback.
|
Provides a method to find a solution to a problem based on user feedback.
|
||||||
@@ -68,8 +123,12 @@ class IterationPromptMixin:
|
|||||||
user_feedback_qa=user_feedback_qa,
|
user_feedback_qa=user_feedback_qa,
|
||||||
next_solution_to_try=next_solution_to_try,
|
next_solution_to_try=next_solution_to_try,
|
||||||
bug_hunting_cycles=bug_hunting_cycles,
|
bug_hunting_cycles=bug_hunting_cycles,
|
||||||
|
test_instructions=json.loads(self.current_state.current_task.get("test_instructions") or "[]"),
|
||||||
)
|
)
|
||||||
llm_solution: str = await llm(convo)
|
llm_solution: str = await llm(convo)
|
||||||
|
|
||||||
|
llm_solution = await self.chat_with_breakdown(convo, llm_solution)
|
||||||
|
|
||||||
return llm_solution
|
return llm_solution
|
||||||
|
|
||||||
|
|
||||||
@@ -96,9 +155,12 @@ class RelevantFilesMixin:
|
|||||||
.require_schema(RelevantFiles)
|
.require_schema(RelevantFiles)
|
||||||
)
|
)
|
||||||
|
|
||||||
while not done and len(convo.messages) < 13:
|
while not done and len(convo.messages) < 23:
|
||||||
llm_response: RelevantFiles = await llm(convo, parser=JSONParser(RelevantFiles), temperature=0)
|
llm_response: RelevantFiles = await llm(convo, parser=JSONParser(RelevantFiles), temperature=0)
|
||||||
action = llm_response.action
|
action = llm_response.action
|
||||||
|
if action is None:
|
||||||
|
convo.remove_last_x_messages(2)
|
||||||
|
continue
|
||||||
|
|
||||||
# Check if there are files to add to the list
|
# Check if there are files to add to the list
|
||||||
if getattr(action, "add_files", None):
|
if getattr(action, "add_files", None):
|
||||||
@@ -110,7 +172,9 @@ class RelevantFilesMixin:
|
|||||||
# Remove files from relevant_files that are in remove_files
|
# Remove files from relevant_files that are in remove_files
|
||||||
relevant_files.difference_update(action.remove_files)
|
relevant_files.difference_update(action.remove_files)
|
||||||
|
|
||||||
read_files = [file for file in self.current_state.files if file.path in getattr(action, "read_files", [])]
|
read_files = [
|
||||||
|
file for file in self.current_state.files if file.path in (getattr(action, "read_files", []) or [])
|
||||||
|
]
|
||||||
|
|
||||||
convo.remove_last_x_messages(1)
|
convo.remove_last_x_messages(1)
|
||||||
convo.assistant(llm_response.original_response)
|
convo.assistant(llm_response.original_response)
|
||||||
@@ -121,6 +185,41 @@ class RelevantFilesMixin:
|
|||||||
|
|
||||||
existing_files = {file.path for file in self.current_state.files}
|
existing_files = {file.path for file in self.current_state.files}
|
||||||
relevant_files = [path for path in relevant_files if path in existing_files]
|
relevant_files = [path for path in relevant_files if path in existing_files]
|
||||||
|
self.current_state.relevant_files = relevant_files
|
||||||
self.next_state.relevant_files = relevant_files
|
self.next_state.relevant_files = relevant_files
|
||||||
|
|
||||||
return AgentResponse.done(self)
|
return AgentResponse.done(self)
|
||||||
|
|
||||||
|
|
||||||
|
class FileDiffMixin:
|
||||||
|
"""
|
||||||
|
Provides a method to generate a diff between two files.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_line_changes(self, old_content: str, new_content: str) -> tuple[int, int]:
|
||||||
|
"""
|
||||||
|
Get the number of added and deleted lines between two files.
|
||||||
|
|
||||||
|
This uses Python difflib to produce a unified diff, then counts
|
||||||
|
the number of added and deleted lines.
|
||||||
|
|
||||||
|
:param old_content: old file content
|
||||||
|
:param new_content: new file content
|
||||||
|
:return: a tuple (added_lines, deleted_lines)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from_lines = old_content.splitlines(keepends=True)
|
||||||
|
to_lines = new_content.splitlines(keepends=True)
|
||||||
|
|
||||||
|
diff_gen = unified_diff(from_lines, to_lines)
|
||||||
|
|
||||||
|
added_lines = 0
|
||||||
|
deleted_lines = 0
|
||||||
|
|
||||||
|
for line in diff_gen:
|
||||||
|
if line.startswith("+") and not line.startswith("+++"): # Exclude the file headers
|
||||||
|
added_lines += 1
|
||||||
|
elif line.startswith("-") and not line.startswith("---"): # Exclude the file headers
|
||||||
|
deleted_lines += 1
|
||||||
|
|
||||||
|
return added_lines, deleted_lines
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import os
|
||||||
from typing import List, Optional, Union
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
from core.agents.architect import Architect
|
from core.agents.architect import Architect
|
||||||
@@ -9,6 +10,8 @@ from core.agents.developer import Developer
|
|||||||
from core.agents.error_handler import ErrorHandler
|
from core.agents.error_handler import ErrorHandler
|
||||||
from core.agents.executor import Executor
|
from core.agents.executor import Executor
|
||||||
from core.agents.external_docs import ExternalDocumentation
|
from core.agents.external_docs import ExternalDocumentation
|
||||||
|
from core.agents.frontend import Frontend
|
||||||
|
from core.agents.git import GitMixin
|
||||||
from core.agents.human_input import HumanInput
|
from core.agents.human_input import HumanInput
|
||||||
from core.agents.importer import Importer
|
from core.agents.importer import Importer
|
||||||
from core.agents.legacy_handler import LegacyHandler
|
from core.agents.legacy_handler import LegacyHandler
|
||||||
@@ -22,12 +25,11 @@ from core.agents.troubleshooter import Troubleshooter
|
|||||||
from core.db.models.project_state import IterationStatus, TaskStatus
|
from core.db.models.project_state import IterationStatus, TaskStatus
|
||||||
from core.log import get_logger
|
from core.log import get_logger
|
||||||
from core.telemetry import telemetry
|
from core.telemetry import telemetry
|
||||||
from core.ui.base import ProjectStage
|
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Orchestrator(BaseAgent):
|
class Orchestrator(BaseAgent, GitMixin):
|
||||||
"""
|
"""
|
||||||
Main agent that controls the flow of the process.
|
Main agent that controls the flow of the process.
|
||||||
|
|
||||||
@@ -56,6 +58,11 @@ class Orchestrator(BaseAgent):
|
|||||||
await self.init_ui()
|
await self.init_ui()
|
||||||
await self.offline_changes_check()
|
await self.offline_changes_check()
|
||||||
|
|
||||||
|
await self.install_dependencies()
|
||||||
|
|
||||||
|
if self.args.use_git and await self.check_git_installed():
|
||||||
|
await self.init_git_if_needed()
|
||||||
|
|
||||||
# TODO: consider refactoring this into two loop; the outer with one iteration per comitted step,
|
# TODO: consider refactoring this into two loop; the outer with one iteration per comitted step,
|
||||||
# and the inner which runs the agents for the current step until they're done. This would simplify
|
# and the inner which runs the agents for the current step until they're done. This would simplify
|
||||||
# handle_done() and let us do other per-step processing (eg. describing files) in between agent runs.
|
# handle_done() and let us do other per-step processing (eg. describing files) in between agent runs.
|
||||||
@@ -73,6 +80,27 @@ class Orchestrator(BaseAgent):
|
|||||||
)
|
)
|
||||||
responses = await asyncio.gather(*tasks)
|
responses = await asyncio.gather(*tasks)
|
||||||
response = self.handle_parallel_responses(agent[0], responses)
|
response = self.handle_parallel_responses(agent[0], responses)
|
||||||
|
|
||||||
|
should_update_knowledge_base = any(
|
||||||
|
"src/pages/" in single_agent.step.get("save_file", {}).get("path", "")
|
||||||
|
or "src/api/" in single_agent.step.get("save_file", {}).get("path", "")
|
||||||
|
or len(single_agent.step.get("related_api_endpoints")) > 0
|
||||||
|
for single_agent in agent
|
||||||
|
)
|
||||||
|
|
||||||
|
if should_update_knowledge_base:
|
||||||
|
files_with_implemented_apis = [
|
||||||
|
{
|
||||||
|
"path": single_agent.step.get("save_file", {}).get("path", None),
|
||||||
|
"related_api_endpoints": single_agent.step.get("related_api_endpoints"),
|
||||||
|
"line": 0, # TODO implement getting the line number here
|
||||||
|
}
|
||||||
|
for single_agent in agent
|
||||||
|
if len(single_agent.step.get("related_api_endpoints")) > 0
|
||||||
|
]
|
||||||
|
await self.state_manager.update_apis(files_with_implemented_apis)
|
||||||
|
await self.state_manager.update_implemented_pages_and_apis()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
log.debug(f"Running agent {agent.__class__.__name__} (step {self.current_state.step_index})")
|
log.debug(f"Running agent {agent.__class__.__name__} (step {self.current_state.step_index})")
|
||||||
response = await agent.run()
|
response = await agent.run()
|
||||||
@@ -88,6 +116,19 @@ class Orchestrator(BaseAgent):
|
|||||||
# TODO: rollback changes to "next" so they aren't accidentally committed?
|
# TODO: rollback changes to "next" so they aren't accidentally committed?
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
async def install_dependencies(self):
|
||||||
|
# First check if package.json exists
|
||||||
|
package_json_path = os.path.join(self.state_manager.get_full_project_root(), "package.json")
|
||||||
|
if not os.path.exists(package_json_path):
|
||||||
|
# Skip if no package.json found
|
||||||
|
return
|
||||||
|
|
||||||
|
# Then check if node_modules directory exists
|
||||||
|
node_modules_path = os.path.join(self.state_manager.get_full_project_root(), "node_modules")
|
||||||
|
if not os.path.exists(node_modules_path):
|
||||||
|
await self.send_message("Installing project dependencies...")
|
||||||
|
await self.process_manager.run_command("npm install", show_output=False)
|
||||||
|
|
||||||
def handle_parallel_responses(self, agent: BaseAgent, responses: List[AgentResponse]) -> AgentResponse:
|
def handle_parallel_responses(self, agent: BaseAgent, responses: List[AgentResponse]) -> AgentResponse:
|
||||||
"""
|
"""
|
||||||
Handle responses from agents that were run in parallel.
|
Handle responses from agents that were run in parallel.
|
||||||
@@ -215,21 +256,16 @@ class Orchestrator(BaseAgent):
|
|||||||
if prev_response.type == ResponseType.UPDATE_SPECIFICATION:
|
if prev_response.type == ResponseType.UPDATE_SPECIFICATION:
|
||||||
return SpecWriter(self.state_manager, self.ui, prev_response=prev_response)
|
return SpecWriter(self.state_manager, self.ui, prev_response=prev_response)
|
||||||
|
|
||||||
if not state.specification.description:
|
if not state.epics or (state.current_epic and state.current_epic.get("source") == "frontend"):
|
||||||
if state.files:
|
# Build frontend
|
||||||
# The project has been imported, but not analyzed yet
|
return Frontend(self.state_manager, self.ui, process_manager=self.process_manager)
|
||||||
return Importer(self.state_manager, self.ui)
|
elif not state.specification.description:
|
||||||
else:
|
# New project: ask the Spec Writer to refine and save the project specification
|
||||||
# New project: ask the Spec Writer to refine and save the project specification
|
return SpecWriter(self.state_manager, self.ui, process_manager=self.process_manager)
|
||||||
return SpecWriter(self.state_manager, self.ui, process_manager=self.process_manager)
|
|
||||||
elif not state.specification.architecture:
|
elif not state.specification.architecture:
|
||||||
# Ask the Architect to design the project architecture and determine dependencies
|
# Ask the Architect to design the project architecture and determine dependencies
|
||||||
return Architect(self.state_manager, self.ui, process_manager=self.process_manager)
|
return Architect(self.state_manager, self.ui, process_manager=self.process_manager)
|
||||||
elif (
|
elif not self.current_state.unfinished_tasks or (state.specification.templates and not state.files):
|
||||||
not state.epics
|
|
||||||
or not self.current_state.unfinished_tasks
|
|
||||||
or (state.specification.templates and not state.files)
|
|
||||||
):
|
|
||||||
# Ask the Tech Lead to break down the initial project or feature into tasks and apply project templates
|
# Ask the Tech Lead to break down the initial project or feature into tasks and apply project templates
|
||||||
return TechLead(self.state_manager, self.ui, process_manager=self.process_manager)
|
return TechLead(self.state_manager, self.ui, process_manager=self.process_manager)
|
||||||
|
|
||||||
@@ -244,7 +280,7 @@ class Orchestrator(BaseAgent):
|
|||||||
return TechnicalWriter(self.state_manager, self.ui)
|
return TechnicalWriter(self.state_manager, self.ui)
|
||||||
elif current_task_status in [TaskStatus.DOCUMENTED, TaskStatus.SKIPPED]:
|
elif current_task_status in [TaskStatus.DOCUMENTED, TaskStatus.SKIPPED]:
|
||||||
# Task is fully done or skipped, call TaskCompleter to mark it as completed
|
# Task is fully done or skipped, call TaskCompleter to mark it as completed
|
||||||
return TaskCompleter(self.state_manager, self.ui)
|
return TaskCompleter(self.state_manager, self.ui, process_manager=self.process_manager)
|
||||||
|
|
||||||
if not state.steps and not state.iterations:
|
if not state.steps and not state.iterations:
|
||||||
# Ask the Developer to break down current task into actionable steps
|
# Ask the Developer to break down current task into actionable steps
|
||||||
@@ -307,6 +343,8 @@ class Orchestrator(BaseAgent):
|
|||||||
return LegacyHandler(self.state_manager, self.ui, data={"type": "review_task"})
|
return LegacyHandler(self.state_manager, self.ui, data={"type": "review_task"})
|
||||||
elif step_type == "create_readme":
|
elif step_type == "create_readme":
|
||||||
return TechnicalWriter(self.state_manager, self.ui)
|
return TechnicalWriter(self.state_manager, self.ui)
|
||||||
|
elif step_type == "utility_function":
|
||||||
|
return Developer(self.state_manager, self.ui)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unknown step type: {step_type}")
|
raise ValueError(f"Unknown step type: {step_type}")
|
||||||
|
|
||||||
@@ -322,7 +360,7 @@ class Orchestrator(BaseAgent):
|
|||||||
|
|
||||||
input_required_files: list[dict[str, int]] = []
|
input_required_files: list[dict[str, int]] = []
|
||||||
for file in imported_files:
|
for file in imported_files:
|
||||||
for line in self.state_manager.get_input_required(file.content.content):
|
for line in self.state_manager.get_input_required(file.content.content, file.path):
|
||||||
input_required_files.append({"file": file.path, "line": line})
|
input_required_files.append({"file": file.path, "line": line})
|
||||||
|
|
||||||
if input_required_files:
|
if input_required_files:
|
||||||
@@ -340,15 +378,9 @@ class Orchestrator(BaseAgent):
|
|||||||
await self.ui.loading_finished()
|
await self.ui.loading_finished()
|
||||||
|
|
||||||
if self.current_state.epics:
|
if self.current_state.epics:
|
||||||
await self.ui.send_project_stage(ProjectStage.CODING)
|
if len(self.current_state.epics) > 3:
|
||||||
if len(self.current_state.epics) > 2:
|
|
||||||
# We only want to send previous features, ie. exclude current one and the initial project (first epic)
|
# We only want to send previous features, ie. exclude current one and the initial project (first epic)
|
||||||
await self.ui.send_features_list([e["description"] for e in self.current_state.epics[1:-1]])
|
await self.ui.send_features_list([e["description"] for e in self.current_state.epics[2:-1]])
|
||||||
|
|
||||||
elif self.current_state.specification.description:
|
|
||||||
await self.ui.send_project_stage(ProjectStage.ARCHITECTURE)
|
|
||||||
else:
|
|
||||||
await self.ui.send_project_stage(ProjectStage.DESCRIPTION)
|
|
||||||
|
|
||||||
if self.current_state.specification.description:
|
if self.current_state.specification.description:
|
||||||
await self.ui.send_project_description(self.current_state.specification.description)
|
await self.ui.send_project_description(self.current_state.specification.description)
|
||||||
|
|||||||
@@ -7,10 +7,6 @@ from core.db.models.project_state import IterationStatus
|
|||||||
from core.llm.parser import StringParser
|
from core.llm.parser import StringParser
|
||||||
from core.log import get_logger
|
from core.log import get_logger
|
||||||
from core.telemetry import telemetry
|
from core.telemetry import telemetry
|
||||||
from core.templates.example_project import (
|
|
||||||
DEFAULT_EXAMPLE_PROJECT,
|
|
||||||
EXAMPLE_PROJECTS,
|
|
||||||
)
|
|
||||||
|
|
||||||
# If the project description is less than this, perform an analysis using LLM
|
# If the project description is less than this, perform an analysis using LLM
|
||||||
ANALYZE_THRESHOLD = 1500
|
ANALYZE_THRESHOLD = 1500
|
||||||
@@ -37,25 +33,15 @@ class SpecWriter(BaseAgent):
|
|||||||
return await self.initialize_spec()
|
return await self.initialize_spec()
|
||||||
|
|
||||||
async def initialize_spec(self) -> AgentResponse:
|
async def initialize_spec(self) -> AgentResponse:
|
||||||
response = await self.ask_question(
|
# response = await self.ask_question(
|
||||||
"Describe your app in as much detail as possible",
|
# "Describe your app in as much detail as possible",
|
||||||
allow_empty=False,
|
# allow_empty=False,
|
||||||
buttons={
|
# )
|
||||||
"example": "Start an example project",
|
# if response.cancelled:
|
||||||
"import": "Import an existing project",
|
# return AgentResponse.error(self, "No project description")
|
||||||
},
|
#
|
||||||
)
|
# user_description = response.text.strip()
|
||||||
if response.cancelled:
|
user_description = self.current_state.epics[0]["description"]
|
||||||
return AgentResponse.error(self, "No project description")
|
|
||||||
|
|
||||||
if response.button == "import":
|
|
||||||
return AgentResponse.import_project(self)
|
|
||||||
|
|
||||||
if response.button == "example":
|
|
||||||
await self.prepare_example_project(DEFAULT_EXAMPLE_PROJECT)
|
|
||||||
return AgentResponse.done(self)
|
|
||||||
|
|
||||||
user_description = response.text.strip()
|
|
||||||
|
|
||||||
complexity = await self.check_prompt_complexity(user_description)
|
complexity = await self.check_prompt_complexity(user_description)
|
||||||
await telemetry.trace_code_event(
|
await telemetry.trace_code_event(
|
||||||
@@ -67,9 +53,9 @@ class SpecWriter(BaseAgent):
|
|||||||
)
|
)
|
||||||
|
|
||||||
reviewed_spec = user_description
|
reviewed_spec = user_description
|
||||||
if len(user_description) < ANALYZE_THRESHOLD and complexity != Complexity.SIMPLE:
|
# if len(user_description) < ANALYZE_THRESHOLD and complexity != Complexity.SIMPLE:
|
||||||
initial_spec = await self.analyze_spec(user_description)
|
# initial_spec = await self.analyze_spec(user_description)
|
||||||
reviewed_spec = await self.review_spec(desc=user_description, spec=initial_spec)
|
# reviewed_spec = await self.review_spec(desc=user_description, spec=initial_spec)
|
||||||
|
|
||||||
self.next_state.specification = self.current_state.specification.clone()
|
self.next_state.specification = self.current_state.specification.clone()
|
||||||
self.next_state.specification.original_description = user_description
|
self.next_state.specification.original_description = user_description
|
||||||
@@ -95,7 +81,9 @@ class SpecWriter(BaseAgent):
|
|||||||
convo = AgentConvo(self).template("add_new_feature", feature_description=feature_description)
|
convo = AgentConvo(self).template("add_new_feature", feature_description=feature_description)
|
||||||
llm_response: str = await llm(convo, temperature=0, parser=StringParser())
|
llm_response: str = await llm(convo, temperature=0, parser=StringParser())
|
||||||
updated_spec = llm_response.strip()
|
updated_spec = llm_response.strip()
|
||||||
await self.ui.generate_diff("project_specification", self.current_state.specification.description, updated_spec)
|
await self.ui.generate_diff(
|
||||||
|
"project_specification", self.current_state.specification.description, updated_spec, source=self.ui_source
|
||||||
|
)
|
||||||
user_response = await self.ask_question(
|
user_response = await self.ask_question(
|
||||||
"Do you accept these changes to the project specification?",
|
"Do you accept these changes to the project specification?",
|
||||||
buttons={"yes": "Yes", "no": "No"},
|
buttons={"yes": "Yes", "no": "No"},
|
||||||
@@ -113,38 +101,28 @@ class SpecWriter(BaseAgent):
|
|||||||
self.next_state.current_iteration["status"] = IterationStatus.FIND_SOLUTION
|
self.next_state.current_iteration["status"] = IterationStatus.FIND_SOLUTION
|
||||||
self.next_state.flag_iterations_as_modified()
|
self.next_state.flag_iterations_as_modified()
|
||||||
else:
|
else:
|
||||||
complexity = await self.check_prompt_complexity(user_response.text)
|
complexity = await self.check_prompt_complexity(feature_description)
|
||||||
self.next_state.current_epic["complexity"] = complexity
|
self.next_state.current_epic["complexity"] = complexity
|
||||||
|
|
||||||
return AgentResponse.done(self)
|
return AgentResponse.done(self)
|
||||||
|
|
||||||
async def check_prompt_complexity(self, prompt: str) -> str:
|
async def check_prompt_complexity(self, prompt: str) -> str:
|
||||||
|
is_feature = self.current_state.epics and len(self.current_state.epics) > 2
|
||||||
await self.send_message("Checking the complexity of the prompt ...")
|
await self.send_message("Checking the complexity of the prompt ...")
|
||||||
llm = self.get_llm(SPEC_WRITER_AGENT_NAME)
|
llm = self.get_llm(SPEC_WRITER_AGENT_NAME)
|
||||||
convo = AgentConvo(self).template("prompt_complexity", prompt=prompt)
|
convo = AgentConvo(self).template(
|
||||||
|
"prompt_complexity",
|
||||||
|
prompt=prompt,
|
||||||
|
is_feature=is_feature,
|
||||||
|
)
|
||||||
llm_response: str = await llm(convo, temperature=0, parser=StringParser())
|
llm_response: str = await llm(convo, temperature=0, parser=StringParser())
|
||||||
|
log.info(f"Complexity check response: {llm_response}")
|
||||||
return llm_response.lower()
|
return llm_response.lower()
|
||||||
|
|
||||||
async def prepare_example_project(self, example_name: str):
|
|
||||||
example_description = EXAMPLE_PROJECTS[example_name]["description"].strip()
|
|
||||||
|
|
||||||
log.debug(f"Starting example project: {example_name}")
|
|
||||||
await self.send_message(f"Starting example project with description:\n\n{example_description}")
|
|
||||||
|
|
||||||
spec = self.current_state.specification.clone()
|
|
||||||
spec.example_project = example_name
|
|
||||||
spec.description = example_description
|
|
||||||
spec.complexity = EXAMPLE_PROJECTS[example_name]["complexity"]
|
|
||||||
self.next_state.specification = spec
|
|
||||||
|
|
||||||
telemetry.set("initial_prompt", spec.description)
|
|
||||||
telemetry.set("example_project", example_name)
|
|
||||||
telemetry.set("is_complex_app", spec.complexity != Complexity.SIMPLE)
|
|
||||||
|
|
||||||
async def analyze_spec(self, spec: str) -> str:
|
async def analyze_spec(self, spec: str) -> str:
|
||||||
msg = (
|
msg = (
|
||||||
"Your project description seems a bit short. "
|
"Your project description seems a bit short. "
|
||||||
"The better you can describe the project, the better GPT Pilot will understand what you'd like to build.\n\n"
|
"The better you can describe the project, the better Pythagora will understand what you'd like to build.\n\n"
|
||||||
f"Here are some tips on how to better describe the project: {INITIAL_PROJECT_HOWTO_URL}\n\n"
|
f"Here are some tips on how to better describe the project: {INITIAL_PROJECT_HOWTO_URL}\n\n"
|
||||||
"Let's start by refining your project idea:"
|
"Let's start by refining your project idea:"
|
||||||
)
|
)
|
||||||
@@ -160,12 +138,9 @@ class SpecWriter(BaseAgent):
|
|||||||
if len(response) > 500:
|
if len(response) > 500:
|
||||||
# The response is too long for it to be a question, assume it's the updated spec
|
# The response is too long for it to be a question, assume it's the updated spec
|
||||||
confirm = await self.ask_question(
|
confirm = await self.ask_question(
|
||||||
(
|
("Would you like to change or add anything? Write it out here."),
|
||||||
"Can we proceed with this project description? If so, just press Continue. "
|
|
||||||
"Otherwise, please tell me what's missing or what you'd like to add."
|
|
||||||
),
|
|
||||||
allow_empty=True,
|
allow_empty=True,
|
||||||
buttons={"continue": "Continue"},
|
buttons={"continue": "No thanks, the spec looks good"},
|
||||||
)
|
)
|
||||||
if confirm.cancelled or confirm.button == "continue" or confirm.text == "":
|
if confirm.cancelled or confirm.button == "continue" or confirm.text == "":
|
||||||
updated_spec = response.strip()
|
updated_spec = response.strip()
|
||||||
@@ -186,18 +161,40 @@ class SpecWriter(BaseAgent):
|
|||||||
n_questions += 1
|
n_questions += 1
|
||||||
user_response = await self.ask_question(
|
user_response = await self.ask_question(
|
||||||
response,
|
response,
|
||||||
buttons={"skip": "Skip questions"},
|
buttons={"skip": "Skip this question", "skip_all": "No more questions"},
|
||||||
|
verbose=False,
|
||||||
)
|
)
|
||||||
if user_response.cancelled or user_response.button == "skip":
|
if user_response.cancelled or user_response.button == "skip_all":
|
||||||
convo.user(
|
convo.user(
|
||||||
"This is enough clarification, you have all the information. "
|
"This is enough clarification, you have all the information. "
|
||||||
"Please output the spec now, without additional comments or questions."
|
"Please output the spec now, without additional comments or questions."
|
||||||
)
|
)
|
||||||
response: str = await llm(convo)
|
response: str = await llm(convo)
|
||||||
return response.strip()
|
confirm = await self.ask_question(
|
||||||
|
("Would you like to change or add anything? Write it out here."),
|
||||||
|
allow_empty=True,
|
||||||
|
buttons={"continue": "No thanks, the spec looks good"},
|
||||||
|
)
|
||||||
|
if confirm.cancelled or confirm.button == "continue" or confirm.text == "":
|
||||||
|
updated_spec = response.strip()
|
||||||
|
await telemetry.trace_code_event(
|
||||||
|
"spec-writer-questions",
|
||||||
|
{
|
||||||
|
"num_questions": n_questions,
|
||||||
|
"num_answers": n_answers,
|
||||||
|
"new_spec": updated_spec,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return updated_spec
|
||||||
|
convo.user(confirm.text)
|
||||||
|
continue
|
||||||
|
|
||||||
n_answers += 1
|
n_answers += 1
|
||||||
convo.user(user_response.text)
|
if user_response.button == "skip":
|
||||||
|
convo.user("Skip this question.")
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
convo.user(user_response.text)
|
||||||
|
|
||||||
async def review_spec(self, desc: str, spec: str) -> str:
|
async def review_spec(self, desc: str, spec: str) -> str:
|
||||||
convo = AgentConvo(self).template("review_spec", desc=desc, spec=spec)
|
convo = AgentConvo(self).template("review_spec", desc=desc, spec=spec)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from core.agents.base import BaseAgent
|
from core.agents.base import BaseAgent
|
||||||
|
from core.agents.git import GitMixin
|
||||||
from core.agents.response import AgentResponse
|
from core.agents.response import AgentResponse
|
||||||
from core.log import get_logger
|
from core.log import get_logger
|
||||||
from core.telemetry import telemetry
|
from core.telemetry import telemetry
|
||||||
@@ -6,11 +7,14 @@ from core.telemetry import telemetry
|
|||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class TaskCompleter(BaseAgent):
|
class TaskCompleter(BaseAgent, GitMixin):
|
||||||
agent_type = "pythagora"
|
agent_type = "pythagora"
|
||||||
display_name = "Pythagora"
|
display_name = "Pythagora"
|
||||||
|
|
||||||
async def run(self) -> AgentResponse:
|
async def run(self) -> AgentResponse:
|
||||||
|
if self.state_manager.git_available and self.state_manager.git_used:
|
||||||
|
await self.git_commit()
|
||||||
|
|
||||||
current_task_index1 = self.current_state.tasks.index(self.current_state.current_task) + 1
|
current_task_index1 = self.current_state.tasks.index(self.current_state.current_task) + 1
|
||||||
self.next_state.action = f"Task #{current_task_index1} complete"
|
self.next_state.action = f"Task #{current_task_index1} complete"
|
||||||
self.next_state.complete_task()
|
self.next_state.complete_task()
|
||||||
|
|||||||
@@ -1,28 +1,39 @@
|
|||||||
|
import json
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from core.agents.base import BaseAgent
|
from core.agents.base import BaseAgent
|
||||||
from core.agents.convo import AgentConvo
|
from core.agents.convo import AgentConvo
|
||||||
|
from core.agents.mixins import RelevantFilesMixin
|
||||||
from core.agents.response import AgentResponse
|
from core.agents.response import AgentResponse
|
||||||
from core.config import TECH_LEAD_PLANNING
|
from core.config import TECH_LEAD_EPIC_BREAKDOWN, TECH_LEAD_PLANNING
|
||||||
|
from core.db.models import Complexity
|
||||||
from core.db.models.project_state import TaskStatus
|
from core.db.models.project_state import TaskStatus
|
||||||
from core.llm.parser import JSONParser
|
from core.llm.parser import JSONParser
|
||||||
from core.log import get_logger
|
from core.log import get_logger
|
||||||
from core.telemetry import telemetry
|
from core.telemetry import telemetry
|
||||||
from core.templates.example_project import EXAMPLE_PROJECTS
|
|
||||||
from core.templates.registry import PROJECT_TEMPLATES
|
from core.templates.registry import PROJECT_TEMPLATES
|
||||||
from core.ui.base import ProjectStage, success_source
|
from core.ui.base import ProjectStage, pythagora_source, success_source
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class APIEndpoint(BaseModel):
|
||||||
|
description: str = Field(description="Description of an API endpoint.")
|
||||||
|
method: str = Field(description="HTTP method of the API endpoint.")
|
||||||
|
endpoint: str = Field(description="URL of the API endpoint.")
|
||||||
|
request_body: dict = Field(description="Request body of the API endpoint.")
|
||||||
|
response_body: dict = Field(description="Response body of the API endpoint.")
|
||||||
|
|
||||||
|
|
||||||
class Epic(BaseModel):
|
class Epic(BaseModel):
|
||||||
description: str = Field(description=("Description of an epic."))
|
description: str = Field(description="Description of an epic.")
|
||||||
|
|
||||||
|
|
||||||
class Task(BaseModel):
|
class Task(BaseModel):
|
||||||
description: str = Field(description="Description of a task.")
|
description: str = Field(description="Description of a task.")
|
||||||
|
related_api_endpoints: list[APIEndpoint] = Field(description="API endpoints that will be implemented in this task.")
|
||||||
testing_instructions: str = Field(description="Instructions for testing the task.")
|
testing_instructions: str = Field(description="Instructions for testing the task.")
|
||||||
|
|
||||||
|
|
||||||
@@ -34,38 +45,34 @@ class EpicPlan(BaseModel):
|
|||||||
plan: list[Task] = Field(description="List of tasks that need to be done to implement the entire epic.")
|
plan: list[Task] = Field(description="List of tasks that need to be done to implement the entire epic.")
|
||||||
|
|
||||||
|
|
||||||
class TechLead(BaseAgent):
|
class TechLead(RelevantFilesMixin, BaseAgent):
|
||||||
agent_type = "tech-lead"
|
agent_type = "tech-lead"
|
||||||
display_name = "Tech Lead"
|
display_name = "Tech Lead"
|
||||||
|
|
||||||
async def run(self) -> AgentResponse:
|
async def run(self) -> AgentResponse:
|
||||||
if len(self.current_state.epics) == 0:
|
# Building frontend is the first epic
|
||||||
if self.current_state.specification.example_project:
|
if len(self.current_state.epics) == 1:
|
||||||
self.plan_example_project()
|
self.create_initial_project_epic()
|
||||||
else:
|
|
||||||
self.create_initial_project_epic()
|
|
||||||
return AgentResponse.done(self)
|
return AgentResponse.done(self)
|
||||||
|
|
||||||
await self.ui.send_project_stage(ProjectStage.CODING)
|
# if self.current_state.specification.templates and len(self.current_state.files) < 2:
|
||||||
|
# await self.apply_project_templates()
|
||||||
if self.current_state.specification.templates and not self.current_state.files:
|
# self.next_state.action = "Apply project templates"
|
||||||
await self.apply_project_templates()
|
# await self.ui.send_epics_and_tasks(
|
||||||
self.next_state.action = "Apply project templates"
|
# self.next_state.current_epic["sub_epics"],
|
||||||
await self.ui.send_epics_and_tasks(
|
# self.next_state.tasks,
|
||||||
self.next_state.current_epic["sub_epics"],
|
# )
|
||||||
self.next_state.tasks,
|
#
|
||||||
)
|
# inputs = []
|
||||||
|
# for file in self.next_state.files:
|
||||||
inputs = []
|
# input_required = self.state_manager.get_input_required(file.content.content)
|
||||||
for file in self.next_state.files:
|
# if input_required:
|
||||||
input_required = self.state_manager.get_input_required(file.content.content)
|
# inputs += [{"file": file.path, "line": line} for line in input_required]
|
||||||
if input_required:
|
#
|
||||||
inputs += [{"file": file.path, "line": line} for line in input_required]
|
# if inputs:
|
||||||
|
# return AgentResponse.input_required(self, inputs)
|
||||||
if inputs:
|
# else:
|
||||||
return AgentResponse.input_required(self, inputs)
|
# return AgentResponse.done(self)
|
||||||
else:
|
|
||||||
return AgentResponse.done(self)
|
|
||||||
|
|
||||||
if self.current_state.current_epic:
|
if self.current_state.current_epic:
|
||||||
self.next_state.action = "Create a development plan"
|
self.next_state.action = "Create a development plan"
|
||||||
@@ -75,7 +82,7 @@ class TechLead(BaseAgent):
|
|||||||
|
|
||||||
def create_initial_project_epic(self):
|
def create_initial_project_epic(self):
|
||||||
log.debug("Creating initial project Epic")
|
log.debug("Creating initial project Epic")
|
||||||
self.next_state.epics = [
|
self.next_state.epics = self.current_state.epics + [
|
||||||
{
|
{
|
||||||
"id": uuid4().hex,
|
"id": uuid4().hex,
|
||||||
"name": "Initial Project",
|
"name": "Initial Project",
|
||||||
@@ -88,6 +95,8 @@ class TechLead(BaseAgent):
|
|||||||
"sub_epics": [],
|
"sub_epics": [],
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
self.next_state.relevant_files = None
|
||||||
|
self.next_state.modified_files = {}
|
||||||
|
|
||||||
async def apply_project_templates(self):
|
async def apply_project_templates(self):
|
||||||
state = self.current_state
|
state = self.current_state
|
||||||
@@ -138,19 +147,21 @@ class TechLead(BaseAgent):
|
|||||||
"Do you have a new feature to add to the project? Just write it here:",
|
"Do you have a new feature to add to the project? Just write it here:",
|
||||||
buttons={"continue": "continue", "end": "No, I'm done"},
|
buttons={"continue": "continue", "end": "No, I'm done"},
|
||||||
allow_empty=False,
|
allow_empty=False,
|
||||||
|
extra_info="restart_app",
|
||||||
)
|
)
|
||||||
|
|
||||||
if response.button == "end" or response.cancelled or not response.text:
|
if response.button == "end" or response.cancelled or not response.text:
|
||||||
await self.ui.send_message("Thanks for using Pythagora!")
|
await self.ui.send_message("Thank you for using Pythagora!", source=pythagora_source)
|
||||||
return AgentResponse.exit(self)
|
return AgentResponse.exit(self)
|
||||||
|
|
||||||
|
feature_description = response.text
|
||||||
self.next_state.epics = self.current_state.epics + [
|
self.next_state.epics = self.current_state.epics + [
|
||||||
{
|
{
|
||||||
"id": uuid4().hex,
|
"id": uuid4().hex,
|
||||||
"name": f"Feature #{len(self.current_state.epics)}",
|
"name": f"Feature #{len(self.current_state.epics)}",
|
||||||
"test_instructions": None,
|
"test_instructions": None,
|
||||||
"source": "feature",
|
"source": "feature",
|
||||||
"description": response.text,
|
"description": feature_description,
|
||||||
"summary": None,
|
"summary": None,
|
||||||
"completed": False,
|
"completed": False,
|
||||||
"complexity": None, # Determined and defined in SpecWriter
|
"complexity": None, # Determined and defined in SpecWriter
|
||||||
@@ -159,12 +170,15 @@ class TechLead(BaseAgent):
|
|||||||
]
|
]
|
||||||
# Orchestrator will rerun us to break down the new feature epic
|
# Orchestrator will rerun us to break down the new feature epic
|
||||||
self.next_state.action = f"Start of feature #{len(self.current_state.epics)}"
|
self.next_state.action = f"Start of feature #{len(self.current_state.epics)}"
|
||||||
return AgentResponse.update_specification(self, response.text)
|
return AgentResponse.update_specification(self, feature_description)
|
||||||
|
|
||||||
async def plan_epic(self, epic) -> AgentResponse:
|
async def plan_epic(self, epic) -> AgentResponse:
|
||||||
log.debug(f"Planning tasks for the epic: {epic['name']}")
|
log.debug(f"Planning tasks for the epic: {epic['name']}")
|
||||||
await self.send_message("Creating the development plan ...")
|
await self.send_message("Creating the development plan ...")
|
||||||
|
|
||||||
|
if epic.get("source") == "feature":
|
||||||
|
await self.get_relevant_files(user_feedback=epic.get("description"))
|
||||||
|
|
||||||
llm = self.get_llm(TECH_LEAD_PLANNING)
|
llm = self.get_llm(TECH_LEAD_PLANNING)
|
||||||
convo = (
|
convo = (
|
||||||
AgentConvo(self)
|
AgentConvo(self)
|
||||||
@@ -174,6 +188,7 @@ class TechLead(BaseAgent):
|
|||||||
task_type=self.current_state.current_epic.get("source", "app"),
|
task_type=self.current_state.current_epic.get("source", "app"),
|
||||||
# FIXME: we're injecting summaries to initial description
|
# FIXME: we're injecting summaries to initial description
|
||||||
existing_summary=None,
|
existing_summary=None,
|
||||||
|
get_only_api_files=True,
|
||||||
)
|
)
|
||||||
.require_schema(DevelopmentPlan)
|
.require_schema(DevelopmentPlan)
|
||||||
)
|
)
|
||||||
@@ -181,12 +196,12 @@ class TechLead(BaseAgent):
|
|||||||
response: DevelopmentPlan = await llm(convo, parser=JSONParser(DevelopmentPlan))
|
response: DevelopmentPlan = await llm(convo, parser=JSONParser(DevelopmentPlan))
|
||||||
|
|
||||||
convo.remove_last_x_messages(1)
|
convo.remove_last_x_messages(1)
|
||||||
formatted_tasks = [f"Epic #{index}: {task.description}" for index, task in enumerate(response.plan, start=1)]
|
formatted_epics = [f"Epic #{index}: {epic.description}" for index, epic in enumerate(response.plan, start=1)]
|
||||||
tasks_string = "\n\n".join(formatted_tasks)
|
epics_string = "\n\n".join(formatted_epics)
|
||||||
convo = convo.assistant(tasks_string)
|
convo = convo.assistant(epics_string)
|
||||||
llm = self.get_llm(TECH_LEAD_PLANNING)
|
llm = self.get_llm(TECH_LEAD_EPIC_BREAKDOWN)
|
||||||
|
|
||||||
if epic.get("source") == "feature" or epic.get("complexity") == "simple":
|
if epic.get("source") == "feature" or epic.get("complexity") == Complexity.SIMPLE:
|
||||||
await self.send_message(f"Epic 1: {epic['name']}")
|
await self.send_message(f"Epic 1: {epic['name']}")
|
||||||
self.next_state.current_epic["sub_epics"] = [
|
self.next_state.current_epic["sub_epics"] = [
|
||||||
{
|
{
|
||||||
@@ -206,12 +221,8 @@ class TechLead(BaseAgent):
|
|||||||
}
|
}
|
||||||
for task in response.plan
|
for task in response.plan
|
||||||
]
|
]
|
||||||
await self.ui.send_epics_and_tasks(
|
|
||||||
self.next_state.current_epic["sub_epics"],
|
|
||||||
self.next_state.tasks,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
self.next_state.current_epic["sub_epics"] = self.next_state.current_epic["sub_epics"] + [
|
self.next_state.current_epic["sub_epics"] = [
|
||||||
{
|
{
|
||||||
"id": sub_epic_number,
|
"id": sub_epic_number,
|
||||||
"description": sub_epic.description,
|
"description": sub_epic.description,
|
||||||
@@ -221,7 +232,10 @@ class TechLead(BaseAgent):
|
|||||||
for sub_epic_number, sub_epic in enumerate(response.plan, start=1):
|
for sub_epic_number, sub_epic in enumerate(response.plan, start=1):
|
||||||
await self.send_message(f"Epic {sub_epic_number}: {sub_epic.description}")
|
await self.send_message(f"Epic {sub_epic_number}: {sub_epic.description}")
|
||||||
convo = convo.template(
|
convo = convo.template(
|
||||||
"epic_breakdown", epic_number=sub_epic_number, epic_description=sub_epic.description
|
"epic_breakdown",
|
||||||
|
epic_number=sub_epic_number,
|
||||||
|
epic_description=sub_epic.description,
|
||||||
|
get_only_api_files=True,
|
||||||
).require_schema(EpicPlan)
|
).require_schema(EpicPlan)
|
||||||
await self.send_message("Creating tasks for this epic ...")
|
await self.send_message("Creating tasks for this epic ...")
|
||||||
epic_plan: EpicPlan = await llm(convo, parser=JSONParser(EpicPlan))
|
epic_plan: EpicPlan = await llm(convo, parser=JSONParser(EpicPlan))
|
||||||
@@ -233,15 +247,32 @@ class TechLead(BaseAgent):
|
|||||||
"pre_breakdown_testing_instructions": task.testing_instructions,
|
"pre_breakdown_testing_instructions": task.testing_instructions,
|
||||||
"status": TaskStatus.TODO,
|
"status": TaskStatus.TODO,
|
||||||
"sub_epic_id": sub_epic_number,
|
"sub_epic_id": sub_epic_number,
|
||||||
|
"related_api_endpoints": [rae.model_dump() for rae in (task.related_api_endpoints or [])],
|
||||||
}
|
}
|
||||||
for task in epic_plan.plan
|
for task in epic_plan.plan
|
||||||
]
|
]
|
||||||
convo.remove_last_x_messages(2)
|
convo.remove_last_x_messages(2)
|
||||||
|
|
||||||
await self.ui.send_epics_and_tasks(
|
await self.ui.send_epics_and_tasks(
|
||||||
self.next_state.current_epic["sub_epics"],
|
self.next_state.current_epic["sub_epics"],
|
||||||
self.next_state.tasks,
|
self.next_state.tasks,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await self.ui.send_project_stage({"stage": ProjectStage.OPEN_PLAN})
|
||||||
|
response = await self.ask_question(
|
||||||
|
"Open and edit your development plan in the Progress tab",
|
||||||
|
buttons={"done_editing": "I'm done editing, the plan looks good"},
|
||||||
|
default="done_editing",
|
||||||
|
buttons_only=True,
|
||||||
|
extra_info="edit_plan",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.update_epics_and_tasks(response.text)
|
||||||
|
|
||||||
|
await self.ui.send_epics_and_tasks(
|
||||||
|
self.next_state.current_epic["sub_epics"],
|
||||||
|
self.next_state.tasks,
|
||||||
|
)
|
||||||
|
|
||||||
await telemetry.trace_code_event(
|
await telemetry.trace_code_event(
|
||||||
"development-plan",
|
"development-plan",
|
||||||
@@ -252,23 +283,37 @@ class TechLead(BaseAgent):
|
|||||||
)
|
)
|
||||||
return AgentResponse.done(self)
|
return AgentResponse.done(self)
|
||||||
|
|
||||||
def plan_example_project(self):
|
def update_epics_and_tasks(self, edited_plan_string):
|
||||||
example_name = self.current_state.specification.example_project
|
edited_plan = json.loads(edited_plan_string)
|
||||||
log.debug(f"Planning example project: {example_name}")
|
updated_tasks = []
|
||||||
|
|
||||||
example = EXAMPLE_PROJECTS[example_name]
|
existing_tasks_map = {task["description"]: task for task in self.next_state.tasks}
|
||||||
self.next_state.epics = [
|
|
||||||
{
|
self.next_state.current_epic["sub_epics"] = []
|
||||||
"name": "Initial Project",
|
for sub_epic_number, sub_epic in enumerate(edited_plan, start=1):
|
||||||
"description": example["description"],
|
self.next_state.current_epic["sub_epics"].append(
|
||||||
"completed": False,
|
{
|
||||||
"complexity": example["complexity"],
|
"id": sub_epic_number,
|
||||||
"sub_epics": [
|
"description": sub_epic["description"],
|
||||||
{
|
}
|
||||||
"id": 1,
|
)
|
||||||
"description": "Single Epic Example",
|
|
||||||
}
|
for task in sub_epic["tasks"]:
|
||||||
],
|
original_task = existing_tasks_map.get(task["description"])
|
||||||
}
|
if original_task and task == original_task:
|
||||||
]
|
updated_task = original_task.copy()
|
||||||
self.next_state.tasks = example["plan"]
|
updated_task["sub_epic_id"] = sub_epic_number
|
||||||
|
updated_tasks.append(updated_task)
|
||||||
|
else:
|
||||||
|
updated_tasks.append(
|
||||||
|
{
|
||||||
|
"id": uuid4().hex,
|
||||||
|
"description": task["description"],
|
||||||
|
"instructions": None,
|
||||||
|
"pre_breakdown_testing_instructions": None,
|
||||||
|
"status": TaskStatus.TODO,
|
||||||
|
"sub_epic_id": sub_epic_number,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.next_state.tasks = updated_tasks
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import json
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
@@ -5,7 +6,7 @@ from pydantic import BaseModel, Field
|
|||||||
|
|
||||||
from core.agents.base import BaseAgent
|
from core.agents.base import BaseAgent
|
||||||
from core.agents.convo import AgentConvo
|
from core.agents.convo import AgentConvo
|
||||||
from core.agents.mixins import IterationPromptMixin, RelevantFilesMixin
|
from core.agents.mixins import ChatWithBreakdownMixin, IterationPromptMixin, RelevantFilesMixin, TestSteps
|
||||||
from core.agents.response import AgentResponse
|
from core.agents.response import AgentResponse
|
||||||
from core.config import TROUBLESHOOTER_GET_RUN_COMMAND
|
from core.config import TROUBLESHOOTER_GET_RUN_COMMAND
|
||||||
from core.db.models.file import File
|
from core.db.models.file import File
|
||||||
@@ -13,6 +14,7 @@ from core.db.models.project_state import IterationStatus, TaskStatus
|
|||||||
from core.llm.parser import JSONParser, OptionalCodeBlockParser
|
from core.llm.parser import JSONParser, OptionalCodeBlockParser
|
||||||
from core.log import get_logger
|
from core.log import get_logger
|
||||||
from core.telemetry import telemetry
|
from core.telemetry import telemetry
|
||||||
|
from core.ui.base import ProjectStage, pythagora_source
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
@@ -29,7 +31,7 @@ class RouteFilePaths(BaseModel):
|
|||||||
files: list[str] = Field(description="List of paths for files that contain routes")
|
files: list[str] = Field(description="List of paths for files that contain routes")
|
||||||
|
|
||||||
|
|
||||||
class Troubleshooter(IterationPromptMixin, RelevantFilesMixin, BaseAgent):
|
class Troubleshooter(ChatWithBreakdownMixin, IterationPromptMixin, RelevantFilesMixin, BaseAgent):
|
||||||
agent_type = "troubleshooter"
|
agent_type = "troubleshooter"
|
||||||
display_name = "Troubleshooter"
|
display_name = "Troubleshooter"
|
||||||
|
|
||||||
@@ -72,10 +74,12 @@ class Troubleshooter(IterationPromptMixin, RelevantFilesMixin, BaseAgent):
|
|||||||
self.next_state.flag_tasks_as_modified()
|
self.next_state.flag_tasks_as_modified()
|
||||||
return AgentResponse.done(self)
|
return AgentResponse.done(self)
|
||||||
else:
|
else:
|
||||||
await self.send_message("Here are instructions on how to test the app:\n\n" + user_instructions)
|
await self.ui.send_project_stage({"stage": ProjectStage.TEST_APP})
|
||||||
|
await self.ui.send_message("Test the app by following these steps:", source=pythagora_source)
|
||||||
|
|
||||||
|
await self.send_message("")
|
||||||
await self.ui.stop_app()
|
await self.ui.stop_app()
|
||||||
await self.ui.send_test_instructions(user_instructions)
|
await self.ui.send_test_instructions(user_instructions, project_state_id=str(self.current_state.id))
|
||||||
|
|
||||||
# Developer sets iteration as "completed" when it generates the step breakdown, so we can't
|
# Developer sets iteration as "completed" when it generates the step breakdown, so we can't
|
||||||
# use "current_iteration" here
|
# use "current_iteration" here
|
||||||
@@ -106,7 +110,6 @@ class Troubleshooter(IterationPromptMixin, RelevantFilesMixin, BaseAgent):
|
|||||||
else:
|
else:
|
||||||
# should be - elif change_description is not None: - but to prevent bugs with the extension
|
# should be - elif change_description is not None: - but to prevent bugs with the extension
|
||||||
# this might be caused if we show the input field instead of buttons
|
# this might be caused if we show the input field instead of buttons
|
||||||
await self.get_relevant_files(user_feedback)
|
|
||||||
iteration_status = IterationStatus.NEW_FEATURE_REQUESTED
|
iteration_status = IterationStatus.NEW_FEATURE_REQUESTED
|
||||||
|
|
||||||
self.next_state.iterations = self.current_state.iterations + [
|
self.next_state.iterations = self.current_state.iterations + [
|
||||||
@@ -155,6 +158,7 @@ class Troubleshooter(IterationPromptMixin, RelevantFilesMixin, BaseAgent):
|
|||||||
task=task,
|
task=task,
|
||||||
iteration=None,
|
iteration=None,
|
||||||
current_task_index=current_task_index,
|
current_task_index=current_task_index,
|
||||||
|
related_api_endpoints=task.get("related_api_endpoints", []),
|
||||||
)
|
)
|
||||||
.assistant(self.current_state.current_task["instructions"])
|
.assistant(self.current_state.current_task["instructions"])
|
||||||
)
|
)
|
||||||
@@ -179,18 +183,30 @@ class Troubleshooter(IterationPromptMixin, RelevantFilesMixin, BaseAgent):
|
|||||||
await self.send_message("Determining how to test the app ...")
|
await self.send_message("Determining how to test the app ...")
|
||||||
|
|
||||||
route_files = await self._get_route_files()
|
route_files = await self._get_route_files()
|
||||||
|
current_task = self.current_state.current_task
|
||||||
|
|
||||||
llm = self.get_llm()
|
llm = self.get_llm()
|
||||||
convo = self._get_task_convo().template(
|
convo = (
|
||||||
"define_user_review_goal", task=self.current_state.current_task, route_files=route_files
|
self._get_task_convo()
|
||||||
|
.template(
|
||||||
|
"define_user_review_goal",
|
||||||
|
task=current_task,
|
||||||
|
route_files=route_files,
|
||||||
|
current_task_index=self.current_state.tasks.index(current_task),
|
||||||
|
)
|
||||||
|
.require_schema(TestSteps)
|
||||||
)
|
)
|
||||||
user_instructions: str = await llm(convo)
|
user_instructions: TestSteps = await llm(convo, parser=JSONParser(TestSteps))
|
||||||
|
|
||||||
user_instructions = user_instructions.strip()
|
if len(user_instructions.steps) == 0:
|
||||||
if user_instructions.lower() == "done":
|
await self.ui.send_message(
|
||||||
|
"No testing required for this task, moving on to the next one.", source=pythagora_source
|
||||||
|
)
|
||||||
log.debug(f"Nothing to do for user testing for task {self.current_state.current_task['description']}")
|
log.debug(f"Nothing to do for user testing for task {self.current_state.current_task['description']}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
user_instructions = json.dumps([test.dict() for test in user_instructions.steps])
|
||||||
|
|
||||||
return user_instructions
|
return user_instructions
|
||||||
|
|
||||||
async def _get_route_files(self) -> list[File]:
|
async def _get_route_files(self) -> list[File]:
|
||||||
@@ -231,38 +247,62 @@ class Troubleshooter(IterationPromptMixin, RelevantFilesMixin, BaseAgent):
|
|||||||
|
|
||||||
is_loop = False
|
is_loop = False
|
||||||
should_iterate = True
|
should_iterate = True
|
||||||
|
extra_info = "restart_app" if not self.current_state.iterations else None
|
||||||
|
|
||||||
test_message = "Please check if the app is working"
|
while True:
|
||||||
if user_instructions:
|
await self.ui.send_project_stage({"stage": ProjectStage.GET_USER_FEEDBACK})
|
||||||
hint = " Here is a description of what should be working:\n\n" + user_instructions
|
|
||||||
|
|
||||||
if run_command:
|
test_message = "Please check if the app is working"
|
||||||
await self.ui.send_run_command(run_command)
|
if user_instructions:
|
||||||
|
hint = " Here is a description of what should be working:\n\n" + user_instructions
|
||||||
|
|
||||||
buttons = {
|
if run_command:
|
||||||
"continue": "Everything works",
|
await self.ui.send_run_command(run_command)
|
||||||
"change": "I want to make a change",
|
|
||||||
"bug": "There is an issue",
|
|
||||||
}
|
|
||||||
|
|
||||||
user_response = await self.ask_question(
|
buttons = {
|
||||||
test_message, buttons=buttons, default="continue", buttons_only=True, hint=hint
|
"continue": "Everything works",
|
||||||
)
|
"change": "I want to make a change",
|
||||||
if user_response.button == "continue" or user_response.cancelled:
|
"bug": "There is an issue",
|
||||||
should_iterate = False
|
}
|
||||||
|
|
||||||
elif user_response.button == "change":
|
user_response = await self.ask_question(
|
||||||
user_description = await self.ask_question(
|
test_message,
|
||||||
"Please describe the change you want to make to the project specification (one at a time)"
|
buttons=buttons,
|
||||||
|
default="continue",
|
||||||
|
buttons_only=True,
|
||||||
|
hint=hint,
|
||||||
|
extra_info=extra_info,
|
||||||
)
|
)
|
||||||
change_description = user_description.text
|
extra_info = None
|
||||||
|
|
||||||
elif user_response.button == "bug":
|
if user_response.button == "continue" or user_response.cancelled:
|
||||||
user_description = await self.ask_question(
|
should_iterate = False
|
||||||
"Please describe the issue you found (one at a time) and share any relevant server logs",
|
break
|
||||||
buttons={"copy_server_logs": "Copy Server Logs"},
|
|
||||||
)
|
elif user_response.button == "change":
|
||||||
bug_report = user_description.text
|
await self.ui.send_project_stage({"stage": ProjectStage.DESCRIBE_CHANGE})
|
||||||
|
user_description = await self.ask_question(
|
||||||
|
"Please describe the change you want to make to the project specification (one at a time)",
|
||||||
|
buttons={"back": "Back"},
|
||||||
|
)
|
||||||
|
if user_description.button == "back":
|
||||||
|
continue
|
||||||
|
change_description = user_description.text
|
||||||
|
await self.get_relevant_files(user_feedback=change_description)
|
||||||
|
break
|
||||||
|
|
||||||
|
elif user_response.button == "bug":
|
||||||
|
await self.ui.send_project_stage({"stage": ProjectStage.DESCRIBE_ISSUE})
|
||||||
|
user_description = await self.ask_question(
|
||||||
|
"Please describe the issue you found (one at a time) and share any relevant server logs",
|
||||||
|
extra_info="collect_logs",
|
||||||
|
buttons={"back": "Back"},
|
||||||
|
)
|
||||||
|
if user_description.button == "back":
|
||||||
|
continue
|
||||||
|
bug_report = user_description.text
|
||||||
|
await self.get_relevant_files(user_feedback=bug_report)
|
||||||
|
break
|
||||||
|
|
||||||
return should_iterate, is_loop, bug_report, change_description
|
return should_iterate, is_loop, bug_report, change_description
|
||||||
|
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ def parse_arguments() -> Namespace:
|
|||||||
--email: User's email address, if provided
|
--email: User's email address, if provided
|
||||||
--extension-version: Version of the VSCode extension, if used
|
--extension-version: Version of the VSCode extension, if used
|
||||||
--no-check: Disable initial LLM API check
|
--no-check: Disable initial LLM API check
|
||||||
|
--use-git: Use Git for version control
|
||||||
:return: Parsed arguments object.
|
:return: Parsed arguments object.
|
||||||
"""
|
"""
|
||||||
version = get_version()
|
version = get_version()
|
||||||
@@ -136,6 +137,7 @@ def parse_arguments() -> Namespace:
|
|||||||
parser.add_argument("--email", help="User's email address", required=False)
|
parser.add_argument("--email", help="User's email address", required=False)
|
||||||
parser.add_argument("--extension-version", help="Version of the VSCode extension", required=False)
|
parser.add_argument("--extension-version", help="Version of the VSCode extension", required=False)
|
||||||
parser.add_argument("--no-check", help="Disable initial LLM API check", action="store_true")
|
parser.add_argument("--no-check", help="Disable initial LLM API check", action="store_true")
|
||||||
|
parser.add_argument("--use-git", help="Use Git for version control", action="store_true", required=False)
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import atexit
|
||||||
|
import signal
|
||||||
import sys
|
import sys
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from asyncio import run
|
from asyncio import run
|
||||||
@@ -8,16 +10,32 @@ from core.cli.helpers import delete_project, init, list_projects, list_projects_
|
|||||||
from core.config import LLMProvider, get_config
|
from core.config import LLMProvider, get_config
|
||||||
from core.db.session import SessionManager
|
from core.db.session import SessionManager
|
||||||
from core.db.v0importer import LegacyDatabaseImporter
|
from core.db.v0importer import LegacyDatabaseImporter
|
||||||
|
from core.llm.anthropic_client import CustomAssertionError
|
||||||
from core.llm.base import APIError, BaseLLMClient
|
from core.llm.base import APIError, BaseLLMClient
|
||||||
from core.log import get_logger
|
from core.log import get_logger
|
||||||
from core.state.state_manager import StateManager
|
from core.state.state_manager import StateManager
|
||||||
from core.telemetry import telemetry
|
from core.telemetry import telemetry
|
||||||
from core.ui.base import UIBase, UIClosedError, UserInput, pythagora_source
|
from core.ui.base import ProjectStage, UIBase, UIClosedError, UserInput, pythagora_source
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def run_project(sm: StateManager, ui: UIBase) -> bool:
|
telemetry_sent = False
|
||||||
|
|
||||||
|
|
||||||
|
async def cleanup(ui: UIBase):
|
||||||
|
global telemetry_sent
|
||||||
|
if not telemetry_sent:
|
||||||
|
await telemetry.send()
|
||||||
|
telemetry_sent = True
|
||||||
|
await ui.stop()
|
||||||
|
|
||||||
|
|
||||||
|
def sync_cleanup(ui: UIBase):
|
||||||
|
asyncio.run(cleanup(ui))
|
||||||
|
|
||||||
|
|
||||||
|
async def run_project(sm: StateManager, ui: UIBase, args) -> bool:
|
||||||
"""
|
"""
|
||||||
Work on the project.
|
Work on the project.
|
||||||
|
|
||||||
@@ -26,13 +44,14 @@ async def run_project(sm: StateManager, ui: UIBase) -> bool:
|
|||||||
|
|
||||||
:param sm: State manager.
|
:param sm: State manager.
|
||||||
:param ui: User interface.
|
:param ui: User interface.
|
||||||
|
:param args: Command-line arguments.
|
||||||
:return: True if the orchestrator exited successfully, False otherwise.
|
:return: True if the orchestrator exited successfully, False otherwise.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
telemetry.set("app_id", str(sm.project.id))
|
telemetry.set("app_id", str(sm.project.id))
|
||||||
telemetry.set("initial_prompt", sm.current_state.specification.description)
|
telemetry.set("initial_prompt", sm.current_state.specification.description)
|
||||||
|
|
||||||
orca = Orchestrator(sm, ui)
|
orca = Orchestrator(sm, ui, args=args)
|
||||||
success = False
|
success = False
|
||||||
try:
|
try:
|
||||||
success = await orca.run()
|
success = await orca.run()
|
||||||
@@ -49,6 +68,14 @@ async def run_project(sm: StateManager, ui: UIBase) -> bool:
|
|||||||
)
|
)
|
||||||
telemetry.set("end_result", "failure:api-error")
|
telemetry.set("end_result", "failure:api-error")
|
||||||
await sm.rollback()
|
await sm.rollback()
|
||||||
|
except CustomAssertionError as err:
|
||||||
|
log.warning(f"Anthropic assertion error occurred: {str(err)}")
|
||||||
|
await ui.send_message(
|
||||||
|
f"Stopping Pythagora due to an error inside Anthropic SDK. {str(err)}",
|
||||||
|
source=pythagora_source,
|
||||||
|
)
|
||||||
|
telemetry.set("end_result", "failure:assertion-error")
|
||||||
|
await sm.rollback()
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
log.error(f"Uncaught exception: {err}", exc_info=True)
|
log.error(f"Uncaught exception: {err}", exc_info=True)
|
||||||
stack_trace = telemetry.record_crash(err)
|
stack_trace = telemetry.record_crash(err)
|
||||||
@@ -125,12 +152,48 @@ async def start_new_project(sm: StateManager, ui: UIBase) -> bool:
|
|||||||
:param ui: User interface.
|
:param ui: User interface.
|
||||||
:return: True if the project was created successfully, False otherwise.
|
:return: True if the project was created successfully, False otherwise.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
stack = await ui.ask_question(
|
||||||
|
"What do you want to use to build your app?",
|
||||||
|
allow_empty=False,
|
||||||
|
buttons={"node": "Node.js", "other": "Other (coming soon)"},
|
||||||
|
buttons_only=True,
|
||||||
|
source=pythagora_source,
|
||||||
|
full_screen=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if stack.button == "other":
|
||||||
|
language = await ui.ask_question(
|
||||||
|
"What language you want to use?",
|
||||||
|
allow_empty=False,
|
||||||
|
source=pythagora_source,
|
||||||
|
full_screen=True,
|
||||||
|
)
|
||||||
|
await telemetry.trace_code_event(
|
||||||
|
"stack-choice-other",
|
||||||
|
{"language": language.text},
|
||||||
|
)
|
||||||
|
await ui.send_message("Thank you for submitting your request to support other languages.")
|
||||||
|
return False
|
||||||
|
elif stack.button == "node":
|
||||||
|
await telemetry.trace_code_event(
|
||||||
|
"stack-choice",
|
||||||
|
{"language": "node"},
|
||||||
|
)
|
||||||
|
elif stack.button == "python":
|
||||||
|
await telemetry.trace_code_event(
|
||||||
|
"stack-choice",
|
||||||
|
{"language": "python"},
|
||||||
|
)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
|
await ui.send_project_stage({"stage": ProjectStage.PROJECT_NAME})
|
||||||
user_input = await ui.ask_question(
|
user_input = await ui.ask_question(
|
||||||
"What is the project name?",
|
"What is the project name?",
|
||||||
allow_empty=False,
|
allow_empty=False,
|
||||||
source=pythagora_source,
|
source=pythagora_source,
|
||||||
|
full_screen=True,
|
||||||
)
|
)
|
||||||
except (KeyboardInterrupt, UIClosedError):
|
except (KeyboardInterrupt, UIClosedError):
|
||||||
user_input = UserInput(cancelled=True)
|
user_input = UserInput(cancelled=True)
|
||||||
@@ -178,7 +241,7 @@ async def run_pythagora_session(sm: StateManager, ui: UIBase, args: Namespace):
|
|||||||
if not success:
|
if not success:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return await run_project(sm, ui)
|
return await run_project(sm, ui, args)
|
||||||
|
|
||||||
|
|
||||||
async def async_main(
|
async def async_main(
|
||||||
@@ -194,6 +257,7 @@ async def async_main(
|
|||||||
:param args: Command-line arguments.
|
:param args: Command-line arguments.
|
||||||
:return: True if the application ran successfully, False otherwise.
|
:return: True if the application ran successfully, False otherwise.
|
||||||
"""
|
"""
|
||||||
|
global telemetry_sent
|
||||||
|
|
||||||
if args.list:
|
if args.list:
|
||||||
await list_projects(db)
|
await list_projects(db)
|
||||||
@@ -223,9 +287,23 @@ async def async_main(
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
telemetry.start()
|
telemetry.start()
|
||||||
success = await run_pythagora_session(sm, ui, args)
|
|
||||||
await telemetry.send()
|
# Set up signal handlers
|
||||||
await ui.stop()
|
def signal_handler(sig, frame):
|
||||||
|
if not telemetry_sent:
|
||||||
|
sync_cleanup(ui)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
for sig in (signal.SIGINT, signal.SIGTERM):
|
||||||
|
signal.signal(sig, signal_handler)
|
||||||
|
|
||||||
|
# Register the cleanup function
|
||||||
|
atexit.register(sync_cleanup, ui)
|
||||||
|
|
||||||
|
try:
|
||||||
|
success = await run_pythagora_session(sm, ui, args)
|
||||||
|
finally:
|
||||||
|
await cleanup(ui)
|
||||||
|
|
||||||
return success
|
return success
|
||||||
|
|
||||||
|
|||||||
@@ -43,8 +43,10 @@ TASK_BREAKDOWN_AGENT_NAME = "Developer.breakdown_current_task"
|
|||||||
TROUBLESHOOTER_BUG_REPORT = "Troubleshooter.generate_bug_report"
|
TROUBLESHOOTER_BUG_REPORT = "Troubleshooter.generate_bug_report"
|
||||||
TROUBLESHOOTER_GET_RUN_COMMAND = "Troubleshooter.get_run_command"
|
TROUBLESHOOTER_GET_RUN_COMMAND = "Troubleshooter.get_run_command"
|
||||||
TECH_LEAD_PLANNING = "TechLead.plan_epic"
|
TECH_LEAD_PLANNING = "TechLead.plan_epic"
|
||||||
|
TECH_LEAD_EPIC_BREAKDOWN = "TechLead.epic_breakdown"
|
||||||
SPEC_WRITER_AGENT_NAME = "SpecWriter"
|
SPEC_WRITER_AGENT_NAME = "SpecWriter"
|
||||||
GET_RELEVANT_FILES_AGENT_NAME = "get_relevant_files"
|
GET_RELEVANT_FILES_AGENT_NAME = "get_relevant_files"
|
||||||
|
FRONTEND_AGENT_NAME = "Frontend"
|
||||||
|
|
||||||
# Endpoint for the external documentation
|
# Endpoint for the external documentation
|
||||||
EXTERNAL_DOCUMENTATION_API = "http://docs-pythagora-io-439719575.us-east-1.elb.amazonaws.com"
|
EXTERNAL_DOCUMENTATION_API = "http://docs-pythagora-io-439719575.us-east-1.elb.amazonaws.com"
|
||||||
@@ -101,7 +103,7 @@ class ProviderConfig(_StrictModel):
|
|||||||
ge=0.0,
|
ge=0.0,
|
||||||
)
|
)
|
||||||
read_timeout: float = Field(
|
read_timeout: float = Field(
|
||||||
default=20.0,
|
default=60.0,
|
||||||
description="Timeout (in seconds) for receiving a new chunk of data from the response stream",
|
description="Timeout (in seconds) for receiving a new chunk of data from the response stream",
|
||||||
ge=0.0,
|
ge=0.0,
|
||||||
)
|
)
|
||||||
@@ -156,7 +158,7 @@ class LLMConfig(_StrictModel):
|
|||||||
ge=0.0,
|
ge=0.0,
|
||||||
)
|
)
|
||||||
read_timeout: float = Field(
|
read_timeout: float = Field(
|
||||||
default=20.0,
|
default=60.0,
|
||||||
description="Timeout (in seconds) for receiving a new chunk of data from the response stream",
|
description="Timeout (in seconds) for receiving a new chunk of data from the response stream",
|
||||||
ge=0.0,
|
ge=0.0,
|
||||||
)
|
)
|
||||||
@@ -228,7 +230,7 @@ class DBConfig(_StrictModel):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
url: str = Field(
|
url: str = Field(
|
||||||
"sqlite+aiosqlite:///pythagora.db",
|
"sqlite+aiosqlite:///data/database/pythagora.db",
|
||||||
description="Database connection URL",
|
description="Database connection URL",
|
||||||
)
|
)
|
||||||
debug_sql: bool = Field(False, description="Log all SQL queries to the console")
|
debug_sql: bool = Field(False, description="Log all SQL queries to the console")
|
||||||
@@ -325,12 +327,12 @@ class Config(_StrictModel):
|
|||||||
DEFAULT_AGENT_NAME: AgentLLMConfig(),
|
DEFAULT_AGENT_NAME: AgentLLMConfig(),
|
||||||
CHECK_LOGS_AGENT_NAME: AgentLLMConfig(
|
CHECK_LOGS_AGENT_NAME: AgentLLMConfig(
|
||||||
provider=LLMProvider.ANTHROPIC,
|
provider=LLMProvider.ANTHROPIC,
|
||||||
model="claude-3-5-sonnet-20240620",
|
model="claude-3-5-sonnet-20241022",
|
||||||
temperature=0.5,
|
temperature=0.5,
|
||||||
),
|
),
|
||||||
CODE_MONKEY_AGENT_NAME: AgentLLMConfig(
|
CODE_MONKEY_AGENT_NAME: AgentLLMConfig(
|
||||||
provider=LLMProvider.OPENAI,
|
provider=LLMProvider.ANTHROPIC,
|
||||||
model="gpt-4-0125-preview",
|
model="claude-3-5-sonnet-20241022",
|
||||||
temperature=0.0,
|
temperature=0.0,
|
||||||
),
|
),
|
||||||
CODE_REVIEW_AGENT_NAME: AgentLLMConfig(
|
CODE_REVIEW_AGENT_NAME: AgentLLMConfig(
|
||||||
@@ -343,9 +345,19 @@ class Config(_StrictModel):
|
|||||||
model="gpt-4o-mini-2024-07-18",
|
model="gpt-4o-mini-2024-07-18",
|
||||||
temperature=0.0,
|
temperature=0.0,
|
||||||
),
|
),
|
||||||
PARSE_TASK_AGENT_NAME: AgentLLMConfig(
|
FRONTEND_AGENT_NAME: AgentLLMConfig(
|
||||||
|
provider=LLMProvider.ANTHROPIC,
|
||||||
|
model="claude-3-5-sonnet-20241022",
|
||||||
|
temperature=0.0,
|
||||||
|
),
|
||||||
|
GET_RELEVANT_FILES_AGENT_NAME: AgentLLMConfig(
|
||||||
provider=LLMProvider.OPENAI,
|
provider=LLMProvider.OPENAI,
|
||||||
model="gpt-4-0125-preview",
|
model="gpt-4o-2024-05-13",
|
||||||
|
temperature=0.5,
|
||||||
|
),
|
||||||
|
PARSE_TASK_AGENT_NAME: AgentLLMConfig(
|
||||||
|
provider=LLMProvider.ANTHROPIC,
|
||||||
|
model="claude-3-5-sonnet-20241022",
|
||||||
temperature=0.0,
|
temperature=0.0,
|
||||||
),
|
),
|
||||||
SPEC_WRITER_AGENT_NAME: AgentLLMConfig(
|
SPEC_WRITER_AGENT_NAME: AgentLLMConfig(
|
||||||
@@ -355,7 +367,7 @@ class Config(_StrictModel):
|
|||||||
),
|
),
|
||||||
TASK_BREAKDOWN_AGENT_NAME: AgentLLMConfig(
|
TASK_BREAKDOWN_AGENT_NAME: AgentLLMConfig(
|
||||||
provider=LLMProvider.ANTHROPIC,
|
provider=LLMProvider.ANTHROPIC,
|
||||||
model="claude-3-5-sonnet-20240620",
|
model="claude-3-5-sonnet-20241022",
|
||||||
temperature=0.5,
|
temperature=0.5,
|
||||||
),
|
),
|
||||||
TECH_LEAD_PLANNING: AgentLLMConfig(
|
TECH_LEAD_PLANNING: AgentLLMConfig(
|
||||||
@@ -363,6 +375,11 @@ class Config(_StrictModel):
|
|||||||
model="claude-3-5-sonnet-20240620",
|
model="claude-3-5-sonnet-20240620",
|
||||||
temperature=0.5,
|
temperature=0.5,
|
||||||
),
|
),
|
||||||
|
TECH_LEAD_EPIC_BREAKDOWN: AgentLLMConfig(
|
||||||
|
provider=LLMProvider.ANTHROPIC,
|
||||||
|
model="claude-3-5-sonnet-20241022",
|
||||||
|
temperature=0.5,
|
||||||
|
),
|
||||||
TROUBLESHOOTER_BUG_REPORT: AgentLLMConfig(
|
TROUBLESHOOTER_BUG_REPORT: AgentLLMConfig(
|
||||||
provider=LLMProvider.ANTHROPIC,
|
provider=LLMProvider.ANTHROPIC,
|
||||||
model="claude-3-5-sonnet-20240620",
|
model="claude-3-5-sonnet-20240620",
|
||||||
@@ -479,6 +496,7 @@ def adapt_for_bedrock(config: Config) -> Config:
|
|||||||
return config
|
return config
|
||||||
|
|
||||||
replacement_map = {
|
replacement_map = {
|
||||||
|
"claude-3-5-sonnet-20241022": "us.anthropic.claude-3-5-sonnet-20241022-v2:0",
|
||||||
"claude-3-5-sonnet-20240620": "us.anthropic.claude-3-5-sonnet-20240620-v1:0",
|
"claude-3-5-sonnet-20240620": "us.anthropic.claude-3-5-sonnet-20240620-v1:0",
|
||||||
"claude-3-sonnet-20240229": "us.anthropic.claude-3-sonnet-20240229-v1:0",
|
"claude-3-sonnet-20240229": "us.anthropic.claude-3-sonnet-20240229-v1:0",
|
||||||
"claude-3-haiku-20240307": "us.anthropic.claude-3-haiku-20240307-v1:0",
|
"claude-3-haiku-20240307": "us.anthropic.claude-3-haiku-20240307-v1:0",
|
||||||
|
|||||||
@@ -25,3 +25,35 @@ THINKING_LOGS = [
|
|||||||
"Pythagora is working for you, so relax!",
|
"Pythagora is working for you, so relax!",
|
||||||
"Pythagora might take some time to figure this out...",
|
"Pythagora might take some time to figure this out...",
|
||||||
]
|
]
|
||||||
|
GITIGNORE_CONTENT = """# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
# SQLite databases, data files
|
||||||
|
*.db
|
||||||
|
*.csv
|
||||||
|
|
||||||
|
# Keep environment variables out of version control
|
||||||
|
.env
|
||||||
|
"""
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ version_path_separator = os
|
|||||||
# are written from script.py.mako
|
# are written from script.py.mako
|
||||||
# output_encoding = utf-8
|
# output_encoding = utf-8
|
||||||
|
|
||||||
sqlalchemy.url = sqlite:///pythagora.db
|
sqlalchemy.url = sqlite:///data/database/pythagora.db
|
||||||
|
|
||||||
[post_write_hooks]
|
[post_write_hooks]
|
||||||
# post_write_hooks defines scripts or Python functions that are run
|
# post_write_hooks defines scripts or Python functions that are run
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
"""Adding knowledge_base field to ProjectState
|
||||||
|
|
||||||
|
Revision ID: f708791b9270
|
||||||
|
Revises: c8905d4ce784
|
||||||
|
Create Date: 2024-12-22 12:13:14.979169
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "f708791b9270"
|
||||||
|
down_revision: Union[str, None] = "c8905d4ce784"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table("project_states", schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column("knowledge_base", sa.JSON(), server_default="{}", nullable=False))
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table("project_states", schema=None) as batch_op:
|
||||||
|
batch_op.drop_column("knowledge_base")
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING, Optional, Union
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
from sqlalchemy import ForeignKey, UniqueConstraint, delete, inspect
|
from sqlalchemy import ForeignKey, UniqueConstraint, delete, inspect
|
||||||
@@ -9,7 +9,7 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|||||||
from sqlalchemy.orm.attributes import flag_modified
|
from sqlalchemy.orm.attributes import flag_modified
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
from core.db.models import Base
|
from core.db.models import Base, FileContent
|
||||||
from core.log import get_logger
|
from core.log import get_logger
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -67,6 +67,7 @@ class ProjectState(Base):
|
|||||||
tasks: Mapped[list[dict]] = mapped_column(default=list)
|
tasks: Mapped[list[dict]] = mapped_column(default=list)
|
||||||
steps: Mapped[list[dict]] = mapped_column(default=list)
|
steps: Mapped[list[dict]] = mapped_column(default=list)
|
||||||
iterations: Mapped[list[dict]] = mapped_column(default=list)
|
iterations: Mapped[list[dict]] = mapped_column(default=list)
|
||||||
|
knowledge_base: Mapped[dict] = mapped_column(default=dict, server_default="{}")
|
||||||
relevant_files: Mapped[Optional[list[str]]] = mapped_column(default=None)
|
relevant_files: Mapped[Optional[list[str]]] = mapped_column(default=None)
|
||||||
modified_files: Mapped[dict] = mapped_column(default=dict)
|
modified_files: Mapped[dict] = mapped_column(default=dict)
|
||||||
docs: Mapped[Optional[list[dict]]] = mapped_column(default=None)
|
docs: Mapped[Optional[list[dict]]] = mapped_column(default=None)
|
||||||
@@ -238,6 +239,7 @@ class ProjectState(Base):
|
|||||||
tasks=deepcopy(self.tasks),
|
tasks=deepcopy(self.tasks),
|
||||||
steps=deepcopy(self.steps),
|
steps=deepcopy(self.steps),
|
||||||
iterations=deepcopy(self.iterations),
|
iterations=deepcopy(self.iterations),
|
||||||
|
knowledge_base=deepcopy(self.knowledge_base),
|
||||||
files=[],
|
files=[],
|
||||||
relevant_files=deepcopy(self.relevant_files),
|
relevant_files=deepcopy(self.relevant_files),
|
||||||
modified_files=deepcopy(self.modified_files),
|
modified_files=deepcopy(self.modified_files),
|
||||||
@@ -257,14 +259,14 @@ class ProjectState(Base):
|
|||||||
|
|
||||||
return new_state
|
return new_state
|
||||||
|
|
||||||
def complete_step(self):
|
def complete_step(self, step_type: str):
|
||||||
if not self.unfinished_steps:
|
if not self.unfinished_steps:
|
||||||
raise ValueError("There are no unfinished steps to complete")
|
raise ValueError("There are no unfinished steps to complete")
|
||||||
if "next_state" in self.__dict__:
|
if "next_state" in self.__dict__:
|
||||||
raise ValueError("Current state is read-only (already has a next state).")
|
raise ValueError("Current state is read-only (already has a next state).")
|
||||||
|
|
||||||
log.debug(f"Completing step {self.unfinished_steps[0]['type']}")
|
log.debug(f"Completing step {self.unfinished_steps[0]['type']}")
|
||||||
self.unfinished_steps[0]["completed"] = True
|
self.get_steps_of_type(step_type)[0]["completed"] = True
|
||||||
flag_modified(self, "steps")
|
flag_modified(self, "steps")
|
||||||
|
|
||||||
def complete_task(self):
|
def complete_task(self):
|
||||||
@@ -305,6 +307,7 @@ class ProjectState(Base):
|
|||||||
log.debug(f"Completing iteration {self.unfinished_iterations[0]}")
|
log.debug(f"Completing iteration {self.unfinished_iterations[0]}")
|
||||||
self.unfinished_iterations[0]["status"] = IterationStatus.DONE
|
self.unfinished_iterations[0]["status"] = IterationStatus.DONE
|
||||||
self.relevant_files = None
|
self.relevant_files = None
|
||||||
|
self.modified_files = {}
|
||||||
self.flag_iterations_as_modified()
|
self.flag_iterations_as_modified()
|
||||||
|
|
||||||
def flag_iterations_as_modified(self):
|
def flag_iterations_as_modified(self):
|
||||||
@@ -327,6 +330,26 @@ class ProjectState(Base):
|
|||||||
"""
|
"""
|
||||||
flag_modified(self, "tasks")
|
flag_modified(self, "tasks")
|
||||||
|
|
||||||
|
def flag_epics_as_modified(self):
|
||||||
|
"""
|
||||||
|
Flag the epic field as having been modified
|
||||||
|
|
||||||
|
Used by Agents that perform modifications within the mutable epics field,
|
||||||
|
to tell the database that it was modified and should get saved (as SQLalchemy
|
||||||
|
can't detect changes in mutable fields by itself).
|
||||||
|
"""
|
||||||
|
flag_modified(self, "epics")
|
||||||
|
|
||||||
|
def flag_knowledge_base_as_modified(self):
|
||||||
|
"""
|
||||||
|
Flag the knowledge base field as having been modified
|
||||||
|
|
||||||
|
Used by Agents that perform modifications within the mutable knowledge base field,
|
||||||
|
to tell the database that it was modified and should get saved (as SQLalchemy
|
||||||
|
can't detect changes in mutable fields by itself).
|
||||||
|
"""
|
||||||
|
flag_modified(self, "knowledge_base")
|
||||||
|
|
||||||
def set_current_task_status(self, status: str):
|
def set_current_task_status(self, status: str):
|
||||||
"""
|
"""
|
||||||
Set the status of the current task.
|
Set the status of the current task.
|
||||||
@@ -354,6 +377,17 @@ class ProjectState(Base):
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_file_content_by_path(self, path: str) -> Union[FileContent, str]:
|
||||||
|
"""
|
||||||
|
Get a file from the current project state, by the file path.
|
||||||
|
|
||||||
|
:param path: The file path.
|
||||||
|
:return: The file object, or None if not found.
|
||||||
|
"""
|
||||||
|
file = self.get_file_by_path(path)
|
||||||
|
|
||||||
|
return file.content.content if file else ""
|
||||||
|
|
||||||
def save_file(self, path: str, content: "FileContent", external: bool = False) -> "File":
|
def save_file(self, path: str, content: "FileContent", external: bool = False) -> "File":
|
||||||
"""
|
"""
|
||||||
Save a file to the project state.
|
Save a file to the project state.
|
||||||
@@ -443,3 +477,19 @@ class ProjectState(Base):
|
|||||||
"""
|
"""
|
||||||
li = self.unfinished_steps
|
li = self.unfinished_steps
|
||||||
return [step for step in li if step.get("type") == step_type] if li else []
|
return [step for step in li if step.get("type") == step_type] if li else []
|
||||||
|
|
||||||
|
def has_frontend(self) -> bool:
|
||||||
|
"""
|
||||||
|
Check if there is a frontend epic in the project state.
|
||||||
|
|
||||||
|
:return: True if there is a frontend epic, False otherwise.
|
||||||
|
"""
|
||||||
|
return self.epics and any(epic.get("source") == "frontend" for epic in self.epics)
|
||||||
|
|
||||||
|
def is_feature(self) -> bool:
|
||||||
|
"""
|
||||||
|
Check if the current epic is a feature.
|
||||||
|
|
||||||
|
:return: True if the current epic is a feature, False otherwise.
|
||||||
|
"""
|
||||||
|
return self.epics and self.current_epic and self.current_epic.get("source") == "feature"
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import time
|
|
||||||
|
|
||||||
from sqlalchemy import event
|
from sqlalchemy import event
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||||
|
|
||||||
@@ -35,16 +33,6 @@ class SessionManager:
|
|||||||
self.recursion_depth = 0
|
self.recursion_depth = 0
|
||||||
|
|
||||||
event.listen(self.engine.sync_engine, "connect", self._on_connect)
|
event.listen(self.engine.sync_engine, "connect", self._on_connect)
|
||||||
event.listen(self.engine.sync_engine, "before_cursor_execute", self.before_cursor_execute)
|
|
||||||
event.listen(self.engine.sync_engine, "after_cursor_execute", self.after_cursor_execute)
|
|
||||||
|
|
||||||
def before_cursor_execute(self, conn, cursor, statement, parameters, context, executemany):
|
|
||||||
conn.info.setdefault("query_start_time", []).append(time.time())
|
|
||||||
log.debug(f"Executing SQL: {statement}")
|
|
||||||
|
|
||||||
def after_cursor_execute(self, conn, cursor, statement, parameters, context, executemany):
|
|
||||||
total = time.time() - conn.info["query_start_time"].pop(-1)
|
|
||||||
log.debug(f"SQL execution time: {total:.3f} seconds")
|
|
||||||
|
|
||||||
def _on_connect(self, dbapi_connection, _):
|
def _on_connect(self, dbapi_connection, _):
|
||||||
"""Connection event handler"""
|
"""Connection event handler"""
|
||||||
@@ -56,7 +44,6 @@ class SessionManager:
|
|||||||
# it's a local file. PostgreSQL or other database use a real connection pool
|
# it's a local file. PostgreSQL or other database use a real connection pool
|
||||||
# by default.
|
# by default.
|
||||||
dbapi_connection.execute("pragma foreign_keys=on")
|
dbapi_connection.execute("pragma foreign_keys=on")
|
||||||
dbapi_connection.execute("PRAGMA journal_mode=WAL;")
|
|
||||||
|
|
||||||
async def start(self) -> AsyncSession:
|
async def start(self) -> AsyncSession:
|
||||||
if self.session is not None:
|
if self.session is not None:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import asyncio
|
||||||
import datetime
|
import datetime
|
||||||
import zoneinfo
|
import zoneinfo
|
||||||
from typing import Optional
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
from anthropic import AsyncAnthropic, RateLimitError
|
from anthropic import AsyncAnthropic, RateLimitError
|
||||||
from httpx import Timeout
|
from httpx import Timeout
|
||||||
@@ -18,6 +19,10 @@ MAX_TOKENS = 4096
|
|||||||
MAX_TOKENS_SONNET = 8192
|
MAX_TOKENS_SONNET = 8192
|
||||||
|
|
||||||
|
|
||||||
|
class CustomAssertionError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class AnthropicClient(BaseLLMClient):
|
class AnthropicClient(BaseLLMClient):
|
||||||
provider = LLMProvider.ANTHROPIC
|
provider = LLMProvider.ANTHROPIC
|
||||||
|
|
||||||
@@ -38,7 +43,7 @@ class AnthropicClient(BaseLLMClient):
|
|||||||
Adapt the conversation messages to the format expected by the Anthropic Claude model.
|
Adapt the conversation messages to the format expected by the Anthropic Claude model.
|
||||||
|
|
||||||
Claude only recognizes "user" and "assistant" roles, and requires them to be switched
|
Claude only recognizes "user" and "assistant" roles, and requires them to be switched
|
||||||
for each message (ie. no consecutive messages from the same role).
|
for each message (i.e. no consecutive messages from the same role).
|
||||||
|
|
||||||
:param convo: Conversation to adapt.
|
:param convo: Conversation to adapt.
|
||||||
:return: Adapted conversation messages.
|
:return: Adapted conversation messages.
|
||||||
@@ -61,50 +66,60 @@ class AnthropicClient(BaseLLMClient):
|
|||||||
return messages
|
return messages
|
||||||
|
|
||||||
async def _make_request(
|
async def _make_request(
|
||||||
self,
|
self, convo: Convo, temperature: Optional[float] = None, json_mode: bool = False, retry_count: int = 1
|
||||||
convo: Convo,
|
) -> Tuple[str, int, int]:
|
||||||
temperature: Optional[float] = None,
|
async def single_attempt() -> Tuple[str, int, int]:
|
||||||
json_mode: bool = False,
|
messages = self._adapt_messages(convo)
|
||||||
) -> tuple[str, int, int]:
|
completion_kwargs = {
|
||||||
messages = self._adapt_messages(convo)
|
"max_tokens": MAX_TOKENS,
|
||||||
completion_kwargs = {
|
"model": self.config.model,
|
||||||
"max_tokens": MAX_TOKENS,
|
"messages": messages,
|
||||||
"model": self.config.model,
|
"temperature": self.config.temperature if temperature is None else temperature,
|
||||||
"messages": messages,
|
}
|
||||||
"temperature": self.config.temperature if temperature is None else temperature,
|
|
||||||
}
|
|
||||||
|
|
||||||
if "bedrock/anthropic" in self.config.base_url:
|
if "trybricks" in self.config.base_url:
|
||||||
completion_kwargs["extra_headers"] = {"anthropic-version": "bedrock-2023-05-31"}
|
completion_kwargs["extra_headers"] = {"x-request-timeout": f"{int(float(self.config.read_timeout))}s"}
|
||||||
|
|
||||||
if "sonnet" in self.config.model:
|
if "bedrock/anthropic" in self.config.base_url:
|
||||||
if "extra_headers" in completion_kwargs:
|
completion_kwargs["extra_headers"] = {"anthropic-version": "bedrock-2023-05-31"}
|
||||||
completion_kwargs["extra_headers"]["anthropic-beta"] = "max-tokens-3-5-sonnet-2024-07-15"
|
|
||||||
else:
|
|
||||||
completion_kwargs["extra_headers"] = {"anthropic-beta": "max-tokens-3-5-sonnet-2024-07-15"}
|
|
||||||
completion_kwargs["max_tokens"] = MAX_TOKENS_SONNET
|
|
||||||
|
|
||||||
if json_mode:
|
if "sonnet" in self.config.model:
|
||||||
completion_kwargs["response_format"] = {"type": "json_object"}
|
completion_kwargs["max_tokens"] = MAX_TOKENS_SONNET
|
||||||
|
|
||||||
response = []
|
if json_mode:
|
||||||
async with self.client.messages.stream(**completion_kwargs) as stream:
|
completion_kwargs["response_format"] = {"type": "json_object"}
|
||||||
async for content in stream.text_stream:
|
|
||||||
response.append(content)
|
|
||||||
if self.stream_handler:
|
|
||||||
await self.stream_handler(content)
|
|
||||||
|
|
||||||
# TODO: get tokens from the final message
|
response = []
|
||||||
final_message = await stream.get_final_message()
|
async with self.client.messages.stream(**completion_kwargs) as stream:
|
||||||
final_message.content
|
async for content in stream.text_stream:
|
||||||
|
response.append(content)
|
||||||
|
if self.stream_handler:
|
||||||
|
await self.stream_handler(content)
|
||||||
|
|
||||||
response_str = "".join(response)
|
try:
|
||||||
|
final_message = await stream.get_final_message()
|
||||||
|
final_message.content # Access content to verify it exists
|
||||||
|
except AssertionError:
|
||||||
|
log.debug("Anthropic package AssertionError")
|
||||||
|
raise CustomAssertionError("No final message received.")
|
||||||
|
|
||||||
# Tell the stream handler we're done
|
response_str = "".join(response)
|
||||||
if self.stream_handler:
|
|
||||||
await self.stream_handler(None)
|
|
||||||
|
|
||||||
return response_str, final_message.usage.input_tokens, final_message.usage.output_tokens
|
# Tell the stream handler we're done
|
||||||
|
if self.stream_handler:
|
||||||
|
await self.stream_handler(None)
|
||||||
|
|
||||||
|
return response_str, final_message.usage.input_tokens, final_message.usage.output_tokens
|
||||||
|
|
||||||
|
for attempt in range(retry_count + 1):
|
||||||
|
try:
|
||||||
|
return await single_attempt()
|
||||||
|
except CustomAssertionError as e:
|
||||||
|
if attempt == retry_count: # If this was our last attempt
|
||||||
|
raise CustomAssertionError(f"Request failed after {retry_count + 1} attempts: {str(e)}")
|
||||||
|
# Add a small delay before retrying
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
continue
|
||||||
|
|
||||||
def rate_limit_sleep(self, err: RateLimitError) -> Optional[datetime.timedelta]:
|
def rate_limit_sleep(self, err: RateLimitError) -> Optional[datetime.timedelta]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ class BaseLLMClient:
|
|||||||
[
|
[
|
||||||
"We sent too large request to the LLM, resulting in an error. ",
|
"We sent too large request to the LLM, resulting in an error. ",
|
||||||
"This is usually caused by including framework files in an LLM request. ",
|
"This is usually caused by including framework files in an LLM request. ",
|
||||||
"Here's how you can get GPT Pilot to ignore those extra files: ",
|
"Here's how you can get Pythagora to ignore those extra files: ",
|
||||||
"https://bit.ly/faq-token-limit-error",
|
"https://bit.ly/faq-token-limit-error",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,62 @@
|
|||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Optional, Union
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
from pydantic import BaseModel, ValidationError, create_model
|
from pydantic import BaseModel, ValidationError, create_model
|
||||||
|
|
||||||
|
|
||||||
|
class CodeBlock(BaseModel):
|
||||||
|
description: str
|
||||||
|
content: str
|
||||||
|
|
||||||
|
|
||||||
|
class ParsedBlocks(BaseModel):
|
||||||
|
original_response: str
|
||||||
|
blocks: List[CodeBlock]
|
||||||
|
|
||||||
|
|
||||||
|
class DescriptiveCodeBlockParser:
|
||||||
|
"""
|
||||||
|
Parse Markdown code blocks with their descriptions from a string.
|
||||||
|
Returns both the original response and structured data about each block.
|
||||||
|
|
||||||
|
Each block entry contains:
|
||||||
|
- description: The text line immediately preceding the code block
|
||||||
|
- content: The actual content of the code block
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
>>> parser = DescriptiveCodeBlockParser()
|
||||||
|
>>> text = '''file: next.config.js
|
||||||
|
... ```js
|
||||||
|
... module.exports = {
|
||||||
|
... reactStrictMode: true,
|
||||||
|
... };
|
||||||
|
... ```'''
|
||||||
|
>>> result = parser(text)
|
||||||
|
>>> assert result.blocks[0].description == "file: next.config.js"
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.pattern = re.compile(r"^(.*?)\n```([a-z0-9]+\n)?(.*?)^```\s*", re.DOTALL | re.MULTILINE)
|
||||||
|
|
||||||
|
def __call__(self, text: str) -> ParsedBlocks:
|
||||||
|
# Store original response
|
||||||
|
original_response = text.strip()
|
||||||
|
|
||||||
|
# Find all blocks with their preceding text
|
||||||
|
blocks = []
|
||||||
|
for match in self.pattern.finditer(text):
|
||||||
|
description = match.group(1).strip()
|
||||||
|
content = match.group(3).strip()
|
||||||
|
|
||||||
|
# Only add block if we have both description and content
|
||||||
|
if description and content:
|
||||||
|
blocks.append(CodeBlock(description=description, content=content))
|
||||||
|
|
||||||
|
return ParsedBlocks(original_response=original_response, blocks=blocks)
|
||||||
|
|
||||||
|
|
||||||
class MultiCodeBlockParser:
|
class MultiCodeBlockParser:
|
||||||
"""
|
"""
|
||||||
Parse multiple Markdown code blocks from a string.
|
Parse multiple Markdown code blocks from a string.
|
||||||
|
|||||||
@@ -225,6 +225,7 @@ class ProcessManager:
|
|||||||
cwd: str = ".",
|
cwd: str = ".",
|
||||||
env: Optional[dict[str, str]] = None,
|
env: Optional[dict[str, str]] = None,
|
||||||
timeout: float = MAX_COMMAND_TIMEOUT,
|
timeout: float = MAX_COMMAND_TIMEOUT,
|
||||||
|
show_output: Optional[bool] = True,
|
||||||
) -> tuple[Optional[int], str, str]:
|
) -> tuple[Optional[int], str, str]:
|
||||||
"""
|
"""
|
||||||
Run command and wait for it to finish.
|
Run command and wait for it to finish.
|
||||||
@@ -236,6 +237,7 @@ class ProcessManager:
|
|||||||
:param cwd: Working directory.
|
:param cwd: Working directory.
|
||||||
:param env: Environment variables.
|
:param env: Environment variables.
|
||||||
:param timeout: Timeout in seconds.
|
:param timeout: Timeout in seconds.
|
||||||
|
:param show_output: Show output in the ui.
|
||||||
:return: Tuple of (status code, stdout, stderr).
|
:return: Tuple of (status code, stdout, stderr).
|
||||||
"""
|
"""
|
||||||
timeout = min(timeout, MAX_COMMAND_TIMEOUT)
|
timeout = min(timeout, MAX_COMMAND_TIMEOUT)
|
||||||
@@ -245,7 +247,7 @@ class ProcessManager:
|
|||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
while process.is_running and (time.time() - t0) < timeout:
|
while process.is_running and (time.time() - t0) < timeout:
|
||||||
out, err = await process.read_output(BUSY_WAIT_INTERVAL)
|
out, err = await process.read_output(BUSY_WAIT_INTERVAL)
|
||||||
if self.output_handler and (out or err):
|
if self.output_handler and (out or err) and show_output:
|
||||||
await self.output_handler(out, err)
|
await self.output_handler(out, err)
|
||||||
|
|
||||||
if process.is_running:
|
if process.is_running:
|
||||||
@@ -256,7 +258,7 @@ class ProcessManager:
|
|||||||
await process.wait()
|
await process.wait()
|
||||||
|
|
||||||
out, err = await process.read_output()
|
out, err = await process.read_output()
|
||||||
if self.output_handler and (out or err):
|
if self.output_handler and (out or err) and show_output:
|
||||||
await self.output_handler(out, err)
|
await self.output_handler(out, err)
|
||||||
|
|
||||||
if terminated:
|
if terminated:
|
||||||
|
|||||||
@@ -20,10 +20,14 @@ A part of the app is already finished.
|
|||||||
|
|
||||||
{% include "partials/user_feedback.prompt" %}
|
{% include "partials/user_feedback.prompt" %}
|
||||||
|
|
||||||
{% if current_task.test_instructions is defined %}
|
{% if test_instructions %}
|
||||||
Here are the test instructions the user was following when the issue occurred:
|
Here are the test instructions the user was following when the issue occurred:
|
||||||
```
|
```
|
||||||
{{ current_task.test_instructions }}
|
{% for step in test_instructions %}
|
||||||
|
Step #{{ loop.index }}
|
||||||
|
Action: {{ step.action }}
|
||||||
|
Expected result: {{ step.result }}
|
||||||
|
{% endfor %}
|
||||||
```
|
```
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@@ -38,4 +42,4 @@ Based on this information, you need to figure out where is the problem that the
|
|||||||
If you think we should add more logs around the code to better understand the problem, tell me code snippets in which we should add the logs. If you think you know where the issue is, don't add any new logs but explain what log print tell point you to the problem, what the problem is, what is the solution to this problem and how the solution will fix the problem. What is your answer?
|
If you think we should add more logs around the code to better understand the problem, tell me code snippets in which we should add the logs. If you think you know where the issue is, don't add any new logs but explain what log print tell point you to the problem, what the problem is, what is the solution to this problem and how the solution will fix the problem. What is your answer?
|
||||||
|
|
||||||
**IMPORTANT**
|
**IMPORTANT**
|
||||||
You cannot answer with "Ensure that...", "Make sure that...", etc. In these cases, explain how should the reader of your message ensure what you want them to ensure. In most cases, they will need to add some logs to ensure something in which case tell them where to add them.
|
If you want code to be written, write **ALL NEW CODE** that needs to be written. If you want to create a new file, write the entire content of that file and if you want to update an existing file, write the new code that needs to be written/updated. You cannot answer with "Ensure that...", "Make sure that...", etc. In these cases, explain how should the reader of your message ensure what you want them to ensure. In most cases, they will need to add some logs to ensure something in which case tell them where to add them.
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
{% if backend_logs is not none %}Here are the logs we added to the backend:
|
{% if backend_logs and backend_logs|trim %}
|
||||||
|
Here are the logs we added to the backend:
|
||||||
```
|
```
|
||||||
{{ backend_logs }}
|
{{ backend_logs }}
|
||||||
```
|
```
|
||||||
{% endif %}{% if frontend_logs is not none %}
|
{% endif %}{% if frontend_logs and frontend_logs|trim %}
|
||||||
Here are the logs we added to the frontend:
|
Here are the logs we added to the frontend:
|
||||||
```
|
```
|
||||||
{{ frontend_logs }}
|
{{ frontend_logs }}
|
||||||
```
|
```
|
||||||
{% endif %}{% if user_feedback is not none %}
|
{% endif %}{% if user_feedback and user_feedback|trim %}
|
||||||
Finally, here is a hint from a human who tested the app:
|
Finally, here is a hint from a human who tested the app:
|
||||||
```
|
```
|
||||||
{{ user_feedback }}
|
{{ user_feedback }}
|
||||||
@@ -16,4 +17,9 @@ When you're thinking about what to do next, take into the account human's feedba
|
|||||||
{% endif %}{% if fix_attempted %}
|
{% endif %}{% if fix_attempted %}
|
||||||
The problem wasn't solved with the last changes. You have 2 options - to tell me exactly where is the problem happening or to add more logs to better determine where is the problem. If you think we should add more logs around the code to better understand the problem, tell me code snippets in which we should add the logs. If you think you know where the issue is, don't add any new logs but explain what log print tell point you to the problem, what the problem is, what is the solution to this problem and how the solution will fix the problem. What is your answer? Make sure not to repeat mistakes from before that didn't work.
|
The problem wasn't solved with the last changes. You have 2 options - to tell me exactly where is the problem happening or to add more logs to better determine where is the problem. If you think we should add more logs around the code to better understand the problem, tell me code snippets in which we should add the logs. If you think you know where the issue is, don't add any new logs but explain what log print tell point you to the problem, what the problem is, what is the solution to this problem and how the solution will fix the problem. What is your answer? Make sure not to repeat mistakes from before that didn't work.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if backend_logs is none and frontend_logs is none and user_feedback is none and fix_attempted == false %}Human didn't supply any data{% endif %}
|
{% if not (backend_logs and backend_logs|trim) and
|
||||||
|
not (frontend_logs and frontend_logs|trim) and
|
||||||
|
not (user_feedback and user_feedback|trim) and
|
||||||
|
not fix_attempted %}
|
||||||
|
Human didn't supply any data
|
||||||
|
{% endif %}
|
||||||
|
|||||||
@@ -1,22 +1,20 @@
|
|||||||
|
{% if file_content %}
|
||||||
You are working on a project and your job is to implement new code changes based on given instructions.
|
You are working on a project and your job is to implement new code changes based on given instructions.
|
||||||
Now you have to implement ALL changes that are related to `{{ file_name }}` described in development instructions listed below.
|
Now you have to implement ALL changes that are related to `{{ file_name }}` described in development instructions listed below.
|
||||||
Make sure you don't make any mistakes, especially ones that could affect rest of project. Your changes will be reviewed by very detailed reviewer. Because of that, it is extremely important that you are STRICTLY following ALL the following rules while implementing changes:
|
Make sure you don't make any mistakes, especially ones that could affect rest of project. Your changes will be reviewed by very detailed reviewer. Because of that, it is extremely important that you are STRICTLY following ALL the following rules while implementing changes:
|
||||||
|
{% else %}
|
||||||
|
You are working on a project and your job is to create a new file `{{ file_name }}` based on given instructions. The file should be thoroughly described in the development instructions listed below. You need to follow the coding rules that will be listed below, read the development instructions and respond with the full contents of the file `{{ file_name }}`.
|
||||||
|
{% endif %}
|
||||||
{% include "partials/coding_rules.prompt" %}
|
{% include "partials/coding_rules.prompt" %}
|
||||||
|
|
||||||
You are currently working on this task:
|
|
||||||
```
|
|
||||||
{{ state.current_task.description }}
|
|
||||||
```
|
|
||||||
|
|
||||||
{% include "partials/user_feedback.prompt" %}
|
{% include "partials/user_feedback.prompt" %}
|
||||||
|
|
||||||
Here are development instructions and now you have to focus only on changes in `{{ file_name }}`:
|
Here are development instructions that were sent to you by a senior developer that you need to carefully follow. Focus only on the code changes for the file `{{ file_name }}`:
|
||||||
---start_of_development_instructions---
|
~~~START_OF_DEVELOPMENT_INSTRUCTIONS~~~
|
||||||
|
|
||||||
{{ instructions }}
|
{{ instructions }}
|
||||||
|
|
||||||
---end_of_development_instructions---
|
~~~END_OF_DEVELOPMENT_INSTRUCTIONS~~~
|
||||||
|
|
||||||
{% if rework_feedback is defined %}
|
{% if rework_feedback is defined %}
|
||||||
You previously made changes to file `{{ file_name }}` but not all changes were accepted, and the reviewer provided feedback on the changes that you must rework:
|
You previously made changes to file `{{ file_name }}` but not all changes were accepted, and the reviewer provided feedback on the changes that you must rework:
|
||||||
@@ -28,12 +26,15 @@ The reviewer accepted some of your changes, and the file now looks like this:
|
|||||||
{{ file_content }}
|
{{ file_content }}
|
||||||
```
|
```
|
||||||
{% elif file_content %}
|
{% elif file_content %}
|
||||||
Here is how `{{ file_name }}` looks like currently:
|
Now, take a look at how `{{ file_name }}` looks like currently:
|
||||||
```
|
```
|
||||||
{{ file_content }}
|
{{ file_content }}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Ok, now, you have to follow the instructions about `{{ file_name }}` from the development instructions carefully. Reply **ONLY** with the full contents of the file `{{ file_name }}` and nothing else. Do not make any changes to the file that are not mentioned in the development instructions - you must **STRICTLY** follow the instructions.
|
||||||
{% else %}
|
{% else %}
|
||||||
You need to create a new file `{{ file_name }}`.
|
You need to create a new file `{{ file_name }}` so respond **ONLY** with the full contents of that file from the development instructions that you read.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% include "partials/files_list.prompt" %}
|
** IMPORTANT **
|
||||||
|
Remember, you must **NOT** add anything in your response that is not strictly the code from the file. Do not start or end the response with an explanation or a comment - you must respond with only the code from the file because your response will be directly saved to a file and run.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
You are working on an app called "{{ state.branch.project.name }}" and you need to write code for the entire {% if state.epics|length > 1 %}feature{% else %}app{% endif %} based on the tasks that the tech lead gives you. So that you understand better what you're working on, you're given other specs for "{{ state.branch.project.name }}" as well.
|
You are working on an app called "{{ state.branch.project.name }}" and you are a primary developer who needs to write and maintain the code for this app.You are currently working on the implementation of one task that I will tell you below. Before that, here is the context of the app you're working on. Each section of the context starts with `~~SECTION_NAME~~` and ends with ~~END_OF_SECTION_NAME~~`.
|
||||||
|
|
||||||
{% include "partials/project_details.prompt" %}
|
{% include "partials/project_details.prompt" %}
|
||||||
{% include "partials/features_list.prompt" %}
|
{% include "partials/features_list.prompt" %}
|
||||||
@@ -10,6 +10,7 @@ You are working on an app called "{{ state.branch.project.name }}" and you need
|
|||||||
**IMPORTANT**
|
**IMPORTANT**
|
||||||
Remember, I created an empty folder where I will start writing files that you tell me and that are needed for this app.
|
Remember, I created an empty folder where I will start writing files that you tell me and that are needed for this app.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
~~DEVELOPMENT_INSTRUCTIONS~~
|
||||||
{% include "partials/relative_paths.prompt" %}
|
{% include "partials/relative_paths.prompt" %}
|
||||||
|
|
||||||
DO NOT specify commands to create any folders or files, they will be created automatically - just specify the relative path to each file that needs to be written.
|
DO NOT specify commands to create any folders or files, they will be created automatically - just specify the relative path to each file that needs to be written.
|
||||||
@@ -20,20 +21,38 @@ DO NOT specify commands to create any folders or files, they will be created aut
|
|||||||
{% include "partials/file_size_limit.prompt" %}
|
{% include "partials/file_size_limit.prompt" %}
|
||||||
{% include "partials/breakdown_code_instructions.prompt" %}
|
{% include "partials/breakdown_code_instructions.prompt" %}
|
||||||
|
|
||||||
Never use the port 5000 to run the app, it's reserved.
|
{% if state.has_frontend() %}
|
||||||
|
The entire backend API needs to be on /api/... routes!
|
||||||
|
|
||||||
--IMPLEMENTATION INSTRUCTIONS--
|
** IMPORTANT - Mocking API requests **
|
||||||
|
Frontend side is making requests to the backend by calling functions that are defined in the folder client/api/. During the frontend implementation, API requests are mocked with dummy data that is defined in this folder and the API response data structure is defined in a comment above each API calling function. Whenever you need to implement an API endpoint, you must first find the function on the frontend that should call that API, remove the mocked data and make sure that the API call is properly done and that the response is parsed in a proper way. Whenever you do this, make sure to tell me explicitly which API calling function is being changed and what will be the response from the API.
|
||||||
|
Whenever you add an API request from the frontend, make sure to wrap the request in try/catch block and in the catch block, return `throw new Error(error?.response?.data?.error || error.message);` - in the place where the API request function is being called, show a toast message with an error.
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
** IMPORTANT - current implementation **
|
||||||
|
Pay close attention to the currently implemented files, and DO NOT tell me to implementat something that is already implemented. Similarly, do not change the current implementation if you think it is working correctly. It is not necessary for you to change files - you can leave the files as they are and just tell me that they are correctly implemented.
|
||||||
|
~~END_OF_DEVELOPMENT_INSTRUCTIONS~~
|
||||||
|
|
||||||
|
~~DEVELOPMENT_PLAN~~
|
||||||
We've broken the development of this {% if state.epics|length > 1 %}feature{% else %}app{% endif %} down to these tasks:
|
We've broken the development of this {% if state.epics|length > 1 %}feature{% else %}app{% endif %} down to these tasks:
|
||||||
```
|
```
|
||||||
{% for task in state.tasks %}
|
{% for task in state.tasks %}
|
||||||
{{ loop.index }}. {{ task.description }}{% if task.get("status") == "done" %} (completed){% endif %}
|
{{ loop.index }}. {{ task.description }}{% if task.get("status") == "done" %} (completed){% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
```
|
```
|
||||||
|
~~END_OF_DEVELOPMENT_PLAN~~
|
||||||
|
|
||||||
You are currently working on task #{{ current_task_index + 1 }} with the following description:
|
You are currently working on task #{{ current_task_index + 1 }} with the following description:
|
||||||
```
|
```
|
||||||
{{ task.description }}
|
{{ task.description }}
|
||||||
```
|
```
|
||||||
|
{% if related_api_endpoints|length > 0 %}
|
||||||
|
In this task, you need to focus on implementing the following endpoints:{% for api in related_api_endpoints %}{{ "`" ~ api.endpoint ~ "`" }}{% if not loop.last %},{% endif %}{% endfor %}
|
||||||
|
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
You must implement the backend API endpoints, remove the mocked that on the frontend side, and replace it with the real API request, implement the database model (if it's not implemented already), and implement the utility function (eg. 3rd party integration) that is needed for this endpoint. Whenever you need to create a utility function that uses a 3rd party integration, you **MUST** mock that function (create the function, return a mocked data, and specify the structure of the input and the output of the function in the comment above the function).
|
||||||
|
|
||||||
{% if task.get('pre_breakdown_testing_instructions') is not none %}
|
{% if task.get('pre_breakdown_testing_instructions') is not none %}
|
||||||
Here is how this task should be tested:
|
Here is how this task should be tested:
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -11,10 +11,12 @@ We've broken the development of the project down to these tasks:
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
{% if state.current_task %}
|
||||||
The next task we need to work on, and have to focus on, is this task:
|
The next task we need to work on, and have to focus on, is this task:
|
||||||
```
|
```
|
||||||
{{ state.current_task.description }}
|
{{ state.current_task.description }}
|
||||||
```
|
```
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if user_feedback %}User who was using the app sent you this feedback:
|
{% if user_feedback %}User who was using the app sent you this feedback:
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -4,9 +4,20 @@ when converting this message to steps.
|
|||||||
|
|
||||||
Each step can be either:
|
Each step can be either:
|
||||||
|
|
||||||
* `command` - command to run (must be able to run on a {{ os }} machine, assume current working directory is project root folder)
|
* `command`
|
||||||
* `save_file` - create or update ONE file (only provide file path, not contents)
|
- command to run
|
||||||
* `human_intervention` - if you need the human to do something, use this type of step and explain in details what you want the human to do. NEVER use `human_intervention` for testing, as testing will be done separately by a dedicated QA after all the steps are done. Also you MUST NOT use `human_intervention` to ask the human to write or review code.
|
- assume current working directory is project root folder, which means you MUST add `cd server && <cmd>` or `cd client && <cmd>` if they have to be executed inside `./server` or `./client` folders
|
||||||
|
- must be able to run on a {{ os }} machine
|
||||||
|
* `save_file`
|
||||||
|
- create or update ONE file (only provide file path, not contents)
|
||||||
|
* `human_intervention`
|
||||||
|
- if you need the human to do something, use this type of step and explain in details what you want the human to do
|
||||||
|
- NEVER use `human_intervention` for testing, as testing will be done separately by a dedicated QA after all the steps are done
|
||||||
|
- NEVER use `human_intervention` to ask the human to write or review code
|
||||||
|
* `utility_function`
|
||||||
|
- if there is a utility function mentioned, use this type of step and specify the file in which it needs to be put in, the name of the function, the description of what this function should do, the status of the function, the input parameters for this function, and the format of the return value
|
||||||
|
- if the function is going to be mocked, then put the status `mocked` and if it's going to be implemented in this task, put the status `implemented`
|
||||||
|
- for each utility function that needs to be mocked or implemented, you must provide a separate `utility_function` step
|
||||||
|
|
||||||
**IMPORTANT**: If multiple changes are required for same file, you must provide single `save_file` step for each file.
|
**IMPORTANT**: If multiple changes are required for same file, you must provide single `save_file` step for each file.
|
||||||
|
|
||||||
@@ -25,17 +36,30 @@ Examples:
|
|||||||
{
|
{
|
||||||
"type": "save_file",
|
"type": "save_file",
|
||||||
"save_file": {
|
"save_file": {
|
||||||
"path": "server.js"
|
"path": "server/server.js"
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": {
|
"command": {
|
||||||
"command": "mv index.js public/index.js"",
|
"command": "cd server && npm install puppeteer",
|
||||||
"timeout": 5,
|
"timeout": 30,
|
||||||
"success_message": "",
|
"success_message": "",
|
||||||
"command_id": "move_index_file"
|
"command_id": "install_puppeteer"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": {
|
||||||
|
"command": "cd client && npm install lucide-react",
|
||||||
|
"timeout": 30,
|
||||||
|
"success_message": "",
|
||||||
|
"command_id": "install_lucide_react"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "human_intervention",
|
||||||
|
"human_intervention_description": "1. Open the AWS Management Console (https://aws.amazon.com/console/). 2. Navigate to the S3 service and create a new bucket. 3. Configure the bucket with public read access by adjusting the permissions. 4. Upload your static website files to the bucket. 5. Enable static website hosting in the bucket settings and note the website endpoint URL. 6. Add the endpoint URL to your application’s configuration file as: WEBSITE_URL=your_website_endpoint"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,8 @@
|
|||||||
A coding task has been implemented for the new project we're working on.
|
A coding task has been implemented for the new project, "{{ state.branch.project.name }}", we're working on.
|
||||||
|
|
||||||
{% include "partials/project_details.prompt" %}
|
Your job is to analyze the output of the command that was ran and determine if the command was successfully executed.
|
||||||
{% include "partials/files_list.prompt" %}
|
|
||||||
|
|
||||||
We've broken the development of the project down to these tasks:
|
The current task we are working on is: {{ current_task.description }}
|
||||||
```
|
|
||||||
{% for task in state.tasks %}
|
|
||||||
{{ loop.index }}. {{ task.description }}{% if task.get("status") == "done" %} (completed){% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
```
|
|
||||||
|
|
||||||
The current task is: {{ current_task.description }}
|
|
||||||
|
|
||||||
Here are the detailed instructions for the current task:
|
|
||||||
```
|
|
||||||
{{ current_task.instructions }}
|
|
||||||
```
|
|
||||||
{# FIXME: the above stands in place of a previous (task breakdown) convo, and is duplicated in define_user_review_goal and debug prompts #}
|
|
||||||
|
|
||||||
{% if task_steps and step_index is not none -%}
|
{% if task_steps and step_index is not none -%}
|
||||||
The current task has been split into multiple steps, and each step is one of the following:
|
The current task has been split into multiple steps, and each step is one of the following:
|
||||||
|
|||||||
40
core/prompts/frontend/build_frontend.prompt
Normal file
40
core/prompts/frontend/build_frontend.prompt
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{% if user_feedback %}You're currently working on a frontend of an app that has the following description:
|
||||||
|
{% else %}Create a very modern styled app with the following description:{% endif %}
|
||||||
|
|
||||||
|
```
|
||||||
|
{{ description }}
|
||||||
|
```
|
||||||
|
|
||||||
|
{{ state.specification.template_summary }}
|
||||||
|
|
||||||
|
{% include "partials/files_list.prompt" %}
|
||||||
|
|
||||||
|
Use material design and nice icons for the design to be appealing and modern. Use the following libraries to make it very modern and slick:
|
||||||
|
1. Shadcn: For the core UI components, providing modern, accessible, and customizable elements. You have already access to all components from this library inside ./src/components/ui folder, so do not modify/code them!
|
||||||
|
2. Use lucide icons (npm install lucide-react)
|
||||||
|
3. Heroicons: For a set of sleek, customizable icons that integrate well with modern designs.
|
||||||
|
4. React Hook Form: For efficient form handling with minimal re-rendering, ensuring a smooth user experience in form-heavy applications.
|
||||||
|
5. Use Tailwind built-in animations to enhance the visual appeal of the app
|
||||||
|
|
||||||
|
Choose a flat color palette and make sure that the text is readable and follow design best practices to make the text readable. Also, Implement these design features onto the page - gradient background, frosted glass effects, rounded corner, buttons need to be in the brand colors, and interactive feedback on hover and focus.
|
||||||
|
|
||||||
|
IMPORTANT: Text needs to be readable and in positive typography space - this is especially true for modals - they must have a bright background
|
||||||
|
|
||||||
|
You must create all code for all pages of this website. If this is a some sort of a dashboard, put the navigation in the sidebar.
|
||||||
|
|
||||||
|
**IMPORTANT**
|
||||||
|
Make sure to implement all functionality (button clicks, form submissions, etc.) and use mock data for all interactions to make the app look and feel real. **ALL MOCK DATA MUST** be in the `api/` folder and it **MUST NOT** ever be hardcoded in the components.
|
||||||
|
The body content should not overlap with the header navigation bar or footer navigation bar or the side navigation bar.
|
||||||
|
|
||||||
|
{% if user_feedback %}
|
||||||
|
User who was using the app "{{ state.branch.project.name }}" sent you this feedback:
|
||||||
|
```
|
||||||
|
{{ user_feedback }}
|
||||||
|
```
|
||||||
|
Now, start by writing all code that's needed to fix the problem that the user reported. Think about how routes are set up, how are variables called, and other important things, and mention files by name and where should all new functionality be called from. Then, tell me all the code that needs to be written to fix this issue.
|
||||||
|
{% else %}
|
||||||
|
Now, start by writing all code that's needed to get the frontend built for this app. Think about how routes are set up, how are variables called, and other important things, and mention files by name and where should all new functionality be called from. Then, tell me all the code that needs to be written to implement the frontend for this app and have it fully working and all commands that need to be run.
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
IMPORTANT: When suggesting/making changes in the file you must provide full content of the file! Do not use placeholders, or comments, or truncation in any way, but instead provide the full content of the file even the parts that are unchanged!
|
||||||
|
When you want to run a command you must put `command:` before the command and then the command itself like shown in the examples in system prompt. NEVER run `npm run start` or `npm run dev` commands, user will run them after you provide the code. The user is using {{ os }}, so the commands must run on that operating system
|
||||||
115
core/prompts/frontend/system.prompt
Normal file
115
core/prompts/frontend/system.prompt
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
You are a world class frontend software developer.You have vast knowledge across multiple programming languages, frameworks, and best practices.
|
||||||
|
|
||||||
|
You write modular, well-organized code split across files that are not too big, so that the codebase is maintainable. You include proper error handling and logging for your clean, readable, production-level quality code.
|
||||||
|
|
||||||
|
Your job is to quickly build frontend components and features using Vite for the app that user requested. Make sure to focus only on the things that are requested and do not spend time on anything else.
|
||||||
|
|
||||||
|
IMPORTANT: Think HOLISTICALLY and COMPREHENSIVELY BEFORE creating any code. This means:
|
||||||
|
- Consider ALL relevant files in the project
|
||||||
|
- Review ALL previous file changes and user modifications (as shown in diffs, see diff_spec)
|
||||||
|
- Analyze the entire project context and dependencies
|
||||||
|
- Anticipate potential impacts on other parts of the system
|
||||||
|
|
||||||
|
IMPORTANT: Always provide the FULL, updated content of the file. This means:
|
||||||
|
- Include ALL code, even if parts are unchanged
|
||||||
|
- NEVER use placeholders like "// rest of the code remains the same..." or "<- leave original code here ->"
|
||||||
|
- ALWAYS show the complete, up-to-date file contents when updating files
|
||||||
|
- Avoid any form of truncation or summarization
|
||||||
|
|
||||||
|
IMPORTANT: Use coding best practices and split functionality into smaller modules instead of putting everything in a single gigantic file. Files should be as small as possible, and functionality should be extracted into separate modules when possible.
|
||||||
|
- Ensure code is clean, readable, and maintainable.
|
||||||
|
- Adhere to proper naming conventions and consistent formatting.
|
||||||
|
- Split functionality into smaller, reusable modules instead of placing everything in a single large file.
|
||||||
|
- Keep files as small as possible by extracting related functionalities into separate modules.
|
||||||
|
- Use imports to connect these modules together effectively.
|
||||||
|
|
||||||
|
IMPORTANT: Prefer writing Node.js scripts instead of shell scripts.
|
||||||
|
|
||||||
|
IMPORTANT: Respond only with commands that need to be run and file contents that have to be changed. Do not provide explanations or justifications.
|
||||||
|
|
||||||
|
IMPORTANT: Make sure you install all the necessary dependencies inside the correct folder. For example, if you are working on the frontend, make sure to install all the dependencies inside the "client" folder like this:
|
||||||
|
command:
|
||||||
|
```bash
|
||||||
|
cd client && npm install <package-name>
|
||||||
|
```
|
||||||
|
NEVER run `npm run start` or `npm run dev` commands, user will run them after you provide the code.
|
||||||
|
|
||||||
|
IMPORTANT: The order of the actions is very important. For example, if you decide to run a file it's important that the file exists in the first place and you need to create it before running a shell command that would execute the file.
|
||||||
|
|
||||||
|
IMPORTANT: Make sure to implement all functionality (button clicks, form submissions, etc.) and use MOCK DATA for all interactions to make the app look and feel real. MOCK DATA should be used for all interactions.
|
||||||
|
|
||||||
|
IMPORTANT: Put full path of file you are editing! Mostly you will work with files inside "client/" folder so don't forget to put it in file path, for example DO `client/src/App.tsx` instead of `src/App.tsx`.
|
||||||
|
|
||||||
|
{% include "partials/file_naming.prompt" %}
|
||||||
|
|
||||||
|
Here are the examples:
|
||||||
|
|
||||||
|
---start_of_examples---
|
||||||
|
|
||||||
|
------------------------example_1---------------------------
|
||||||
|
Prompt:
|
||||||
|
Create a new file called `components/MyComponent.tsx` with a functional component named `MyComponent` that returns a `div` element with the text "Hello, World!".
|
||||||
|
|
||||||
|
Your response:
|
||||||
|
command:
|
||||||
|
```bash
|
||||||
|
npm init -y
|
||||||
|
npm install <package-name>
|
||||||
|
```
|
||||||
|
file: App.tsx
|
||||||
|
```tsx
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export const MyComponent: React.FC = () => {
|
||||||
|
return <div>Hello, World!</div>;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
------------------------example_1_end---------------------------
|
||||||
|
|
||||||
|
------------------------example_2---------------------------
|
||||||
|
Prompt:
|
||||||
|
Create snake game.
|
||||||
|
|
||||||
|
Your response:
|
||||||
|
command:
|
||||||
|
```bash
|
||||||
|
cd client && npm install shadcn/ui
|
||||||
|
node scripts/createInitialLeaderboard.js
|
||||||
|
```
|
||||||
|
file: client/components/Snake.tsx
|
||||||
|
```tsx
|
||||||
|
import React from 'react';
|
||||||
|
...
|
||||||
|
```
|
||||||
|
file: client/components/Food.tsx
|
||||||
|
```tsx
|
||||||
|
...
|
||||||
|
```
|
||||||
|
file: client/components/Score.tsx
|
||||||
|
```tsx
|
||||||
|
...
|
||||||
|
```
|
||||||
|
file: client/components/GameOver.tsx
|
||||||
|
```tsx
|
||||||
|
...
|
||||||
|
```
|
||||||
|
------------------------example_2_end---------------------------
|
||||||
|
|
||||||
|
------------------------example_3---------------------------
|
||||||
|
Prompt:
|
||||||
|
Create a script that counts to 10.
|
||||||
|
|
||||||
|
Your response:
|
||||||
|
file: countToTen.js
|
||||||
|
```js
|
||||||
|
for (let i = 1; i <= 10; i++) {
|
||||||
|
console.log(i);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
command:
|
||||||
|
```bash
|
||||||
|
node countToTen.js
|
||||||
|
```
|
||||||
|
------------------------example_3_end---------------------------
|
||||||
|
|
||||||
|
---end_of_examples---
|
||||||
@@ -1 +1,2 @@
|
|||||||
Make sure that the user doesn't have to test anything with commands but that all features are reflected in the frontend and all information that user sees in the browser should on a stylized page and not as a plain text or JSON.
|
Make sure that the user doesn't have to test anything with commands but that all features are reflected in the frontend and all information that user sees in the browser should on a stylized page and not as a plain text or JSON.
|
||||||
|
Also, ensure proper error handling. Whenever an error happens, show the user what does the error say (never use generic error messages like "Something went wrong" or "Internal server error"). Show the error in the logs as well as in the frontend (usually a toast message or a label).
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# RULES FOR IMPLEMENTING CODE CHANGES
|
# RULES FOR IMPLEMENTING CODE CHANGES
|
||||||
---start_of_coding_rules---
|
~~~START_OF_CODING_RULES~~~
|
||||||
|
|
||||||
## Rule 1: Scope of your coding task
|
## Rule 1: Scope of your coding task
|
||||||
You must implement everything mentioned in the instructions that is related to this file. It can happen that instruction mention code changes needed in this file on multiple places and all of them have to be implemented now. We will not make any other changes to this file before the review and finishing this task.
|
You must implement everything mentioned in the instructions that is related to this file. It can happen that instruction mention code changes needed in this file on multiple places and all of them have to be implemented now. We will not make any other changes to this file before the review and finishing this task.
|
||||||
@@ -36,4 +36,8 @@ Whenever you write code, make sure to log code execution so that when a develope
|
|||||||
## Rule 6: Error handling
|
## Rule 6: Error handling
|
||||||
Whenever you write code, make sure to add error handling for all edge cases you can think of because this app will be used in production so there shouldn't be any crashes. Whenever you log the error, you **MUST** log the entire error message and trace and not only the error message. If the description above mentions the exact code that needs to be added but doesn't contain enough error handlers, you need to add the error handlers inside that code yourself.
|
Whenever you write code, make sure to add error handling for all edge cases you can think of because this app will be used in production so there shouldn't be any crashes. Whenever you log the error, you **MUST** log the entire error message and trace and not only the error message. If the description above mentions the exact code that needs to be added but doesn't contain enough error handlers, you need to add the error handlers inside that code yourself.
|
||||||
|
|
||||||
---end_of_coding_rules---
|
{% if state.has_frontend() %}
|
||||||
|
## Rule 7: Showing errors on the frontend
|
||||||
|
If there is an error in the API request, return the error message by throwing an error in the client/api/<FILE> file that makes the actual API request. In the .tsx file that called the API function, catch the error and show the error message to the user by showing `error.message` inside the toast's `description` value.
|
||||||
|
~~~END_OF_CODING_RULES~~~
|
||||||
|
{% endif %}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{% if state.epics|length > 2 %}
|
{% if state.epics|length > 3 %}
|
||||||
|
|
||||||
Here is the list of features that were previously implemented on top of initial high level description of "{{ state.branch.project.name }}":
|
Here is the list of features that were previously implemented on top of initial high level description of "{{ state.branch.project.name }}":
|
||||||
```
|
```
|
||||||
{% for feature in state.epics[1:-1] %}
|
{% for feature in state.epics[2:-1] %}
|
||||||
- {{ loop.index }}. {{ feature.description }}
|
- {{ loop.index }}. {{ feature.description }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
```
|
```
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if state.epics|length > 1 %}
|
{% if state.epics|length > 2 %}
|
||||||
|
|
||||||
Here is the feature that you are implementing right now:
|
Here is the feature that you are implementing right now:
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1 +1,35 @@
|
|||||||
**IMPORTANT**: When creating and naming new files, ensure the file naming (camelCase, kebab-case, underscore_case, etc) is consistent with the best practices and coding style of the language.
|
**IMPORTANT**: When creating and naming new files, ensure the file naming (camelCase, kebab-case, underscore_case, etc) is consistent within the project.
|
||||||
|
|
||||||
|
**IMPORTANT**: Folder/file structure
|
||||||
|
The project uses controllers, models, and services on the server side. You **MUST** strictly follow this structure when you think about the implementation. The folder structure is as follows:
|
||||||
|
```
|
||||||
|
server/
|
||||||
|
├── config/
|
||||||
|
│ ├── database.js # Database configuration
|
||||||
|
│ └── ... # Other configurations
|
||||||
|
│
|
||||||
|
├── models/
|
||||||
|
│ ├── User.js # User model/schema definition
|
||||||
|
│ └── ... # Other models
|
||||||
|
│
|
||||||
|
├── routes/
|
||||||
|
│ ├── middleware/
|
||||||
|
│ │ ├── auth.js # Authentication middleware
|
||||||
|
│ │ └── ... # Other middleware
|
||||||
|
│ │
|
||||||
|
│ ├── index.js # Main route file
|
||||||
|
│ ├── authRoutes.js # Authentication routes
|
||||||
|
│ └── ... # Other route files
|
||||||
|
│
|
||||||
|
├── services/
|
||||||
|
│ ├── userService.js # User-related business services
|
||||||
|
│ └── ... # Other services
|
||||||
|
│
|
||||||
|
├── utils/
|
||||||
|
│ ├── password.js # Password hashing and validation
|
||||||
|
│ └── ... # Other utility functions
|
||||||
|
│
|
||||||
|
├── .env # Environment variables
|
||||||
|
├── server.js # Server entry point
|
||||||
|
└── ... # Other project files
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
These files are currently implemented in the project:
|
These files are currently implemented on the frontend that contain all API requests to the backend with structure that you need to follow:
|
||||||
{% for file in state.files %}
|
{% for file in state.files %}
|
||||||
|
{% if not state.has_frontend() or (state.has_frontend() and state.epics|length > 1 and 'client/src/components/ui' not in file.path ) or (state.has_frontend() and state.epics|length == 1 ) %}
|
||||||
* `{{ file.path }}{% if file.meta.get("description") %}: {{file.meta.description}}{% endif %}`
|
* `{{ file.path }}{% if file.meta.get("description") %}: {{file.meta.description}}{% endif %}`
|
||||||
{% endfor %}
|
{% endif %}{% endfor %}
|
||||||
|
These files are currently implemented in the project on the backend:
|
||||||
|
{% for file in state.files %}{% if 'server/' in file.path %}
|
||||||
|
* `{{ file.path }}{% if file.meta.get("description") %}: {{file.meta.description}}{% endif %}`
|
||||||
|
{% endif %}{% endfor %}
|
||||||
|
|||||||
@@ -1,15 +1,28 @@
|
|||||||
{% if state.relevant_files %}
|
{% if state.relevant_files %}
|
||||||
|
~~FILE_DESCRIPTIONS_IN_THE_CODEBASE~~
|
||||||
{% include "partials/files_descriptions.prompt" %}
|
{% include "partials/files_descriptions.prompt" %}
|
||||||
|
~~END_OF_FILE_DESCRIPTIONS_IN_THE_CODEBASE~~
|
||||||
|
|
||||||
|
~~RELEVANT_FILES_IMPLEMENTATION~~
|
||||||
{% include "partials/files_list_relevant.prompt" %}
|
{% include "partials/files_list_relevant.prompt" %}
|
||||||
{% elif state.files %}
|
{% elif state.files %}
|
||||||
|
~~RELEVANT_FILES_IMPLEMENTATION~~
|
||||||
These files are currently implemented in the project:
|
These files are currently implemented in the project:
|
||||||
---START_OF_FILES---
|
---START_OF_FRONTEND_API_FILES---
|
||||||
{% for file in state.files %}
|
{% for file in state.files %}{% if ((get_only_api_files is not defined or not get_only_api_files) and 'client/' in file.path) or 'client/src/api/' in file.path %}
|
||||||
**`{{ file.path }}`** ({{file.content.content.splitlines()|length}} lines of code):
|
**`{{ file.path }}`** ({{file.content.content.splitlines()|length}} lines of code):
|
||||||
```
|
```
|
||||||
{{ file.content.content }}```
|
{{ file.content.content }}```
|
||||||
|
|
||||||
{% endfor %}
|
{% endif %}{% endfor %}
|
||||||
---END_OF_FILES---
|
---END_OF_FRONTEND_API_FILES---
|
||||||
|
---START_OF_BACKEND_FILES---
|
||||||
|
{% for file in state.files %}{% if 'server/' in file.path %}
|
||||||
|
**`{{ file.path }}`** ({{file.content.content.splitlines()|length}} lines of code):
|
||||||
|
```
|
||||||
|
{{ file.content.content }}```
|
||||||
|
|
||||||
|
{% endif %}{% endfor %}
|
||||||
|
---END_OF_BACKEND_FILES---
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
~~END_OF_RELEVANT_FILES_IMPLEMENTATION~~
|
||||||
|
|||||||
@@ -1,9 +1,30 @@
|
|||||||
Here are the complete contents of files relevant to this task:
|
Here are the complete contents of files relevant to this task:
|
||||||
---START_OF_FILES---
|
{% if state.has_frontend() %}
|
||||||
|
---START_OF_FRONTEND_API_FILES---
|
||||||
{% for file in state.relevant_file_objects %}
|
{% for file in state.relevant_file_objects %}
|
||||||
File **`{{ file.path }}`** ({{file.content.content.splitlines()|length}} lines of code):
|
{% if 'client/' in file.path %}
|
||||||
|
{% if (state.epics|length > 1 and 'client/src/components/ui' not in file.path ) or state.epics|length == 1 %}
|
||||||
|
**`{{ file.path }}`** ({{file.content.content.splitlines()|length}} lines of code):
|
||||||
|
```
|
||||||
|
{{ file.content.content }}
|
||||||
|
```
|
||||||
|
{% endif %}{% endif %}{% endfor %}
|
||||||
|
---END_OF_FRONTEND_API_FILES---
|
||||||
|
---START_OF_BACKEND_FILES---
|
||||||
|
{% for file in state.relevant_file_objects %}{% if 'server/' in file.path %}
|
||||||
|
**`{{ file.path }}`** ({{file.content.content.splitlines()|length}} lines of code):
|
||||||
```
|
```
|
||||||
{{ file.content.content }}```
|
{{ file.content.content }}```
|
||||||
|
|
||||||
|
{% endif %}{% endfor %}
|
||||||
|
---END_OF_BACKEND_FILES---
|
||||||
|
{% else %}
|
||||||
|
---START_OF_FILES---
|
||||||
|
{% for file in state.relevant_file_objects %}
|
||||||
|
**`{{ file.path }}`** ({{file.content.content.splitlines()|length}} lines of code):
|
||||||
|
```
|
||||||
|
{{ file.content.content }}
|
||||||
|
```
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
---END_OF_FILES---
|
---END_OF_FILES---
|
||||||
|
{% endif %}
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
Here is the current relevant files list:
|
Here is the current relevant files list:
|
||||||
{% if relevant_files %}{{ relevant_files }}{% else %}[]{% endif %}
|
{% if relevant_files %}{{ relevant_files }}{% else %}[]{% endif %}
|
||||||
|
|
||||||
Now, with multiple iterations you have to find relevant files for the current task. Here are commands that you can use:
|
Now, with multiple iterations you have to find relevant files for the current task. Here are the actions that you can use:
|
||||||
- `read_files` - List of files that you want to read.
|
- `read_files` - List of files that you want to read.
|
||||||
- `add_files` - Add file to the list of relevant files.
|
- `add_files` - Add file to the list of relevant files.
|
||||||
- `remove_files` - Remove file from the list of relevant files.
|
- `remove_files` - Remove file from the list of relevant files.
|
||||||
- `finished` - Boolean command that you will use when you finish with finding relevant files.
|
- `finished` - Boolean that you will use when you finish with finding relevant files.
|
||||||
|
|
||||||
Make sure to follow these rules:
|
IMPORTANT:
|
||||||
|
# Make sure to follow these rules:
|
||||||
- All files that you want to read or add to the list of relevant files, must exist in the project. Do not ask to read or add file that does not exist! In the first message you have list of all files that currently exist in the project.
|
- All files that you want to read or add to the list of relevant files, must exist in the project. Do not ask to read or add file that does not exist! In the first message you have list of all files that currently exist in the project.
|
||||||
- Do not repeat actions that you have already done. For example if you already added "index.js" to the list of relevant files you must not add it again.
|
- Do not repeat same action for same file that you have already done. For example if you already added "server.js" to the list of relevant files you must not add "server.js" again.
|
||||||
- You must read the file before adding it to the list of relevant files. Do not `add_files` that you didn't read and see the content of the file.
|
- You must read the file before adding it to the list of relevant files. Do not add that you didn't read and see the content of the file.
|
||||||
- Focus only on your current task `{{ state.current_task.description }}` when selecting relevant files.
|
{% if state.current_task %}- Focus only on your current task `{{ state.current_task.description }}` when selecting relevant files.{% endif %}
|
||||||
|
{% if not state.has_frontend() %}- IMPORTANT: You must read and add relevant files from both "client/" and "server/" folders so that implementation can be done correctly! The "client" and "server" must work seamlessly together!{% endif %}
|
||||||
|
- IMPORTANT: Execute only 1 action per request! Once your response is processed you will be able to choose next action.
|
||||||
|
- IMPORTANT: It is much better to read and add more files than not adding files that are relevant!
|
||||||
|
- If there is even a slight chance a file is relevant, you should read it. Make sure to read files in client/src/pages folder that could be relevant.
|
||||||
|
- Read as many files as possible. You can read and add up to 20 files at once and if you need to see more file contents you can do that in next iterations. Make sure you don't miss any files that are relevant!
|
||||||
|
|||||||
@@ -1,22 +1,11 @@
|
|||||||
|
~~APP_DESCRIPTION~~
|
||||||
Here is a high level description of "{{ state.branch.project.name }}":
|
Here is a high level description of "{{ state.branch.project.name }}":
|
||||||
```
|
```
|
||||||
{{ state.specification.description }}
|
{{ state.specification.description }}
|
||||||
```
|
```
|
||||||
|
~~END_OF_APP_DESCRIPTION~~
|
||||||
{% if state.specification.system_dependencies %}
|
|
||||||
|
|
||||||
Here are the technologies that should be used for this project:
|
|
||||||
{% for tech in state.specification.system_dependencies %}
|
|
||||||
* {{ tech.name }} - {{ tech.description }}
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
{% if state.specification.package_dependencies %}
|
|
||||||
|
|
||||||
{% for tech in state.specification.package_dependencies %}
|
|
||||||
* {{ tech.name }} - {{ tech.description }}
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
{% if state.specification.template_summary %}
|
{% if state.specification.template_summary %}
|
||||||
|
~~INFORMATION_ABOUT_THE_CODEBASE~~
|
||||||
{{ state.specification.template_summary }}
|
{{ state.specification.template_summary }}
|
||||||
|
~~END_OF_INFORMATION_ABOUT_THE_CODEBASE~~
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -1,26 +1,35 @@
|
|||||||
{# This is actually creation of tasks and not epics. Reason why this prompt uses word "epic" instead of "task" is that LLM gives very detailed description and creates very big plan if we ask him to create tasks. When asked to create epics he focuses on much bigger picture and gives high level description which is what we want. #}
|
{# This is actually creation of tasks and not epics. Reason why this prompt uses word "epic" instead of "task" is that LLM gives very detailed description and creates very big plan if we ask him to create tasks. When asked to create epics he focuses on much bigger picture and gives high level description which is what we want. #}
|
||||||
# Rules for creating epics
|
# Rules for creating epics
|
||||||
---start_of_rules_for_creating_epics---
|
~~~START_OF_RULES_FOR_CREATING_EPICS~~~
|
||||||
|
|
||||||
## Rule #1
|
## Rule #1
|
||||||
Every epic must have only coding involved. There should never be a epic that is only testing or ensuring something works. There shouldn't be a epic for researching, deployment, writing documentation, testing or anything that is not writing the actual code. Testing if app works will be done as part of each epic.
|
Every epic must have only coding involved. There should never be a epic that is only testing or ensuring something works. There shouldn't be a epic for researching, deployment, writing documentation, testing or anything that is not writing the actual code. Testing if app works will be done as part of each epic.
|
||||||
Do not leave anything for interpretation, e.g. if something can be done in multiple ways, specify which way should be used and be as clear as possible.
|
Do not leave anything for interpretation, e.g. if something can be done in multiple ways, specify which way should be used and be as clear as possible.
|
||||||
|
|
||||||
## Rule #2
|
## Rule #2
|
||||||
This rule applies to epic scope.
|
|
||||||
Each epic must be deliverable that can be verified by non technical user. Each epic must have frontend interface, backend implementation, and a way for non technical user to test epic. Do not use words "backend" and "frontend" in epic descriptions. All details mentioned in project description must be fully implemented once all epics are finished.
|
|
||||||
|
|
||||||
## Rule #3
|
|
||||||
This rule applies to the number of epics you will create.
|
This rule applies to the number of epics you will create.
|
||||||
Every app should have different number of epics depending on complexity. Think epic by epic and create the minimum number of epics that are needed to develop this app.
|
Every app should have different number of epics depending on complexity. Think epic by epic and create the minimum number of epics that are needed to develop this app.
|
||||||
Simple apps should have only 1 epic. More complex apps should have more epics. Do not create more epics than needed.
|
Simple apps should have only 1 epic. More complex apps should have more epics. Do not create more epics than needed.
|
||||||
|
|
||||||
## Rule #4
|
## Rule #3
|
||||||
This rule applies to writing epic 'description'.
|
This rule applies to writing epic 'description'.
|
||||||
Every epic must have a clear, high level, and short 1-2 sentence 'description'. It must be very clear so that even non technical users who are reviewing it and just moved to this project can understand what is goal for the epic.
|
Every epic must have a clear, high level, and short 1-2 sentence 'description'. It must be very clear so that even non technical users who are reviewing it and just moved to this project can understand what is goal for the epic.
|
||||||
|
|
||||||
## Rule #5
|
** MOST IMPORTANT RULES **
|
||||||
This rule applies to order of epics.
|
## Rule #4 (MOST IMPORTANT RULE)
|
||||||
Epics will be executed in same order that you output them. You must order them in a logical way so that epics that depend on other functionalities are implemented in later stage.
|
This rule applies to thinking about the API endpoints specified above between START_OF_FRONTEND_API_FILES and END_OF_FRONTEND_API_FILES.
|
||||||
|
Each epic must be related to one or more API endpoints that are called from the frontend files. Go through all API endpoints called from the frontend - if there are multiple endpoints related to a single entity (for example, CRUD operations on a database model), you can put them in the same epic but otherwise, make sure that API endpoints for different entities are in different epics. The epics you create **MUST** cover **ALL** API endpoints mentioned in the frontend files above.
|
||||||
|
|
||||||
---end_of_rules_for_creating_epics---
|
## Rule #5 (MOST IMPORTANT RULE)
|
||||||
|
This rule applies to order of epics.
|
||||||
|
Epics will be executed in same order that you output them. You must order them in a logical way so that epics that depend on other functionalities are implemented in later stage. The general order should be as follows:
|
||||||
|
{% if task_type == 'app' %}
|
||||||
|
- The first epic **MUST** be to implement authentication. In the first epic, the first task **MUST** be to remove the mocked data for making a register and login API requests from the frontend (in the client/src/api/auth.js file) - do **NOT** split this task into multiple tasks. Next task, if needed, should be to update the User model with more required parameters like different roles, different user data, etc. In the second task, make sure to add all necessary fields to the User model in the backend.
|
||||||
|
{% endif %}
|
||||||
|
- The {% if task_type == 'app' %}second{% else %}first{% endif %} epic **MUST** be to create any necessary scripts (if there are any). For example, a script to create an admin user, a script to seed the database, etc.
|
||||||
|
- {% if task_type == 'app' %}Finally{% else %}Then{% endif %}, all other epics must be about creating database models and CRUD operations (each epic must contain CRUD operations only for one single model - never for multiple). Pay attention to the API requests inside files in `client/api/` folder because they are currently using mocked data and whenever you implement an API endpoint, you just need to replace the mocked data with the real API request to the backend.
|
||||||
|
|
||||||
|
## Rule #6
|
||||||
|
Create epics for things that are not yet implemented. Do not reimplement what's already done. If something is already implemented, do not create epic for it. Continue from the implementation already there.
|
||||||
|
|
||||||
|
~~~END_OF_RULES_FOR_CREATING_EPICS~~~
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
**IMPORTANT**: Pay attention to file paths: if the command or argument is a file or folder from the project, use paths relative to the project root.
|
IMPORTANT: Pay attention to file paths: if the command or argument is a file or folder from the project, use paths relative to the project root.
|
||||||
For example:
|
For example:
|
||||||
- use `dirname/filename.py` instead of `/path/to/project/dirname/filename.py`
|
- use `dirname/filename.py` instead of `/path/to/project/dirname/filename.py`
|
||||||
- use `filename.js` instead of `./filename.js`
|
- use `filename.js` instead of `./filename.js`
|
||||||
|
|||||||
7
core/prompts/pythagora/commit.prompt
Normal file
7
core/prompts/pythagora/commit.prompt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
You are working on an app called "{{ state.branch.project.name }}" and you need to generate commit message for next "git commit" command.
|
||||||
|
|
||||||
|
Here are the changes that were made from last commit:
|
||||||
|
{{ git_diff }}
|
||||||
|
|
||||||
|
Respond ONLY with the commit message that you would use for the next "git commit" command, nothing else. Do not use quotes, backticks or anything else, just plain text.
|
||||||
|
Commit message should be short and descriptive of the changes made since last commit.
|
||||||
@@ -50,7 +50,7 @@ Here's an EXAMPLE initial prompt:
|
|||||||
---start-of-example-output---
|
---start-of-example-output---
|
||||||
Online forum similar to Hacker News (news.ycombinator.com), with a simple and clean interface, where people can post links or text posts, and other people can upvote, downvote and comment on. Reading is open to anonymous users, but users must register to post, upvote, downvote or comment. Use simple username+password authentication. The forum should be implemented in Node.js with Express framework, using MongoDB and Mongoose ORM.
|
Online forum similar to Hacker News (news.ycombinator.com), with a simple and clean interface, where people can post links or text posts, and other people can upvote, downvote and comment on. Reading is open to anonymous users, but users must register to post, upvote, downvote or comment. Use simple username+password authentication. The forum should be implemented in Node.js with Express framework, using MongoDB and Mongoose ORM.
|
||||||
|
|
||||||
The UI should use EJS view engine, Bootstrap for styling and plain vanilla JavaScript. Design should be simple and look like Hacker News, with a top bar for navigation, using a blue color scheme instead of the orange color in HN. The footer in each page should just be "Built using GPT Pilot".
|
The UI should use EJS view engine, Bootstrap for styling and plain vanilla JavaScript. Design should be simple and look like Hacker News, with a top bar for navigation, using a blue color scheme instead of the orange color in HN. The footer in each page should just be "Built using Pythagora".
|
||||||
|
|
||||||
Each story has a title (one-line text), a link (optional, URL to an external article being shared on AI News), and text (text to show in the post). Link and text are mutually exclusive - if the submitter tries to use both, show them an error.
|
Each story has a title (one-line text), a link (optional, URL to an external article being shared on AI News), and text (text to show in the post). Link and text are mutually exclusive - if the submitter tries to use both, show them an error.
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
|
{% if is_feature %}
|
||||||
|
Here is the app description that is fully built already:
|
||||||
|
```
|
||||||
|
{{ state.specification.description }}
|
||||||
|
```
|
||||||
|
Now I will show you the feature description that needs to be added to the app:
|
||||||
|
{% endif %}
|
||||||
```
|
```
|
||||||
{{ prompt }}
|
{{ prompt }}
|
||||||
```
|
```
|
||||||
|
|
||||||
The above is a user prompt for application/software tool they are trying to develop. Determine the complexity of the user's request. Do NOT respond with thoughts, reasoning, explanations or anything similar, return ONLY a string representation of the complexity level. Use the following scale:
|
{% if not is_feature %}The above is a user prompt for application/software tool they are trying to develop. {% endif %}Determine the complexity of the user's request. Do NOT respond with thoughts, reasoning, explanations or anything similar, return ONLY a string representation of the complexity level. Use the following scale:
|
||||||
"hard" for high complexity
|
"hard" for high complexity
|
||||||
"moderate" for moderate complexity
|
"moderate" for moderate complexity
|
||||||
"simple" for low complexity
|
"simple" for low complexity
|
||||||
|
|||||||
@@ -1 +1,19 @@
|
|||||||
Ok, great. Now, you need to take the epic #{{ epic_number }} "{{ epic_description }}" and break it down into smaller tasks. Each task is one testable whole that the user can test and commit. Each task will be one commit that has to be testable by a human. Return the list of tasks for the Epic #{{ epic_number }}. For each task, write the the task description and a description of how a human should test if the task is successfully implemented or not. Keep in mind that there can be 1 task or multiple, depending on the complexity of the epic. The epics will be implemented one by one so make sure that the user needs to be able to test each task you write - for example, if something will be implemented in the epics after the epic #{{ epic_number }}, then you cannot write it here because the user won't be able to test it.
|
Ok, great. Now, you need to take the epic #{{ epic_number }} ("{{ epic_description }}") and break it down into smaller tasks. Each task is one testable whole that the user can test and commit. Each task will be one commit that has to be testable by a human. Return the list of tasks for the Epic #{{ epic_number }}. For each task, write the task description and a description of how a human should test if the task is successfully implemented or not. Keep in mind that there can be 1 task or multiple, depending on the complexity of the epic. The epics will be implemented one by one so make sure that the user needs to be able to test each task you write - for example, if something will be implemented in the epics after the epic #{{ epic_number }}, then you cannot write it here because the user won't be able to test it.
|
||||||
|
|
||||||
|
You need to specify tasks so that all these API endpoints get implemented completely. For each API endpoint that needs to be implemented, make sure to create a separate task so each task has only one API endpoint to implement. Also, you must not create tasks that don't have an endpoint that they are related to - for example, sometimes there is no "update" endpoint for a specific entity so you don't need to create a task for that.
|
||||||
|
|
||||||
|
You can think of tasks as a unit of functionality that needs to have a frontend component and a backend component (don't split backend and frontend of the same functionality in separate tasks).
|
||||||
|
|
||||||
|
**IMPORTANT: components of a single task**
|
||||||
|
When thinking about the scope of a single task, here are the components that need to be put into the same task:
|
||||||
|
1. The implementation of the backend API endpoint together with the frontend API request implementation (removing the mocked data and replacing it with the real API request)
|
||||||
|
2. The implementation of the database model
|
||||||
|
3. The utility function (eg. 3rd party integration) that is needed for this endpoint.
|
||||||
|
|
||||||
|
**IMPORTANT: order of tasks**
|
||||||
|
The tasks you create **MUST** be in the order that they should be implemented. When CRUD operations need to be implemented, first implement the Create operation, then Read, Update, and Delete.
|
||||||
|
|
||||||
|
{% if state.has_frontend() and not state.is_feature() and (state.options|default({})).get('auth', True) %}
|
||||||
|
**IMPORTANT**
|
||||||
|
If you are working on the Epic #1 that needs to implement the authentication system. The first task **MUST** be to remove the mocked data for authentication (register and login). After that, add any custom authentication requirements like different roles, different user data, etc.
|
||||||
|
{% endif %}
|
||||||
|
|||||||
2
core/prompts/tech-lead/filter_files.prompt
Normal file
2
core/prompts/tech-lead/filter_files.prompt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
{# This is the same template as for Developer's filter files #}
|
||||||
|
{% extends "developer/filter_files.prompt" %}
|
||||||
2
core/prompts/tech-lead/filter_files_loop.prompt
Normal file
2
core/prompts/tech-lead/filter_files_loop.prompt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
{# This is the same template as for Developer's filter files #}
|
||||||
|
{% extends "developer/filter_files_loop.prompt" %}
|
||||||
@@ -21,8 +21,13 @@ Finally, here is the description of new feature that needs to be added to the ap
|
|||||||
{% if epic.complexity and epic.complexity == 'simple' %}
|
{% if epic.complexity and epic.complexity == 'simple' %}
|
||||||
This is very low complexity {{ task_type }} and because of that, you have to create ONLY one task that is sufficient to fully implement it.
|
This is very low complexity {{ task_type }} and because of that, you have to create ONLY one task that is sufficient to fully implement it.
|
||||||
{% else %}
|
{% else %}
|
||||||
Before we go into the coding part, your job is to split the development process of creating this app into smaller epics.
|
Before we go into the coding part, your job is to split the development process of building the backend for this app into epics. Above, you can see a part of the backend that's already built and the files from the frontend that make requests to the backend. The rest of the frontend is built but is not shown above because it is not necessary for you to create a list of epics.
|
||||||
Now, based on the project details provided{% if task_type == 'feature' %} and new feature description{% endif %}, think epic by epic and create the entire development plan{% if task_type == 'feature' %} for new feature{% elif task_type == 'app' %}. {% if state.files %}Continue from the existing code listed above{% else %}Start from the project setup{% endif %} and specify each epic until the moment when the entire app should be fully working{% if state.files %}. You should not reimplement what's already done - just continue from the implementation already there{% endif %}{% endif %} while strictly following these rules:
|
Now, based on the project details provided{% if task_type == 'feature' %} and new feature description{% endif %}, think epic by epic and create the entire development plan{% if task_type == 'feature' %} for new feature{% elif task_type == 'app' %}. {% if state.files %}Continue from the existing code listed above{% else %}Start from the project setup{% endif %} and specify each epic until the moment when the entire app should be fully working{% if state.files %}. IMPORTANT: You should not reimplement what's already done - just continue from the implementation already there.{% endif %}{% endif %}
|
||||||
|
|
||||||
|
IMPORTANT!
|
||||||
|
Frontend is already built and you don't need to create epics for it. You only need to create epics for backend implementation and connect it to existing frontend. Keep in mind that some backend functionality is already implemented.{% if task_type == 'app' and (state.options|default({})).get('auth', True) %} The first epic **MUST** be to implement the authentication system.{% endif %}
|
||||||
|
|
||||||
|
Strictly follow these rules:
|
||||||
|
|
||||||
{% include "partials/project_tasks.prompt" %}
|
{% include "partials/project_tasks.prompt" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
You are working on an app called "{{ state.branch.project.name }}".
|
|
||||||
|
|
||||||
{% include "partials/project_details.prompt" %}
|
|
||||||
|
|
||||||
{# This is actually updating of tasks and not epics. Reason why this prompt uses word "epic" instead of "task" is that LLM gives very detailed description and creates very big plan if we ask him to create tasks. When asked to create epics he focuses on much bigger picture and gives high level description which is what we want. #}
|
|
||||||
Development plan for that {{ task_type }} was created and the {{ task_type }} was then broken down to smaller epics so that it's easier for development.
|
|
||||||
|
|
||||||
Here are epics that are finished so far:
|
|
||||||
```
|
|
||||||
{% for task in finished_tasks %}
|
|
||||||
- Epic #{{ loop.index }}
|
|
||||||
Description: {{ task.description }}
|
|
||||||
|
|
||||||
{% endfor %}
|
|
||||||
```
|
|
||||||
|
|
||||||
Here are epics that still have to be implemented:
|
|
||||||
```
|
|
||||||
{% for task in state.unfinished_tasks %}
|
|
||||||
- Epic #{{ finished_tasks|length + loop.index }}
|
|
||||||
Description: {{ task.description }}
|
|
||||||
|
|
||||||
{% endfor %}
|
|
||||||
```
|
|
||||||
|
|
||||||
{% if finished_tasks %}
|
|
||||||
This is the last epic you were working on:
|
|
||||||
```
|
|
||||||
{{ finished_tasks[-1].description }}
|
|
||||||
```
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
While working on that last epic, you were iterating based on user feedbacks for this {{ task_type }}. Here is list of all iterations:
|
|
||||||
```
|
|
||||||
{% for iteration in state.iterations %}
|
|
||||||
- Iteration #{{ loop.index }}:
|
|
||||||
|
|
||||||
User feedback: {{ iteration.user_feedback }}
|
|
||||||
Developer solution: {{ iteration.description }}
|
|
||||||
{% endfor %}
|
|
||||||
```
|
|
||||||
|
|
||||||
{% if modified_files|length > 0 %}
|
|
||||||
Here are files that were modified during this epic implementation:
|
|
||||||
---start_of_current_files---
|
|
||||||
{% for file in modified_files %}
|
|
||||||
**{{ file.path }}** ({{ file.content.content.splitlines()|length }} lines of code):
|
|
||||||
```
|
|
||||||
{{ file.content.content }}
|
|
||||||
```
|
|
||||||
{% endfor %}
|
|
||||||
---end_of_current_files---
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% include "partials/project_tasks.prompt" %}
|
|
||||||
|
|
||||||
You need to think step by step what was done in last epic and update development plan if needed. All iterations that were mentioned were executed and finished successfully and that needs to be reflected in updated development plan.
|
|
||||||
As output you have to give 2 things:
|
|
||||||
1. Reword/update current epic, "updated_current_epic", ONLY IF NECESSARY, based on what is implemented so far. Consider current epic description, all iterations that were implemented during this epic and all changes that were made to the code.
|
|
||||||
|
|
||||||
2. Give me updated list of epics that still have to be implemented. Take into consideration all epics in current development plan, previous epics that were finished and everything that was implemented in this epic. There should be minimum possible number of epics that still have to be executed to finish the app. You must list only epics that need implementation and were not done in scope of previous epics or during iterations on current epic. Do not create new epics, only remove epics from list of epics that still have to be implemented in case they were implemented during current epic.
|
|
||||||
@@ -1,15 +1,3 @@
|
|||||||
{% if route_files %}
|
|
||||||
Here is a list of files the contain route definitions, and their contents. If any of the steps in testing instructions use URLs, use the routes defined in these files.
|
|
||||||
|
|
||||||
---START_OF_FILES---
|
|
||||||
{% for file in route_files %}
|
|
||||||
File **`{{ file.path }}`** ({{file.content.content.splitlines()|length}} lines of code):
|
|
||||||
```
|
|
||||||
{{ file.content.content }}```
|
|
||||||
|
|
||||||
{% endfor %}
|
|
||||||
---END_OF_FILES---
|
|
||||||
{% endif %}
|
|
||||||
How can a human user test if this task was completed successfully?
|
How can a human user test if this task was completed successfully?
|
||||||
|
|
||||||
Please list actions, step by step, in order, that the user should take to verify the task. After each action, describe what the expected response is.
|
Please list actions, step by step, in order, that the user should take to verify the task. After each action, describe what the expected response is.
|
||||||
@@ -26,22 +14,63 @@ Follow these important rules when compiling a list of actions the user will take
|
|||||||
6. Assume system services, such as the database, are already set up and running. Don't ask user to install or run any software other than the app they're testing.
|
6. Assume system services, such as the database, are already set up and running. Don't ask user to install or run any software other than the app they're testing.
|
||||||
7. Don't ask the user to test things which aren't implemented yet (eg. opening a theoretical web page that doesn't exist yet, or clicking on a button that isn't implemented yet)
|
7. Don't ask the user to test things which aren't implemented yet (eg. opening a theoretical web page that doesn't exist yet, or clicking on a button that isn't implemented yet)
|
||||||
8. Think about if there is something that user needs to do manually to make the next testing step work
|
8. Think about if there is something that user needs to do manually to make the next testing step work
|
||||||
|
9. The human has the option to press the "Start App" button so don't instruct them to start the app in any way.
|
||||||
|
10. If the user needs to run a database command, make sure to specify the entire command that needs to be run
|
||||||
|
|
||||||
Remember, these rules are very important and you must follow them!
|
Remember, these rules are very important and you must follow them!
|
||||||
|
|
||||||
Here is an example output with a few user steps:
|
Here is an example output with a few user steps:
|
||||||
---example---
|
---example---
|
||||||
### Step 1
|
{
|
||||||
Action: Start the server using `npm start`
|
"steps": [
|
||||||
Expected result: You should see the message "Connected to database" or similar
|
{
|
||||||
|
"title": "Submit the form",
|
||||||
### Step 2
|
"action": "Open your web browser and visit 'http://localhost:5173/form'. Click on the 'Submit' button in the web form",
|
||||||
Action: Open your web browser and visit http://localhost:3000/
|
"result": "Form is submitted, the page is reloaded, and 'Thank you' message is shown",
|
||||||
Expected result: Web page opens and you see a "Hello World" message with a contact form
|
},
|
||||||
|
{
|
||||||
### Step 3
|
"title": "Check email",
|
||||||
Action: Click on the "Submit" button in the web form
|
"action": "Check your email inbox for the magic link. Click on the magic link to log in.",
|
||||||
Expected result: Form is submitted, page is reloaded and "Thank you" message is shown
|
"result": "You should be redirected back to the home page. The login status should now display 'Logged in as [your email]' and the 'Logout' button should be visible.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Log out",
|
||||||
|
"action": "Click on the 'Logout' button",
|
||||||
|
"result": "You should be redirected back to the home page. You should not be able to access the form and the 'Login' button should be visible.",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
---end_of_example---
|
---end_of_example---
|
||||||
|
|
||||||
If nothing needs to be tested for this task, instead of outputting the steps, just output a single word: DONE
|
If nothing needs to be tested for this task, instead of outputting the steps, just output an empty list like this:
|
||||||
|
---example_when_test_not_needed---
|
||||||
|
{
|
||||||
|
"steps": []
|
||||||
|
}
|
||||||
|
---end_of_example_when_test_not_needed---
|
||||||
|
|
||||||
|
When you think about the testing instructions for the human, keep in mind the tasks that have been already implemented, the task that the human needs to test right now, and the tasks that are still not implemented. If something is not implemented yet, the user will not be able to test a functionality related to that. For example, if a task is to implement a feature that enables the user to create a company record and if you see that the feature to retrieve a list of company records is still not implemented, you cannot tell the human to open the page for viewing company records because it's still not implemented. In this example, you should tell the human to look into a database or some other way that they can verify if the company records are created. The current situation is like this:
|
||||||
|
Here are the tasks that are implemented:
|
||||||
|
```
|
||||||
|
{% for task in state.tasks %}
|
||||||
|
{% if loop.index - 1 < current_task_index %}
|
||||||
|
{{ loop.index }}. {{ task.description }}{% if task.get("status") == "done" %} (completed){% endif %}
|
||||||
|
|
||||||
|
{% endif %}{% endfor %}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here is the task that the human needs to test:
|
||||||
|
```
|
||||||
|
{{ current_task_index + 1 }}. {{ task.description }}
|
||||||
|
```
|
||||||
|
|
||||||
|
And here are the tasks that are still NOT implemented:
|
||||||
|
```
|
||||||
|
{% for task in state.tasks %}
|
||||||
|
{% if loop.index - 1 > current_task_index %}
|
||||||
|
{{ loop.index }}. {{ task.description }}{% if task.get("status") == "done" %} (completed){% endif %}
|
||||||
|
|
||||||
|
{% endif %}{% endfor %}
|
||||||
|
```
|
||||||
|
|
||||||
|
Knowing these rules, tell me, please list actions, step by step, in order, that the user should take to verify the task.
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
{# This is the same template as for Developer's filter files because Troubleshooter is reusing it in a conversation #}
|
{# This is the same template as for Developer's filter files #}
|
||||||
{% extends "developer/filter_files.prompt" %}
|
{% extends "developer/filter_files.prompt" %}
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
{# This is the same template as for Developer's filter files because Troubleshooter is reusing it in a conversation #}
|
{# This is the same template as for Developer's filter files #}
|
||||||
{% extends "developer/filter_files_loop.prompt" %}
|
{% extends "developer/filter_files_loop.prompt" %}
|
||||||
@@ -21,14 +21,17 @@ A part of the app is already finished.
|
|||||||
|
|
||||||
{% include "partials/user_feedback.prompt" %}
|
{% include "partials/user_feedback.prompt" %}
|
||||||
|
|
||||||
{% if state.current_task.test_instructions is defined %}
|
{% if test_instructions %}
|
||||||
User was testing the current implementation of the app when they requested some changes to the app. These are the testing instructions:
|
User was testing the current implementation of the app when they requested some changes to the app. These are the testing instructions:
|
||||||
```
|
```
|
||||||
{{ state.current_task.test_instructions }}
|
{% for step in test_instructions %}
|
||||||
|
Step #{{ loop.index }}
|
||||||
|
Action: {{ step.action }}
|
||||||
|
Expected result: {{ step.result }}
|
||||||
|
{% endfor %}
|
||||||
```
|
```
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
{% if next_solution_to_try is not none %}
|
{% if next_solution_to_try is not none %}
|
||||||
Focus on solving this issue in the following way:
|
Focus on solving this issue in the following way:
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ from contextlib import asynccontextmanager
|
|||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING, Optional
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
|
from sqlalchemy import inspect, select
|
||||||
from tenacity import retry, stop_after_attempt, wait_fixed
|
from tenacity import retry, stop_after_attempt, wait_fixed
|
||||||
|
|
||||||
from core.config import FileSystemType, get_config
|
from core.config import FileSystemType, get_config
|
||||||
from core.db.models import Branch, ExecLog, File, FileContent, LLMRequest, Project, ProjectState, UserInput
|
from core.db.models import Branch, ExecLog, File, FileContent, LLMRequest, Project, ProjectState, UserInput
|
||||||
from core.db.models.specification import Specification
|
from core.db.models.specification import Complexity, Specification
|
||||||
from core.db.session import SessionManager
|
from core.db.session import SessionManager
|
||||||
from core.disk.ignore import IgnoreMatcher
|
from core.disk.ignore import IgnoreMatcher
|
||||||
from core.disk.vfs import LocalDiskVFS, MemoryVFS, VirtualFileSystem
|
from core.disk.vfs import LocalDiskVFS, MemoryVFS, VirtualFileSystem
|
||||||
@@ -49,6 +50,9 @@ class StateManager:
|
|||||||
self.next_state = None
|
self.next_state = None
|
||||||
self.current_session = None
|
self.current_session = None
|
||||||
self.blockDb = False
|
self.blockDb = False
|
||||||
|
self.git_available = False
|
||||||
|
self.git_used = False
|
||||||
|
self.options = {}
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def db_blocker(self):
|
async def db_blocker(self):
|
||||||
@@ -136,7 +140,7 @@ class StateManager:
|
|||||||
|
|
||||||
The returned ProjectState will have branch and branch.project
|
The returned ProjectState will have branch and branch.project
|
||||||
relationships preloaded. All other relationships must be
|
relationships preloaded. All other relationships must be
|
||||||
excplicitly loaded using ProjectState.awaitable_attrs or
|
explicitly loaded using ProjectState.awaitable_attrs or
|
||||||
AsyncSession.refresh.
|
AsyncSession.refresh.
|
||||||
|
|
||||||
:param project_id: Project ID (keyword-only, optional).
|
:param project_id: Project ID (keyword-only, optional).
|
||||||
@@ -211,11 +215,26 @@ class StateManager:
|
|||||||
self.current_state.tasks,
|
self.current_state.tasks,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
telemetry.set(
|
||||||
|
"architecture",
|
||||||
|
{
|
||||||
|
"system_dependencies": self.current_state.specification.system_dependencies,
|
||||||
|
"package_dependencies": self.current_state.specification.package_dependencies,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
telemetry.set("example_project", self.current_state.specification.example_project)
|
||||||
|
telemetry.set("is_complex_app", self.current_state.specification.complexity != Complexity.SIMPLE)
|
||||||
|
telemetry.set("templates", self.current_state.specification.templates)
|
||||||
|
|
||||||
return self.current_state
|
return self.current_state
|
||||||
|
|
||||||
@retry(stop=stop_after_attempt(3), wait=wait_fixed(1))
|
@retry(stop=stop_after_attempt(3), wait=wait_fixed(1))
|
||||||
async def commit_with_retry(self):
|
async def commit_with_retry(self):
|
||||||
await self.current_session.commit()
|
try:
|
||||||
|
await self.current_session.commit()
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Commit failed: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
async def commit(self) -> ProjectState:
|
async def commit(self) -> ProjectState:
|
||||||
"""
|
"""
|
||||||
@@ -348,6 +367,8 @@ class StateManager:
|
|||||||
telemetry.inc("num_tasks")
|
telemetry.inc("num_tasks")
|
||||||
if not self.next_state.unfinished_tasks:
|
if not self.next_state.unfinished_tasks:
|
||||||
if len(self.current_state.epics) == 1:
|
if len(self.current_state.epics) == 1:
|
||||||
|
telemetry.set("end_result", "success:frontend")
|
||||||
|
elif len(self.current_state.epics) == 2:
|
||||||
telemetry.set("end_result", "success:initial-project")
|
telemetry.set("end_result", "success:initial-project")
|
||||||
else:
|
else:
|
||||||
telemetry.set("end_result", "success:feature")
|
telemetry.set("end_result", "success:feature")
|
||||||
@@ -393,8 +414,8 @@ class StateManager:
|
|||||||
file_content = await FileContent.store(self.current_session, hash, content)
|
file_content = await FileContent.store(self.current_session, hash, content)
|
||||||
|
|
||||||
file = self.next_state.save_file(path, file_content)
|
file = self.next_state.save_file(path, file_content)
|
||||||
if self.ui and not from_template:
|
# if self.ui and not from_template:
|
||||||
await self.ui.open_editor(self.file_system.get_full_path(path))
|
# await self.ui.open_editor(self.file_system.get_full_path(path))
|
||||||
if metadata:
|
if metadata:
|
||||||
file.meta = metadata
|
file.meta = metadata
|
||||||
|
|
||||||
@@ -586,15 +607,147 @@ class StateManager:
|
|||||||
"""
|
"""
|
||||||
return not bool(self.file_system.list())
|
return not bool(self.file_system.list())
|
||||||
|
|
||||||
|
def get_implemented_pages(self) -> list[str]:
|
||||||
|
"""
|
||||||
|
Get the list of implemented pages.
|
||||||
|
|
||||||
|
:return: List of implemented pages.
|
||||||
|
"""
|
||||||
|
# TODO - use self.current_state plus response from the FE iteration
|
||||||
|
page_files = [file.path for file in self.next_state.files if "client/src/pages" in file.path]
|
||||||
|
return page_files
|
||||||
|
|
||||||
|
async def update_implemented_pages_and_apis(self):
|
||||||
|
modified = False
|
||||||
|
pages = self.get_implemented_pages()
|
||||||
|
apis = await self.get_apis()
|
||||||
|
|
||||||
|
# Get the current state of pages and apis from knowledge_base
|
||||||
|
current_pages = self.next_state.knowledge_base.get("pages", None)
|
||||||
|
current_apis = self.next_state.knowledge_base.get("apis", None)
|
||||||
|
|
||||||
|
# Check if pages or apis have changed
|
||||||
|
if pages != current_pages or apis != current_apis:
|
||||||
|
modified = True
|
||||||
|
|
||||||
|
if modified:
|
||||||
|
self.next_state.knowledge_base["pages"] = pages
|
||||||
|
self.next_state.knowledge_base["apis"] = apis
|
||||||
|
self.next_state.flag_knowledge_base_as_modified()
|
||||||
|
await self.ui.knowledge_base_update(self.next_state.knowledge_base)
|
||||||
|
|
||||||
|
async def update_utility_functions(self, utility_function: dict):
|
||||||
|
"""
|
||||||
|
Update the knowledge base with the utility function.
|
||||||
|
|
||||||
|
:param utility_function: Utility function to update.
|
||||||
|
"""
|
||||||
|
matched = False
|
||||||
|
for kb_util_func in self.next_state.knowledge_base.get("utility_functions", []):
|
||||||
|
if (
|
||||||
|
utility_function["function_name"] == kb_util_func["function_name"]
|
||||||
|
and utility_function["file"] == kb_util_func["file"]
|
||||||
|
):
|
||||||
|
kb_util_func["return_value"] = utility_function["return_value"]
|
||||||
|
kb_util_func["input_value"] = utility_function["input_value"]
|
||||||
|
kb_util_func["status"] = utility_function["status"]
|
||||||
|
matched = True
|
||||||
|
self.next_state.flag_knowledge_base_as_modified()
|
||||||
|
break
|
||||||
|
|
||||||
|
if not matched:
|
||||||
|
if "utility_functions" not in self.next_state.knowledge_base:
|
||||||
|
self.next_state.knowledge_base["utility_functions"] = []
|
||||||
|
self.next_state.knowledge_base["utility_functions"].append(utility_function)
|
||||||
|
|
||||||
|
self.next_state.flag_knowledge_base_as_modified()
|
||||||
|
await self.ui.knowledge_base_update(self.next_state.knowledge_base)
|
||||||
|
|
||||||
|
async def get_apis(self) -> list[dict]:
|
||||||
|
"""
|
||||||
|
Get the list of APIs.
|
||||||
|
|
||||||
|
:return: List of APIs.
|
||||||
|
"""
|
||||||
|
apis = []
|
||||||
|
for file in self.next_state.files:
|
||||||
|
if "client/src/api" not in file.path:
|
||||||
|
continue
|
||||||
|
session = inspect(file).async_session
|
||||||
|
result = await session.execute(select(FileContent).where(FileContent.id == file.content_id))
|
||||||
|
file_content = result.scalar_one_or_none()
|
||||||
|
content = file_content.content
|
||||||
|
lines = content.splitlines()
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if "// Description:" in line:
|
||||||
|
# TODO: Make this better!!!
|
||||||
|
description = line.split("Description:")[1]
|
||||||
|
endpoint = lines[i + 1].split("Endpoint:")[1] if len(lines[i + 1].split("Endpoint:")) > 1 else ""
|
||||||
|
request = lines[i + 2].split("Request:")[1] if len(lines[i + 2].split("Request:")) > 1 else ""
|
||||||
|
response = lines[i + 3].split("Response:")[1] if len(lines[i + 3].split("Response:")) > 1 else ""
|
||||||
|
backend = (
|
||||||
|
next(
|
||||||
|
(
|
||||||
|
api
|
||||||
|
for api in self.current_state.knowledge_base.get("apis", [])
|
||||||
|
if api["endpoint"] == endpoint.strip()
|
||||||
|
),
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
.get("locations", {})
|
||||||
|
.get("backend", None)
|
||||||
|
)
|
||||||
|
apis.append(
|
||||||
|
{
|
||||||
|
"description": description.strip(),
|
||||||
|
"endpoint": endpoint.strip(),
|
||||||
|
"request": request.strip(),
|
||||||
|
"response": response.strip(),
|
||||||
|
"locations": {
|
||||||
|
"frontend": {
|
||||||
|
"path": file.path,
|
||||||
|
"line": i - 1,
|
||||||
|
},
|
||||||
|
"backend": backend,
|
||||||
|
},
|
||||||
|
"status": "implemented" if backend is not None else "mocked",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return apis
|
||||||
|
|
||||||
|
async def update_apis(self, files_with_implemented_apis: list[dict] = []):
|
||||||
|
"""
|
||||||
|
Update the list of APIs.
|
||||||
|
|
||||||
|
"""
|
||||||
|
apis = await self.get_apis()
|
||||||
|
for file in files_with_implemented_apis:
|
||||||
|
for endpoint in file["related_api_endpoints"]:
|
||||||
|
api = next((api for api in apis if (endpoint in api["endpoint"])), None)
|
||||||
|
if api is not None:
|
||||||
|
api["status"] = "implemented"
|
||||||
|
api["locations"]["backend"] = {
|
||||||
|
"path": file["path"],
|
||||||
|
"line": file["line"],
|
||||||
|
}
|
||||||
|
self.next_state.knowledge_base["apis"] = apis
|
||||||
|
self.next_state.flag_knowledge_base_as_modified()
|
||||||
|
await self.ui.knowledge_base_update(self.next_state.knowledge_base)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_input_required(content: str) -> list[int]:
|
def get_input_required(content: str, file_path: str) -> list[int]:
|
||||||
"""
|
"""
|
||||||
Get the list of lines containing INPUT_REQUIRED keyword.
|
Get the list of lines containing INPUT_REQUIRED keyword.
|
||||||
|
|
||||||
:param content: The file content to search.
|
:param content: The file content to search.
|
||||||
|
:param file_path: The file path.
|
||||||
:return: Indices of lines with INPUT_REQUIRED keyword, starting from 1.
|
:return: Indices of lines with INPUT_REQUIRED keyword, starting from 1.
|
||||||
"""
|
"""
|
||||||
lines = []
|
lines = []
|
||||||
|
|
||||||
|
if ".env" not in file_path:
|
||||||
|
return lines
|
||||||
|
|
||||||
for i, line in enumerate(content.splitlines(), start=1):
|
for i, line in enumerate(content.splitlines(), start=1):
|
||||||
if "INPUT_REQUIRED" in line:
|
if "INPUT_REQUIRED" in line:
|
||||||
lines.append(i)
|
lines.append(i)
|
||||||
|
|||||||
@@ -69,9 +69,9 @@ class Telemetry:
|
|||||||
self.data = {
|
self.data = {
|
||||||
# System platform
|
# System platform
|
||||||
"platform": sys.platform,
|
"platform": sys.platform,
|
||||||
# Python version used for GPT Pilot
|
# Python version used
|
||||||
"python_version": sys.version,
|
"python_version": sys.version,
|
||||||
# GPT Pilot version
|
# Core version
|
||||||
"pilot_version": get_version(),
|
"pilot_version": get_version(),
|
||||||
# Pythagora VSCode Extension version
|
# Pythagora VSCode Extension version
|
||||||
"extension_version": None,
|
"extension_version": None,
|
||||||
@@ -86,8 +86,8 @@ class Telemetry:
|
|||||||
"updated_prompt": None,
|
"updated_prompt": None,
|
||||||
# App complexity
|
# App complexity
|
||||||
"is_complex_app": None,
|
"is_complex_app": None,
|
||||||
# Optional template used for the project
|
# Optional templates used for the project
|
||||||
"template": None,
|
"templates": None,
|
||||||
# Optional, example project selected by the user
|
# Optional, example project selected by the user
|
||||||
"example_project": None,
|
"example_project": None,
|
||||||
# Optional user contact email
|
# Optional user contact email
|
||||||
@@ -136,7 +136,7 @@ class Telemetry:
|
|||||||
"num_tasks": 0,
|
"num_tasks": 0,
|
||||||
# Number of seconds elapsed during development
|
# Number of seconds elapsed during development
|
||||||
"elapsed_time": 0,
|
"elapsed_time": 0,
|
||||||
# Total number of lines created by GPT Pilot
|
# Total number of lines created by Pythagora
|
||||||
"created_lines": 0,
|
"created_lines": 0,
|
||||||
# End result of development:
|
# End result of development:
|
||||||
# - success:initial-project
|
# - success:initial-project
|
||||||
@@ -150,7 +150,7 @@ class Telemetry:
|
|||||||
"is_continuation": False,
|
"is_continuation": False,
|
||||||
# Optional user feedback
|
# Optional user feedback
|
||||||
"user_feedback": None,
|
"user_feedback": None,
|
||||||
# If GPT Pilot crashes, record diagnostics
|
# If Core crashes, record diagnostics
|
||||||
"crash_diagnostics": None,
|
"crash_diagnostics": None,
|
||||||
# Statistics for large requests
|
# Statistics for large requests
|
||||||
"large_requests": None,
|
"large_requests": None,
|
||||||
@@ -319,7 +319,7 @@ class Telemetry:
|
|||||||
"median_time": sorted(self.slow_requests)[n_slow // 2] if n_slow > 0 else None,
|
"median_time": sorted(self.slow_requests)[n_slow // 2] if n_slow > 0 else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
async def send(self, event: str = "pilot-telemetry"):
|
async def send(self, event: str = "pythagora-core-telemetry"):
|
||||||
"""
|
"""
|
||||||
Send telemetry data to the phone-home endpoint.
|
Send telemetry data to the phone-home endpoint.
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ class NoOptions(BaseModel):
|
|||||||
Options class for templates that do not require any options.
|
Options class for templates that do not require any options.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
extra = "allow"
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -99,6 +102,7 @@ class BaseProjectTemplate:
|
|||||||
"random_secret": uuid4().hex,
|
"random_secret": uuid4().hex,
|
||||||
"options": self.options_dict,
|
"options": self.options_dict,
|
||||||
},
|
},
|
||||||
|
self.state_manager.file_system.root,
|
||||||
self.filter,
|
self.filter,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
91
core/templates/info/vite_react/summary.tpl
Normal file
91
core/templates/info/vite_react/summary.tpl
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
IMPORTANT:
|
||||||
|
This app has 2 parts:
|
||||||
|
|
||||||
|
** #1 Frontend **
|
||||||
|
* ReactJS based frontend in `client/` folder using Vite devserver
|
||||||
|
* Integrated shadcn-ui component library with Tailwind CSS framework
|
||||||
|
* Client-side routing using `react-router-dom` with page components defined in `client/src/pages/` and other components in `client/src/components`
|
||||||
|
* It is running on port 5173 and this port should be used for user testing when possible
|
||||||
|
* All requests to the backend need to go to an endpoint that starts with `/api/` (e.g. `/api/companies`)
|
||||||
|
* Implememented pages:
|
||||||
|
* Home - home (index) page (`/`){% if options.auth %}
|
||||||
|
* Login - login page (`/login/`) - on login, stores the auth tokens to `accessToken` and `refreshToken` variables in local storage
|
||||||
|
* Register - register page (`/register/`) - on register, store **ONLY** the `accessToken` variable in local storage{% endif %}
|
||||||
|
|
||||||
|
** #2 Backend **
|
||||||
|
* Express-based server implementing REST API endpoints in `api/`
|
||||||
|
* Has codebase inside "server/" folder
|
||||||
|
* Backend is running on port 3000
|
||||||
|
* MongoDB database support with Mongoose{% if options.auth %}
|
||||||
|
* Token-based authentication (using bearer access and refresh tokens)
|
||||||
|
* User authentication (email + password):
|
||||||
|
* login/register API endpoints in `/server/routes/auth.js`
|
||||||
|
* authorization middleware in `/server/routes/middleware/auth.js`
|
||||||
|
* user management logic in `/server/routes/services/user.js`
|
||||||
|
* User authentication is implemented and doesn't require any additional work{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
Concurrently is used to run both client and server together with a single command (`npm run start`).
|
||||||
|
|
||||||
|
** IMPORTANT - Mocking data on the frontend **
|
||||||
|
All API requests from the frontend to the backend must be defined in files inside the api folder (you must never make requests directly from the components) and the data must be mocked during the frontend implementation. Make sure to always add an API request whenever something needs to be sent or fetched from the backend.
|
||||||
|
When you add mock data, make sure to mock the data in files in the `client/src/api` folder and above each mocked API request, add a structure that is expected from the API with fields `Description`, `Endpoint`, `Request`, and `Response`. You **MUST NOT** add mock data anywhere else in the frontend codebase.
|
||||||
|
Mocking example:
|
||||||
|
|
||||||
|
The base client/src/api/api.ts is already created so here are 2 examples for how to write functions to get data from the backend with the mock data:
|
||||||
|
—EXAMPLE_1 (file `client/src/api/companies.ts`) —
|
||||||
|
import api from './api';
|
||||||
|
|
||||||
|
// Description: Get a list of Companies
|
||||||
|
// Endpoint: GET /api/companies
|
||||||
|
// Request: {}
|
||||||
|
// Response: { companies: Array<{ domain: string, name: string, lastContact: string }> }
|
||||||
|
export const getCompanies = () => {
|
||||||
|
// Mocking the response
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve({
|
||||||
|
companies: [
|
||||||
|
{domain: 'google.com', name: 'Google', lastContact: '2021-08-01'},
|
||||||
|
{domain: 'facebook.com', name: 'Facebook', lastContact: '2021-08-02'},
|
||||||
|
{domain: 'microsoft.com', name: 'Microsoft', lastContact: '2021-08-03'},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
// Uncomment the below lines to make an actual API call
|
||||||
|
// try {
|
||||||
|
// return await api.get('/api/companies', data);
|
||||||
|
// } catch (error) {
|
||||||
|
// throw new Error(error?.response?.data?.error || error.message);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
—END_OF_EXAMPLE_1—
|
||||||
|
|
||||||
|
—EXAMPLE_2 (file `client/src/api/work.ts`) —
|
||||||
|
import api from './api';
|
||||||
|
|
||||||
|
// Description: Add a new Work
|
||||||
|
// Endpoint: POST /api/work
|
||||||
|
// Request: { work: string, driveLink: string }
|
||||||
|
// Response: { success: boolean, message: string }
|
||||||
|
export const addWork = (data: { work: string; driveLink: string }) => {
|
||||||
|
// Mocking the response
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve({success: true, message: 'Work added successfully'});
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
// Uncomment the below lines to make an actual API call
|
||||||
|
// try {
|
||||||
|
// return await api.post('/api/work/add', data);
|
||||||
|
// } catch (error) {
|
||||||
|
// throw new Error(error?.response?.data?.error || error.message);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
—END_OF_EXAMPLE_2—
|
||||||
|
|
||||||
|
Whenever you add an API request from the frontend, make sure to wrap the request in try/catch block and in the catch block, return `throw new Error(error?.response?.data?.message || error.message);` - in the place where the API request function is being called, show a toast message with an error.
|
||||||
|
|
||||||
|
**IMPORTANT**
|
||||||
|
Mongodb is being used as a database so whenever you need to take an `id` of an object on frontend, make sure to take `_id`. For example, if you have a company object, whenever you want to set an id for an element, you should get `company._id` instead of `company.id`.
|
||||||
@@ -13,9 +13,9 @@ class JavascriptReactProjectTemplate(BaseProjectTemplate):
|
|||||||
".gitignore": "Specifies patterns to exclude files and directories from being tracked by Git version control system. It is used to prevent certain files from being committed to the repository.",
|
".gitignore": "Specifies patterns to exclude files and directories from being tracked by Git version control system. It is used to prevent certain files from being committed to the repository.",
|
||||||
"package.json": "Standard Nodejs package metadata file, specifies dependencies and start scripts. It also specifies that the project is a module.",
|
"package.json": "Standard Nodejs package metadata file, specifies dependencies and start scripts. It also specifies that the project is a module.",
|
||||||
"public/.gitkeep": "Empty file",
|
"public/.gitkeep": "Empty file",
|
||||||
"src/App.css": "Contains styling rules for the root element of the application, setting a maximum width, centering it on the page, adding padding, and aligning text to the center.",
|
"src/app.css": "Contains styling rules for the root element of the application, setting a maximum width, centering it on the page, adding padding, and aligning text to the center.",
|
||||||
"src/index.css": "Defines styling rules for the root element, body, and h1 elements of a web page.",
|
"src/index.css": "Defines styling rules for the root element, body, and h1 elements of a web page.",
|
||||||
"src/App.jsx": "Defines a functional component that serves as the root component in the project. The component is exported as the default export. References: src/App.css",
|
"src/App.jsx": "Defines a functional component that serves as the root component in the project. The component is exported as the default export. References: src/app.css",
|
||||||
"src/main.jsx": "Main entry point for a React application. It imports necessary modules, renders the main component 'App' inside a 'React.StrictMode' component, and mounts it to the root element in the HTML document. References: App.jsx, index.css",
|
"src/main.jsx": "Main entry point for a React application. It imports necessary modules, renders the main component 'App' inside a 'React.StrictMode' component, and mounts it to the root element in the HTML document. References: App.jsx, index.css",
|
||||||
"src/assets/.gitkeep": "Empty file",
|
"src/assets/.gitkeep": "Empty file",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ from enum import Enum
|
|||||||
|
|
||||||
from core.log import get_logger
|
from core.log import get_logger
|
||||||
|
|
||||||
from .javascript_react import JavascriptReactProjectTemplate
|
# from .javascript_react import JavascriptReactProjectTemplate
|
||||||
from .node_express_mongoose import NodeExpressMongooseProjectTemplate
|
# from .node_express_mongoose import NodeExpressMongooseProjectTemplate
|
||||||
|
from .vite_react import ViteReactProjectTemplate
|
||||||
|
|
||||||
# from .react_express import ReactExpressProjectTemplate
|
# from .react_express import ReactExpressProjectTemplate
|
||||||
|
|
||||||
@@ -13,13 +14,15 @@ log = get_logger(__name__)
|
|||||||
class ProjectTemplateEnum(str, Enum):
|
class ProjectTemplateEnum(str, Enum):
|
||||||
"""Choices of available project templates."""
|
"""Choices of available project templates."""
|
||||||
|
|
||||||
JAVASCRIPT_REACT = JavascriptReactProjectTemplate.name
|
# JAVASCRIPT_REACT = JavascriptReactProjectTemplate.name
|
||||||
NODE_EXPRESS_MONGOOSE = NodeExpressMongooseProjectTemplate.name
|
# NODE_EXPRESS_MONGOOSE = NodeExpressMongooseProjectTemplate.name
|
||||||
|
VITE_REACT = ViteReactProjectTemplate.name
|
||||||
# REACT_EXPRESS = ReactExpressProjectTemplate.name
|
# REACT_EXPRESS = ReactExpressProjectTemplate.name
|
||||||
|
|
||||||
|
|
||||||
PROJECT_TEMPLATES = {
|
PROJECT_TEMPLATES = {
|
||||||
JavascriptReactProjectTemplate.name: JavascriptReactProjectTemplate,
|
# JavascriptReactProjectTemplate.name: JavascriptReactProjectTemplate,
|
||||||
NodeExpressMongooseProjectTemplate.name: NodeExpressMongooseProjectTemplate,
|
# NodeExpressMongooseProjectTemplate.name: NodeExpressMongooseProjectTemplate,
|
||||||
|
ViteReactProjectTemplate.name: ViteReactProjectTemplate,
|
||||||
# ReactExpressProjectTemplate.name: ReactExpressProjectTemplate,
|
# ReactExpressProjectTemplate.name: ReactExpressProjectTemplate,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
from os import walk
|
from os import walk
|
||||||
from os.path import join, relpath
|
from os.path import join, relpath
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -67,12 +68,13 @@ class Renderer:
|
|||||||
tpl_object = self.jinja_env.get_template(template)
|
tpl_object = self.jinja_env.get_template(template)
|
||||||
return tpl_object.render(context)
|
return tpl_object.render(context)
|
||||||
|
|
||||||
def render_tree(self, root: str, context: Any, filter: Callable = None) -> dict[str, str]:
|
def render_tree(self, root: str, context: Any, full_root_dir: str, filter: Callable = None) -> dict[str, str]:
|
||||||
"""
|
"""
|
||||||
Render a tree folder structure of templates using provided context
|
Render a tree folder structure of templates using provided context
|
||||||
|
|
||||||
:param root: Root of the tree (relative to `template_dir`).
|
:param root: Root of the tree (relative to `template_dir`).
|
||||||
:param context: Context to render the templates with.
|
:param context: Context to render the templates with.
|
||||||
|
:param full_root_dir: Full path to the root of the tree.
|
||||||
:param filter: If defined, will be called for each file to check if it
|
:param filter: If defined, will be called for each file to check if it
|
||||||
needs to be processed and determine output file path.
|
needs to be processed and determine output file path.
|
||||||
:return: A flat dictionary with path => content structure.
|
:return: A flat dictionary with path => content structure.
|
||||||
@@ -103,15 +105,31 @@ class Renderer:
|
|||||||
for path, subdirs, files in walk(full_root):
|
for path, subdirs, files in walk(full_root):
|
||||||
for file in files:
|
for file in files:
|
||||||
file_path = join(path, file) # actual full path of the template file
|
file_path = join(path, file) # actual full path of the template file
|
||||||
tpl_location = relpath(file_path, self.template_dir) # template relative to template_dir
|
|
||||||
output_location = Path(file_path).relative_to(full_root).as_posix() # template relative to tree root
|
output_location = Path(file_path).relative_to(full_root).as_posix() # template relative to tree root
|
||||||
|
|
||||||
|
# Skip .DS_Store files
|
||||||
|
if file == ".DS_Store":
|
||||||
|
continue
|
||||||
|
elif file.endswith(
|
||||||
|
(".png", ".jpg", ".jpeg", ".gif", ".ico", ".svg", ".woff", ".woff2", ".ttf", ".eot")
|
||||||
|
):
|
||||||
|
with open(file_path, "rb") as f:
|
||||||
|
content = f.read()
|
||||||
|
final_path = join(full_root_dir, output_location)
|
||||||
|
os.makedirs(os.path.dirname(final_path), exist_ok=True)
|
||||||
|
with open(final_path, "wb") as out:
|
||||||
|
out.write(content)
|
||||||
|
continue
|
||||||
|
|
||||||
|
tpl_location = relpath(file_path, self.template_dir) # template relative to template_dir
|
||||||
|
|
||||||
if filter:
|
if filter:
|
||||||
output_location = filter(output_location)
|
output_location = filter(output_location)
|
||||||
if not output_location:
|
if not output_location:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
contents = self.render_template(tpl_location, context)
|
contents = self.render_template(tpl_location, context)
|
||||||
retval[output_location] = contents
|
if contents != "":
|
||||||
|
retval[output_location] = contents
|
||||||
|
|
||||||
return retval
|
return retval
|
||||||
|
|||||||
56
core/templates/tree/add_raw_tags.py
Normal file
56
core/templates/tree/add_raw_tags.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def add_raw_tags_to_file(file_path):
|
||||||
|
"""Add {% raw %} at the beginning and {% endraw %} at the end of the file, if not already present."""
|
||||||
|
try:
|
||||||
|
# Open the file and read the contents
|
||||||
|
with open(file_path, "r", encoding="utf-8") as file:
|
||||||
|
content = file.read()
|
||||||
|
|
||||||
|
# Check if the tags are already present
|
||||||
|
if content.startswith("{% raw %}") and content.endswith("{% endraw %}\n"):
|
||||||
|
print(f"Skipping file (tags already added): {file_path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Add {% raw %} at the beginning and {% endraw %} at the end
|
||||||
|
modified_content = f"{'{% raw %}'}\n{content}\n{'{% endraw %}'}"
|
||||||
|
|
||||||
|
# Write the modified content back to the file
|
||||||
|
with open(file_path, "w", encoding="utf-8") as file:
|
||||||
|
file.write(modified_content)
|
||||||
|
|
||||||
|
print(f"Processed file: {file_path}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing {file_path}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def process_directory(directory):
|
||||||
|
"""Recursively process all files in the given directory."""
|
||||||
|
for root, dirs, files in os.walk(directory):
|
||||||
|
for file in files:
|
||||||
|
# Construct the full file path
|
||||||
|
file_path = os.path.join(root, file)
|
||||||
|
|
||||||
|
# Process the file
|
||||||
|
add_raw_tags_to_file(file_path)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Check if the directory path argument is provided
|
||||||
|
if len(sys.argv) != 2:
|
||||||
|
print("Usage: python add_raw_tags.py <directory_path>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Get the directory path from the command line argument
|
||||||
|
directory_path = sys.argv[1]
|
||||||
|
|
||||||
|
# Check if the provided directory exists
|
||||||
|
if not os.path.isdir(directory_path):
|
||||||
|
print(f"Error: The directory '{directory_path}' does not exist.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Process the directory
|
||||||
|
process_directory(directory_path)
|
||||||
@@ -1,98 +1,94 @@
|
|||||||
import React, { useState } from 'react';
|
import { useState } from "react"
|
||||||
import axios from 'axios';
|
import { useForm } from "react-hook-form"
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from "react-router-dom"
|
||||||
import { AlertDestructive } from "@/components/ui/alert";
|
import { Button } from "@/components/ui/button"
|
||||||
import { Button } from "@/components/ui/button";
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card"
|
||||||
import { Input } from "@/components/ui/input";
|
import { useToast } from "@/hooks/useToast"
|
||||||
import { Label } from "@/components/ui/label";
|
import { LogIn } from "lucide-react"
|
||||||
|
import { useAuth } from "@/contexts/AuthContext"
|
||||||
|
|
||||||
export default function Login() {
|
type LoginForm = {
|
||||||
const [email, setEmail] = useState('');
|
email: string
|
||||||
const [password, setPassword] = useState('');
|
password: string
|
||||||
const [loading, setLoading] = useState(false);
|
}
|
||||||
const [error, setError] = useState('');
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
export function Login() {
|
||||||
e.preventDefault();
|
const [loading, setLoading] = useState(false)
|
||||||
setLoading(true);
|
const { toast } = useToast()
|
||||||
setLoading('');
|
const navigate = useNavigate()
|
||||||
|
const { login } = useAuth()
|
||||||
|
const { register, handleSubmit } = useForm<LoginForm>()
|
||||||
|
|
||||||
|
const onSubmit = async (data: LoginForm) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/api/auth/login', { email, password });
|
setLoading(true)
|
||||||
localStorage.setItem('token', response.data.token); // Save token to local storage
|
await login(data.email, data.password)
|
||||||
navigate('/'); // Redirect to Home
|
toast({
|
||||||
} catch (error) {
|
title: "Success",
|
||||||
console.error('Login error:', error);
|
description: "Logged in successfully",
|
||||||
setError(error.response?.data?.error || 'An unexpected error occurred');
|
})
|
||||||
|
navigate("/")
|
||||||
|
} catch (error: any) {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Error",
|
||||||
|
description: error.message || "An error occurred",
|
||||||
|
})
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="loginPage" className="w-full h-screen flex items-center justify-center px-4">
|
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-background to-secondary p-4">
|
||||||
<Card className="mx-auto max-w-sm">
|
<Card className="w-full max-w-md">
|
||||||
{error && <AlertDestructive title="Login Error" description={error} />}
|
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-2xl">Login</CardTitle>
|
<CardTitle>Welcome back</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>Enter your credentials to continue</CardDescription>
|
||||||
Enter your email below to login to your account
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<form onSubmit={handleSubmit} className="grid gap-4">
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||||
<div className="grid gap-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="email">Email</Label>
|
<Label htmlFor="email">Email</Label>
|
||||||
<Input
|
<Input
|
||||||
id="email"
|
id="email"
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="m@example.com"
|
placeholder="Enter your email"
|
||||||
value={email}
|
autoComplete="email"
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
{...register("email", { required: true })}
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center">
|
<Label htmlFor="password">Password</Label>
|
||||||
<Label htmlFor="password">Password</Label>
|
|
||||||
{false && <a href="#" className="ml-auto inline-block text-sm underline">
|
|
||||||
Forgot your password?
|
|
||||||
</a>}
|
|
||||||
</div>
|
|
||||||
<Input
|
<Input
|
||||||
id="password"
|
id="password"
|
||||||
type="password"
|
type="password"
|
||||||
value={password}
|
placeholder="Enter your password"
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
autoComplete="current-password"
|
||||||
required
|
{...register("password", { required: true })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button type="submit" className="w-full" disabled={loading}>
|
<Button type="submit" className="w-full" disabled={loading}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<>
|
"Loading..."
|
||||||
<span className="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
|
||||||
Loading...
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
'Login'
|
<>
|
||||||
|
<LogIn className="mr-2 h-4 w-4" />
|
||||||
|
Sign In
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
<div className="mt-4 text-center text-sm">
|
|
||||||
Don't have an account?{" "}
|
|
||||||
<a href="/register/" className="underline">
|
|
||||||
Sign up
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
7
core/templates/tree/vite_react/.gitignore
vendored
Normal file
7
core/templates/tree/vite_react/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
server/node_modules/
|
||||||
|
server/.env
|
||||||
|
server/package-lock.json
|
||||||
|
client/node_modules/
|
||||||
|
client/package-lock.json
|
||||||
|
node_modules/
|
||||||
|
package-lock.json
|
||||||
23
core/templates/tree/vite_react/client/components.json
Normal file
23
core/templates/tree/vite_react/client/components.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{% raw %}
|
||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "default",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.js",
|
||||||
|
"css": "src/index.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide"
|
||||||
|
}
|
||||||
|
{% endraw %}
|
||||||
31
core/templates/tree/vite_react/client/eslint.config.js
Normal file
31
core/templates/tree/vite_react/client/eslint.config.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{% raw %}
|
||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ['dist'] },
|
||||||
|
{
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
{% endraw %}
|
||||||
13
core/templates/tree/vite_react/client/index.html
Normal file
13
core/templates/tree/vite_react/client/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>{{ project_name }}</title>
|
||||||
|
<script src="https://s3.us-east-1.amazonaws.com/assets.pythagora.ai/scripts/utils.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
81
core/templates/tree/vite_react/client/package.json
Normal file
81
core/templates/tree/vite_react/client/package.json
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
{
|
||||||
|
"name": "vite_client",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"start": "npm run dev",
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@hookform/resolvers": "^3.9.1",
|
||||||
|
"@radix-ui/react-accordion": "^1.2.1",
|
||||||
|
"@radix-ui/react-alert-dialog": "^1.1.2",
|
||||||
|
"@radix-ui/react-aspect-ratio": "^1.1.0",
|
||||||
|
"@radix-ui/react-avatar": "^1.1.1",
|
||||||
|
"@radix-ui/react-checkbox": "^1.1.2",
|
||||||
|
"@radix-ui/react-collapsible": "^1.1.1",
|
||||||
|
"@radix-ui/react-context-menu": "^2.2.2",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.2",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||||
|
"@radix-ui/react-hover-card": "^1.1.2",
|
||||||
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
|
"@radix-ui/react-menubar": "^1.1.2",
|
||||||
|
"@radix-ui/react-navigation-menu": "^1.2.1",
|
||||||
|
"@radix-ui/react-popover": "^1.1.2",
|
||||||
|
"@radix-ui/react-progress": "^1.1.0",
|
||||||
|
"@radix-ui/react-radio-group": "^1.2.1",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.2.1",
|
||||||
|
"@radix-ui/react-select": "^2.1.2",
|
||||||
|
"@radix-ui/react-separator": "^1.1.0",
|
||||||
|
"@radix-ui/react-slider": "^1.2.1",
|
||||||
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
|
"@radix-ui/react-switch": "^1.1.1",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.1",
|
||||||
|
"@radix-ui/react-toast": "^1.2.2",
|
||||||
|
"@radix-ui/react-toggle": "^1.1.0",
|
||||||
|
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||||
|
"@radix-ui/react-tooltip": "^1.1.4",
|
||||||
|
"axios": "^1.7.8",
|
||||||
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.0.0",
|
||||||
|
"date-fns": "^3.6.0",
|
||||||
|
"embla-carousel-react": "^8.5.1",
|
||||||
|
"input-otp": "^1.4.1",
|
||||||
|
"lucide-react": "^0.460.0",
|
||||||
|
"next-themes": "^0.4.3",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-day-picker": "^8.10.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-hook-form": "^7.53.2",
|
||||||
|
"react-resizable-panels": "^2.1.7",
|
||||||
|
"react-router-dom": "^7.0.1",
|
||||||
|
"recharts": "^2.13.3",
|
||||||
|
"sonner": "^1.7.0",
|
||||||
|
"tailwind-merge": "^2.5.4",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"vaul": "^1.1.1",
|
||||||
|
"zod": "^3.23.8"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.13.0",
|
||||||
|
"@types/node": "^22.9.1",
|
||||||
|
"@types/react": "^18.3.12",
|
||||||
|
"@types/react-dom": "^18.3.1",
|
||||||
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"eslint": "^9.13.0",
|
||||||
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.14",
|
||||||
|
"globals": "^15.11.0",
|
||||||
|
"postcss": "^8.4.49",
|
||||||
|
"tailwindcss": "^3.4.15",
|
||||||
|
"typescript": "~5.6.2",
|
||||||
|
"typescript-eslint": "^8.11.0",
|
||||||
|
"vite": "^5.4.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
9
core/templates/tree/vite_react/client/postcss.config.js
Normal file
9
core/templates/tree/vite_react/client/postcss.config.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{% raw %}
|
||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
{% endraw %}
|
||||||
BIN
core/templates/tree/vite_react/client/public/favicon.ico
Normal file
BIN
core/templates/tree/vite_react/client/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 662 B |
45
core/templates/tree/vite_react/client/src/App.css
Normal file
45
core/templates/tree/vite_react/client/src/App.css
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{% raw %}
|
||||||
|
#root {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 6em;
|
||||||
|
padding: 1.5em;
|
||||||
|
will-change: filter;
|
||||||
|
transition: filter 300ms;
|
||||||
|
}
|
||||||
|
.logo:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #646cffaa);
|
||||||
|
}
|
||||||
|
.logo.react:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes logo-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
a:nth-of-type(2) .logo {
|
||||||
|
animation: logo-spin infinite 20s linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-the-docs {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
{% endraw %}
|
||||||
26
core/templates/tree/vite_react/client/src/App.tsx
Normal file
26
core/templates/tree/vite_react/client/src/App.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { BrowserRouter as Router, Routes, Route } from "react-router-dom"
|
||||||
|
import { ThemeProvider } from "./components/ui/theme-provider"
|
||||||
|
import { Toaster } from "./components/ui/toaster"
|
||||||
|
import { AuthProvider } from "./contexts/AuthContext"
|
||||||
|
import { Login } from "./pages/Login"
|
||||||
|
import { Register } from "./pages/Register"
|
||||||
|
import { Layout } from "./components/Layout"
|
||||||
|
import { ProtectedRoute } from "./components/ProtectedRoute"
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<AuthProvider>
|
||||||
|
<ThemeProvider defaultTheme="light" storageKey="ui-theme">
|
||||||
|
<Router>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/login" element={<Login />} />
|
||||||
|
<Route path="/register" element={<Register />} />
|
||||||
|
<Route path="/" element={<ProtectedRoute> <Layout /> </ProtectedRoute>} />
|
||||||
|
</Routes>
|
||||||
|
</Router>
|
||||||
|
<Toaster />
|
||||||
|
</ThemeProvider>
|
||||||
|
</AuthProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
||||||
67
core/templates/tree/vite_react/client/src/api/api.ts
Normal file
67
core/templates/tree/vite_react/client/src/api/api.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import axios, { AxiosRequestConfig, AxiosError } from 'axios';
|
||||||
|
|
||||||
|
const backendURL = '';
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: backendURL,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
validateStatus: (status) => {
|
||||||
|
return status >= 200 && status < 300;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let accessToken: string | null = null;
|
||||||
|
{% if options.auth %}
|
||||||
|
// Axios request interceptor: Attach access token to headers
|
||||||
|
api.interceptors.request.use(
|
||||||
|
(config: AxiosRequestConfig): AxiosRequestConfig => {
|
||||||
|
if (!accessToken) {
|
||||||
|
accessToken = localStorage.getItem('accessToken');
|
||||||
|
}
|
||||||
|
if (accessToken && config.headers) {
|
||||||
|
config.headers.Authorization = `Bearer ${accessToken}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error: AxiosError): Promise<AxiosError> => Promise.reject(error)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Axios response interceptor: Handle 401 errors
|
||||||
|
api.interceptors.response.use(
|
||||||
|
(response) => response, // If the response is successful, return it
|
||||||
|
async (error: AxiosError): Promise<any> => {
|
||||||
|
const originalRequest = error.config as AxiosRequestConfig & { _retry?: boolean };
|
||||||
|
|
||||||
|
// If the error is due to an expired access token
|
||||||
|
if ([401, 403].includes(error.response?.status) && !originalRequest._retry) {
|
||||||
|
originalRequest._retry = true; // Mark the request as retried
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Attempt to refresh the token
|
||||||
|
const { data } = await axios.post<{ accessToken: string }>(`${backendURL}/api/auth/refresh`, {
|
||||||
|
refreshToken: localStorage.getItem('refreshToken'),
|
||||||
|
});
|
||||||
|
accessToken = data.accessToken;
|
||||||
|
|
||||||
|
// Retry the original request with the new token
|
||||||
|
if (originalRequest.headers) {
|
||||||
|
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
|
||||||
|
}
|
||||||
|
return api(originalRequest);
|
||||||
|
} catch (err) {
|
||||||
|
// If refresh fails, clear tokens and redirect to login
|
||||||
|
localStorage.removeItem('refreshToken');
|
||||||
|
localStorage.removeItem('accessToken');
|
||||||
|
accessToken = null;
|
||||||
|
window.location.href = '/login'; // Redirect to login page
|
||||||
|
return Promise.reject(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(error); // Pass other errors through
|
||||||
|
}
|
||||||
|
);
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
export default api;
|
||||||
44
core/templates/tree/vite_react/client/src/api/auth.ts
Normal file
44
core/templates/tree/vite_react/client/src/api/auth.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{% if options.auth %}
|
||||||
|
import api from './api';
|
||||||
|
|
||||||
|
// Description: Login user functionality
|
||||||
|
// Endpoint: POST /api/auth/login
|
||||||
|
// Request: { email: string, password: string }
|
||||||
|
// Response: { accessToken: string, refreshToken: string }
|
||||||
|
export const login = async (email: string, password: string) => {
|
||||||
|
try {
|
||||||
|
return { accessToken: '123', refreshToken: '123' }; // this is just a placeholder - remove when the backend is being implemented
|
||||||
|
const response = await api.post('/api/auth/login', { email, password });
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login error:', error);
|
||||||
|
throw new Error(error?.response?.data?.message || error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Description: Register user functionality
|
||||||
|
// Endpoint: POST /api/auth/register
|
||||||
|
// Request: { email: string, password: string }
|
||||||
|
// Response: { email: string }
|
||||||
|
export const register = async (email: string, password: string) => {
|
||||||
|
try {
|
||||||
|
return { email: 'jake@example.com' }; // this is just a placeholder - remove when the backend is being implemented
|
||||||
|
const response = await api.post('/api/auth/register', {email, password});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error?.response?.data?.message || error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Description: Logout
|
||||||
|
// Endpoint: POST /api/auth/logout
|
||||||
|
// Request: {}
|
||||||
|
// Response: { success: boolean, message: string }
|
||||||
|
export const logout = async () => {
|
||||||
|
try {
|
||||||
|
return await api.post('/api/auth/logout');
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error?.response?.data?.message || error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
{% endif %}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{% raw %}
|
||||||
|
export function Footer() {
|
||||||
|
return (
|
||||||
|
<footer className="fixed bottom-0 w-full bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-t">
|
||||||
|
<div className="container flex h-14 items-center justify-between">
|
||||||
|
<p className="mx-6 text-sm text-muted-foreground">
|
||||||
|
Built by <a href="https://pythagora.ai" target="_blank" rel="noopener noreferrer">Pythagora</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{% endraw %}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{% raw %}
|
||||||
|
import { Bell{% endraw %}{% if options.auth %}, LogOut{% endif %}{% raw %} } from "lucide-react"
|
||||||
|
import { Button } from "./ui/button"
|
||||||
|
import { ThemeToggle } from "./ui/theme-toggle"
|
||||||
|
import { useAuth } from "@/contexts/AuthContext"
|
||||||
|
import { useNavigate } from "react-router-dom"
|
||||||
|
|
||||||
|
export function Header() {
|
||||||
|
{% endraw %}
|
||||||
|
{% if options.auth %}
|
||||||
|
const { logout } = useAuth()
|
||||||
|
{% endif %}
|
||||||
|
const navigate = useNavigate()
|
||||||
|
{% if options.auth %}
|
||||||
|
const handleLogout = () => {
|
||||||
|
logout()
|
||||||
|
navigate("/login")
|
||||||
|
}
|
||||||
|
{% endif %}
|
||||||
|
return (
|
||||||
|
<header className="fixed top-0 z-50 w-full border-b bg-background/80 backdrop-blur-sm">
|
||||||
|
<div className="flex h-16 items-center justify-between px-6">
|
||||||
|
<div className="text-xl font-bold">Home</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<ThemeToggle />
|
||||||
|
{% if options.auth %}
|
||||||
|
<Button variant="ghost" size="icon" onClick={handleLogout}>
|
||||||
|
<LogOut className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { Outlet } from "react-router-dom"
|
||||||
|
import { Header } from "./Header"
|
||||||
|
import { Footer } from "./Footer"
|
||||||
|
|
||||||
|
export function Layout() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-background to-secondary">
|
||||||
|
<Header />
|
||||||
|
<div className="flex h-[calc(100vh-4rem)] pt-16">
|
||||||
|
<main className="flex-1 overflow-y-auto p-6">
|
||||||
|
<div className="mx-auto max-w-7xl">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{% raw %}
|
||||||
|
import { Navigate, useLocation } from "react-router-dom";
|
||||||
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
|
|
||||||
|
export function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
||||||
|
const { isAuthenticated } = useAuth();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return <Navigate to="/login" state={{ from: location }} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
|
{% endraw %}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
{% raw %}
|
||||||
|
import * as React from "react"
|
||||||
|
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||||
|
import { ChevronDown } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Accordion = AccordionPrimitive.Root
|
||||||
|
|
||||||
|
const AccordionItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AccordionPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn("border-b", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AccordionItem.displayName = "AccordionItem"
|
||||||
|
|
||||||
|
const AccordionTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<AccordionPrimitive.Header className="flex">
|
||||||
|
<AccordionPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||||
|
</AccordionPrimitive.Trigger>
|
||||||
|
</AccordionPrimitive.Header>
|
||||||
|
))
|
||||||
|
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
||||||
|
|
||||||
|
const AccordionContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<AccordionPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||||
|
</AccordionPrimitive.Content>
|
||||||
|
))
|
||||||
|
|
||||||
|
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
||||||
|
|
||||||
|
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||||
|
|
||||||
|
{% endraw %}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
{% raw %}
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { buttonVariants } from "@/components/ui/button"
|
||||||
|
|
||||||
|
const AlertDialog = AlertDialogPrimitive.Root
|
||||||
|
|
||||||
|
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
||||||
|
|
||||||
|
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
||||||
|
|
||||||
|
const AlertDialogOverlay = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Overlay
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
||||||
|
|
||||||
|
const AlertDialogContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPortal>
|
||||||
|
<AlertDialogOverlay />
|
||||||
|
<AlertDialogPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</AlertDialogPortal>
|
||||||
|
))
|
||||||
|
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const AlertDialogHeader = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col space-y-2 text-center sm:text-left",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
AlertDialogHeader.displayName = "AlertDialogHeader"
|
||||||
|
|
||||||
|
const AlertDialogFooter = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
AlertDialogFooter.displayName = "AlertDialogFooter"
|
||||||
|
|
||||||
|
const AlertDialogTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-lg font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
||||||
|
|
||||||
|
const AlertDialogDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogDescription.displayName =
|
||||||
|
AlertDialogPrimitive.Description.displayName
|
||||||
|
|
||||||
|
const AlertDialogAction = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Action
|
||||||
|
ref={ref}
|
||||||
|
className={cn(buttonVariants(), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
||||||
|
|
||||||
|
const AlertDialogCancel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Cancel
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({ variant: "outline" }),
|
||||||
|
"mt-2 sm:mt-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogPortal,
|
||||||
|
AlertDialogOverlay,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
}
|
||||||
|
|
||||||
|
{% endraw %}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
{% raw %}
|
||||||
|
import * as React from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const alertVariants = cva(
|
||||||
|
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-background text-foreground",
|
||||||
|
destructive:
|
||||||
|
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const Alert = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||||
|
>(({ className, variant, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
role="alert"
|
||||||
|
className={cn(alertVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Alert.displayName = "Alert"
|
||||||
|
|
||||||
|
const AlertTitle = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLHeadingElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<h5
|
||||||
|
ref={ref}
|
||||||
|
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertTitle.displayName = "AlertTitle"
|
||||||
|
|
||||||
|
const AlertDescription = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDescription.displayName = "AlertDescription"
|
||||||
|
|
||||||
|
export { Alert, AlertTitle, AlertDescription }
|
||||||
|
|
||||||
|
{% endraw %}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{% raw %}
|
||||||
|
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
|
||||||
|
|
||||||
|
const AspectRatio = AspectRatioPrimitive.Root
|
||||||
|
|
||||||
|
export { AspectRatio }
|
||||||
|
|
||||||
|
{% endraw %}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
{% raw %}
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Avatar = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AvatarPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Avatar.displayName = AvatarPrimitive.Root.displayName
|
||||||
|
|
||||||
|
const AvatarImage = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AvatarPrimitive.Image
|
||||||
|
ref={ref}
|
||||||
|
className={cn("aspect-square h-full w-full", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
||||||
|
|
||||||
|
const AvatarFallback = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AvatarPrimitive.Fallback
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
||||||
|
|
||||||
|
export { Avatar, AvatarImage, AvatarFallback }
|
||||||
|
|
||||||
|
{% endraw %}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{% raw %}
|
||||||
|
import * as React from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||||
|
secondary:
|
||||||
|
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
destructive:
|
||||||
|
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||||
|
outline: "text-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface BadgeProps
|
||||||
|
extends React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
VariantProps<typeof badgeVariants> {}
|
||||||
|
|
||||||
|
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants }
|
||||||
|
|
||||||
|
{% endraw %}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
{% raw %}
|
||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Breadcrumb = React.forwardRef<
|
||||||
|
HTMLElement,
|
||||||
|
React.ComponentPropsWithoutRef<"nav"> & {
|
||||||
|
separator?: React.ReactNode
|
||||||
|
}
|
||||||
|
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
|
||||||
|
Breadcrumb.displayName = "Breadcrumb"
|
||||||
|
|
||||||
|
const BreadcrumbList = React.forwardRef<
|
||||||
|
HTMLOListElement,
|
||||||
|
React.ComponentPropsWithoutRef<"ol">
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ol
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
BreadcrumbList.displayName = "BreadcrumbList"
|
||||||
|
|
||||||
|
const BreadcrumbItem = React.forwardRef<
|
||||||
|
HTMLLIElement,
|
||||||
|
React.ComponentPropsWithoutRef<"li">
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<li
|
||||||
|
ref={ref}
|
||||||
|
className={cn("inline-flex items-center gap-1.5", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
BreadcrumbItem.displayName = "BreadcrumbItem"
|
||||||
|
|
||||||
|
const BreadcrumbLink = React.forwardRef<
|
||||||
|
HTMLAnchorElement,
|
||||||
|
React.ComponentPropsWithoutRef<"a"> & {
|
||||||
|
asChild?: boolean
|
||||||
|
}
|
||||||
|
>(({ asChild, className, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "a"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
ref={ref}
|
||||||
|
className={cn("transition-colors hover:text-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
BreadcrumbLink.displayName = "BreadcrumbLink"
|
||||||
|
|
||||||
|
const BreadcrumbPage = React.forwardRef<
|
||||||
|
HTMLSpanElement,
|
||||||
|
React.ComponentPropsWithoutRef<"span">
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<span
|
||||||
|
ref={ref}
|
||||||
|
role="link"
|
||||||
|
aria-disabled="true"
|
||||||
|
aria-current="page"
|
||||||
|
className={cn("font-normal text-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
BreadcrumbPage.displayName = "BreadcrumbPage"
|
||||||
|
|
||||||
|
const BreadcrumbSeparator = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"li">) => (
|
||||||
|
<li
|
||||||
|
role="presentation"
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children ?? <ChevronRight />}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
|
||||||
|
|
||||||
|
const BreadcrumbEllipsis = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) => (
|
||||||
|
<span
|
||||||
|
role="presentation"
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
<span className="sr-only">More</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
|
||||||
|
|
||||||
|
export {
|
||||||
|
Breadcrumb,
|
||||||
|
BreadcrumbList,
|
||||||
|
BreadcrumbItem,
|
||||||
|
BreadcrumbLink,
|
||||||
|
BreadcrumbPage,
|
||||||
|
BreadcrumbSeparator,
|
||||||
|
BreadcrumbEllipsis,
|
||||||
|
}
|
||||||
|
|
||||||
|
{% endraw %}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
{% raw %}
|
||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||||
|
outline:
|
||||||
|
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-10 px-4 py-2",
|
||||||
|
sm: "h-9 rounded-md px-3",
|
||||||
|
lg: "h-11 rounded-md px-8",
|
||||||
|
icon: "h-10 w-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Button.displayName = "Button"
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
|
|
||||||
|
{% endraw %}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
{% raw %}
|
||||||
|
import * as React from "react"
|
||||||
|
import { ChevronLeft, ChevronRight } from "lucide-react"
|
||||||
|
import { DayPicker } from "react-day-picker"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { buttonVariants } from "@/components/ui/button"
|
||||||
|
|
||||||
|
export type CalendarProps = React.ComponentProps<typeof DayPicker>
|
||||||
|
|
||||||
|
function Calendar({
|
||||||
|
className,
|
||||||
|
classNames,
|
||||||
|
showOutsideDays = true,
|
||||||
|
...props
|
||||||
|
}: CalendarProps) {
|
||||||
|
return (
|
||||||
|
<DayPicker
|
||||||
|
showOutsideDays={showOutsideDays}
|
||||||
|
className={cn("p-3", className)}
|
||||||
|
classNames={{
|
||||||
|
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
|
||||||
|
month: "space-y-4",
|
||||||
|
caption: "flex justify-center pt-1 relative items-center",
|
||||||
|
caption_label: "text-sm font-medium",
|
||||||
|
nav: "space-x-1 flex items-center",
|
||||||
|
nav_button: cn(
|
||||||
|
buttonVariants({ variant: "outline" }),
|
||||||
|
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
|
||||||
|
),
|
||||||
|
nav_button_previous: "absolute left-1",
|
||||||
|
nav_button_next: "absolute right-1",
|
||||||
|
table: "w-full border-collapse space-y-1",
|
||||||
|
head_row: "flex",
|
||||||
|
head_cell:
|
||||||
|
"text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
|
||||||
|
row: "flex w-full mt-2",
|
||||||
|
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
|
||||||
|
day: cn(
|
||||||
|
buttonVariants({ variant: "ghost" }),
|
||||||
|
"h-9 w-9 p-0 font-normal aria-selected:opacity-100"
|
||||||
|
),
|
||||||
|
day_range_end: "day-range-end",
|
||||||
|
day_selected:
|
||||||
|
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
||||||
|
day_today: "bg-accent text-accent-foreground",
|
||||||
|
day_outside:
|
||||||
|
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
|
||||||
|
day_disabled: "text-muted-foreground opacity-50",
|
||||||
|
day_range_middle:
|
||||||
|
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
||||||
|
day_hidden: "invisible",
|
||||||
|
...classNames,
|
||||||
|
}}
|
||||||
|
components={{
|
||||||
|
IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
|
||||||
|
IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Calendar.displayName = "Calendar"
|
||||||
|
|
||||||
|
export { Calendar }
|
||||||
|
|
||||||
|
{% endraw %}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
{% raw %}
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Card = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Card.displayName = "Card"
|
||||||
|
|
||||||
|
const CardHeader = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardHeader.displayName = "CardHeader"
|
||||||
|
|
||||||
|
const CardTitle = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"text-2xl font-semibold leading-none tracking-tight",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardTitle.displayName = "CardTitle"
|
||||||
|
|
||||||
|
const CardDescription = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardDescription.displayName = "CardDescription"
|
||||||
|
|
||||||
|
const CardContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||||
|
))
|
||||||
|
CardContent.displayName = "CardContent"
|
||||||
|
|
||||||
|
const CardFooter = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex items-center p-6 pt-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardFooter.displayName = "CardFooter"
|
||||||
|
|
||||||
|
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||||
|
|
||||||
|
{% endraw %}
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
{% raw %}
|
||||||
|
import * as React from "react"
|
||||||
|
import useEmblaCarousel, {
|
||||||
|
type UseEmblaCarouselType,
|
||||||
|
} from "embla-carousel-react"
|
||||||
|
import { ArrowLeft, ArrowRight } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
|
||||||
|
type CarouselApi = UseEmblaCarouselType[1]
|
||||||
|
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
|
||||||
|
type CarouselOptions = UseCarouselParameters[0]
|
||||||
|
type CarouselPlugin = UseCarouselParameters[1]
|
||||||
|
|
||||||
|
type CarouselProps = {
|
||||||
|
opts?: CarouselOptions
|
||||||
|
plugins?: CarouselPlugin
|
||||||
|
orientation?: "horizontal" | "vertical"
|
||||||
|
setApi?: (api: CarouselApi) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
type CarouselContextProps = {
|
||||||
|
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
|
||||||
|
api: ReturnType<typeof useEmblaCarousel>[1]
|
||||||
|
scrollPrev: () => void
|
||||||
|
scrollNext: () => void
|
||||||
|
canScrollPrev: boolean
|
||||||
|
canScrollNext: boolean
|
||||||
|
} & CarouselProps
|
||||||
|
|
||||||
|
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
|
||||||
|
|
||||||
|
function useCarousel() {
|
||||||
|
const context = React.useContext(CarouselContext)
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useCarousel must be used within a <Carousel />")
|
||||||
|
}
|
||||||
|
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
const Carousel = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement> & CarouselProps
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
orientation = "horizontal",
|
||||||
|
opts,
|
||||||
|
setApi,
|
||||||
|
plugins,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const [carouselRef, api] = useEmblaCarousel(
|
||||||
|
{
|
||||||
|
...opts,
|
||||||
|
axis: orientation === "horizontal" ? "x" : "y",
|
||||||
|
},
|
||||||
|
plugins
|
||||||
|
)
|
||||||
|
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
|
||||||
|
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
||||||
|
|
||||||
|
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||||
|
if (!api) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setCanScrollPrev(api.canScrollPrev())
|
||||||
|
setCanScrollNext(api.canScrollNext())
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const scrollPrev = React.useCallback(() => {
|
||||||
|
api?.scrollPrev()
|
||||||
|
}, [api])
|
||||||
|
|
||||||
|
const scrollNext = React.useCallback(() => {
|
||||||
|
api?.scrollNext()
|
||||||
|
}, [api])
|
||||||
|
|
||||||
|
const handleKeyDown = React.useCallback(
|
||||||
|
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (event.key === "ArrowLeft") {
|
||||||
|
event.preventDefault()
|
||||||
|
scrollPrev()
|
||||||
|
} else if (event.key === "ArrowRight") {
|
||||||
|
event.preventDefault()
|
||||||
|
scrollNext()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[scrollPrev, scrollNext]
|
||||||
|
)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!api || !setApi) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setApi(api)
|
||||||
|
}, [api, setApi])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!api) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelect(api)
|
||||||
|
api.on("reInit", onSelect)
|
||||||
|
api.on("select", onSelect)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
api?.off("select", onSelect)
|
||||||
|
}
|
||||||
|
}, [api, onSelect])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CarouselContext.Provider
|
||||||
|
value={{
|
||||||
|
carouselRef,
|
||||||
|
api: api,
|
||||||
|
opts,
|
||||||
|
orientation:
|
||||||
|
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
||||||
|
scrollPrev,
|
||||||
|
scrollNext,
|
||||||
|
canScrollPrev,
|
||||||
|
canScrollNext,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
onKeyDownCapture={handleKeyDown}
|
||||||
|
className={cn("relative", className)}
|
||||||
|
role="region"
|
||||||
|
aria-roledescription="carousel"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</CarouselContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Carousel.displayName = "Carousel"
|
||||||
|
|
||||||
|
const CarouselContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
const { carouselRef, orientation } = useCarousel()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={carouselRef} className="overflow-hidden">
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex",
|
||||||
|
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
CarouselContent.displayName = "CarouselContent"
|
||||||
|
|
||||||
|
const CarouselItem = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
const { orientation } = useCarousel()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
role="group"
|
||||||
|
aria-roledescription="slide"
|
||||||
|
className={cn(
|
||||||
|
"min-w-0 shrink-0 grow-0 basis-full",
|
||||||
|
orientation === "horizontal" ? "pl-4" : "pt-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
CarouselItem.displayName = "CarouselItem"
|
||||||
|
|
||||||
|
const CarouselPrevious = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
React.ComponentProps<typeof Button>
|
||||||
|
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
||||||
|
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
ref={ref}
|
||||||
|
variant={variant}
|
||||||
|
size={size}
|
||||||
|
className={cn(
|
||||||
|
"absolute h-8 w-8 rounded-full",
|
||||||
|
orientation === "horizontal"
|
||||||
|
? "-left-12 top-1/2 -translate-y-1/2"
|
||||||
|
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
disabled={!canScrollPrev}
|
||||||
|
onClick={scrollPrev}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Previous slide</span>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
CarouselPrevious.displayName = "CarouselPrevious"
|
||||||
|
|
||||||
|
const CarouselNext = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
React.ComponentProps<typeof Button>
|
||||||
|
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
||||||
|
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
ref={ref}
|
||||||
|
variant={variant}
|
||||||
|
size={size}
|
||||||
|
className={cn(
|
||||||
|
"absolute h-8 w-8 rounded-full",
|
||||||
|
orientation === "horizontal"
|
||||||
|
? "-right-12 top-1/2 -translate-y-1/2"
|
||||||
|
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
disabled={!canScrollNext}
|
||||||
|
onClick={scrollNext}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ArrowRight className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Next slide</span>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
CarouselNext.displayName = "CarouselNext"
|
||||||
|
|
||||||
|
export {
|
||||||
|
type CarouselApi,
|
||||||
|
Carousel,
|
||||||
|
CarouselContent,
|
||||||
|
CarouselItem,
|
||||||
|
CarouselPrevious,
|
||||||
|
CarouselNext,
|
||||||
|
}
|
||||||
|
|
||||||
|
{% endraw %}
|
||||||
@@ -0,0 +1,368 @@
|
|||||||
|
{% raw %}
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as RechartsPrimitive from "recharts"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||||
|
const THEMES = { light: "", dark: ".dark" } as const
|
||||||
|
|
||||||
|
export type ChartConfig = {
|
||||||
|
[k in string]: {
|
||||||
|
label?: React.ReactNode
|
||||||
|
icon?: React.ComponentType
|
||||||
|
} & (
|
||||||
|
| { color?: string; theme?: never }
|
||||||
|
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChartContextProps = {
|
||||||
|
config: ChartConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChartContext = React.createContext<ChartContextProps | null>(null)
|
||||||
|
|
||||||
|
function useChart() {
|
||||||
|
const context = React.useContext(ChartContext)
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useChart must be used within a <ChartContainer />")
|
||||||
|
}
|
||||||
|
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChartContainer = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.ComponentProps<"div"> & {
|
||||||
|
config: ChartConfig
|
||||||
|
children: React.ComponentProps<
|
||||||
|
typeof RechartsPrimitive.ResponsiveContainer
|
||||||
|
>["children"]
|
||||||
|
}
|
||||||
|
>(({ id, className, children, config, ...props }, ref) => {
|
||||||
|
const uniqueId = React.useId()
|
||||||
|
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChartContext.Provider value={{ config }}>
|
||||||
|
<div
|
||||||
|
data-chart={chartId}
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChartStyle id={chartId} config={config} />
|
||||||
|
<RechartsPrimitive.ResponsiveContainer>
|
||||||
|
{children}
|
||||||
|
</RechartsPrimitive.ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
</ChartContext.Provider>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
ChartContainer.displayName = "Chart"
|
||||||
|
|
||||||
|
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||||
|
const colorConfig = Object.entries(config).filter(
|
||||||
|
([_, config]) => config.theme || config.color
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!colorConfig.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<style
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: Object.entries(THEMES)
|
||||||
|
.map(
|
||||||
|
([theme, prefix]) => `
|
||||||
|
${prefix} [data-chart=${id}] {
|
||||||
|
${colorConfig
|
||||||
|
.map(([key, itemConfig]) => {
|
||||||
|
const color =
|
||||||
|
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
||||||
|
itemConfig.color
|
||||||
|
return color ? ` --color-${key}: ${color};` : null
|
||||||
|
})
|
||||||
|
.join("\n")}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.join("\n"),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChartTooltip = RechartsPrimitive.Tooltip
|
||||||
|
|
||||||
|
const ChartTooltipContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||||
|
React.ComponentProps<"div"> & {
|
||||||
|
hideLabel?: boolean
|
||||||
|
hideIndicator?: boolean
|
||||||
|
indicator?: "line" | "dot" | "dashed"
|
||||||
|
nameKey?: string
|
||||||
|
labelKey?: string
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
active,
|
||||||
|
payload,
|
||||||
|
className,
|
||||||
|
indicator = "dot",
|
||||||
|
hideLabel = false,
|
||||||
|
hideIndicator = false,
|
||||||
|
label,
|
||||||
|
labelFormatter,
|
||||||
|
labelClassName,
|
||||||
|
formatter,
|
||||||
|
color,
|
||||||
|
nameKey,
|
||||||
|
labelKey,
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const { config } = useChart()
|
||||||
|
|
||||||
|
const tooltipLabel = React.useMemo(() => {
|
||||||
|
if (hideLabel || !payload?.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const [item] = payload
|
||||||
|
const key = `${labelKey || item.dataKey || item.name || "value"}`
|
||||||
|
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||||
|
const value =
|
||||||
|
!labelKey && typeof label === "string"
|
||||||
|
? config[label as keyof typeof config]?.label || label
|
||||||
|
: itemConfig?.label
|
||||||
|
|
||||||
|
if (labelFormatter) {
|
||||||
|
return (
|
||||||
|
<div className={cn("font-medium", labelClassName)}>
|
||||||
|
{labelFormatter(value, payload)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className={cn("font-medium", labelClassName)}>{value}</div>
|
||||||
|
}, [
|
||||||
|
label,
|
||||||
|
labelFormatter,
|
||||||
|
payload,
|
||||||
|
hideLabel,
|
||||||
|
labelClassName,
|
||||||
|
config,
|
||||||
|
labelKey,
|
||||||
|
])
|
||||||
|
|
||||||
|
if (!active || !payload?.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const nestLabel = payload.length === 1 && indicator !== "dot"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{!nestLabel ? tooltipLabel : null}
|
||||||
|
<div className="grid gap-1.5">
|
||||||
|
{payload.map((item, index) => {
|
||||||
|
const key = `${nameKey || item.name || item.dataKey || "value"}`
|
||||||
|
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||||
|
const indicatorColor = color || item.payload.fill || item.color
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.dataKey}
|
||||||
|
className={cn(
|
||||||
|
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
||||||
|
indicator === "dot" && "items-center"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{formatter && item?.value !== undefined && item.name ? (
|
||||||
|
formatter(item.value, item.name, item, index, item.payload)
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{itemConfig?.icon ? (
|
||||||
|
<itemConfig.icon />
|
||||||
|
) : (
|
||||||
|
!hideIndicator && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
|
||||||
|
{
|
||||||
|
"h-2.5 w-2.5": indicator === "dot",
|
||||||
|
"w-1": indicator === "line",
|
||||||
|
"w-0 border-[1.5px] border-dashed bg-transparent":
|
||||||
|
indicator === "dashed",
|
||||||
|
"my-0.5": nestLabel && indicator === "dashed",
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--color-bg": indicatorColor,
|
||||||
|
"--color-border": indicatorColor,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-1 justify-between leading-none",
|
||||||
|
nestLabel ? "items-end" : "items-center"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="grid gap-1.5">
|
||||||
|
{nestLabel ? tooltipLabel : null}
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{itemConfig?.label || item.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{item.value && (
|
||||||
|
<span className="font-mono font-medium tabular-nums text-foreground">
|
||||||
|
{item.value.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ChartTooltipContent.displayName = "ChartTooltip"
|
||||||
|
|
||||||
|
const ChartLegend = RechartsPrimitive.Legend
|
||||||
|
|
||||||
|
const ChartLegendContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.ComponentProps<"div"> &
|
||||||
|
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||||
|
hideIcon?: boolean
|
||||||
|
nameKey?: string
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const { config } = useChart()
|
||||||
|
|
||||||
|
if (!payload?.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center justify-center gap-4",
|
||||||
|
verticalAlign === "top" ? "pb-3" : "pt-3",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{payload.map((item) => {
|
||||||
|
const key = `${nameKey || item.dataKey || "value"}`
|
||||||
|
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.value}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{itemConfig?.icon && !hideIcon ? (
|
||||||
|
<itemConfig.icon />
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="h-2 w-2 shrink-0 rounded-[2px]"
|
||||||
|
style={{
|
||||||
|
backgroundColor: item.color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{itemConfig?.label}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ChartLegendContent.displayName = "ChartLegend"
|
||||||
|
|
||||||
|
// Helper to extract item config from a payload.
|
||||||
|
function getPayloadConfigFromPayload(
|
||||||
|
config: ChartConfig,
|
||||||
|
payload: unknown,
|
||||||
|
key: string
|
||||||
|
) {
|
||||||
|
if (typeof payload !== "object" || payload === null) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const payloadPayload =
|
||||||
|
"payload" in payload &&
|
||||||
|
typeof payload.payload === "object" &&
|
||||||
|
payload.payload !== null
|
||||||
|
? payload.payload
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
let configLabelKey: string = key
|
||||||
|
|
||||||
|
if (
|
||||||
|
key in payload &&
|
||||||
|
typeof payload[key as keyof typeof payload] === "string"
|
||||||
|
) {
|
||||||
|
configLabelKey = payload[key as keyof typeof payload] as string
|
||||||
|
} else if (
|
||||||
|
payloadPayload &&
|
||||||
|
key in payloadPayload &&
|
||||||
|
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
||||||
|
) {
|
||||||
|
configLabelKey = payloadPayload[
|
||||||
|
key as keyof typeof payloadPayload
|
||||||
|
] as string
|
||||||
|
}
|
||||||
|
|
||||||
|
return configLabelKey in config
|
||||||
|
? config[configLabelKey]
|
||||||
|
: config[key as keyof typeof config]
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
ChartContainer,
|
||||||
|
ChartTooltip,
|
||||||
|
ChartTooltipContent,
|
||||||
|
ChartLegend,
|
||||||
|
ChartLegendContent,
|
||||||
|
ChartStyle,
|
||||||
|
}
|
||||||
|
|
||||||
|
{% endraw %}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user