Spaces:
Build error
Build error
| 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); | |
| // Scroll to bottom when new messages arrive | |
| useEffect(() => { | |
| messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); | |
| }, [messages]); | |
| // Add initial assistant greeting | |
| 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 { | |
| // Build conversation history for context | |
| 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]); | |
| // Check if task was completed by the agent | |
| if (response.taskCompleted) { | |
| setTaskStatus('done'); | |
| onTaskCompleted(); | |
| } else if (response.taskStatus && response.taskStatus !== taskStatus) { | |
| // response.taskStatus may be a plain string from the API; ensure it's one of the | |
| // allowed Task.status values before calling the typed state setter. | |
| 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> | |
| ); | |
| } | |