ProjectMemory / frontend /src /pages /TaskSolverPage.tsx
Amal Nimmy Lal
feat : Project Memory
35765b5
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>
);
}