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
# ========== 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 ==========
def _import_tasks_recursive(
@@ -161,6 +202,9 @@ def _import_tasks_recursive(
title=task_data.title,
description=task_data.description,
status=task_data.status,
estimated_minutes=task_data.estimated_minutes,
tags=task_data.tags,
flag_color=task_data.flag_color,
sort_order=idx
)
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 datetime import datetime
import enum
@@ -34,6 +34,9 @@ class Task(Base):
description = Column(Text, nullable=True)
status = Column(Enum(TaskStatus), default=TaskStatus.BACKLOG, nullable=False)
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)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

View File

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

View File

@@ -8,32 +8,48 @@
"title": "Cortex Rewire",
"description": "Refactor reasoning layer for improved performance",
"status": "backlog",
"estimated_minutes": 240,
"tags": ["coding", "backend", "refactoring"],
"flag_color": "red",
"subtasks": [
{
"title": "Reflection → fix backend argument bug",
"status": "in_progress",
"estimated_minutes": 90,
"tags": ["coding", "bug-fix"],
"flag_color": "orange",
"subtasks": [
{
"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",
"status": "backlog"
"status": "backlog",
"estimated_minutes": 60,
"tags": ["testing", "coding"]
}
]
},
{
"title": "Reasoning parser cleanup",
"status": "backlog",
"estimated_minutes": 120,
"tags": ["coding", "cleanup"],
"subtasks": [
{
"title": "Remove deprecated parse methods",
"status": "backlog"
"status": "backlog",
"estimated_minutes": 30,
"tags": ["coding"]
},
{
"title": "Optimize regex patterns",
"status": "backlog"
"status": "backlog",
"estimated_minutes": 45,
"tags": ["coding", "performance"]
}
]
}
@@ -43,32 +59,47 @@
"title": "Frontend Overhaul",
"description": "Modernize the UI with new component library",
"status": "backlog",
"estimated_minutes": 480,
"tags": ["frontend", "ui", "coding"],
"flag_color": "blue",
"subtasks": [
{
"title": "Migrate to Tailwind CSS",
"status": "backlog"
"status": "backlog",
"estimated_minutes": 180,
"tags": ["frontend", "styling"]
},
{
"title": "Build new component library",
"status": "backlog",
"estimated_minutes": 360,
"tags": ["frontend", "components"],
"subtasks": [
{
"title": "Button components",
"status": "backlog"
"status": "backlog",
"estimated_minutes": 60,
"tags": ["frontend", "components"]
},
{
"title": "Form components",
"status": "backlog"
"status": "backlog",
"estimated_minutes": 120,
"tags": ["frontend", "components"]
},
{
"title": "Modal components",
"status": "backlog"
"status": "backlog",
"estimated_minutes": 90,
"tags": ["frontend", "components"]
}
]
},
{
"title": "Implement dark mode toggle",
"status": "backlog"
"status": "backlog",
"estimated_minutes": 45,
"tags": ["frontend", "ui"]
}
]
},
@@ -76,36 +107,54 @@
"title": "API v2 Implementation",
"status": "blocked",
"description": "Blocked on database migration completion",
"estimated_minutes": 600,
"tags": ["backend", "api", "coding"],
"flag_color": "yellow",
"subtasks": [
{
"title": "Design new REST endpoints",
"status": "done"
"status": "done",
"estimated_minutes": 120,
"tags": ["design", "api"]
},
{
"title": "Implement GraphQL layer",
"status": "blocked"
"status": "blocked",
"estimated_minutes": 300,
"tags": ["backend", "graphql", "coding"]
},
{
"title": "Add rate limiting",
"status": "backlog"
"status": "backlog",
"estimated_minutes": 90,
"tags": ["backend", "security"]
}
]
},
{
"title": "Documentation Sprint",
"status": "done",
"estimated_minutes": 180,
"tags": ["documentation", "writing"],
"flag_color": "green",
"subtasks": [
{
"title": "API documentation",
"status": "done"
"status": "done",
"estimated_minutes": 60,
"tags": ["documentation"]
},
{
"title": "User guide",
"status": "done"
"status": "done",
"estimated_minutes": 90,
"tags": ["documentation", "tutorial"]
},
{
"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">
<header className="border-b border-cyber-orange/30 bg-cyber-darkest">
<div className="container mx-auto px-4 py-4">
<h1 className="text-2xl font-bold text-cyber-orange">
TESSERACT
<span className="ml-3 text-sm text-gray-500">Task Decomposition Engine</span>
</h1>
<div className="flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold text-cyber-orange">
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>
</header>

View File

@@ -1,5 +1,5 @@
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 {
getProjectTasks,
createTask,
@@ -17,10 +17,11 @@ const STATUSES = [
function TaskCard({ task, allTasks, onUpdate, onDragStart }) {
const [isEditing, setIsEditing] = useState(false)
const [editTitle, setEditTitle] = useState(task.title)
const [showSubtasks, setShowSubtasks] = useState(false)
const subtasks = allTasks.filter(t => t.parent_task_id === task.id)
const hasSubtasks = subtasks.length > 0
// Find parent task if this is a subtask
const parentTask = task.parent_task_id
? allTasks.find(t => t.id === task.parent_task_id)
: null
const handleSave = async () => {
try {
@@ -75,8 +76,15 @@ function TaskCard({ task, allTasks, onUpdate, onDragStart }) {
</div>
) : (
<>
<div className="flex justify-between items-start mb-2">
<span className="text-gray-200 text-sm flex-1">{task.title}</span>
<div className="flex justify-between items-start">
<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">
<button
onClick={() => setIsEditing(true)}
@@ -92,27 +100,6 @@ function TaskCard({ task, allTasks, onUpdate, onDragStart }) {
</button>
</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>
@@ -255,9 +242,6 @@ function KanbanView({ projectId }) {
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 (
<div>
<h3 className="text-xl font-semibold text-gray-300 mb-4">Kanban Board</h3>
@@ -267,7 +251,7 @@ function KanbanView({ projectId }) {
<KanbanColumn
key={status.key}
status={status}
tasks={rootTasks.filter(t => t.status === status.key)}
tasks={allTasks.filter(t => t.status === status.key)}
allTasks={allTasks}
projectId={projectId}
onUpdate={loadTasks}
@@ -277,7 +261,7 @@ function KanbanView({ projectId }) {
))}
</div>
{rootTasks.length === 0 && (
{allTasks.length === 0 && (
<div className="text-center py-16 text-gray-500">
<p className="text-lg mb-2">No tasks yet</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 (
<div className="mb-2">
<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 ${
level > 0 ? 'ml-6' : ''
}`}
style={{ marginLeft: `${level * 1.5}rem` }}
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 */}
{hasSubtasks && (
@@ -176,7 +175,7 @@ function TaskNode({ task, projectId, onUpdate, level = 0 }) {
{/* Add Subtask Form */}
{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">
<input
type="text"

View File

@@ -56,3 +56,12 @@ export const importJSON = (data) => fetchAPI('/import-json', {
method: 'POST',
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(', ');
}