Files
GoClaw/client/src/components/WebResearchPanel.tsx
Manus 4411db8cd6 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)
2026-03-30 05:39:39 -04:00

277 lines
9.4 KiB
TypeScript

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>
);
}