diff --git a/README.md b/README.md index cfcbaed..0472c25 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,6 @@ A tool for quickly assembling Large Language Model (LLM) prompts from a local fi - **Meta Prompt Management**: Store and retrieve partial prompts (meta prompts) in a dedicated directory. - **AI-Powered Features**: Smart file selection and prompt refinement using LLMs (requires OpenRouter API key). - **Task Management**: Integrated Kanban board, To-Do list, and User Story management. -- **Actor Definition**: Define and manage project actors, with AI-assisted suggestions. - **Copy to Clipboard**: Gather your meta prompt, main instructions, project tree, and selected file contents in one click. - **Cross-Platform**: Built with Electron for Linux, macOS, and Windows compatibility. @@ -189,7 +188,7 @@ A summary of passed/failed tests will be printed. - **`tree-sitter` compilation errors on `npm install`**: This is almost always due to a missing C/C++ build toolchain. Please follow the instructions in the [Prerequisites](#prerequisites) section for your operating system. - **`EBADENGINE` or Node Version Errors**: You are running an unsupported version of Node.js. Please use `nvm` or install Node.js v20+. -- **Port Conflicts**: If default ports (3010, 5010) are in use, modify `ports.ini` and ensure your start scripts or manual commands reflect these changes. +- **Port Conflicts**: If default ports (3010, 5010) are in use, modify `ports.ini` and ensure the commands you use (e.g., `npm run electron:dev`, custom backend starts) pass the updated values along. - **CSS/JS Not Loading in Packaged App**: This is often due to incorrect asset paths. The current `next.config.js` with `assetPrefix: './'` should handle this for the `file://` protocol. --- diff --git a/agents.md b/agents.md deleted file mode 100644 index 7d0266f..0000000 --- a/agents.md +++ /dev/null @@ -1,108 +0,0 @@ -# Project Agents.md Guide for OpenAI Codex - -This Agents.md file provides comprehensive guidance for OpenAI Codex and other AI agents working with this codebase. - -## Project Structure for OpenAI Codex Navigation - -- `/`: The root directory of the monorepo. - - `.codex`: Contains setup scripts for the development environment. - - `components`: React components, including UI primitives from `shadcn/ui`. - - `components/ui`: Reusable, low-level UI components (e.g., `button.tsx`, `dialog.tsx`). - - `docs`: Documentation files, including API references and architecture descriptions. - - `electron`: Electron-specific files for the desktop application wrapper. - - `lib`: Frontend utility functions and custom React hooks. - - `lib/hooks`: Custom React hooks (e.g., `useUndoRedo.ts`, `useHomePageLogic.ts`). - - `pages`: Next.js page components, defining routes. - - `public`: Static assets (images, fonts, etc.). OpenAI Codex should not modify these directly. - - `python_backend`: The Flask backend application and its modules. - - `python_backend/controllers`: Flask blueprints defining API endpoints. - - `python_backend/models`: Pydantic/dataclass models for API request/response validation. - - `python_backend/repositories`: Data access layer, primarily for file-based persistence. - - `python_backend/services`: Business logic for the backend features. - - `python_backend/tests`: Backend test files. - - `python_backend/utils`: Backend utility functions (e.g., response formatting, path handling). - - `sample_project`: Contains sample data, including meta prompts. - - `sample_project/meta_prompts`: Example meta prompt text files. - - `scripts`: Various utility scripts for development, testing, and building. - - `services`: Frontend service hooks for interacting with the backend API. - - `stores`: Zustand stores for global state management on the frontend. - - `styles`: Global CSS and Tailwind CSS configuration. - - `types`: TypeScript type definitions and Zod schemas used across the frontend and backend. - - `views`: Larger, feature-specific React components that compose UI primitives and interact with state/services. - - `views/layout`: Core layout components (e.g., header, main layout, panels). -- `.env.local`: Environment variables for local development. -- `.eslintrc.js`: ESLint configuration for linting JavaScript/TypeScript. -- `.gitignore`: Specifies intentionally untracked files to ignore. -- `ignoreDirs.txt`: Global exclusion patterns for file tree scanning. -- `next.config.js`: Next.js configuration. -- `package.json`: Node.js project metadata and scripts. -- `ports.ini`: Configuration for development server ports. -- `postcss.config.js`: PostCSS configuration, typically for Tailwind CSS. -- `README.md`: Project overview and setup instructions. -- `tailwind.config.js`: Tailwind CSS configuration. -- `tsconfig.json`: TypeScript compiler configuration. - -## Coding Conventions for OpenAI Codex - -### General Conventions for Agents.md Implementation - -- Use TypeScript for all new code generated by OpenAI Codex -- OpenAI Codex should follow the existing code style in each file -- Agents.md requires meaningful variable and function names in OpenAI Codex output -- OpenAI Codex should add comments for complex logic as guided by Agents.md - -### React Components Guidelines for OpenAI Codex - -- OpenAI Codex should use functional components with hooks as specified in Agents.md -- Keep components generated by OpenAI Codex small and focused -- Agents.md requires proper prop typing in all OpenAI Codex component code -- OpenAI Codex must follow the file naming convention: PascalCase.tsx - -### CSS/Styling Standards for OpenAI Codex - -- OpenAI Codex should use Tailwind CSS for styling as documented in Agents.md -- Follow utility-first approach in all OpenAI Codex style implementations -- OpenAI Codex should use custom CSS only when necessary - -## Testing Requirements for OpenAI Codex - -OpenAI Codex should run tests with the following commands: - -```bash -# Run all integration tests with OpenAI Codex -npm test -Pull Request Guidelines for OpenAI Codex - -When OpenAI Codex helps create a PR, please ensure it: - - Includes a clear description of the changes as guided by Agents.md - - References any related issues that OpenAI Codex is addressing - - Ensures all tests pass for code generated by OpenAI Codex - - Includes screenshots for UI changes implemented with OpenAI Codex - - Keeps PRs focused on a single concern as specified in Agents.md - -Programmatic Checks for OpenAI Codex - -Before submitting changes generated by OpenAI Codex, run: - - -# Lint check for OpenAI Codex code -npm run lint - -# Type check for OpenAI Codex TypeScript -npm run type-check - -# Build check for OpenAI Codex implementations -npm run build - - - -IGNORE_WHEN_COPYING_START -Use code with caution. Bash -IGNORE_WHEN_COPYING_END - -All checks must pass before OpenAI Codex generated code can be merged. Agents.md helps ensure OpenAI Codex follows these requirements. \ No newline at end of file diff --git a/components/ErrorBoundary.tsx b/components/ErrorBoundary.tsx index a66693f..f7069fd 100644 --- a/components/ErrorBoundary.tsx +++ b/components/ErrorBoundary.tsx @@ -1,39 +1,46 @@ // components/ErrorBoundary.tsx // Minor change: Using alias path -import React from "react"; +import type { ReactNode } from "react"; import { AlertCircle } from "lucide-react"; -import { Button } from "@/components/ui/button"; // Using alias -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; // Using alias +import { + ErrorBoundary as ReactErrorBoundary, + type FallbackProps, +} from "react-error-boundary"; + +import { Button } from "@/components/ui/button"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; /** * A reusable error boundary using react‑error‑boundary. * Keeps the rest of the app alive and offers a quick reload. */ -export default function ErrorBoundary({ children }: { children: React.ReactNode }) { - const { ErrorBoundary } = require("react-error-boundary"); +interface Props { + readonly children: ReactNode; +} + +function Fallback({ error, resetErrorBoundary }: FallbackProps) { + return ( +
+ + + Something went wrong + + {error.message} + + + +
+ ); +} +export default function ErrorBoundary({ children }: Props) { return ( - ( -
- - - Something went wrong - - {error.message} - - - -
- )} - > - {children} -
+ {children} ); -} \ No newline at end of file +} diff --git a/docs/api_ref.md b/docs/api_ref.md index 9404fb2..b0649ab 100644 --- a/docs/api_ref.md +++ b/docs/api_ref.md @@ -289,14 +289,3 @@ All responses follow the envelope | POST | `/api/user-stories/{id}/tasks?projectPath=` | Add a single task to a story. Body → `{ "taskId": 1 }` | | DELETE | `/api/user-stories/{id}/tasks/{taskId}?projectPath=` | Remove a single task from a story | ``` - -## 10 · Actor Management -| Method | Path | Notes | -| ------ | ---- | ----- | -| GET | `/api/actors?projectPath=` | List actors | -| POST | `/api/actors?projectPath=` | Create a new actor | -| GET | `/api/actors/{id}?projectPath=` | Get a specific actor | -| PUT | `/api/actors/{id}?projectPath=` | Update an actor | -| DELETE | `/api/actors/{id}?projectPath=` | Delete an actor | -| POST | `/api/actors/suggest?projectPath=` | Suggest an actor from description. Body → `{ "description": "..." }` | -| POST | `/api/actors/generate` | Generate actors from project context. Body → `{ "baseDir": "", "treePaths": ["..."] }` | diff --git a/docs/project-structure-detailed.txt b/docs/project-structure-detailed.txt index b6ce130..bc7fb88 100644 --- a/docs/project-structure-detailed.txt +++ b/docs/project-structure-detailed.txt @@ -146,11 +146,9 @@ This project is a full-stack web application designed to help users generate Lar ## Configuration and Scripts -* **`.env.local`**: Stores the backend API URL (`NEXT_PUBLIC_API_URL`) for the frontend. Read by `next.config.js` and `start.js`. -* **`ports.ini`**: Custom configuration file defining frontend and backend ports. Read by `start.js` and `autotest.js`. -* **`start.js`**: Orchestration script to find free ports based on `ports.ini`, ensure the Python venv exists and dependencies are installed, write the correct API URL to `.env.local`, and start both the Flask backend and Next.js frontend development servers concurrently. -* **`start.bat` / `start.sh`**: Simple wrappers to execute `node start.js`. +* **`.env.local`**: Stores the backend API URL (`NEXT_PUBLIC_API_URL`) for the frontend. Read by `next.config.js` and the Electron launch scripts. +* **`ports.ini`**: Custom configuration file defining frontend and backend ports. Read by `autotest.js`. * **`postinstall.js`**: Runs automatically after `npm install`. Attempts to find Python, create the venv, and install Python dependencies. * **`autotest.js`**: Script to run integration tests against the running frontend and backend APIs, checking key endpoints. Reads ports from `ports.ini`. * **`next.config.js`**: Configures Next.js build behavior, ESLint/TypeScript settings, environment variables, and API rewrites (though direct API calls using `NEXT_PUBLIC_API_URL` seem to be the primary method). -* **`ignoreDirs.txt`**: Contains default patterns/directories excluded by the `ProjectService` when building the file tree globally. \ No newline at end of file +* **`ignoreDirs.txt`**: Contains default patterns/directories excluded by the `ProjectService` when building the file tree globally. diff --git a/lib/hooks/useHomePageLogic.original.ts b/lib/hooks/useHomePageLogic.original.ts index 5d4ec64..14e47ac 100644 --- a/lib/hooks/useHomePageLogic.original.ts +++ b/lib/hooks/useHomePageLogic.original.ts @@ -20,7 +20,6 @@ import { import { useExclusionService } from "@/services/exclusionServiceHooks"; import { useTodoService } from "@/services/todoServiceHooks"; import { useAutoSelectService } from "@/services/autoSelectServiceHooks"; - import { useActorWizardService } from "@/services/actorWizardServiceHooks"; import { applyExtensionFilter, @@ -66,11 +65,10 @@ import { const { fetchGlobalExclusions, fetchLocalExclusions } = useExclusionService(); const { loadTodos } = useTodoService(); const { autoSelect, isSelecting } = useAutoSelectService(); - const { generateActors, isGenerating } = useActorWizardService(); // --- Refs & Local UI State --- const treeRef = useRef(null); - const [activeTab, setActiveTab] = useState<"files" | "options" | "tasks" | "actors">( + const [activeTab, setActiveTab] = useState<"files" | "options" | "tasks">( "files", ); // showSettings and setShowSettings are removed, managed by useAppStore @@ -205,8 +203,6 @@ import { // Setters & Handlers handlePathSelected, autoSelect, - generateActors, - isGeneratingActors: isGenerating, openSettingsModal, // Expose store action saveApiKey, setApiKeyDraft, @@ -220,4 +216,4 @@ import { treeRef, fileTree, }; - } \ No newline at end of file + } diff --git a/lib/hooks/useHomePageLogic.ts b/lib/hooks/useHomePageLogic.ts index 5d4ec64..14e47ac 100644 --- a/lib/hooks/useHomePageLogic.ts +++ b/lib/hooks/useHomePageLogic.ts @@ -20,7 +20,6 @@ import { import { useExclusionService } from "@/services/exclusionServiceHooks"; import { useTodoService } from "@/services/todoServiceHooks"; import { useAutoSelectService } from "@/services/autoSelectServiceHooks"; - import { useActorWizardService } from "@/services/actorWizardServiceHooks"; import { applyExtensionFilter, @@ -66,11 +65,10 @@ import { const { fetchGlobalExclusions, fetchLocalExclusions } = useExclusionService(); const { loadTodos } = useTodoService(); const { autoSelect, isSelecting } = useAutoSelectService(); - const { generateActors, isGenerating } = useActorWizardService(); // --- Refs & Local UI State --- const treeRef = useRef(null); - const [activeTab, setActiveTab] = useState<"files" | "options" | "tasks" | "actors">( + const [activeTab, setActiveTab] = useState<"files" | "options" | "tasks">( "files", ); // showSettings and setShowSettings are removed, managed by useAppStore @@ -205,8 +203,6 @@ import { // Setters & Handlers handlePathSelected, autoSelect, - generateActors, - isGeneratingActors: isGenerating, openSettingsModal, // Expose store action saveApiKey, setApiKeyDraft, @@ -220,4 +216,4 @@ import { treeRef, fileTree, }; - } \ No newline at end of file + } diff --git a/lib/hooks/useRefactoredHomePageLogic.ts b/lib/hooks/useRefactoredHomePageLogic.ts index b943fea..383c452 100644 --- a/lib/hooks/useRefactoredHomePageLogic.ts +++ b/lib/hooks/useRefactoredHomePageLogic.ts @@ -56,8 +56,6 @@ export function useRefactoredHomePageLogic() { // Actions from various hooks handlePathSelected: projectLogic.handlePathSelected, autoSelect: serviceActions.autoSelect, - generateActors: serviceActions.generateActors, - isGeneratingActors: serviceActions.isGeneratingActors, openSettingsModal: uiState.openSettingsModal, saveApiKey: serviceActions.saveApiKey, setApiKeyDraft, @@ -71,4 +69,4 @@ export function useRefactoredHomePageLogic() { // Refs treeRef: uiState.treeRef, }; -} \ No newline at end of file +} diff --git a/lib/hooks/useServiceActions.ts b/lib/hooks/useServiceActions.ts index 404a5a0..00105e1 100644 --- a/lib/hooks/useServiceActions.ts +++ b/lib/hooks/useServiceActions.ts @@ -1,7 +1,6 @@ // lib/hooks/useServiceActions.ts import { useCallback } from "react"; import { useAutoSelectService } from "@/services/autoSelectServiceHooks"; -import { useActorWizardService } from "@/services/actorWizardServiceHooks"; import { useSettingsStore } from "@/stores/useSettingStore"; import { useAppStore } from "@/stores/useAppStore"; @@ -17,7 +16,6 @@ export function useServiceActions(apiKeyDraft: string) { // Services const { autoSelect, isSelecting } = useAutoSelectService(); - const { generateActors, isGenerating } = useActorWizardService(); // API key management const saveApiKey = useCallback(() => { @@ -34,11 +32,9 @@ export function useServiceActions(apiKeyDraft: string) { return { // Service states isSelecting, - isGeneratingActors: isGenerating, // Service actions autoSelect, - generateActors, saveApiKey, }; -} \ No newline at end of file +} diff --git a/lib/hooks/useUIState.ts b/lib/hooks/useUIState.ts index b49dcce..8b8bdcd 100644 --- a/lib/hooks/useUIState.ts +++ b/lib/hooks/useUIState.ts @@ -9,7 +9,7 @@ import type { FileTreeViewHandle } from "@/views/FileTreeView"; */ export function useUIState() { // Local UI state - const [activeTab, setActiveTab] = useState<"files" | "options" | "tasks" | "actors">("files"); + const [activeTab, setActiveTab] = useState<"files" | "options" | "tasks">("files"); // Refs const treeRef = useRef(null); @@ -24,7 +24,7 @@ export function useUIState() { // Derived UI state const hasContent = useMemo( - () => metaPrompt.trim() || mainInstructions.trim(), + () => Boolean(metaPrompt.trim() || mainInstructions.trim()), [metaPrompt, mainInstructions], ); @@ -39,4 +39,4 @@ export function useUIState() { openSettingsModal, closeSettingsModal, }; -} \ No newline at end of file +} diff --git a/next-env.d.ts b/next-env.d.ts index 52e831b..254b73c 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +/// // NOTE: This file should not be edited // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. diff --git a/package.json b/package.json index 00f08e7..bd9d4d7 100644 --- a/package.json +++ b/package.json @@ -12,12 +12,10 @@ "dev": "kill-port 3010 && next dev -p 3010 -H 127.0.0.1", "build": "npx --yes node@20 ./node_modules/next/dist/bin/next build", "export": "npm run build", - "start": "echo \"For Electron app, use 'npm run electron:prod:local' or run the packaged application. 'npm start' (next start) is not used with 'output: export'. To serve static 'out' folder, use 'npx serve out -p 3010'\"", + "start": "echo \"For Electron app, use 'npm run electron:prod:local' or run the packaged application. 'npm start' (next start) is not used with 'output: export'. To serve a static export, use 'npm run electron:prod:local'.\"", "lint": "next lint", "backend": "python_backend/venv/bin/python python_backend/app.py", "backend:prod": "python_backend/venv/bin/python -m gunicorn -w 2 -b 127.0.0.1:5010 --chdir python_backend app:app", - "start:dev": "node start.js", - "start:all": "node start.js", "postinstall": "node scripts/postinstall.js", "electron:dev": "concurrently --kill-others \"npm run dev\" \"npm run backend\" \"wait-on tcp:5010 tcp:3010 && node scripts/run-electron.js\"", "electron:prod:local": "npm run build && cross-env APP_ENV=production concurrently --kill-others \"python_backend/venv/bin/python -m gunicorn -w 2 -b 127.0.0.1:5010 --chdir python_backend app:app\" \"node scripts/run-electron.js\"", diff --git a/pages/index.tsx b/pages/index.tsx index 348f60e..51b6e6e 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -45,8 +45,6 @@ export default function Home() { saveApiKey, handlePathSelected, autoSelect, - generateActors, - isGeneratingActors, setActiveTab, setFileSearchTerm, handleRefresh, @@ -69,12 +67,10 @@ export default function Home() {
@@ -162,4 +158,4 @@ export default function Home() { )} ); -} \ No newline at end of file +} diff --git a/python_backend/controllers/actor_controller.py b/python_backend/controllers/actor_controller.py deleted file mode 100644 index 9a09fcb..0000000 --- a/python_backend/controllers/actor_controller.py +++ /dev/null @@ -1,117 +0,0 @@ -# python_backend/controllers/actor_controller.py -from flask import Blueprint, request -from services.actor_service import ActorService -from services.actor_suggest_service import ActorSuggestService -from repositories.file_storage import FileStorageRepository -from utils.response_utils import success_response, error_response -from pydantic import ValidationError # Import Pydantic's ValidationError -import logging - -logger = logging.getLogger(__name__) - -actor_bp = Blueprint("actor_bp", __name__) - -# Dependencies -storage_repo = FileStorageRepository() -actor_service = ActorService(storage_repo) -actor_suggest_service = ActorSuggestService() - -# Helper to get projectPath -def _get_project_path(): - project_path = request.args.get("projectPath") - return project_path - -# ─────────────────────────────────────────────────────────────────── -# GET /api/actors?projectPath=… → list all actors -# POST /api/actors?projectPath=… → create new actor -# ─────────────────────────────────────────────────────────────────── -@actor_bp.route("/api/actors", methods=["GET", "POST"]) -def actors_collection(): - project_path = _get_project_path() - - if request.method == "GET": - try: - actors = actor_service.list_actors(project_path) - # Convert Pydantic models to dictionaries for JSON serialization - return success_response(data=[a.dict() for a in actors]) - except ValueError as e: # Catch invalid project path - return error_response(str(e), status_code=400) - except Exception as e: - logger.exception(f"Error listing actors for project: {project_path}") - return error_response(str(e), "Failed to list actors", 500) - - # ---------- POST (create) ---------- - payload = request.get_json(silent=True) or {} - try: - new_actor = actor_service.create_actor(payload, project_path) - return success_response(data=new_actor.dict(), status_code=201) - except ValidationError as e: - return error_response(f"Validation error: {e.json()}", status_code=400) - except ValueError as e: - return error_response(str(e), status_code=400) - except Exception as e: - logger.exception(f"Error creating actor for project: {project_path}") - return error_response(str(e), "Failed to create actor", 500) - -@actor_bp.route("/api/actors/suggest", methods=["POST"]) -def actor_suggest(): - project_path = _get_project_path() - payload = request.get_json(silent=True) or {} - description = payload.get("description", "") - try: - actors = [a.dict() for a in actor_service.list_actors(project_path)] - actor_id = actor_suggest_service.suggest(description, actors) - return success_response(data={"actorId": actor_id}) - except Exception as e: - logger.exception("Error suggesting actor") - return error_response(str(e), "Failed to suggest actor", 500) - -# ─────────────────────────────────────────────────────────────────── -# GET /api/actors/?projectPath=… → get a single actor -# PUT /api/actors/?projectPath=… → update an actor -# DELETE /api/actors/?projectPath=…→ delete an actor -# ─────────────────────────────────────────────────────────────────── -@actor_bp.route("/api/actors/", methods=["GET", "PUT", "DELETE"]) -def actor_item(actor_id: int): - project_path = _get_project_path() - - if request.method == "GET": - try: - actors = actor_service.list_actors(project_path) # Need to list all to find by ID - actor = next((a for a in actors if a.id == actor_id), None) - if actor is None: - return error_response("Actor not found", status_code=404) - return success_response(data=actor.dict()) - except ValueError as e: # Catch invalid project path - return error_response(str(e), status_code=400) - except Exception as e: - logger.exception(f"Error getting actor {actor_id} for project: {project_path}") - return error_response(str(e), "Failed to retrieve actor", 500) - - elif request.method == "PUT": - patch = request.get_json(silent=True) or {} - try: - updated_actor = actor_service.update_actor(actor_id, patch, project_path) - if updated_actor is None: - return error_response("Actor not found", status_code=404) - return success_response(data=updated_actor.dict()) - except ValidationError as e: - return error_response(f"Validation error: {e.json()}", status_code=400) - except ValueError as e: - return error_response(str(e), status_code=400) - except Exception as e: - logger.exception(f"Error updating actor {actor_id} for project: {project_path}") - return error_response(str(e), "Failed to update actor", 500) - - # ---------- DELETE ---------- - elif request.method == "DELETE": - try: - deleted = actor_service.delete_actor(actor_id, project_path) - if not deleted: - return error_response("Actor not found", status_code=404) - return "", 204 # 204 No Content - except ValueError as e: # Catch invalid project path - return error_response(str(e), status_code=400) - except Exception as e: - logger.exception(f"Error deleting actor {actor_id} for project: {project_path}") - return error_response(str(e), "Failed to delete actor", 500) \ No newline at end of file diff --git a/python_backend/controllers/actor_generator_controller.py b/python_backend/controllers/actor_generator_controller.py deleted file mode 100644 index 732d050..0000000 --- a/python_backend/controllers/actor_generator_controller.py +++ /dev/null @@ -1,43 +0,0 @@ -from __future__ import annotations - -import logging -from flask import Blueprint, request - -from models.actor_generate_request import ActorGenerateRequest -from services.actor_generator_service import ActorGeneratorService -# Import standardized exceptions -from services.service_exceptions import ConfigurationError, UpstreamServiceError -from utils.response_utils import success_response, error_response - -logger = logging.getLogger(__name__) - -actor_gen_bp = Blueprint("actor_gen_bp", __name__, url_prefix="/api/actors") -_service = ActorGeneratorService() - - -@actor_gen_bp.post("/generate") -def generate_actors_endpoint(): - payload = request.get_json(silent=True) or {} - try: - req = ActorGenerateRequest.from_dict(payload) - except ValueError as exc: # Catch validation errors from from_dict - return error_response(str(exc), status_code=400) - except Exception as exc: # Catch any other unexpected error during request model creation - logger.error("Error creating ActorGenerateRequest: %s", exc) - return error_response(f"Invalid request payload: {exc}", status_code=400) - - try: - actors, raw = _service.generate_actors(req) - return success_response(data={"actors": actors, "llmRaw": raw}) - # Use standardized exceptions - except ConfigurationError as exc: - logger.error("Actor generation configuration error: %s", exc) - return error_response(str(exc), status_code=500) # Internal server config error - except UpstreamServiceError as exc: - logger.error("Actor generation upstream error: %s", exc) - return error_response(str(exc), status_code=502) # Bad Gateway from upstream - except Exception as exc: # pragma: no cover - logger.exception("Actor generation failed with an unexpected error.") - # For general exceptions, it's often better to return a generic error message - # unless in debug mode. - return error_response("An unexpected error occurred during actor generation.", status_code=500) \ No newline at end of file diff --git a/python_backend/models/actor_generate_request.py b/python_backend/models/actor_generate_request.py deleted file mode 100644 index c282485..0000000 --- a/python_backend/models/actor_generate_request.py +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass, field -from typing import List, Any, Optional - -@dataclass(slots=True) -class ActorGenerateRequest: - """Validated payload for /api/actors/generate.""" - treePaths: List[str] = field(default_factory=list) - baseDir: Optional[str] = None - - @classmethod - def from_dict(cls, data: dict[str, Any]) -> "ActorGenerateRequest": - if not isinstance(data, dict): - raise ValueError("JSON body must be an object.") - tree_paths = data.get("treePaths") - base_dir = data.get("baseDir") or data.get("projectPath") - if ( - not isinstance(tree_paths, list) - or not all(isinstance(p, str) for p in tree_paths) - or not tree_paths - ): - raise ValueError("'treePaths' must be a non-empty array of strings.") - if base_dir is not None and not isinstance(base_dir, str): - raise ValueError("'baseDir' must be a string if provided.") - return cls(treePaths=[p.strip() for p in tree_paths], baseDir=base_dir) diff --git a/python_backend/models/actor_model.py b/python_backend/models/actor_model.py deleted file mode 100644 index f6ed689..0000000 --- a/python_backend/models/actor_model.py +++ /dev/null @@ -1,28 +0,0 @@ -# python_backend/models/actor_model.py -from typing import List, Optional -from pydantic import BaseModel, Field, ValidationError, validator - -class ActorModel(BaseModel): - """ - Pydantic model for an Actor in the project. - Actors represent distinct roles, users, or external systems interacting with the product. - """ - id: int = Field(..., description="Unique identifier for the actor") - name: str = Field(..., min_length=1, max_length=100, description="Name of the actor (e.g., 'Developer', 'LLM Provider')") - role: str = Field(..., min_length=1, description="Description of the actor's role or primary activities") - permissions: List[str] = Field(default_factory=list, description="List of key actions or access rights within the system") - goals: List[str] = Field(default_factory=list, description="List of primary goals or problems the actor solves using the system") - - class Config: - # Allows Pydantic to work with non-dict input (e.g., from ORM models if applicable) - from_attributes = True - - @validator('permissions', 'goals', pre=True, always=True) - def ensure_list_of_strings(cls, v): - if v is None: - return [] - if not isinstance(v, list): - raise ValueError('must be a list') - if not all(isinstance(item, str) for item in v): - raise ValueError('all items must be strings') - return v \ No newline at end of file diff --git a/python_backend/services/actor_generator_service.py b/python_backend/services/actor_generator_service.py deleted file mode 100644 index 827c5f0..0000000 --- a/python_backend/services/actor_generator_service.py +++ /dev/null @@ -1,340 +0,0 @@ -from __future__ import annotations - -import os -import json -import logging -import pathlib -from typing import Any, Dict, List, Optional, Tuple - -import httpx - -from models.actor_generate_request import ActorGenerateRequest -from services.service_exceptions import ( - wrap_service_methods, - ConfigurationError, - UpstreamServiceError, - InvalidInputError, # Potentially useful, though not used in this revision -) -from repositories.file_storage import FileStorageRepository -from services.codemap_service import CodemapService - -logger = logging.getLogger(__name__) - -@wrap_service_methods # Applied decorator -class ActorGeneratorService: - _URL: str = "https://openrouter.ai/api/v1/chat/completions" - _DEFAULT_MODEL: str = "meta-llama/llama-4-maverick:free" # Example, ensure it's a good JSON model - - _SCHEMA: Dict[str, Any] = { - "name": "actors", - "strict": True, - "schema": { - "type": "object", - "properties": { - "actors": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "role": {"type": "string"}, - "permissions": {"type": "array", "items": {"type": "string"}}, - "goals": {"type": "array", "items": {"type": "string"}}, - }, - "required": ["name", "role"], - }, - } - }, - "required": ["actors"], - "additionalProperties": False, - }, - } - - _README_MAX_CHARS = 8000 - _TREE_MAX_LINES = 30000 - _SUMMARY_MAX_CHARS = 10000 - _TOKEN_HARD_LIMIT = 45000 - - # Removed local ConfigError and UpstreamError classes - - def __init__(self) -> None: - self.api_key: Optional[str] = os.getenv("OPENROUTER_API_KEY") - self.model: str = os.getenv("ACTOR_MODEL", self._DEFAULT_MODEL) - if not self.api_key: - # This warning is fine at init; generate_actors will raise ConfigurationError if key is missing during use. - logger.warning( - "ActorGeneratorService started WITHOUT OPENROUTER_API_KEY – actor generation will fail." - ) - self._storage = FileStorageRepository() - self._codemap = CodemapService(storage_repo=self._storage) - - # ------------------------------------------------------------------ public - def generate_actors( - self, request_obj: ActorGenerateRequest, *, timeout: float = 30.0 - ) -> Tuple[List[Dict[str, Any]], str]: - if not self.api_key: - # Raise standardized ConfigurationError - raise ConfigurationError("OPENROUTER_API_KEY is not set on the server.") - - prompt = self._build_prompt(request_obj) - logger.info("▶︎ Actor generation prompt (first 1000 chars)\n%s", prompt[:1000]) - - payload: Dict[str, Any] = { - "model": self.model, - "messages": [ - { - "role": "system", - "content": ( - "You are a strict JSON generator. " - "Identify user actors according to the provided guide and project context. " - "Return ONLY the JSON matching the schema." - ), - }, - {"role": "user", "content": prompt}, - ], - "temperature": 0.3, - "max_tokens": 1024, # Increased slightly for potentially complex actor lists - "response_format": {"type": "json_schema", "json_schema": self._SCHEMA}, - "structured_outputs": True, - } - headers: Dict[str, str] = { - "Authorization": f"Bearer {self.api_key}", - "Content-Type": "application/json", - "HTTP-Referer": os.getenv("HTTP_REFERER", "http://localhost"), # Add a default referer - "X-Title": os.getenv("X_TITLE", "CodeToPromptGenerator"), # Add a default title - } - - try: - with httpx.Client(timeout=timeout) as client: - response = client.post(self._URL, json=payload, headers=headers) - response.raise_for_status() # Will raise HTTPStatusError for 4xx/5xx - except httpx.TimeoutException as exc: - logger.error("Timeout calling OpenRouter for actor generation: %s", exc) - raise UpstreamServiceError("OpenRouter request timed out") from exc - except httpx.RequestError as exc: # Covers ConnectError, ReadError, etc. - logger.error("Request error calling OpenRouter for actor generation: %s", exc) - raise UpstreamServiceError(f"Could not connect to OpenRouter: {exc}") from exc - except httpx.HTTPStatusError as exc: - error_detail = "Unknown error" - try: - error_response_json = exc.response.json() - error_detail = error_response_json.get("error", {}).get("message", exc.response.text) - except json.JSONDecodeError: - error_detail = exc.response.text - logger.error( - "HTTP error from OpenRouter (%s) for actor generation: %.200s", - exc.response.status_code, - error_detail - ) - raise UpstreamServiceError( - f"OpenRouter API error ({exc.response.status_code}): {error_detail}" - ) from exc - - try: - data = response.json() - if not isinstance(data, dict) or "choices" not in data or not data["choices"]: - logger.error("Invalid response structure from OpenRouter (actor generation): %s", data) - raise UpstreamServiceError("Invalid response structure from OpenRouter (actor generation)") - - choice = data["choices"][0] - if not isinstance(choice, dict) or "message" not in choice: - logger.error("Invalid 'choice' structure in OpenRouter response: %s", choice) - raise UpstreamServiceError("Invalid 'choice' structure in OpenRouter response") - - message = choice.get("message", {}) - if not isinstance(message, dict) or "content" not in message: - logger.error("Invalid 'message' structure in OpenRouter choice: %s", message) - raise UpstreamServiceError("Invalid 'message' structure in OpenRouter choice") - - content = message.get("content", "").strip() - if not content: # Handle empty content string from LLM - logger.warning("LLM returned empty content for actor generation.") - raise UpstreamServiceError("LLM returned empty content.") - - except json.JSONDecodeError as exc: # If response.json() itself fails - logger.error("Failed to parse main OpenRouter JSON response: %s. Response text: %.200s", exc, response.text) - raise UpstreamServiceError(f"Malformed JSON response from OpenRouter gateway. Status: {response.status_code}") from exc - except Exception as exc: # Catch other potential errors during parsing of main response - logger.error("Failed to parse OpenRouter response: %s", exc) - raise UpstreamServiceError("Unexpected error parsing OpenRouter response.") from exc - - logger.debug("Raw LLM content for actors: %.500s", content) # Log the content before parsing - - try: - # The content string itself is expected to be a JSON string by the schema - obj = json.loads(content) - actors_data = obj.get("actors") - if not isinstance(actors_data, list): - logger.error("LLM content missing 'actors' array or it's not a list. Content: %.200s", content) - raise ValueError("LLM content missing 'actors' array or not a list.") - except json.JSONDecodeError as exc: - logger.error("Failed to decode actors JSON from LLM content: %s. Content: %.500s", exc, content) - raise UpstreamServiceError(f"Failed to parse JSON from model's message. Error: {exc}. Content prefix: {content[:200]}...") from exc - except ValueError as exc: # Catch our specific ValueError for missing 'actors' - raise UpstreamServiceError(str(exc)) from exc - - # Add IDs if missing (though schema doesn't define ID, frontend might expect it) - # This part should ideally be handled by the schema or how actors are stored/used. - # For now, we'll keep the ID assignment if it's helpful for frontend. - processed_actors: List[Dict[str, Any]] = [] - for i, actor_item_raw in enumerate(actors_data, 1): - if isinstance(actor_item_raw, dict): - # Basic validation against expected keys from schema - actor_item = { - "name": actor_item_raw.get("name"), - "role": actor_item_raw.get("role"), - "permissions": actor_item_raw.get("permissions", []), - "goals": actor_item_raw.get("goals", []) - } - if not actor_item["name"] or not actor_item["role"]: - logger.warning("Skipping actor due to missing name or role: %s", actor_item_raw) - continue - actor_item["id"] = i # Assign a temporary ID - processed_actors.append(actor_item) - else: - logger.warning("Skipping non-dict item in actors list: %s", actor_item_raw) - - if not processed_actors and actors_data: # If all items were invalid - raise UpstreamServiceError("Model returned actor data, but none were valid.") - - return processed_actors, content - - - # ---------------------------------------------------------------- helpers - def _build_prompt(self, req: ActorGenerateRequest) -> str: - tree_block = self._tree_text(req.treePaths) - graph_block = self._build_graph(req) - readme_block = self._read_readme(req.baseDir) - guide = ( - "You are an expert at identifying actors (personas, users, roles, or external systems) " - "that interact with a software project. Analyze the provided project context carefully.\n" - "Product Context:\n" - "The project is a 'Code to Prompt Generator' tool. It allows software developers to select files from their local codebase, " - "write meta-instructions (like persona, output format) and main instructions (the specific task for the LLM), " - "and then combines these with the content of selected files to generate a comprehensive prompt for a Large Language Model (LLM). " - "The tool features project tree navigation, file selection with token counts, exclusion management (global and per-project), " - "meta prompt saving/loading, AI-powered smart file selection, AI-powered prompt refinement, " - "and integrated task management (Kanban, To-Do, User Stories).\n\n" - "Follow this step-by-step guide to identify actors:\n" - "1. Understand the product context (software tool for developers to generate LLM prompts from code).\n" - "2. Identify primary users (e.g., different types of developers, QA engineers, product managers if they use it for prompt generation for documentation) and any external systems it interacts with (e.g., LLM APIs, file system).\n" - "3. For each identified actor, extract its name, define its primary role in relation to the 'Code to Prompt Generator'.\n" - "4. List key permissions or actions each actor can perform *within this specific tool*.\n" - "5. List primary goals or problems each actor solves *using this specific tool*.\n" - "Focus *only* on actors directly relevant to the 'Code to Prompt Generator' tool itself. " - "Aim for 2-4 distinct, high-level actors. Avoid overly granular roles unless clearly distinct in function within the tool." - ) - return ( - f"{guide}\n\n" - f"### README (if available)\n{readme_block}\n\n" - f"### Project Tree (Abbreviated)\n{tree_block}\n\n" - f"### File Graph (Key File Summaries)\n{graph_block}\n\n" - "Based on all the above, provide the list of actors in the specified JSON format." - ) - - def _read_readme(self, base_dir: Optional[str]) -> str: - if not base_dir: - return "N/A" - for name in ("README.md", "readme.md", "Readme.md"): - path = os.path.join(base_dir, name) - if os.path.isfile(path): - try: - txt = self._storage.read_text(path) or "" - txt = txt.strip()[: self._README_MAX_CHARS] - if len(txt) < self._README_MAX_CHARS: - return txt if txt else "N/A (README is empty)" - return txt + "\n... (README truncated)" - except Exception as exc: - logger.warning("Failed to read README %s: %s", path, exc) - return "N/A (Error reading README)" - return "N/A (README not found)" - - def _tree_text(self, paths: List[str]) -> str: - if not paths: - return "N/A (Project tree not provided or empty)" - out: List[str] = [] - for rel in paths[: self._TREE_MAX_LINES]: - depth = rel.count("/") - out.append(" " * depth + "• " + rel.rsplit("/", 1)[-1]) - if len(paths) > self._TREE_MAX_LINES: - out.append(f"… (+{len(paths) - self._TREE_MAX_LINES} more files, truncated)") - return "\n".join(out) - - def _build_graph(self, req: ActorGenerateRequest) -> str: - if not req.baseDir or not os.path.isdir(req.baseDir) or not req.treePaths: - return "N/A (File summaries not available)" - - # Heuristic: select a few key files for summary (e.g., entry points, core services) - # This is a simplified approach. A more advanced one would involve more context. - key_files_heuristics = ["main.py", "app.py", "index.ts", "index.js", "service.py", "controller.py", "store.ts", "hook.ts"] - paths_for_summary = [p for p in req.treePaths if any(hf in p.lower() for hf in key_files_heuristics)] - if not paths_for_summary and req.treePaths: # Fallback if no heuristic match - paths_for_summary = req.treePaths[:5] # Summarize first 5 files - else: - paths_for_summary = paths_for_summary[:5] # Limit to 5 key files - - if not paths_for_summary: - return "N/A (No key files identified for summary)" - - blocks: List[str] = [] - used_tokens = 0 - for rel in paths_for_summary: - abs_path = os.path.normpath(os.path.join(req.baseDir, rel)) - if not os.path.isfile(abs_path): - continue - summary = self._file_summary(abs_path, rel) - if summary == "N/A (Error extracting summary)": # Skip problematic files - continue - - est_tokens = len(summary) // 4 # Rough estimate - if used_tokens + est_tokens > self._TOKEN_HARD_LIMIT // 3: # Limit graph block size - blocks.append("... (File graph summaries truncated due to size limit)") - break - lang = pathlib.Path(abs_path).suffix.lstrip(".") or "txt" - blocks.append( - f'\n```{lang}\n{summary}\n```\n' - ) - used_tokens += est_tokens - return "\n\n".join(blocks) if blocks else "N/A (Could not generate file summaries)" - - def _file_summary(self, abs_path: str, rel_path: str) -> str: - try: - # Using base directory of the file for codemap extraction - file_dir = os.path.dirname(abs_path) - file_name = os.path.basename(abs_path) - codemap = self._codemap.extract_codemap(file_dir, [file_name]) - data = codemap.get(file_name) - except Exception as exc: - logger.warning("Codemap extraction failed for %s: %s", rel_path, exc) - return "N/A (Error extracting summary)" # Return specific string for error - - if data and "error" not in data: - parts: List[str] = [] - cls = data.get("classes") - if isinstance(cls, list) and cls: - parts.append(f"Classes: {', '.join(cls[:5])}") # Limit displayed items - fns = data.get("functions") - if isinstance(fns, list) and fns: - parts.append(f"Functions: {', '.join(fns[:8])}") # Limit displayed items - refs = data.get("references") - if isinstance(refs, list) and refs: - parts.append(f"Key Refs: {', '.join(refs[:10])}") # Limit displayed items - if parts: - s = "; ".join(parts) - return s[: self._SUMMARY_MAX_CHARS] if len(s) > self._SUMMARY_MAX_CHARS else s - return "No key symbols found." - elif data and data.get("error"): - return f"N/A (Summary error: {data.get('error')})" - - # Fallback if codemap fails or returns no data (should be rare with new error handling) - text = self._storage.read_text(abs_path) or "" - return self._cheap_summary(text) - - - def _cheap_summary(self, text: str, max_len: int = _SUMMARY_MAX_CHARS) -> str: - if not text: - return "File is empty." - # Basic summary: first few lines - summary_lines = text.splitlines()[:5] - summary = "\n".join(summary_lines).strip() - return summary[:max_len] if len(summary) > max_len else summary \ No newline at end of file diff --git a/python_backend/services/actor_service.py b/python_backend/services/actor_service.py deleted file mode 100644 index d00cbfc..0000000 --- a/python_backend/services/actor_service.py +++ /dev/null @@ -1,198 +0,0 @@ -# python_backend/services/actor_service.py -import os -import json -import logging -from typing import List, Dict, Any, Optional -from datetime import datetime -from services.service_exceptions import wrap_service_methods -from repositories.file_storage import FileStorageRepository -from models.actor_model import ActorModel # Import the Pydantic model -from pydantic import ValidationError - -logger = logging.getLogger(__name__) -@wrap_service_methods -class ActorService: - def __init__(self, storage_repo: FileStorageRepository): - self.storage = storage_repo - self._actors_file = "actors.json" - self.codetoprompt_dir_name = ".codetoprompt" - self._default_actors_data = [ - { - "id": 1, - "name": "Developer", - "role": "Primary user who interacts with the application's UI to generate LLM prompts from code and manage project-related data.", - "permissions": [ - "Manage project path and file selection", - "Manage global and local file exclusions", - "Manage meta prompts and main instructions", - "Trigger AI-powered smart selection and prompt refinement", - "Manage Kanban tasks, Todo items, and User Stories", - "Manage selection groups of files", - "Copy generated prompt to clipboard", - "Configure OpenRouter API key" - ], - "goals": [ - "Efficiently create context-rich LLM prompts from codebase", - "Organize development tasks and requirements within the tool", - "Leverage AI for improved prompt engineering workflow" - ] - }, - { - "id": 2, - "name": "LLM Provider", - "role": "External AI service (e.g., OpenRouter) that provides language model inference capabilities for prompt refinement and smart file selection.", - "permissions": [ - "Receive text and code context for processing", - "Return refined prompt text", - "Return a list of suggested file paths" - ], - "goals": [ - "Process natural language and code context using AI models", - "Support intelligent automation features within the tool" - ] - }, - { - "id": 3, - "name": "Local File System", - "role": "The local storage system (disk) where project files reside and application data is persistently stored.", - "permissions": [ - "Read project directories and file contents", - "Write application configuration files (e.g., .codetoprompt/ files, ignoreDirs.txt)", - "Store and retrieve JSON data for tasks, stories, and selection groups" - ], - "goals": [ - "Ensure persistence of user data and configurations", - "Provide access to the local codebase for prompt generation" - ] - } - ] - - def _get_project_dir(self, project_path: Optional[str]) -> str: - """ - Get the .codetoprompt directory for a project or the global one. - Ensures the directory exists. - """ - if not project_path: - target_dir = os.path.expanduser(f"~/{self.codetoprompt_dir_name}") - else: - if not os.path.isdir(project_path): - raise ValueError(f"Project path '{project_path}' is not a valid directory.") - target_dir = os.path.join(project_path, self.codetoprompt_dir_name) - - try: - os.makedirs(target_dir, exist_ok=True) - except OSError as e: - raise IOError(f"Failed to create directory {target_dir}: {e}") from e - return target_dir - - def _get_actors_file_path(self, project_path: Optional[str]) -> str: - """Constructs the full path to the actors JSON file.""" - project_dir = self._get_project_dir(project_path) - return os.path.join(project_dir, self._actors_file) - - def _load_actors(self, project_path: Optional[str]) -> List[ActorModel]: - """ - Loads actors from the project's JSON file. - If the file doesn't exist, it initializes with default actors and saves them. - """ - file_path = self._get_actors_file_path(project_path) - data = self.storage.read_json(file_path, default={"actors": []}) - - actors_raw = data.get("actors", []) if isinstance(data, dict) else [] - - # If no actors are loaded and it's a new project or empty file, populate with defaults - if not actors_raw: - try: - # Validate and convert default data to ActorModel instances - actors = [ActorModel(**actor_data) for actor_data in self._default_actors_data] - self._save_actors(actors, project_path) # Save defaults to disk - logger.info(f"Initialized actors file with default actors at {file_path}") - return actors - except ValidationError as e: - logger.error(f"Failed to validate default actor data: {e}") - return [] - except IOError as e: - logger.error(f"Failed to save default actors to {file_path}: {e}") - return [] - - # Validate and convert loaded data to ActorModel instances - loaded_actors: List[ActorModel] = [] - for actor_data in actors_raw: - try: - loaded_actors.append(ActorModel(**actor_data)) - except ValidationError as e: - logger.warning(f"Skipping invalid actor data in {file_path}: {actor_data}. Error: {e}") - except TypeError as e: # Handle cases where data is not a dict - logger.warning(f"Skipping malformed actor data in {file_path}: {actor_data}. Error: {e}") - - return loaded_actors - - def _save_actors(self, actors: List[ActorModel], project_path: Optional[str]) -> None: - """Saves a list of ActorModel instances to the project's JSON file.""" - file_path = self._get_actors_file_path(project_path) - # Convert Pydantic models back to dicts for JSON serialization - # Pydantic v1.x uses ``dict()`` instead of ``model_dump()``. - actors_data = [actor.dict() for actor in actors] - self.storage.write_json(file_path, {"actors": actors_data}) - - def list_actors(self, project_path: Optional[str]) -> List[ActorModel]: - """Returns all actors for the given project path (or global defaults).""" - return self._load_actors(project_path) - - def create_actor(self, actor_data: Dict[str, Any], project_path: Optional[str]) -> ActorModel: - """ - Creates a new actor. - Expects actor_data to be a dict suitable for ActorModel (without ID). - """ - actors = self._load_actors(project_path) - - # Generate new ID - new_id = max((a.id for a in actors), default=0) + 1 - - # Add ID and validate with Pydantic model - full_actor_data = {"id": new_id, **actor_data} - try: - new_actor = ActorModel(**full_actor_data) - except ValidationError as e: - raise ValueError(f"Invalid actor data: {e.errors()}") from e - - actors.append(new_actor) - self._save_actors(actors, project_path) - return new_actor - - def update_actor(self, actor_id: int, updates: Dict[str, Any], project_path: Optional[str]) -> Optional[ActorModel]: - """ - Updates an existing actor by ID. - Updates dict contains fields to change. - """ - actors = self._load_actors(project_path) - actor_index = next((i for i, a in enumerate(actors) if a.id == actor_id), None) - - if actor_index is None: - return None - - # Apply updates and re-validate the entire model - existing_actor = actors[actor_index].dict() # Get dict representation - updated_data = {**existing_actor, **updates} - - try: - updated_actor = ActorModel(**updated_data) - except ValidationError as e: - raise ValueError(f"Invalid update data: {e.errors()}") from e - - actors[actor_index] = updated_actor - self._save_actors(actors, project_path) - return updated_actor - - def delete_actor(self, actor_id: int, project_path: Optional[str]) -> bool: - """Deletes an actor by ID.""" - actors = self._load_actors(project_path) - original_count = len(actors) - - actors = [a for a in actors if a.id != actor_id] - - if len(actors) < original_count: - self._save_actors(actors, project_path) - return True - - return False \ No newline at end of file diff --git a/python_backend/services/actor_suggest_service.py b/python_backend/services/actor_suggest_service.py deleted file mode 100644 index f6abd59..0000000 --- a/python_backend/services/actor_suggest_service.py +++ /dev/null @@ -1,39 +0,0 @@ -# python_backend/services/actor_suggest_service.py -"""Simple actor suggestion service. - -This service provides a naive implementation that chooses an actor based on -keyword matching. It can be extended to use LLMs similar to ``AutoselectService`` -when an API key is available. -""" -from __future__ import annotations - -import logging -from typing import List, Dict, Any - -from services.service_exceptions import wrap_service_methods - -logger = logging.getLogger(__name__) -@wrap_service_methods -class ActorSuggestService: - def suggest(self, description: str, actors: List[Dict[str, Any]]) -> int | None: - """Return the actor id that best matches *description*.""" - if not description or not actors: - return None - desc = description.lower() - best_id = actors[0].get("id") - best_score = 0 - for actor in actors: - score = 0 - for field in ("name", "role"): - val = str(actor.get(field, "")).lower() - if val and val in desc: - score += 2 - for field in ("permissions", "goals"): - items = actor.get(field) or [] - if isinstance(items, list): - score += sum(1 for it in items if str(it).lower() in desc) - if score > best_score: - best_score = score - best_id = actor.get("id") - logger.info("Suggested actor %s with score %s", best_id, best_score) - return best_id diff --git a/python_backend/services/user_story_service.py b/python_backend/services/user_story_service.py index e59f9fb..be1ce8c 100644 --- a/python_backend/services/user_story_service.py +++ b/python_backend/services/user_story_service.py @@ -88,7 +88,6 @@ def create_story(self, story_data: Dict[str, Any], project_path: Optional[str]) new_story = { "id": new_id, "title": story_data["title"], - "actorId": story_data.get("actorId"), "description": story_data.get("description"), "acceptanceCriteria": story_data.get("acceptanceCriteria"), "priority": story_data.get("priority", "medium"), @@ -229,4 +228,4 @@ def get_tasks_for_story(self, story_id: int, project_path: Optional[str]) -> Lis def get_stories_for_task(self, task_id: int, project_path: Optional[str]) -> List[int]: """Get all user story IDs associated with a task""" relations = self._load_relations(project_path) - return [r["userStoryId"] for r in relations if r["taskId"] == task_id] \ No newline at end of file + return [r["userStoryId"] for r in relations if r["taskId"] == task_id] diff --git a/services/actorServiceHooks.ts b/services/actorServiceHooks.ts deleted file mode 100644 index a26c2cf..0000000 --- a/services/actorServiceHooks.ts +++ /dev/null @@ -1,141 +0,0 @@ -// services/actorServiceHooks.ts -import { useCallback } from "react"; -import { z } from "zod"; -import { fetchApi } from "@/services/apiService"; -import { useActorStore } from "@/stores/useActorStore"; -import { useProjectStore } from "@/stores/useProjectStore"; -import { useAppStore } from "@/stores/useAppStore"; -import { Actor, ActorSchema } from '@/types'; - -/* ------------------- zod schemas ------------------- */ -const ArraySchema = z.array(ActorSchema); - -function safeParse(schema: T, data: unknown): z.infer | null { - if (data === null || data === undefined) { - return null; - } - - const result = schema.safeParse(data); - if (!result.success) { - console.error("Schema validation failed", result.error.flatten()); - useAppStore.getState().setError("Received malformed data from server. Check console for details."); - return null; - } - return result.data; -} - -export function useActorService() { - const { projectPath } = useProjectStore(); - const { - setActors, - setLoading, - setSaving, - addActorOptimistic, - updateActorOptimistic, - removeActorOptimistic, - } = useActorStore(); - - const { setError } = useAppStore(); - - const baseQueryParam = projectPath ? `projectPath=${encodeURIComponent(projectPath)}` : ''; - const baseEndpoint = "/api/actors"; - - /* ---------------- list actors ---------------- */ - const loadActors = useCallback(async () => { - // Actors can be global or project-specific - setLoading(true); - const raw = await fetchApi(`${baseEndpoint}?${baseQueryParam}`); - const data = safeParse(ArraySchema, raw); - setActors(data ?? []); - setLoading(false); - }, [projectPath, setActors, setLoading, baseQueryParam]); // projectPath is a dependency for baseQueryParam - - /* ---------------- create actor ---------------- */ - const createActor = useCallback(async (draft: Omit) => { - setSaving(true); - setError(null); - - // Optimistic update - // Assign a temporary ID for optimistic rendering - const tempId = Date.now(); // Simple timestamp-based ID - const optimisticActor: Actor = { id: tempId, ...draft }; - addActorOptimistic(optimisticActor); - - const raw = await fetchApi(`${baseEndpoint}?${baseQueryParam}`, { - method: "POST", - body: JSON.stringify(draft), - }); - - const actor = safeParse(ActorSchema, raw); - - if (actor) { - // Replace optimistic actor with real one or just reload - // For simplicity and consistency with other services, we'll reload. - await loadActors(); - } else { - // Revert optimistic update on failure - removeActorOptimistic(tempId); - // Ensure UI is consistent if API fails - await loadActors(); - } - setSaving(false); - return actor; - }, [addActorOptimistic, loadActors, removeActorOptimistic, setSaving, setError, baseQueryParam]); - - /* ---------------- update actor ---------------- */ - const updateActor = useCallback(async (actorData: Partial & Pick) => { - setSaving(true); - setError(null); - - // Optimistic update - updateActorOptimistic(actorData.id, actorData); - - const url = `${baseEndpoint}/${actorData.id}?${baseQueryParam}`; - - const raw = await fetchApi(url, { - method: "PUT", - body: JSON.stringify(actorData), - }); - - const updatedActor = safeParse(ActorSchema, raw); - - if (updatedActor) { - // For simplicity and consistency with other services, we'll reload. - await loadActors(); - } else { - // Revert optimistic update on failure - await loadActors(); // Reload to get actual state - } - setSaving(false); - return updatedActor; - }, [updateActorOptimistic, loadActors, setSaving, setError, baseQueryParam]); - - /* ---------------- delete actor ---------------- */ - const deleteActor = useCallback(async (actorId: number) => { - setSaving(true); - setError(null); - - // Optimistic update - removeActorOptimistic(actorId); - - const url = `${baseEndpoint}/${actorId}?${baseQueryParam}`; - // fetchApi returns null for 204 No Content, which is expected for DELETE success - const response = await fetchApi(url, { method: 'DELETE' }); - - setSaving(false); - const wasErrorSet = useAppStore.getState().error !== null; - if (wasErrorSet) { - // Revert optimistic update on failure - await loadActors(); // Reload to get actual state - return false; - } - return true; // Success - }, [removeActorOptimistic, loadActors, setSaving, setError, baseQueryParam]); - - return { - loadActors, - createActor, - updateActor, - deleteActor, - }; -} \ No newline at end of file diff --git a/services/actorSuggestServiceHooks.ts b/services/actorSuggestServiceHooks.ts deleted file mode 100644 index fc521c3..0000000 --- a/services/actorSuggestServiceHooks.ts +++ /dev/null @@ -1,20 +0,0 @@ -// services/actorSuggestServiceHooks.ts -import { useCallback, useState } from "react"; -import { fetchApi } from "@/services/apiService"; - -export function useActorSuggestService() { - const [isSuggesting, setIsSuggesting] = useState(false); - - const suggestActor = useCallback(async (description: string): Promise => { - if (!description.trim()) return null; - setIsSuggesting(true); - const res = await fetchApi<{ actorId: number }>("/api/actors/suggest", { - method: "POST", - body: JSON.stringify({ description }), - }); - setIsSuggesting(false); - return res ? res.actorId : null; - }, []); - - return { suggestActor, isSuggesting }; -} diff --git a/services/actorWizardServiceHooks.ts b/services/actorWizardServiceHooks.ts deleted file mode 100644 index 14cc135..0000000 --- a/services/actorWizardServiceHooks.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { useCallback, useState } from "react"; -import { fetchApi } from "@/services/apiService"; -import { useProjectStore } from "@/stores/useProjectStore"; -import { useActorStore } from "@/stores/useActorStore"; -import { flattenTree } from "@/lib/fileFilters"; -import { ActorSchema } from "@/types"; -import { z } from "zod"; -import { useAppStore } from "@/stores/useAppStore"; - -export function useActorWizardService() { - const projectStore = useProjectStore; - const actorStore = useActorStore; - const { setError } = useAppStore(); - const [isGenerating, setIsGenerating] = useState(false); - - const generateActors = useCallback(async () => { - const { projectPath, fileTree } = projectStore.getState(); - if (!projectPath || isGenerating) return; - const treePaths = flattenTree(fileTree); - setIsGenerating(true); - const res = await fetchApi<{ actors: unknown }>("/api/actors/generate", { - method: "POST", - body: JSON.stringify({ baseDir: projectPath, treePaths }), - }); - setIsGenerating(false); - if (!res) return; - const parsed = z.array(ActorSchema).safeParse(res.actors); - if (!parsed.success) { - console.error("Actor wizard schema validation failed", parsed.error); - setError("Received malformed data from server."); - return; - } - actorStore.getState().setActors(parsed.data); - }, [isGenerating, projectStore, actorStore, setError]); - - return { generateActors, isGenerating }; -} diff --git a/services/autoSelectServiceHooks.ts b/services/autoSelectServiceHooks.ts index a5d6733..d07ea69 100644 --- a/services/autoSelectServiceHooks.ts +++ b/services/autoSelectServiceHooks.ts @@ -23,37 +23,37 @@ export function useAutoSelectService() { /* ---- main action ---- */ const autoSelect = useCallback( async (opts?: { debug?: boolean }) => { - const { projectPath, fileTree } = projectStore.getState(); + const { projectPath, fileTree, setSelectedFilePaths } = projectStore.getState(); if (!projectPath || isSelecting) return; - const treePaths = flattenTree(fileTree); // <── FIX: use helper, not store fn + const treePaths = flattenTree(fileTree); + if (treePaths.length === 0) return; const request: AutoSelectRequest = { - baseDir: projectPath, - treePaths, // full project tree + baseDir: projectPath, + treePaths, instructions: promptStore.getState().mainInstructions, }; - // const url = `/api/autoselect${opts?.debug ? "?debug=1" : ""}`; - const url = "/api/autoselect?debug=1"; + const url = `/api/autoselect${opts?.debug ? "?debug=1" : ""}`; + try { setIsSelecting(true); const res = await fetchApi(url, { method: "POST", - body: JSON.stringify(request), + body: JSON.stringify(request), }); - if (!res) return; - // Update selected paths in the project store - projectStore.getState().setSelectedFilePaths(res.selected); + if (!res?.selected?.length) return; + + setSelectedFilePaths(res.selected); - // Optional debug → pretty-print codemap if (opts?.debug && res.codemap) { console.groupCollapsed("🗺️ Codemap summaries"); - Object.entries(res.codemap).forEach(([file, info]) => - console.info(file, info), - ); + Object.entries(res.codemap).forEach(([file, info]) => { + console.info(file, info); + }); console.groupEnd(); } } finally { diff --git a/services/codemapServiceHooks.ts b/services/codemapServiceHooks.ts index a32f45f..55d2aec 100644 --- a/services/codemapServiceHooks.ts +++ b/services/codemapServiceHooks.ts @@ -38,5 +38,8 @@ export function useCodemapExtractor() { return res; }; - return useSWRMutation("/api/codemap/extract", poster); + return useSWRMutation>( + "/api/codemap/extract", + poster, + ); } diff --git a/start.bat b/start.bat deleted file mode 100644 index d09dcb8..0000000 --- a/start.bat +++ /dev/null @@ -1,3 +0,0 @@ -@echo off -echo Starting Code to Prompt Generator Tool... -node start.js \ No newline at end of file diff --git a/start.sh b/start.sh deleted file mode 100755 index 22a26af..0000000 --- a/start.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash -# --------------------------------------------------------------------------- -# start.sh — Development startup script for Code-to-Prompt (Tauri) -# -# This script now simply reminds the user of the correct Tauri command. -# Tauri's dev command handles starting the frontend dev server and the -# Tauri application shell, which in turn manages the Python backend. -# --------------------------------------------------------------------------- -set -euo pipefail - -echo "🚀 To start Code-to-Prompt Generator in development mode, run:" -echo "" -echo " npm run electron:dev -echo "" -echo "This will:" -echo " 1. Start the Next.js development server (via 'npm run dev')." -echo " 2. Launch the Tauri application window." -echo " 3. The Tauri app will start the Python backend process." -echo "" - -# Optional: You could add a check here to see if dependencies are installed -# if [ ! -d "node_modules" ] || [ ! -d "src-tauri/target" ]; then -# echo "⚠️ Dependencies might not be installed. Run 'npm install' first." -# fi - -exit 0 \ No newline at end of file diff --git a/stores/useActorStore.ts b/stores/useActorStore.ts deleted file mode 100644 index 3253928..0000000 --- a/stores/useActorStore.ts +++ /dev/null @@ -1,58 +0,0 @@ -// stores/useActorStore.ts -import { create } from 'zustand'; -import { devtools } from 'zustand/middleware'; -import { immer } from 'zustand/middleware/immer'; -import { Actor } from '@/types'; - -interface ActorState { - actors: Actor[]; - isLoading: boolean; - isSaving: boolean; - - // Actions - setLoading: (flag: boolean) => void; - setSaving: (flag: boolean) => void; - setActors: (actors: Actor[]) => void; - - // Optimistic updates for better UX - addActorOptimistic: (actor: Actor) => void; - updateActorOptimistic: (id: number, updates: Partial) => void; - removeActorOptimistic: (id: number) => void; - - // Getters - getActorById: (id: number) => Actor | undefined; -} - -export const useActorStore = create()( - devtools( - immer((set, get) => ({ - actors: [], - isLoading: false, - isSaving: false, - - // Actions - setLoading: (flag) => set({ isLoading: flag }), - setSaving: (flag) => set({ isSaving: flag }), - setActors: (actors) => set({ actors }), - - // Optimistic updates - addActorOptimistic: (actor) => set((state) => { - state.actors.push(actor); - }), - updateActorOptimistic: (id, updates) => set((state) => { - const index = state.actors.findIndex(a => a.id === id); - if (index !== -1) { - state.actors[index] = { ...state.actors[index], ...updates }; - } - }), - removeActorOptimistic: (id) => set((state) => { - state.actors = state.actors.filter(a => a.id !== id); - }), - - // Getters - getActorById: (id) => { - return get().actors.find(a => a.id === id); - }, - })) - ) -); \ No newline at end of file diff --git a/stores/useExclusionStore.ts b/stores/useExclusionStore.ts index 50fff4b..45a2a27 100644 --- a/stores/useExclusionStore.ts +++ b/stores/useExclusionStore.ts @@ -7,13 +7,11 @@ interface ExclusionState { setGlobalExclusions: (exclusions: string[]) => void; localExclusions: string[]; setLocalExclusions: (exclusions: string[]) => void; - extensionFilters: string[]; // Renamed from filterExtensions for clarity + extensionFilters: string[]; setExtensionFilters: (filters: string[]) => void; addExtensionFilter: (filter: string) => void; removeExtensionFilter: (filter: string) => void; clearExtensionFilters: () => void; - extensionInput: string; // Keep input state local to component or here? Decide based on usage. Keep local for now. - // setExtensionInput: (input: string) => void; isLoadingGlobal: boolean; setIsLoadingGlobal: (loading: boolean) => void; isLoadingLocal: boolean; @@ -46,8 +44,6 @@ export const useExclusionStore = create((set, get) => ({ extensionFilters: state.extensionFilters.filter((f) => f !== filter) })), clearExtensionFilters: () => set({ extensionFilters: [] }), - // extensionInput: '', // Keep local to component managing the input - // setExtensionInput: (input) => set({ extensionInput: input }), isLoadingGlobal: false, setIsLoadingGlobal: (loading) => set({ isLoadingGlobal: loading }), isLoadingLocal: false, diff --git a/stores/useSelectionGroupStore.ts b/stores/useSelectionGroupStore.ts index 3b42928..eb2209b 100644 --- a/stores/useSelectionGroupStore.ts +++ b/stores/useSelectionGroupStore.ts @@ -16,6 +16,7 @@ interface GroupState { groups: PersistedGroups; /* CRUD helpers */ + createGroup :(projectPath: string, group: string, paths: string[]) => void; saveGroup :(projectPath: string, group: string, paths: string[]) => void; deleteGroup :(projectPath: string, group: string) => void; renameGroup :(projectPath: string, oldName: string, newName: string) => void; @@ -30,6 +31,9 @@ export const useSelectionGroupStore = create((set, get) => ({ /* init from localStorage (noop on server) */ groups: typeof window === 'undefined' ? {} : readLS(), + createGroup: (projectPath, group, paths) => + get().saveGroup(projectPath, group, paths), + saveGroup: (projectPath, group, paths) => set(state => { const next: PersistedGroups = { ...state.groups }; diff --git a/types/global.d.ts b/types/global.d.ts new file mode 100644 index 0000000..3b8f6f7 --- /dev/null +++ b/types/global.d.ts @@ -0,0 +1,11 @@ +declare module "picomatch" { + const picomatch: (pattern: string | string[], options?: Record) => (input: string) => boolean; + export default picomatch; +} + +declare module "react-window" { + export const FixedSizeList: any; + export const VariableSizeList: any; + export type ListOnScrollProps = any; + export type ListChildComponentProps = any; +} diff --git a/types/index.ts b/types/index.ts index 32c04d6..7681845 100644 --- a/types/index.ts +++ b/types/index.ts @@ -58,12 +58,16 @@ export type CodemapResponse = Record; /* ═══════════════ Auto‑select models ═══════════════ */ export interface AutoSelectRequest { - projectPath: string; instructions: string; treePaths: string[]; // flattened *relative* paths + baseDir?: string; // optional absolute project root } -export type AutoSelectResponse = string[]; // list of *relative* paths +export interface AutoSelectResponse { + selected: string[]; // list of *relative* paths + llmRaw?: string; // raw model output for debugging + codemap?: CodemapResponse; // optional debug summaries (when ?debug=1) +} /* █████ KANBAN ██████████████████████████████████████████████████████ */ export const KanbanStatusValues = ['todo', 'in-progress', 'done'] as const; @@ -111,7 +115,6 @@ export const TaskSchema = KanbanItemSchema.extend({ export interface UserStory { id: number; title: string; - actorId?: number | null; description?: string | null; acceptanceCriteria?: string | null; priority: KanbanPriority; // Reusing KanbanPriority @@ -124,7 +127,6 @@ export interface UserStory { export const UserStorySchema = z.object({ id: z.number().int().nonnegative(), title: z.string().min(1).max(256), // Max length from KanbanItemModel or adjust - actorId: z.number().int().optional().nullable(), description: z.string().optional().nullable(), acceptanceCriteria: z.string().optional().nullable(), priority: z.enum(KanbanPriorityValues), @@ -133,21 +135,3 @@ export const UserStorySchema = z.object({ createdAt: z.string().datetime({ offset: true }), taskIds: z.array(z.number().int()).optional(), // Array of task IDs }); - - -/* █████ ACTOR ██████████████████████████████████████████████████████ */ -export interface Actor { - id: number; - name: string; - role: string; - permissions?: string[]; - goals?: string[]; -} - -export const ActorSchema = z.object({ - id: z.number().int().nonnegative(), - name: z.string().min(1).max(100), - role: z.string().min(1), - permissions: z.array(z.string()).optional(), - goals: z.array(z.string()).optional(), -}); \ No newline at end of file diff --git a/views/ActorEditModal.tsx b/views/ActorEditModal.tsx deleted file mode 100644 index a47813e..0000000 --- a/views/ActorEditModal.tsx +++ /dev/null @@ -1,353 +0,0 @@ -// views/ActorEditModal.tsx -import React, { useState, useEffect } from 'react'; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogFooter, - DialogDescription, -} from '@/components/ui/dialog'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Textarea } from '@/components/ui/textarea'; -import { Label } from '@/components/ui/label'; -import { Badge } from '@/components/ui/badge'; -import { - Loader2, - AlertCircle, - Edit2, - Users, // Icon for Actor name - AlignLeft, // Icon for Role - KeyRound, // Icon for Permissions - Target, // Icon for Goals - Check, - X, - Plus, // For add button -} from 'lucide-react'; -import { cn } from '@/lib/utils'; -import { Actor } from '@/types'; -// No direct store usage in this component, props are passed down. -// If it were to use a store, example: -// import { useActorStore } from '@/stores/useActorStore'; - -interface ActorEditModalProps { - actor: Actor | null; // Null for creation, object for editing - isOpen: boolean; - onClose: () => void; - onSave: ( - id: number | null, // null for new actor - data: Partial> - ) => Promise; - isSaving: boolean; -} - -const ActorEditModal: React.FC = ({ - actor, - isOpen, - onClose, - onSave, - isSaving, -}) => { - const [name, setName] = useState(''); - const [role, setRole] = useState(''); - const [permissions, setPermissions] = useState([]); - const [goals, setGoals] = useState([]); - const [error, setError] = useState(null); - const [nameFocused, setNameFocused] = useState(false); - - // For managing new permission/goal input - const [newPermission, setNewPermission] = useState(''); - const [newGoal, setNewGoal] = useState(''); - - useEffect(() => { - if (actor) { - setName(actor.name); - setRole(actor.role); - setPermissions(actor.permissions || []); - setGoals(actor.goals || []); - setError(null); - } else { - // For new actor - setName(''); - setRole(''); - setPermissions([]); - setGoals([]); - setError(null); - } - setNewPermission(''); - setNewGoal(''); - }, [actor, isOpen]); - - useEffect(() => { - if (isOpen && !actor) { // Only auto-focus for new actors - const timer = setTimeout(() => { - setNameFocused(true); - }, 100); - return () => clearTimeout(timer); - } - setNameFocused(false); - }, [isOpen, actor]); - - const handleAddPermission = () => { - const trimmed = newPermission.trim(); - if (trimmed && !permissions.includes(trimmed)) { - setPermissions(prev => [...prev, trimmed]); - setNewPermission(''); - } - }; - - const handleRemovePermission = (perm: string) => { - setPermissions(prev => prev.filter(p => p !== perm)); - }; - - const handleAddGoal = () => { - const trimmed = newGoal.trim(); - if (trimmed && !goals.includes(trimmed)) { - setGoals(prev => [...prev, trimmed]); - setNewGoal(''); - } - }; - - const handleRemoveGoal = (goal: string) => { - setGoals(prev => prev.filter(g => g !== goal)); - }; - - const handleSave = async () => { - if (!name.trim()) { - setError('Actor Name is required'); - return; - } - if (!role.trim()) { - setError('Actor Role is required'); - return; - } - - const updatedData: Partial> = { - name: name.trim(), - role: role.trim(), - permissions: permissions.length > 0 ? permissions : undefined, // Send as undefined if empty - goals: goals.length > 0 ? goals : undefined, // Send as undefined if empty - }; - - try { - await onSave(actor?.id || null, updatedData); - } catch (err) { - setError('Failed to save changes'); - } - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { - e.preventDefault(); - if (!isSaving && name.trim() && role.trim()) { - handleSave(); - } - } else if (e.key === 'Escape') { - e.preventDefault(); - onClose(); - } - }; - - const handlePermissionInputKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - e.preventDefault(); - handleAddPermission(); - } - }; - - const handleGoalInputKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - e.preventDefault(); - handleAddGoal(); - } - }; - - - return ( - !open && onClose()}> - - - - - {actor ? 'Edit Actor' : 'Create New Actor'} - - - {actor ? 'Update the details of this actor' : 'Define a new actor for your project'} - - - - {error && ( -
- - {error} -
- )} - -
- {/* Name Field */} -
- - { - setName(e.target.value); - if (error && e.target.value.trim()) setError(null); - }} - className={cn( - "h-10 bg-[rgba(var(--color-bg-secondary),0.8)] border-[rgba(var(--color-border),0.7)] focus-glow transition-all", - error && !name.trim() && "border-rose-500 bg-rose-500/5" - )} - placeholder="e.g., Developer, External API, Admin" - autoFocus={nameFocused} - /> -
- - {/* Role Field */} -
- -