|
|
import { useState, useRef, useEffect } from 'react'; |
|
|
import ReactMarkdown from 'react-markdown'; |
|
|
import { useUser } from '../context/UserContext'; |
|
|
import { useProject } from '../context/ProjectContext'; |
|
|
import { api } from '../api/client'; |
|
|
import type { Task } from '../types'; |
|
|
|
|
|
interface Message { |
|
|
id: string; |
|
|
role: 'user' | 'assistant'; |
|
|
content: string; |
|
|
timestamp: Date; |
|
|
} |
|
|
|
|
|
interface TaskSolverPageProps { |
|
|
task: Task; |
|
|
onBack: () => void; |
|
|
onTaskCompleted: () => void; |
|
|
} |
|
|
|
|
|
export function TaskSolverPage({ task, onBack, onTaskCompleted }: TaskSolverPageProps) { |
|
|
const { user } = useUser(); |
|
|
const { currentProject } = useProject(); |
|
|
|
|
|
const [messages, setMessages] = useState<Message[]>([]); |
|
|
const [input, setInput] = useState(''); |
|
|
const [isLoading, setIsLoading] = useState(false); |
|
|
const [taskStatus, setTaskStatus] = useState(task.status); |
|
|
const messagesEndRef = useRef<HTMLDivElement>(null); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); |
|
|
}, [messages]); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
const greeting: Message = { |
|
|
id: 'greeting', |
|
|
role: 'assistant', |
|
|
content: `I'm here to help you with this task: "${task.title}"\n\n${task.description ? `Description: ${task.description}\n\n` : ''}Feel free to ask me questions, share your progress, or request coding help. When you're done, just let me know what you accomplished and I'll help complete the task.`, |
|
|
timestamp: new Date(), |
|
|
}; |
|
|
setMessages([greeting]); |
|
|
}, [task]); |
|
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => { |
|
|
e.preventDefault(); |
|
|
if (!input.trim() || isLoading || !user || !currentProject) return; |
|
|
|
|
|
const userMessage: Message = { |
|
|
id: `user-${Date.now()}`, |
|
|
role: 'user', |
|
|
content: input.trim(), |
|
|
timestamp: new Date(), |
|
|
}; |
|
|
|
|
|
setMessages((prev) => [...prev, userMessage]); |
|
|
setInput(''); |
|
|
setIsLoading(true); |
|
|
|
|
|
try { |
|
|
|
|
|
const history = messages.map((m) => ({ |
|
|
role: m.role, |
|
|
content: m.content, |
|
|
})); |
|
|
|
|
|
const response = await api.taskChat( |
|
|
currentProject.id, |
|
|
task.id, |
|
|
user.id, |
|
|
input.trim(), |
|
|
history |
|
|
); |
|
|
|
|
|
const assistantMessage: Message = { |
|
|
id: `assistant-${Date.now()}`, |
|
|
role: 'assistant', |
|
|
content: response.message, |
|
|
timestamp: new Date(), |
|
|
}; |
|
|
|
|
|
setMessages((prev) => [...prev, assistantMessage]); |
|
|
|
|
|
|
|
|
if (response.taskCompleted) { |
|
|
setTaskStatus('done'); |
|
|
onTaskCompleted(); |
|
|
} else if (response.taskStatus && response.taskStatus !== taskStatus) { |
|
|
|
|
|
|
|
|
const isTaskStatus = (s: any): s is Task['status'] => |
|
|
s === 'todo' || s === 'in_progress' || s === 'done'; |
|
|
|
|
|
if (isTaskStatus(response.taskStatus)) { |
|
|
setTaskStatus(response.taskStatus); |
|
|
} |
|
|
} |
|
|
} catch (err) { |
|
|
const errorMessage: Message = { |
|
|
id: `error-${Date.now()}`, |
|
|
role: 'assistant', |
|
|
content: `Sorry, I encountered an error: ${err instanceof Error ? err.message : 'Unknown error'}`, |
|
|
timestamp: new Date(), |
|
|
}; |
|
|
setMessages((prev) => [...prev, errorMessage]); |
|
|
} finally { |
|
|
setIsLoading(false); |
|
|
} |
|
|
}; |
|
|
|
|
|
const getStatusColor = (status: string) => { |
|
|
switch (status) { |
|
|
case 'todo': |
|
|
return 'bg-yellow-500'; |
|
|
case 'in_progress': |
|
|
return 'bg-blue-500'; |
|
|
case 'done': |
|
|
return 'bg-green-500'; |
|
|
default: |
|
|
return 'bg-gray-500'; |
|
|
} |
|
|
}; |
|
|
|
|
|
const getStatusLabel = (status: string) => { |
|
|
switch (status) { |
|
|
case 'todo': |
|
|
return 'Todo'; |
|
|
case 'in_progress': |
|
|
return 'In Progress'; |
|
|
case 'done': |
|
|
return 'Done'; |
|
|
default: |
|
|
return status; |
|
|
} |
|
|
}; |
|
|
|
|
|
return ( |
|
|
<div className="h-screen bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900 flex flex-col overflow-hidden"> |
|
|
{/* Fixed Header */} |
|
|
<header className="flex-shrink-0 bg-white/5 backdrop-blur-lg border-b border-white/10 sticky top-0 z-10"> |
|
|
<div className="max-w-5xl mx-auto px-4 py-4"> |
|
|
<div className="flex items-center justify-between"> |
|
|
<div className="flex items-center gap-4"> |
|
|
<button |
|
|
onClick={onBack} |
|
|
className="text-purple-300 hover:text-white transition-colors flex items-center gap-1" |
|
|
> |
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" /> |
|
|
</svg> |
|
|
Back |
|
|
</button> |
|
|
<div className="h-6 w-px bg-white/20" /> |
|
|
<h1 className="text-lg font-semibold text-white truncate max-w-md">{task.title}</h1> |
|
|
</div> |
|
|
<div className="flex items-center gap-3"> |
|
|
<span className={`px-3 py-1 rounded-full text-white text-xs font-medium ${getStatusColor(taskStatus)}`}> |
|
|
{getStatusLabel(taskStatus)} |
|
|
</span> |
|
|
{user && ( |
|
|
<span className="text-purple-300 text-sm"> |
|
|
{user.firstName} |
|
|
</span> |
|
|
)} |
|
|
</div> |
|
|
</div> |
|
|
{task.description && ( |
|
|
<p className="mt-2 text-purple-300/70 text-sm max-w-2xl">{task.description}</p> |
|
|
)} |
|
|
</div> |
|
|
</header> |
|
|
|
|
|
{/* Chat Area */} |
|
|
<div className="flex-1 overflow-hidden flex flex-col max-w-5xl mx-auto w-full"> |
|
|
{/* Messages */} |
|
|
<div className="flex-1 overflow-y-auto p-4 space-y-4"> |
|
|
{messages.map((message) => ( |
|
|
<div |
|
|
key={message.id} |
|
|
className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`} |
|
|
> |
|
|
<div |
|
|
className={`max-w-[80%] rounded-xl px-4 py-3 ${ |
|
|
message.role === 'user' |
|
|
? 'bg-purple-600 text-white' |
|
|
: 'bg-white/10 text-white border border-white/10' |
|
|
}`} |
|
|
> |
|
|
{message.role === 'assistant' ? ( |
|
|
<div className="prose prose-sm prose-invert max-w-none prose-p:my-1 prose-pre:bg-black/30 prose-pre:border prose-pre:border-white/10 prose-code:text-purple-300 prose-code:before:content-none prose-code:after:content-none prose-headings:text-white prose-headings:font-semibold prose-a:text-purple-400 prose-strong:text-white prose-li:my-0.5"> |
|
|
<ReactMarkdown>{message.content}</ReactMarkdown> |
|
|
</div> |
|
|
) : ( |
|
|
<p className="text-sm whitespace-pre-wrap">{message.content}</p> |
|
|
)} |
|
|
<p className="text-xs mt-2 opacity-50"> |
|
|
{message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} |
|
|
</p> |
|
|
</div> |
|
|
</div> |
|
|
))} |
|
|
|
|
|
{isLoading && ( |
|
|
<div className="flex justify-start"> |
|
|
<div className="bg-white/10 rounded-xl px-4 py-3 border border-white/10"> |
|
|
<div className="flex items-center gap-2"> |
|
|
<div className="w-2 h-2 bg-purple-400 rounded-full animate-bounce" /> |
|
|
<div className="w-2 h-2 bg-purple-400 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }} /> |
|
|
<div className="w-2 h-2 bg-purple-400 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }} /> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
)} |
|
|
|
|
|
<div ref={messagesEndRef} /> |
|
|
</div> |
|
|
|
|
|
{/* Fixed Input Area */} |
|
|
<div className="flex-shrink-0 p-4 border-t border-white/10 bg-white/5"> |
|
|
{taskStatus === 'done' ? ( |
|
|
<div className="text-center py-4"> |
|
|
<p className="text-green-400 font-medium">Task completed!</p> |
|
|
<button |
|
|
onClick={onBack} |
|
|
className="mt-2 text-purple-300 hover:text-white text-sm underline" |
|
|
> |
|
|
Return to dashboard |
|
|
</button> |
|
|
</div> |
|
|
) : ( |
|
|
<form onSubmit={handleSubmit} className="flex gap-3"> |
|
|
<input |
|
|
type="text" |
|
|
value={input} |
|
|
onChange={(e) => setInput(e.target.value)} |
|
|
placeholder="Ask a question, share progress, or describe what you did..." |
|
|
className="flex-1 px-4 py-3 bg-white/5 border border-white/10 rounded-lg text-white placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-purple-500" |
|
|
disabled={isLoading} |
|
|
/> |
|
|
<button |
|
|
type="submit" |
|
|
disabled={isLoading || !input.trim()} |
|
|
className="px-6 py-3 bg-purple-600 hover:bg-purple-700 disabled:bg-purple-800 disabled:cursor-not-allowed text-white font-medium rounded-lg transition-all" |
|
|
> |
|
|
Send |
|
|
</button> |
|
|
</form> |
|
|
)} |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
} |
|
|
|