Files
sitemente/components/mission-control/TaskCardsPanel.tsx
T

280 lines
9.9 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useState, useEffect } from 'react'
import { Task, TaskStatus } from '@/lib/mission-control/types'
interface TaskCardsPanelProps {
tasks: Task[]
toggleTask: (id: string) => void
}
const projects = ['sitemente', 'holacompi', 'arabredox', 'infrastructure', 'trading']
export function TaskCardsPanel({ tasks, toggleTask }: TaskCardsPanelProps) {
const [selectedProject, setSelectedProject] = useState<string>('sitemente')
const [selectedTaskId, setSelectedTaskId] = useState<string | null>(null)
const [command, setCommand] = useState('')
const [responses, setResponses] = useState<{id: string, task: string, command: string, reply: string, createdAt: string}[]>([])
// Poll for replies every 10 seconds
useEffect(() => {
const pollReplies = async () => {
try {
const res = await fetch('/api/command-history')
if (res.ok) {
const data = await res.json()
// Get entries with replies that haven't been shown
const withReplies = (data.history || []).filter((h: any) => h.reply && h.reply.length > 0)
setResponses(withReplies.slice(-5).reverse())
}
} catch (e) {}
}
pollReplies()
const interval = setInterval(pollReplies, 10000)
return () => clearInterval(interval)
}, [])
const projectTasks = tasks.filter(t =>
t.project === selectedProject &&
(t.status === 'todo' || t.status === 'in_progress')
)
const handleTaskClick = (taskId: string) => {
setSelectedTaskId(selectedTaskId === taskId ? null : taskId)
setCommand('')
}
const handleConfirm = () => {
if (!selectedTaskId || !command.trim()) return
const task = tasks.find(t => t.id === selectedTaskId)
if (!task) return
fetch('/api/command-history', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
task: task.title,
command: command,
project: selectedProject,
action: 'continue-task'
})
}).catch(() => {})
fetch('/api/command', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
command: command,
task: task.title,
action: 'continue-task'
})
}).catch(() => {})
alert(`✅ Sent to Horus: "${command}"`)
setCommand('')
setSelectedTaskId(null)
}
const handleQuickChat = () => {
if (!command.trim()) return
// Save quick chat to history - this will notify Horus via the API
fetch('/api/command-history', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
task: `Quick message - ${selectedProject}`,
command: command,
project: selectedProject,
action: 'quick-message'
})
}).catch(() => {})
alert(`✅ Sent to Horus! I'll reply shortly.`)
setCommand('')
}
const selectedTask = tasks.find(t => t.id === selectedTaskId)
return (
<div className="space-y-4">
{/* Project Tabs */}
<div className="flex gap-2 flex-wrap">
{projects.map(project => {
const count = tasks.filter(t =>
t.project === project &&
(t.status === 'todo' || t.status === 'in_progress')
).length
return (
<button
key={project}
onClick={() => {
setSelectedProject(project)
setSelectedTaskId(null)
}}
className={`px-4 py-2 rounded-lg text-sm font-medium transition ${
selectedProject === project
? 'bg-brand-pink text-white'
: 'bg-white/10 text-white/70 hover:bg-white/20'
}`}
>
{project.charAt(0).toUpperCase() + project.slice(1)}
<span className="ml-2 text-xs opacity-70">({count})</span>
</button>
)
})}
</div>
{/* Task Cards */}
<div className="border border-white/20 rounded-lg p-4 bg-white/5">
<h3 className="text-lg font-bold mb-4">
📋 {selectedProject.toUpperCase()} TASKS ({projectTasks.length})
</h3>
<div className="space-y-2">
{projectTasks.map((task) => (
<div
key={task.id}
className={`p-3 rounded-lg border transition-all ${
selectedTaskId === task.id
? 'border-brand-pink bg-brand-pink/10'
: 'border-white/10 bg-white/5 hover:border-white/30'
}`}
onClick={() => handleTaskClick(task.id)}
>
<div className="flex items-center gap-3">
<button
onClick={(e) => {
e.stopPropagation()
toggleTask(task.id)
}}
className={`w-5 h-5 rounded-full border-2 flex items-center justify-center transition ${
task.status === 'done'
? 'border-green-500 bg-green-500'
: task.status === 'in_progress'
? 'border-yellow-500 bg-yellow-500'
: 'border-white/30 hover:border-white/50'
}`}
>
{task.status === 'done' && (
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
)}
</button>
<div className="flex-1">
<p className={`font-medium ${task.status === 'done' ? 'line-through opacity-50' : ''}`}>
{task.title}
</p>
{task.description && (
<p className="text-xs text-white/50 truncate">{task.description}</p>
)}
</div>
<span className="text-lg">
{selectedTaskId === task.id ? '▼' : '▶'}
</span>
</div>
{/* Expanded Input */}
{selectedTaskId === task.id && (
<div className="mt-3 ml-8">
<textarea
value={command}
onChange={(e) => setCommand(e.target.value)}
placeholder={`What to do with "${task.title}"?`}
className="w-full px-3 py-2 bg-black/50 border border-white/20 rounded text-sm text-white placeholder:text-white/40 focus:outline-none focus:border-brand-pink resize-none"
rows={3}
onClick={(e) => e.stopPropagation()}
autoFocus
/>
<div className="flex gap-2 mt-2">
<button
onClick={(e) => {
e.stopPropagation()
handleConfirm()
}}
disabled={!command.trim()}
className={`px-4 py-1.5 rounded text-xs font-bold ${
command.trim()
? 'bg-brand-pink text-white hover:bg-[#ff7bc0]'
: 'bg-white/10 text-white/30 cursor-not-allowed'
}`}
>
SEND TO HORUS
</button>
<button
onClick={(e) => {
e.stopPropagation()
setSelectedTaskId(null)
setCommand('')
}}
className="px-3 py-1.5 text-xs text-white/60 hover:text-white"
>
Cancel
</button>
</div>
</div>
)}
</div>
))}
</div>
{projectTasks.length === 0 && (
<p className="text-white/50 text-center py-4">No pending tasks </p>
)}
{/* Quick Chat - always available */}
<div className="mt-4 pt-4 border-t border-white/10">
<p className="text-xs text-white/50 mb-2">💬 Quick message for {selectedProject}</p>
<div className="flex gap-2">
<textarea
value={command}
onChange={(e) => setCommand(e.target.value)}
placeholder={`New task or message for ${selectedProject}...`}
className="flex-1 px-3 py-2 bg-black/50 border border-white/20 rounded text-sm text-white placeholder:text-white/40 focus:outline-none focus:border-brand-pink resize-none"
rows={2}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleQuickChat()
}
}}
/>
<button
onClick={handleQuickChat}
disabled={!command.trim()}
className={`px-4 rounded text-sm font-bold ${
command.trim()
? 'bg-brand-pink text-white hover:bg-[#ff7bc0]'
: 'bg-white/10 text-white/30 cursor-not-allowed'
}`}
>
</button>
</div>
</div>
{/* Responses from Horus */}
{responses.length > 0 && (
<div className="mt-4 pt-4 border-t border-white/10">
<p className="text-xs text-white/50 mb-2">💬 Recent replies from Horus</p>
<div className="space-y-2 max-h-40 overflow-y-auto">
{responses.map((r) => (
<div key={r.id} className="p-2 rounded bg-brand-pink/10 border border-brand-pink/30 text-sm">
<p className="text-white/60 text-xs">You: {r.command}</p>
<p className="text-brand-pink mt-1">👁 {r.reply}</p>
</div>
))}
</div>
</div>
)}
</div>
</div>
)
}