Checkpoint: Phase 19 Complete: Task Management System + Web Research Workflow
PHASE 19 COMPLETION SUMMARY:
✅ COMPLETED FEATURES:
1. Task Management System (Phase 19.1-19.7)
- Database schema with tasks table (14 columns)
- Query helpers for CRUD operations
- 7 tRPC endpoints for task management
- TasksPanel React component with real-time updates
- Auto-task creation functions
- Chat UI integration with conversationId tracking
2. Auto-Task Creation Integration (Phase 19.8)
- Integrated into orchestratorChat loop
- Detects missing components from tool errors
- Auto-creates tasks for: tools, skills, agents, components, dependencies
- Tracks task completion status
3. Web Research Workflow (Phase 19.9-19.12)
- server/web-research.ts module with 3 main functions:
* performWebResearch() - Execute web searches with Browser Agent
* compileResearchReport() - Generate markdown reports
* createResearchTasks() - Create research tasks for orchestrator
- 3 tRPC endpoints:
* research.search - Perform web research
* research.compileReport - Compile results into report
* research.createTasks - Create research tasks
- WebResearchPanel React component:
* Search input with real-time results
* Options: max results, screenshots, text extraction
* Result cards with expandable details
* Report download functionality
* Error handling and empty states
4. Unit Tests
- 120 tests pass (out of 121 total)
- Web Research tests: 18 tests covering all functions
- Task tests: 5 tests (1 fails due to missing DB table)
- All other tests pass
ARCHITECTURE:
- Browser Agent integration via Puppeteer
- Task tracking with metadata
- Auto-report compilation in markdown
- Screenshot and text extraction support
- Real-time UI updates via tRPC
NEXT STEPS:
1. Run pnpm db:push on production to create tasks table
2. Commit all changes to Gitea
3. Deploy to production
4. Verify tests pass on production DB
5. Test Web Research workflow end-to-end
TEST RESULTS:
- Test Files: 1 failed | 10 passed (11 total)
- Tests: 1 failed | 120 passed (121 total)
- Only failure: tasks.test.ts (requires production DB table)
This commit is contained in:
276
client/src/components/WebResearchPanel.tsx
Normal file
276
client/src/components/WebResearchPanel.tsx
Normal file
@@ -0,0 +1,276 @@
|
||||
import { useState } from "react";
|
||||
import { trpc } from "@/lib/trpc";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import {
|
||||
Search,
|
||||
Loader2,
|
||||
ExternalLink,
|
||||
Image as ImageIcon,
|
||||
FileText,
|
||||
Download,
|
||||
AlertCircle,
|
||||
} from "lucide-react";
|
||||
|
||||
interface ResearchResult {
|
||||
title: string;
|
||||
url: string;
|
||||
snippet?: string;
|
||||
screenshotUrl?: string;
|
||||
extractedText?: string;
|
||||
}
|
||||
|
||||
export function WebResearchPanel({ conversationId }: { conversationId: string }) {
|
||||
const [query, setQuery] = useState("");
|
||||
const [maxResults, setMaxResults] = useState(5);
|
||||
const [includeScreenshots, setIncludeScreenshots] = useState(false);
|
||||
const [extractText, setExtractText] = useState(false);
|
||||
const [results, setResults] = useState<ResearchResult[]>([]);
|
||||
const [expandedResult, setExpandedResult] = useState<number | null>(null);
|
||||
|
||||
const searchMutation = trpc.research.search.useMutation();
|
||||
const compileReportMutation = trpc.research.compileReport.useMutation();
|
||||
|
||||
const handleSearch = async () => {
|
||||
if (!query.trim()) return;
|
||||
|
||||
try {
|
||||
const result = await searchMutation.mutateAsync({
|
||||
query: query.trim(),
|
||||
maxResults,
|
||||
includeScreenshots,
|
||||
extractText,
|
||||
conversationId,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
setResults(result.results);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Search failed:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCompileReport = async () => {
|
||||
if (results.length === 0) return;
|
||||
|
||||
try {
|
||||
const report = await compileReportMutation.mutateAsync({
|
||||
results: [
|
||||
{
|
||||
query,
|
||||
results,
|
||||
totalResults: results.length,
|
||||
executionTimeMs: 0,
|
||||
},
|
||||
],
|
||||
title: `Research Report: ${query}`,
|
||||
});
|
||||
|
||||
// Download report as markdown
|
||||
const element = document.createElement("a");
|
||||
element.setAttribute(
|
||||
"href",
|
||||
"data:text/markdown;charset=utf-8," + encodeURIComponent(report)
|
||||
);
|
||||
element.setAttribute("download", `research-${Date.now()}.md`);
|
||||
element.style.display = "none";
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
document.body.removeChild(element);
|
||||
} catch (error) {
|
||||
console.error("Report compilation failed:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full gap-3 p-4">
|
||||
{/* Search Input */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-semibold text-muted-foreground">
|
||||
Web Research Query
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
|
||||
placeholder="Search the web..."
|
||||
className="text-xs h-8"
|
||||
disabled={searchMutation.isPending}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleSearch}
|
||||
disabled={searchMutation.isPending || !query.trim()}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
{searchMutation.isPending ? (
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||
) : (
|
||||
<Search className="w-3.5 h-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Options */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-xs font-semibold text-muted-foreground">
|
||||
Max Results:
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
max="20"
|
||||
value={maxResults}
|
||||
onChange={(e) => setMaxResults(parseInt(e.target.value) || 5)}
|
||||
className="text-xs h-6 w-16"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="screenshots"
|
||||
checked={includeScreenshots}
|
||||
onCheckedChange={(checked) => setIncludeScreenshots(checked as boolean)}
|
||||
/>
|
||||
<label htmlFor="screenshots" className="text-xs text-muted-foreground cursor-pointer">
|
||||
<ImageIcon className="w-3 h-3 inline mr-1" />
|
||||
Include Screenshots
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="extract-text"
|
||||
checked={extractText}
|
||||
onCheckedChange={(checked) => setExtractText(checked as boolean)}
|
||||
/>
|
||||
<label htmlFor="extract-text" className="text-xs text-muted-foreground cursor-pointer">
|
||||
<FileText className="w-3 h-3 inline mr-1" />
|
||||
Extract Text
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Results */}
|
||||
{results.length > 0 && (
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="text-xs font-semibold text-muted-foreground">
|
||||
Results ({results.length})
|
||||
</label>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={handleCompileReport}
|
||||
disabled={compileReportMutation.isPending}
|
||||
className="text-xs h-6"
|
||||
>
|
||||
{compileReportMutation.isPending ? (
|
||||
<Loader2 className="w-2.5 h-2.5 animate-spin mr-1" />
|
||||
) : (
|
||||
<Download className="w-2.5 h-2.5 mr-1" />
|
||||
)}
|
||||
Report
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="space-y-2 pr-4">
|
||||
{results.map((result, idx) => (
|
||||
<Card
|
||||
key={idx}
|
||||
className="cursor-pointer hover:bg-secondary/50 transition-colors"
|
||||
onClick={() => setExpandedResult(expandedResult === idx ? null : idx)}
|
||||
>
|
||||
<CardHeader className="p-2">
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<CardTitle className="text-xs font-semibold line-clamp-2">
|
||||
{result.title}
|
||||
</CardTitle>
|
||||
<a
|
||||
href={result.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[10px] text-cyan-400 hover:text-cyan-300 truncate flex items-center gap-1 mt-1"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{result.url}
|
||||
<ExternalLink className="w-2.5 h-2.5 shrink-0" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
{expandedResult === idx && (
|
||||
<CardContent className="p-2 space-y-2 border-t border-border/30">
|
||||
{result.snippet && (
|
||||
<div>
|
||||
<p className="text-[10px] font-mono text-muted-foreground mb-1">
|
||||
SNIPPET
|
||||
</p>
|
||||
<p className="text-xs text-foreground/80 line-clamp-3">
|
||||
{result.snippet}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{result.screenshotUrl && (
|
||||
<div>
|
||||
<p className="text-[10px] font-mono text-muted-foreground mb-1">
|
||||
SCREENSHOT
|
||||
</p>
|
||||
<img
|
||||
src={result.screenshotUrl}
|
||||
alt="Page screenshot"
|
||||
className="w-full rounded border border-border/30 max-h-32 object-cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{result.extractedText && (
|
||||
<div>
|
||||
<p className="text-[10px] font-mono text-muted-foreground mb-1">
|
||||
EXTRACTED TEXT
|
||||
</p>
|
||||
<pre className="text-[9px] text-foreground/70 bg-background/50 rounded p-1 overflow-auto max-h-24 whitespace-pre-wrap break-words">
|
||||
{result.extractedText}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error State */}
|
||||
{searchMutation.isError && (
|
||||
<div className="flex items-start gap-2 p-2 bg-red-500/10 border border-red-500/30 rounded">
|
||||
<AlertCircle className="w-3.5 h-3.5 text-red-500 shrink-0 mt-0.5" />
|
||||
<p className="text-xs text-red-400">
|
||||
{(searchMutation.error as any)?.message || "Search failed"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty State */}
|
||||
{!searchMutation.isPending && results.length === 0 && query && (
|
||||
<div className="flex-1 flex items-center justify-center text-center">
|
||||
<p className="text-xs text-muted-foreground">No results found</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user