Merge branch 'mvp2'

This commit is contained in:
Claude
2025-11-20 05:53:18 +00:00
9 changed files with 175 additions and 56 deletions

View File

@@ -144,6 +144,47 @@ def delete_task(task_id: int, db: Session = Depends(get_db)):
return None return None
# ========== SEARCH ENDPOINT ==========
@app.get("/api/search", response_model=List[schemas.Task])
def search_tasks(
query: str,
project_ids: Optional[str] = None,
db: Session = Depends(get_db)
):
"""
Search tasks across projects by title, description, and tags.
Args:
query: Search term to match against title, description, and tags
project_ids: Comma-separated list of project IDs to search in (optional, searches all if not provided)
"""
# Parse project IDs if provided
project_id_list = None
if project_ids:
try:
project_id_list = [int(pid.strip()) for pid in project_ids.split(',') if pid.strip()]
except ValueError:
raise HTTPException(status_code=400, detail="Invalid project_ids format")
# Build query
tasks_query = db.query(models.Task)
# Filter by project IDs if specified
if project_id_list:
tasks_query = tasks_query.filter(models.Task.project_id.in_(project_id_list))
# Search in title, description, and tags
search_term = f"%{query}%"
tasks = tasks_query.filter(
(models.Task.title.ilike(search_term)) |
(models.Task.description.ilike(search_term)) |
(models.Task.tags.contains([query])) # Exact tag match
).all()
return tasks
# ========== JSON IMPORT ENDPOINT ========== # ========== JSON IMPORT ENDPOINT ==========
def _import_tasks_recursive( def _import_tasks_recursive(
@@ -161,6 +202,9 @@ def _import_tasks_recursive(
title=task_data.title, title=task_data.title,
description=task_data.description, description=task_data.description,
status=task_data.status, status=task_data.status,
estimated_minutes=task_data.estimated_minutes,
tags=task_data.tags,
flag_color=task_data.flag_color,
sort_order=idx sort_order=idx
) )
db_task = crud.create_task(db, task) db_task = crud.create_task(db, task)

View File

@@ -1,4 +1,4 @@
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Enum from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Enum, JSON
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from datetime import datetime from datetime import datetime
import enum import enum
@@ -34,6 +34,9 @@ class Task(Base):
description = Column(Text, nullable=True) description = Column(Text, nullable=True)
status = Column(Enum(TaskStatus), default=TaskStatus.BACKLOG, nullable=False) status = Column(Enum(TaskStatus), default=TaskStatus.BACKLOG, nullable=False)
sort_order = Column(Integer, default=0) sort_order = Column(Integer, default=0)
estimated_minutes = Column(Integer, nullable=True)
tags = Column(JSON, nullable=True)
flag_color = Column(String(50), nullable=True)
created_at = Column(DateTime, default=datetime.utcnow) created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

View File

@@ -11,6 +11,9 @@ class TaskBase(BaseModel):
status: TaskStatus = TaskStatus.BACKLOG status: TaskStatus = TaskStatus.BACKLOG
parent_task_id: Optional[int] = None parent_task_id: Optional[int] = None
sort_order: int = 0 sort_order: int = 0
estimated_minutes: Optional[int] = None
tags: Optional[List[str]] = None
flag_color: Optional[str] = None
class TaskCreate(TaskBase): class TaskCreate(TaskBase):
@@ -23,6 +26,9 @@ class TaskUpdate(BaseModel):
status: Optional[TaskStatus] = None status: Optional[TaskStatus] = None
parent_task_id: Optional[int] = None parent_task_id: Optional[int] = None
sort_order: Optional[int] = None sort_order: Optional[int] = None
estimated_minutes: Optional[int] = None
tags: Optional[List[str]] = None
flag_color: Optional[str] = None
class Task(TaskBase): class Task(TaskBase):
@@ -74,6 +80,9 @@ class ImportSubtask(BaseModel):
title: str title: str
description: Optional[str] = None description: Optional[str] = None
status: TaskStatus = TaskStatus.BACKLOG status: TaskStatus = TaskStatus.BACKLOG
estimated_minutes: Optional[int] = None
tags: Optional[List[str]] = None
flag_color: Optional[str] = None
subtasks: List['ImportSubtask'] = [] subtasks: List['ImportSubtask'] = []

View File

@@ -8,32 +8,48 @@
"title": "Cortex Rewire", "title": "Cortex Rewire",
"description": "Refactor reasoning layer for improved performance", "description": "Refactor reasoning layer for improved performance",
"status": "backlog", "status": "backlog",
"estimated_minutes": 240,
"tags": ["coding", "backend", "refactoring"],
"flag_color": "red",
"subtasks": [ "subtasks": [
{ {
"title": "Reflection → fix backend argument bug", "title": "Reflection → fix backend argument bug",
"status": "in_progress", "status": "in_progress",
"estimated_minutes": 90,
"tags": ["coding", "bug-fix"],
"flag_color": "orange",
"subtasks": [ "subtasks": [
{ {
"title": "Normalize LLM backend arg in reflection calls", "title": "Normalize LLM backend arg in reflection calls",
"status": "in_progress" "status": "in_progress",
"estimated_minutes": 45,
"tags": ["coding"]
}, },
{ {
"title": "Add unit tests for reflection module", "title": "Add unit tests for reflection module",
"status": "backlog" "status": "backlog",
"estimated_minutes": 60,
"tags": ["testing", "coding"]
} }
] ]
}, },
{ {
"title": "Reasoning parser cleanup", "title": "Reasoning parser cleanup",
"status": "backlog", "status": "backlog",
"estimated_minutes": 120,
"tags": ["coding", "cleanup"],
"subtasks": [ "subtasks": [
{ {
"title": "Remove deprecated parse methods", "title": "Remove deprecated parse methods",
"status": "backlog" "status": "backlog",
"estimated_minutes": 30,
"tags": ["coding"]
}, },
{ {
"title": "Optimize regex patterns", "title": "Optimize regex patterns",
"status": "backlog" "status": "backlog",
"estimated_minutes": 45,
"tags": ["coding", "performance"]
} }
] ]
} }
@@ -43,32 +59,47 @@
"title": "Frontend Overhaul", "title": "Frontend Overhaul",
"description": "Modernize the UI with new component library", "description": "Modernize the UI with new component library",
"status": "backlog", "status": "backlog",
"estimated_minutes": 480,
"tags": ["frontend", "ui", "coding"],
"flag_color": "blue",
"subtasks": [ "subtasks": [
{ {
"title": "Migrate to Tailwind CSS", "title": "Migrate to Tailwind CSS",
"status": "backlog" "status": "backlog",
"estimated_minutes": 180,
"tags": ["frontend", "styling"]
}, },
{ {
"title": "Build new component library", "title": "Build new component library",
"status": "backlog", "status": "backlog",
"estimated_minutes": 360,
"tags": ["frontend", "components"],
"subtasks": [ "subtasks": [
{ {
"title": "Button components", "title": "Button components",
"status": "backlog" "status": "backlog",
"estimated_minutes": 60,
"tags": ["frontend", "components"]
}, },
{ {
"title": "Form components", "title": "Form components",
"status": "backlog" "status": "backlog",
"estimated_minutes": 120,
"tags": ["frontend", "components"]
}, },
{ {
"title": "Modal components", "title": "Modal components",
"status": "backlog" "status": "backlog",
"estimated_minutes": 90,
"tags": ["frontend", "components"]
} }
] ]
}, },
{ {
"title": "Implement dark mode toggle", "title": "Implement dark mode toggle",
"status": "backlog" "status": "backlog",
"estimated_minutes": 45,
"tags": ["frontend", "ui"]
} }
] ]
}, },
@@ -76,36 +107,54 @@
"title": "API v2 Implementation", "title": "API v2 Implementation",
"status": "blocked", "status": "blocked",
"description": "Blocked on database migration completion", "description": "Blocked on database migration completion",
"estimated_minutes": 600,
"tags": ["backend", "api", "coding"],
"flag_color": "yellow",
"subtasks": [ "subtasks": [
{ {
"title": "Design new REST endpoints", "title": "Design new REST endpoints",
"status": "done" "status": "done",
"estimated_minutes": 120,
"tags": ["design", "api"]
}, },
{ {
"title": "Implement GraphQL layer", "title": "Implement GraphQL layer",
"status": "blocked" "status": "blocked",
"estimated_minutes": 300,
"tags": ["backend", "graphql", "coding"]
}, },
{ {
"title": "Add rate limiting", "title": "Add rate limiting",
"status": "backlog" "status": "backlog",
"estimated_minutes": 90,
"tags": ["backend", "security"]
} }
] ]
}, },
{ {
"title": "Documentation Sprint", "title": "Documentation Sprint",
"status": "done", "status": "done",
"estimated_minutes": 180,
"tags": ["documentation", "writing"],
"flag_color": "green",
"subtasks": [ "subtasks": [
{ {
"title": "API documentation", "title": "API documentation",
"status": "done" "status": "done",
"estimated_minutes": 60,
"tags": ["documentation"]
}, },
{ {
"title": "User guide", "title": "User guide",
"status": "done" "status": "done",
"estimated_minutes": 90,
"tags": ["documentation", "tutorial"]
}, },
{ {
"title": "Developer setup guide", "title": "Developer setup guide",
"status": "done" "status": "done",
"estimated_minutes": 30,
"tags": ["documentation"]
} }
] ]
} }

View File

@@ -7,10 +7,15 @@ function App() {
<div className="min-h-screen bg-cyber-dark"> <div className="min-h-screen bg-cyber-dark">
<header className="border-b border-cyber-orange/30 bg-cyber-darkest"> <header className="border-b border-cyber-orange/30 bg-cyber-darkest">
<div className="container mx-auto px-4 py-4"> <div className="container mx-auto px-4 py-4">
<h1 className="text-2xl font-bold text-cyber-orange"> <div className="flex justify-between items-center">
TESSERACT <div>
<span className="ml-3 text-sm text-gray-500">Task Decomposition Engine</span> <h1 className="text-2xl font-bold text-cyber-orange">
</h1> TESSERACT
<span className="ml-3 text-sm text-gray-500">Task Decomposition Engine</span>
<span className="ml-2 text-xs text-gray-600">v0.1.3</span>
</h1>
</div>
</div>
</div> </div>
</header> </header>

View File

@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { Plus, Edit2, Trash2, Check, X, ChevronDown, ChevronRight } from 'lucide-react' import { Plus, Edit2, Trash2, Check, X } from 'lucide-react'
import { import {
getProjectTasks, getProjectTasks,
createTask, createTask,
@@ -17,10 +17,11 @@ const STATUSES = [
function TaskCard({ task, allTasks, onUpdate, onDragStart }) { function TaskCard({ task, allTasks, onUpdate, onDragStart }) {
const [isEditing, setIsEditing] = useState(false) const [isEditing, setIsEditing] = useState(false)
const [editTitle, setEditTitle] = useState(task.title) const [editTitle, setEditTitle] = useState(task.title)
const [showSubtasks, setShowSubtasks] = useState(false)
const subtasks = allTasks.filter(t => t.parent_task_id === task.id) // Find parent task if this is a subtask
const hasSubtasks = subtasks.length > 0 const parentTask = task.parent_task_id
? allTasks.find(t => t.id === task.parent_task_id)
: null
const handleSave = async () => { const handleSave = async () => {
try { try {
@@ -75,8 +76,15 @@ function TaskCard({ task, allTasks, onUpdate, onDragStart }) {
</div> </div>
) : ( ) : (
<> <>
<div className="flex justify-between items-start mb-2"> <div className="flex justify-between items-start">
<span className="text-gray-200 text-sm flex-1">{task.title}</span> <div className="flex-1">
<div className="text-gray-200 text-sm">{task.title}</div>
{parentTask && (
<div className="text-xs text-gray-500 mt-1">
subtask of: <span className="text-cyber-orange">{parentTask.title}</span>
</div>
)}
</div>
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity"> <div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<button <button
onClick={() => setIsEditing(true)} onClick={() => setIsEditing(true)}
@@ -92,27 +100,6 @@ function TaskCard({ task, allTasks, onUpdate, onDragStart }) {
</button> </button>
</div> </div>
</div> </div>
{hasSubtasks && (
<div className="mt-2 pt-2 border-t border-cyber-orange/20">
<button
onClick={() => setShowSubtasks(!showSubtasks)}
className="flex items-center gap-1 text-xs text-cyber-orange hover:text-cyber-orange-bright"
>
{showSubtasks ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
{subtasks.length} subtask{subtasks.length !== 1 ? 's' : ''}
</button>
{showSubtasks && (
<div className="mt-2 pl-3 space-y-1">
{subtasks.map(subtask => (
<div key={subtask.id} className="text-xs text-gray-400">
{subtask.title}
</div>
))}
</div>
)}
</div>
)}
</> </>
)} )}
</div> </div>
@@ -255,9 +242,6 @@ function KanbanView({ projectId }) {
return <div className="text-center text-red-400 py-12">{error}</div> return <div className="text-center text-red-400 py-12">{error}</div>
} }
// Only show root-level tasks in Kanban (tasks without parents)
const rootTasks = allTasks.filter(t => !t.parent_task_id)
return ( return (
<div> <div>
<h3 className="text-xl font-semibold text-gray-300 mb-4">Kanban Board</h3> <h3 className="text-xl font-semibold text-gray-300 mb-4">Kanban Board</h3>
@@ -267,7 +251,7 @@ function KanbanView({ projectId }) {
<KanbanColumn <KanbanColumn
key={status.key} key={status.key}
status={status} status={status}
tasks={rootTasks.filter(t => t.status === status.key)} tasks={allTasks.filter(t => t.status === status.key)}
allTasks={allTasks} allTasks={allTasks}
projectId={projectId} projectId={projectId}
onUpdate={loadTasks} onUpdate={loadTasks}
@@ -277,7 +261,7 @@ function KanbanView({ projectId }) {
))} ))}
</div> </div>
{rootTasks.length === 0 && ( {allTasks.length === 0 && (
<div className="text-center py-16 text-gray-500"> <div className="text-center py-16 text-gray-500">
<p className="text-lg mb-2">No tasks yet</p> <p className="text-lg mb-2">No tasks yet</p>
<p className="text-sm">Add tasks using the + button in any column</p> <p className="text-sm">Add tasks using the + button in any column</p>

View File

@@ -85,9 +85,8 @@ function TaskNode({ task, projectId, onUpdate, level = 0 }) {
return ( return (
<div className="mb-2"> <div className="mb-2">
<div <div
className={`flex items-center gap-2 p-3 bg-cyber-darkest border border-cyber-orange/20 rounded hover:border-cyber-orange/40 transition-all group ${ style={{ marginLeft: `${level * 1.5}rem` }}
level > 0 ? 'ml-6' : '' className="flex items-center gap-2 p-3 bg-cyber-darkest border border-cyber-orange/20 rounded hover:border-cyber-orange/40 transition-all group"
}`}
> >
{/* Expand/Collapse */} {/* Expand/Collapse */}
{hasSubtasks && ( {hasSubtasks && (
@@ -176,7 +175,7 @@ function TaskNode({ task, projectId, onUpdate, level = 0 }) {
{/* Add Subtask Form */} {/* Add Subtask Form */}
{showAddSubtask && ( {showAddSubtask && (
<div className={`mt-2 ${level > 0 ? 'ml-6' : ''}`}> <div style={{ marginLeft: `${level * 1.5}rem` }} className="mt-2">
<form onSubmit={handleAddSubtask} className="flex gap-2"> <form onSubmit={handleAddSubtask} className="flex gap-2">
<input <input
type="text" type="text"

View File

@@ -56,3 +56,12 @@ export const importJSON = (data) => fetchAPI('/import-json', {
method: 'POST', method: 'POST',
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
// Search
export const searchTasks = (query, projectIds = null) => {
const params = new URLSearchParams({ query });
if (projectIds && projectIds.length > 0) {
params.append('project_ids', projectIds.join(','));
}
return fetchAPI(`/search?${params.toString()}`);
};

View File

@@ -0,0 +1,17 @@
// Format minutes into display string
export function formatTime(minutes) {
if (!minutes || minutes === 0) return null;
if (minutes < 60) {
return `${minutes}m`;
}
const hours = minutes / 60;
return `${hours.toFixed(1)}h`;
}
// Format tags as comma-separated string
export function formatTags(tags) {
if (!tags || tags.length === 0) return null;
return tags.join(', ');
}