This commit is contained in:
Adam Cohen Hillel 2025-04-12 17:37:45 +01:00
parent a6d2e5b18e
commit 06ca86c5c5
12 changed files with 1427 additions and 439 deletions

View File

@ -1,222 +1,13 @@
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/context/auth-context';
import Link from 'next/link';
import { Button } from '@/components/ui/button';
import { Search, Image, Calculator } from 'lucide-react';
import ComputerViewer from '@/components/ComputerViewer';
export default function HomePage() {
const { user, isLoading } = useAuth();
const router = useRouter();
useEffect(() => {
// Redirect authenticated users to dashboard
if (!isLoading && user) {
router.push('/dashboard');
}
}, [user, isLoading, router]);
// If still loading or user is authenticated (redirecting), show loading
if (isLoading || user) {
return null;
}
return (
<div className="flex flex-col overflow-auto min-h-screen">
{/* Hero Section */}
<section className="py-16 md:py-24 px-6 text-center">
<div className="max-w-3xl mx-auto">
<h1 className="text-3xl md:text-4xl font-medium tracking-tight mb-6">AgentPress</h1>
<p className="text-xl text-zinc-600 mb-6 max-w-2xl mx-auto">
A meticulously AI-powered search engine with RAG and search grounding capabilities. Clean interface and built for everyone.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center mt-8">
<Button size="lg" className="bg-black hover:bg-zinc-800" asChild>
<Link href="/auth/signup">Get Started</Link>
</Button>
<Button size="lg" variant="outline" className="border-zinc-200" asChild>
<Link href="/auth/login">Try Now</Link>
</Button>
</div>
</div>
</section>
{/* RAG & Search Grounding */}
<section className="py-16 bg-zinc-50 px-6">
<div className="max-w-5xl mx-auto">
<h2 className="text-2xl font-medium text-center mb-10">RAG & Search Grounding</h2>
<div className="bg-white rounded-lg shadow-sm border border-zinc-100 p-8">
<div className="space-y-6">
<div className="flex items-start gap-4 p-3 rounded-lg">
<div className="w-6 h-6 rounded-full bg-gray-100 flex items-center justify-center mt-1">
<span className="text-zinc-500">1</span>
</div>
<div>
<h3 className="font-medium mb-1">Enable modern RAG capabilities in your real-world applications</h3>
<p className="text-zinc-500 text-sm">Semantic search provides better context for AI agents</p>
</div>
</div>
<div className="flex items-start gap-4 p-3 rounded-lg bg-green-50">
<div className="w-6 h-6 rounded-full bg-green-100 flex items-center justify-center mt-1 text-green-600">
<span>2</span>
</div>
<div>
<h3 className="font-medium mb-1 text-green-700">Prioritized instant search</h3>
<p className="text-green-600 text-sm">Get results as you type for a responsive experience</p>
</div>
</div>
<div className="flex items-start gap-4 p-3 rounded-lg">
<div className="w-6 h-6 rounded-full bg-blue-100 flex items-center justify-center mt-1 text-blue-600">
<span>3</span>
</div>
<div>
<h3 className="font-medium mb-1">Integration is just a few lines of code for &quot;information access&quot;</h3>
<p className="text-zinc-500 text-sm">Simple API to connect to your comprehensive search</p>
</div>
</div>
</div>
</div>
</div>
</section>
{/* Powered By */}
<section className="py-16 px-6 text-center">
<div className="max-w-4xl mx-auto">
<h2 className="text-xl font-medium mb-10">Powered By</h2>
<div className="flex justify-center gap-12">
<div className="flex flex-col items-center">
<div className="border border-zinc-200 rounded-lg p-6 bg-white shadow-sm w-40 h-20 flex items-center justify-center">
<svg className="h-6" viewBox="0 0 76 65" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M37.5274 0L75.0548 65H0L37.5274 0Z" fill="#000000"/>
</svg>
</div>
<p className="text-xs text-zinc-500 mt-3">Deployed on Vercel</p>
</div>
<div className="flex flex-col items-center">
<div className="border border-zinc-200 rounded-lg p-6 bg-white shadow-sm w-40 h-20 flex items-center justify-center">
<svg className="h-6" viewBox="0 0 95 95" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="95" height="95" rx="47.5" fill="#F0F0F0"/>
<path d="M30 47.5C30 37.835 37.835 30 47.5 30V65C37.835 65 30 57.165 30 47.5Z" fill="#000000"/>
<path d="M47.5 30C57.165 30 65 37.835 65 47.5C65 57.165 57.165 65 47.5 65V30Z" fill="#666666"/>
</svg>
</div>
<p className="text-xs text-zinc-500 mt-3">Design by Tekky</p>
</div>
</div>
</div>
</section>
{/* Stats Section */}
<section className="py-12 px-6 border-t border-zinc-100">
<div className="max-w-4xl mx-auto grid grid-cols-3 gap-4">
<div className="text-center">
<h3 className="text-2xl font-bold">350K+</h3>
<p className="text-sm text-zinc-500">Requests Processed</p>
</div>
<div className="text-center">
<h3 className="text-2xl font-bold">100K+</h3>
<p className="text-sm text-zinc-500">Active Users</p>
</div>
<div className="text-center">
<h3 className="text-2xl font-bold">7K+</h3>
<p className="text-sm text-zinc-500">Connections Built</p>
</div>
</div>
</section>
{/* Featured on Vercel&apos;s Blog */}
<section className="py-16 px-6 bg-zinc-50">
<div className="max-w-5xl mx-auto">
<h2 className="text-xl font-medium mb-6">Featured on Vercel&apos;s Blog</h2>
<div className="flex flex-col md:flex-row gap-12">
<div className="md:w-1/2">
<p className="text-sm mb-4">
Recognized for our innovative use of AI technology and its integration for a seamless experience. Our approach to developer community growth stands out.
</p>
<Link href="#" className="text-sm text-black font-medium hover:underline">
Read the feature
</Link>
</div>
<div className="md:w-1/2 bg-zinc-100 h-48 rounded-lg"></div>
</div>
</div>
</section>
{/* Advanced Features */}
<section className="py-16 px-6">
<div className="max-w-5xl mx-auto">
<h2 className="text-xl font-medium text-center mb-12">Advanced Search Features</h2>
<div className="grid md:grid-cols-3 gap-6">
<div className="border border-zinc-100 rounded-lg p-6 bg-white">
<div className="mb-4 w-10 h-10 rounded-full bg-zinc-100 flex items-center justify-center">
<Search className="h-5 w-5 text-zinc-600" />
</div>
<h3 className="text-md font-medium mb-2">Smart Understanding</h3>
<p className="text-sm text-zinc-500">Easily analyze content and context for better search results</p>
</div>
<div className="border border-zinc-100 rounded-lg p-6 bg-white">
<div className="mb-4 w-10 h-10 rounded-full bg-zinc-100 flex items-center justify-center">
<Image className="h-5 w-5 text-zinc-600" aria-label="Image understanding icon" />
</div>
<h3 className="text-md font-medium mb-2">Image Understanding</h3>
<p className="text-sm text-zinc-500">Get contextual responses based on visual inputs</p>
</div>
<div className="border border-zinc-100 rounded-lg p-6 bg-white">
<div className="mb-4 w-10 h-10 rounded-full bg-zinc-100 flex items-center justify-center">
<Calculator className="h-5 w-5 text-zinc-600" />
</div>
<h3 className="text-md font-medium mb-2">Smart Calculations</h3>
<p className="text-sm text-zinc-500">Perform complex math and calculations in real-time</p>
</div>
</div>
</div>
</section>
{/* Built For Everyone */}
<section className="py-16 px-6 bg-zinc-50">
<div className="max-w-5xl mx-auto">
<h2 className="text-xl font-medium text-center mb-12">Built For Everyone</h2>
<div className="grid md:grid-cols-3 gap-8">
<div>
<h3 className="font-medium mb-4">Students</h3>
<ul className="space-y-2">
<li className="text-sm text-zinc-600"> Research paper analysis</li>
<li className="text-sm text-zinc-600"> Complex calculations</li>
<li className="text-sm text-zinc-600"> Study guidance</li>
</ul>
</div>
<div>
<h3 className="font-medium mb-4">Researchers</h3>
<ul className="space-y-2">
<li className="text-sm text-zinc-600"> Access to paper archives</li>
<li className="text-sm text-zinc-600"> Data visualization</li>
<li className="text-sm text-zinc-600"> Citation formatting</li>
</ul>
</div>
<div>
<h3 className="font-medium mb-4">Professionals</h3>
<ul className="space-y-2">
<li className="text-sm text-zinc-600"> Market research</li>
<li className="text-sm text-zinc-600"> Technical troubleshooting</li>
<li className="text-sm text-zinc-600"> Code reviews</li>
</ul>
</div>
</div>
</div>
</section>
{/* Footer */}
<footer className="py-8 px-6 border-t border-zinc-100 text-center text-sm text-zinc-500">
<p>© 2023 AgentPress. All rights reserved.</p>
</footer>
<ComputerViewer />
</div>
);
}

View File

@ -22,6 +22,8 @@
"clsx": "^2.1.0",
"cmdk": "^0.2.0",
"date-fns": "^3.6.0",
"diff": "^7.0.0",
"framer-motion": "^12.6.5",
"geist": "^1.2.1",
"lucide-react": "^0.368.0",
"next": "latest",
@ -39,6 +41,7 @@
"zod": "^3.24.2"
},
"devDependencies": {
"@types/diff": "^7.0.2",
"@types/node": "20.11.5",
"@types/react": "18.2.48",
"@types/react-dom": "18.2.18",
@ -1291,6 +1294,13 @@
"tslib": "^2.4.0"
}
},
"node_modules/@types/diff": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@types/diff/-/diff-7.0.2.tgz",
"integrity": "sha512-JSWRMozjFKsGlEjiiKajUjIJVKuKdE3oVy2DNtK+fUo8q82nhFZ2CPQwicAIkXrofahDXrWJ7mjelvZphMS98Q==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "20.11.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.5.tgz",
@ -1992,6 +2002,15 @@
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
"license": "Apache-2.0"
},
"node_modules/diff": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
"integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.3.1"
}
},
"node_modules/dlv": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
@ -2113,6 +2132,33 @@
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/framer-motion": {
"version": "12.6.5",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.6.5.tgz",
"integrity": "sha512-MKvnWov0paNjvRJuIy6x418w23tFqRfS6CXHhZrCiSEpXVlo/F+usr8v4/3G6O0u7CpsaO1qop+v4Ip7PRCBqQ==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.6.5",
"motion-utils": "^12.6.5",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -2421,6 +2467,21 @@
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/motion-dom": {
"version": "12.6.5",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.6.5.tgz",
"integrity": "sha512-jpM9TQLXzYMWMJ7Ec7sAj0iis8oIuu6WvjI3yNKJLdrZyrsI/b2cRInDVL8dCl683zQQq19DpL9cSMP+k8T1NA==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.6.5"
}
},
"node_modules/motion-utils": {
"version": "12.6.5",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.6.5.tgz",
"integrity": "sha512-IsOeKsOF+FWBhxQEDFBO6ZYC8/jlidmVbbLpe9/lXSA9j9kzGIMUuIBx2SZY+0reAS0DjZZ1i7dJp4NHrjocPw==",
"license": "MIT"
},
"node_modules/mz": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",

View File

@ -23,6 +23,8 @@
"clsx": "^2.1.0",
"cmdk": "^0.2.0",
"date-fns": "^3.6.0",
"diff": "^7.0.0",
"framer-motion": "^12.6.5",
"geist": "^1.2.1",
"lucide-react": "^0.368.0",
"next": "latest",
@ -40,6 +42,7 @@
"zod": "^3.24.2"
},
"devDependencies": {
"@types/diff": "^7.0.2",
"@types/node": "20.11.5",
"@types/react": "18.2.48",
"@types/react-dom": "18.2.18",

View File

@ -1,13 +1,16 @@
'use client';
import React, { useState, useEffect, useCallback, useRef } from "react";
import { getProject, getMessages, getThread, addUserMessage, startAgent, stopAgent, getAgentRuns, createThread, type Message, type Project, type Thread, type AgentRun } from "@/lib/api";
import React, { useState, useEffect, useCallback, useRef, useContext } from "react";
import { getProject, getMessages, getThread, addUserMessage, startAgent, stopAgent, getAgentRuns, streamAgent, type Message, type Project, type Thread, type AgentRun } from "@/lib/api";
import { useRouter, useSearchParams } from "next/navigation";
import { AlertCircle, ArrowDown, Play, Square } from "lucide-react";
import { AlertCircle, Play, Square, Send, User, Plus } from "lucide-react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Skeleton } from "@/components/ui/skeleton";
import { SUPPORTED_XML_TAGS, ParsedTag } from "@/lib/types/tool-calls";
import { ToolCallsContext } from "@/app/providers";
import { getComponentForTag } from "@/components/tools/tool-components";
interface AgentPageProps {
params: {
@ -15,16 +18,203 @@ interface AgentPageProps {
};
}
// Simple component to handle message formatting
// Parse XML tags in content
function parseXMLTags(content: string): { parts: (string | ParsedTag)[], openTags: Record<string, ParsedTag> } {
const parts: (string | ParsedTag)[] = [];
const openTags: Record<string, ParsedTag> = {};
const tagStack: Array<{tagName: string, position: number}> = [];
// Find all opening and closing tags
let currentPosition = 0;
// Match opening tags with attributes like <tag-name attr="value">
const openingTagRegex = new RegExp(`<(${SUPPORTED_XML_TAGS.join('|')})\\s*([^>]*)>`, 'g');
// Match closing tags like </tag-name>
const closingTagRegex = new RegExp(`</(${SUPPORTED_XML_TAGS.join('|')})>`, 'g');
let match: RegExpExecArray | null;
let matches: { regex: RegExp, match: RegExpExecArray, isOpening: boolean, position: number }[] = [];
// Find all opening tags
while ((match = openingTagRegex.exec(content)) !== null) {
matches.push({
regex: openingTagRegex,
match,
isOpening: true,
position: match.index
});
}
// Find all closing tags
while ((match = closingTagRegex.exec(content)) !== null) {
matches.push({
regex: closingTagRegex,
match,
isOpening: false,
position: match.index
});
}
// Sort matches by their position in the content
matches.sort((a, b) => a.position - b.position);
// Process matches in order
for (const { match, isOpening, position } of matches) {
const tagName = match[1];
const matchEnd = position + match[0].length;
// Add text before this tag if needed
if (position > currentPosition) {
parts.push(content.substring(currentPosition, position));
}
if (isOpening) {
// Parse attributes for opening tags
const attributesStr = match[2]?.trim();
const attributes: Record<string, string> = {};
if (attributesStr) {
// Match attributes in format: name="value" or name='value'
const attrRegex = /(\w+)=["']([^"']*)["']/g;
let attrMatch;
while ((attrMatch = attrRegex.exec(attributesStr)) !== null) {
attributes[attrMatch[1]] = attrMatch[2];
}
}
// Create tag object with unique ID
const parsedTag: ParsedTag = {
tagName,
attributes,
content: '',
isClosing: false,
id: `${tagName}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
rawMatch: match[0]
};
// Add timestamp if not present
if (!parsedTag.timestamp) {
parsedTag.timestamp = Date.now();
}
// Push to parts and track in stack
parts.push(parsedTag);
tagStack.push({ tagName, position: parts.length - 1 });
openTags[tagName] = parsedTag;
} else {
// Handle closing tag
// Find the corresponding opening tag in the stack (last in, first out)
let foundOpeningTag = false;
for (let i = tagStack.length - 1; i >= 0; i--) {
if (tagStack[i].tagName === tagName) {
const openTagIndex = tagStack[i].position;
const openTag = parts[openTagIndex] as ParsedTag;
// Get content between this opening and closing tag pair
const contentStart = position;
let tagContentStart = openTagIndex + 1;
let tagContentEnd = parts.length;
// Mark that we need to capture content between these positions
let contentToCapture = '';
// Collect all content parts between the opening and closing tags
for (let j = tagContentStart; j < tagContentEnd; j++) {
if (typeof parts[j] === 'string') {
contentToCapture += parts[j];
}
}
// Try getting content directly from original text (most reliable approach)
const openTagMatch = openTag.rawMatch || '';
const openTagPosition = content.indexOf(openTagMatch, Math.max(0, openTagIndex > 0 ? currentPosition - 200 : 0));
if (openTagPosition >= 0) {
const openTagEndPosition = openTagPosition + openTagMatch.length;
// Only use if the positions make sense
if (openTagEndPosition > 0 && position > openTagEndPosition) {
// Get content and clean up excessive whitespace but preserve formatting
let extractedContent = content.substring(openTagEndPosition, position);
// Trim leading newline if present
if (extractedContent.startsWith('\n')) {
extractedContent = extractedContent.substring(1);
}
// Trim trailing newline if present
if (extractedContent.endsWith('\n')) {
extractedContent = extractedContent.substring(0, extractedContent.length - 1);
}
contentToCapture = extractedContent;
// Debug info in development
console.log(`[XML Parse] Extracted content for ${tagName}:`, contentToCapture);
}
}
// Update opening tag with collected content
openTag.content = contentToCapture;
openTag.isClosing = true;
// Remove all parts between the opening tag and this position
// because they're now captured in the tag's content
if (tagContentStart < tagContentEnd) {
parts.splice(tagContentStart, tagContentEnd - tagContentStart);
}
// Remove this tag from the stack
tagStack.splice(i, 1);
// Remove from openTags
delete openTags[tagName];
foundOpeningTag = true;
break;
}
}
// If no corresponding opening tag found, add closing tag as text
if (!foundOpeningTag) {
parts.push(match[0]);
}
}
currentPosition = matchEnd;
}
// Add any remaining text
if (currentPosition < content.length) {
parts.push(content.substring(currentPosition));
}
return { parts, openTags };
}
// Simple component to handle message formatting with XML tag support
function MessageContent({ content }: { content: string }) {
const { parts, openTags } = parseXMLTags(content);
return (
<div className="whitespace-pre-wrap">
{content.split('\n').map((line, i) => (
<React.Fragment key={i}>
{line}
{i < content.split('\n').length - 1 && <br />}
</React.Fragment>
))}
{parts.map((part, index) => {
if (typeof part === 'string') {
return (
<React.Fragment key={index}>
{part.split('\n').map((line, i) => (
<React.Fragment key={i}>
{line}
{i < part.split('\n').length - 1 && <br />}
</React.Fragment>
))}
</React.Fragment>
);
} else {
// Render specialized tool component based on tag type
const ToolComponent = getComponentForTag(part);
return <ToolComponent key={index} tag={part} mode="compact" />;
}
})}
</div>
);
}
@ -35,6 +225,7 @@ export default function AgentPage({ params }: AgentPageProps) {
const searchParams = useSearchParams();
const initialMessage = searchParams.get('message');
const messagesEndRef = useRef<HTMLDivElement>(null);
const streamCleanupRef = useRef<(() => void) | null>(null);
const [agent, setAgent] = useState<Project | null>(null);
const [conversation, setConversation] = useState<Thread | null>(null);
@ -44,6 +235,150 @@ export default function AgentPage({ params }: AgentPageProps) {
const [error, setError] = useState<string | null>(null);
const [userMessage, setUserMessage] = useState("");
const [isSending, setIsSending] = useState(false);
const [isStreaming, setIsStreaming] = useState(false);
const [streamContent, setStreamContent] = useState("");
const [currentAgentRunId, setCurrentAgentRunId] = useState<string | null>(null);
// Get tool calls context
const { toolCalls, setToolCalls } = useContext(ToolCallsContext);
// Process messages and stream for tool calls
useEffect(() => {
// Extract tool calls from all messages
const allContent = [...messages.map(msg => msg.content), streamContent].filter(Boolean);
console.log(`[TOOLS] Processing ${allContent.length} content items for tool calls`);
// Create a new array of tags with a better deduplication strategy
const extractedTags: ParsedTag[] = [];
const seenTagIds = new Set<string>();
allContent.forEach((content, idx) => {
console.log(`[TOOLS] Extracting from content #${idx}, length: ${content.length}`);
const { parts, openTags } = parseXMLTags(content);
let tagsFound = 0;
// Mark tool calls vs results based on position and sender
const isUserMessage = idx % 2 === 0; // Assuming alternating user/assistant messages
// Process all parts to mark as tool calls or results
parts.forEach(part => {
if (typeof part !== 'string') {
// Create a unique ID for this tag if not present
if (!part.id) {
part.id = `${part.tagName}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
}
// Add timestamp if not present
if (!part.timestamp) {
part.timestamp = Date.now();
}
// Mark as tool call or result
part.isToolCall = !isUserMessage;
part.status = part.isClosing ? 'completed' : 'running';
// Use ID for deduplication
if (!seenTagIds.has(part.id)) {
seenTagIds.add(part.id);
extractedTags.push(part);
tagsFound++;
}
}
});
// Also add any open tags
Object.values(openTags).forEach(tag => {
// Create a unique ID for this tag if not present
if (!tag.id) {
tag.id = `${tag.tagName}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
}
// Add timestamp if not present
if (!tag.timestamp) {
tag.timestamp = Date.now();
}
// Mark as tool call or result
tag.isToolCall = !isUserMessage;
tag.status = tag.isClosing ? 'completed' : 'running';
// Use ID for deduplication
if (!seenTagIds.has(tag.id)) {
seenTagIds.add(tag.id);
extractedTags.push(tag);
tagsFound++;
}
});
console.log(`[TOOLS] Found ${tagsFound} tags in content #${idx}`);
});
console.log(`[TOOLS] Extracted ${extractedTags.length} total tools`);
// Sort the tools by timestamp (oldest first)
extractedTags.sort((a, b) => {
if (a.timestamp && b.timestamp) {
return a.timestamp - b.timestamp;
}
return 0;
});
// Try to pair tool calls with their results
const pairedTags: ParsedTag[] = [];
const callsByTagName: Record<string, ParsedTag[]> = {};
// Group by tag name first
extractedTags.forEach(tag => {
if (!callsByTagName[tag.tagName]) {
callsByTagName[tag.tagName] = [];
}
callsByTagName[tag.tagName].push(tag);
});
// For each tag type, try to pair calls with results
Object.values(callsByTagName).forEach(tagGroup => {
const toolCalls = tagGroup.filter(tag => tag.isToolCall);
const toolResults = tagGroup.filter(tag => !tag.isToolCall);
// Try to match each tool call with a result
toolCalls.forEach(toolCall => {
// Find the nearest matching result (by timestamp)
const matchingResult = toolResults.find(result =>
!result.isPaired && result.attributes &&
Object.keys(toolCall.attributes).every(key =>
toolCall.attributes[key] === result.attributes[key]
)
);
if (matchingResult) {
// Pair them
toolCall.resultTag = matchingResult;
toolCall.isPaired = true;
toolCall.status = 'completed';
matchingResult.isPaired = true;
// Add to paired list
pairedTags.push(toolCall);
} else {
// No result yet, tool call is still running
toolCall.status = 'running';
pairedTags.push(toolCall);
}
});
// Add any unpaired results
toolResults.filter(result => !result.isPaired).forEach(result => {
pairedTags.push(result);
});
});
console.log(`[TOOLS] Paired ${pairedTags.length} tools, ${pairedTags.filter(t => t.isPaired).length} were paired`);
// Update tool calls in the shared context
setToolCalls(pairedTags);
}, [messages, streamContent, setToolCalls]);
// Scroll to bottom of messages
const scrollToBottom = useCallback(() => {
@ -84,6 +419,13 @@ export default function AgentPage({ params }: AgentPageProps) {
const agentRunsData = await getAgentRuns(threadId);
setAgentRuns(agentRunsData);
// Check if there's a running agent run
const runningAgent = agentRunsData.find(run => run.status === "running");
if (runningAgent) {
setCurrentAgentRunId(runningAgent.id);
handleStreamAgent(runningAgent.id);
}
}
}
} catch (err: any) {
@ -101,41 +443,156 @@ export default function AgentPage({ params }: AgentPageProps) {
}
loadData();
// Clean up streaming on component unmount
return () => {
if (streamCleanupRef.current) {
console.log("[PAGE] Cleaning up stream on unmount");
streamCleanupRef.current();
streamCleanupRef.current = null;
}
};
}, [threadId, initialMessage, router]);
// Scroll to bottom when messages change
useEffect(() => {
scrollToBottom();
}, [messages, scrollToBottom]);
}, [messages, streamContent, scrollToBottom]);
// Check for agent status periodically if an agent is running
useEffect(() => {
if (!conversation) return;
// Handle streaming agent responses
const handleStreamAgent = useCallback((agentRunId: string) => {
// Clean up any existing stream first
if (streamCleanupRef.current) {
console.log("[PAGE] Cleaning up existing stream before starting new one");
streamCleanupRef.current();
streamCleanupRef.current = null;
}
const isAgentRunning = agentRuns.some(run => run.status === "running");
if (!isAgentRunning) return;
setIsStreaming(true);
setStreamContent("");
// Poll for updates every 3 seconds if agent is running
const interval = setInterval(async () => {
try {
const updatedAgentRuns = await getAgentRuns(conversation.thread_id);
setAgentRuns(updatedAgentRuns);
// Also refresh messages
const updatedMessages = await getMessages(conversation.thread_id);
setMessages(updatedMessages);
// If no agent is running anymore, stop polling
if (!updatedAgentRuns.some(run => run.status === "running")) {
clearInterval(interval);
console.log(`[PAGE] Setting up stream for agent run ${agentRunId}`);
const cleanup = streamAgent(agentRunId, {
onMessage: (rawData: string) => {
try {
console.log(`[PAGE] Raw message data:`, rawData);
// Handle data: prefix format (SSE standard)
let processedData = rawData;
let jsonData: {
type?: string;
status?: string;
content?: string;
} | null = null;
if (rawData.startsWith('data: ')) {
processedData = rawData.substring(6).trim();
}
// Try to parse as JSON
try {
jsonData = JSON.parse(processedData);
// Handle status messages
if (jsonData?.type === 'status') {
if (jsonData?.status === 'completed') {
console.log("[PAGE] Detected completion status event");
// Reset streaming on completion
setIsStreaming(false);
// Fetch updated messages
if (threadId) {
getMessages(threadId)
.then(updatedMsgs => {
console.log("[PAGE] Updated messages:");
console.log(updatedMsgs);
setMessages(updatedMsgs);
setStreamContent("");
// Also update agent runs
return getAgentRuns(threadId);
})
.then(updatedRuns => {
setAgentRuns(updatedRuns);
setCurrentAgentRunId(null);
})
.catch(err => console.error("[PAGE] Failed to update after completion:", err));
}
return;
}
return; // Don't process other status messages further
}
// Handle content messages
if (jsonData?.type === 'content' && jsonData?.content) {
setStreamContent(prev => prev + jsonData?.content);
return;
}
} catch (e) {
// If not valid JSON, just append the raw data
console.warn("[PAGE] Failed to parse as JSON:", e);
}
// If we couldn't parse as special format, just append the raw data
if (!jsonData) {
setStreamContent(prev => prev + processedData);
}
} catch (error) {
console.warn("[PAGE] Failed to process message:", error);
}
} catch (err) {
console.error("Error polling agent status:", err);
},
onError: (error: Error | string) => {
console.error("[PAGE] Streaming error:", error);
setIsStreaming(false);
},
onClose: async () => {
console.log("[PAGE] Stream connection closed");
// Set UI state to not streaming
setIsStreaming(false);
try {
// Update messages and agent runs
if (threadId) {
const updatedMessages = await getMessages(threadId);
setMessages(updatedMessages);
const updatedAgentRuns = await getAgentRuns(threadId);
setAgentRuns(updatedAgentRuns);
// Reset current agent run
setCurrentAgentRunId(null);
// Clear streaming content after a short delay
setTimeout(() => {
setStreamContent("");
}, 50);
}
} catch (err) {
console.error("[PAGE] Error checking final status:", err);
// If there was streaming content, add it as a message
if (streamContent) {
const assistantMessage: Message = {
role: 'assistant',
content: streamContent + "\n\n[Connection to agent lost]",
};
setMessages(prev => [...prev, assistantMessage]);
setStreamContent("");
}
}
// Clear cleanup reference
streamCleanupRef.current = null;
}
}, 3000);
});
return () => clearInterval(interval);
}, [agentRuns, conversation]);
// Store cleanup function
streamCleanupRef.current = cleanup;
}, [threadId]);
// Handle sending a message
const handleSendMessage = async () => {
@ -144,21 +601,25 @@ export default function AgentPage({ params }: AgentPageProps) {
setIsSending(true);
try {
// Add user message
await addUserMessage(conversation.thread_id, userMessage);
// Start the agent
await startAgent(conversation.thread_id);
// Add user message optimistically to UI
const userMsg: Message = {
role: 'user',
content: userMessage,
};
setMessages(prev => [...prev, userMsg]);
// Clear the input
setUserMessage("");
// Refresh data
const updatedMessages = await getMessages(conversation.thread_id);
setMessages(updatedMessages);
// Add user message to API and start agent
await addUserMessage(conversation.thread_id, userMessage);
const agentResponse = await startAgent(conversation.thread_id);
const updatedAgentRuns = await getAgentRuns(conversation.thread_id);
setAgentRuns(updatedAgentRuns);
// Set current agent run ID and start streaming
if (agentResponse.agent_run_id) {
setCurrentAgentRunId(agentResponse.agent_run_id);
handleStreamAgent(agentResponse.agent_run_id);
}
} catch (err) {
console.error("Error sending message:", err);
setError(err instanceof Error ? err.message : "Failed to send message");
@ -167,37 +628,36 @@ export default function AgentPage({ params }: AgentPageProps) {
}
};
// Handle starting the agent
const handleRunAgent = async () => {
if (isSending || !conversation) return;
setIsSending(true);
try {
await startAgent(conversation.thread_id);
// Refresh agent runs
const updatedAgentRuns = await getAgentRuns(conversation.thread_id);
setAgentRuns(updatedAgentRuns);
} catch (err) {
console.error("Error starting agent:", err);
setError(err instanceof Error ? err.message : "Failed to start agent");
} finally {
setIsSending(false);
}
};
// Handle stopping the agent
const handleStopAgent = async () => {
try {
// Find the running agent run
const runningAgent = agentRuns.find(run => run.status === "running");
if (runningAgent) {
await stopAgent(runningAgent.id);
const runningAgentId = currentAgentRunId || agentRuns.find(run => run.status === "running")?.id;
if (runningAgentId) {
// Clean up stream first
if (streamCleanupRef.current) {
console.log("[PAGE] Cleaning up stream before stopping agent");
streamCleanupRef.current();
streamCleanupRef.current = null;
}
// Then stop the agent
await stopAgent(runningAgentId);
setIsStreaming(false);
setCurrentAgentRunId(null);
// Refresh agent runs
if (conversation) {
const updatedAgentRuns = await getAgentRuns(conversation.thread_id);
setAgentRuns(updatedAgentRuns);
// Also refresh messages to get any partial responses
const updatedMessages = await getMessages(conversation.thread_id);
setMessages(updatedMessages);
// Clear streaming content
setStreamContent("");
}
}
} catch (err) {
@ -206,8 +666,8 @@ export default function AgentPage({ params }: AgentPageProps) {
}
};
// Check if agent is running
const isAgentRunning = agentRuns.some(run => run.status === "running");
// Check if agent is running either from agent runs list or streaming state
const isAgentRunning = isStreaming || currentAgentRunId !== null || agentRuns.some(run => run.status === "running");
if (error) {
return (
@ -247,9 +707,9 @@ export default function AgentPage({ params }: AgentPageProps) {
}
return (
<div className="flex flex-col h-[calc(100vh-10rem)] max-h-[calc(100vh-10rem)]">
<div className="flex-1 overflow-y-auto p-4 space-y-4" id="messages-container">
{messages.length === 0 ? (
<div className="flex flex-col h-[calc(100vh-10rem)] max-h-[calc(100vh-10rem)] overflow-hidden">
<div className="flex-1 overflow-y-auto p-4 pb-[120px] space-y-4" id="messages-container">
{messages.length === 0 && !streamContent ? (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<h3 className="text-lg font-medium">Start a conversation</h3>
@ -260,29 +720,65 @@ export default function AgentPage({ params }: AgentPageProps) {
</div>
) : (
<>
{messages.map((message, index) => (
<div
key={index}
className={`flex ${
message.role === "user" ? "justify-end" : "justify-start"
}`}
>
{messages.map((message, index) => {
// Skip messages containing "ToolResult("
if (message.content.includes("ToolResult(")) {
return null;
}
return (
<div
className={`max-w-[80%] p-4 rounded-lg ${
message.role === "user"
? "bg-primary text-primary-foreground"
: "bg-muted"
key={index}
className={`flex ${
message.role === "user" ? "justify-end" : "justify-start"
}`}
>
<MessageContent content={message.content} />
<div
className={`max-w-[80%] p-4 rounded-lg ${
message.role === "user"
? "bg-[#f0efe7] text-foreground flex items-start"
: ""
}`}
>
{message.role === "user" && (
<User className="h-5 w-5 mr-2 shrink-0 mt-0.5" />
)}
<MessageContent content={message.content} />
</div>
</div>
);
})}
{/* Show streaming content if available */}
{streamContent && (
<div className="flex justify-start">
<div className="max-w-[80%] p-4">
<div className="whitespace-pre-wrap">
<MessageContent content={streamContent} />
{isStreaming && (
<span
className="inline-block h-4 w-0.5 bg-foreground/50 mx-px"
style={{
opacity: 0.7,
animation: 'cursorBlink 1s ease-in-out infinite',
}}
/>
)}
<style jsx global>{`
@keyframes cursorBlink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
`}</style>
</div>
</div>
</div>
))}
)}
{/* Show a loading indicator if the agent is running */}
{isAgentRunning && (
{/* Show a loading indicator if the agent is running but no stream yet */}
{isAgentRunning && !streamContent && (
<div className="flex justify-start">
<div className="max-w-[80%] p-4 rounded-lg bg-muted">
<div className="max-w-[80%] p-4">
<div className="flex items-center space-x-2">
<div className="h-2 w-2 bg-foreground rounded-full animate-pulse"></div>
<div className="h-2 w-2 bg-foreground rounded-full animate-pulse delay-150"></div>
@ -297,53 +793,40 @@ export default function AgentPage({ params }: AgentPageProps) {
<div ref={messagesEndRef} id="messages-end"></div>
</div>
<div className="p-4 border-t">
<div className="flex flex-col space-y-2">
<div className="flex-1">
<Textarea
value={userMessage}
onChange={(e) => setUserMessage(e.target.value)}
placeholder="Type your message..."
className="resize-none min-h-[80px]"
disabled={isAgentRunning || isSending || !conversation}
onKeyDown={(e) => {
if (e.key === 'Enter' && e.shiftKey === false) {
e.preventDefault();
handleSendMessage();
}
}}
/>
</div>
<div className="flex justify-between items-center">
<div>
{isAgentRunning ? (
<Button
onClick={handleStopAgent}
variant="outline"
size="sm"
>
<Square className="h-4 w-4 mr-2" />
Stop Agent
</Button>
) : (
<Button
onClick={handleRunAgent}
variant="outline"
size="sm"
disabled={messages.length === 0 || isSending || !conversation}
>
<Play className="h-4 w-4 mr-2" />
Run Agent
</Button>
)}
</div>
<div className="absolute bottom-0 left-0 right-0 z-10 bg-background pb-6">
<div className="relative bg-white border border-gray-200 rounded-2xl p-2 mx-4">
{/* style={{ boxShadow: '0 0 15px rgba(0, 0, 0, 0.1), 0 0 8px rgba(0, 0, 0, 0.07)' }}> */}
<Textarea
value={userMessage}
onChange={(e) => setUserMessage(e.target.value)}
placeholder="Type your message..."
className="resize-none min-h-[100px] pr-12 border-0 focus-visible:ring-0 focus-visible:ring-offset-0"
disabled={isAgentRunning || isSending || !conversation}
onKeyDown={(e) => {
if (e.key === 'Enter' && e.shiftKey === false) {
e.preventDefault();
handleSendMessage();
}
}}
/>
<div className="absolute bottom-3 right-3 flex space-x-2">
<Button
onClick={handleSendMessage}
className="ml-2"
disabled={!userMessage.trim() || isAgentRunning || isSending || !conversation}
variant="outline"
className="h-10 w-10 p-0 rounded-2xl border border-gray-200"
disabled={isAgentRunning || isSending || !conversation}
>
Send Message
{isSending && <span className="ml-2">...</span>}
<Plus className="h-4 w-4" />
</Button>
<Button
onClick={isAgentRunning ? handleStopAgent : handleSendMessage}
className="h-10 w-10 p-0 rounded-2xl"
disabled={(!userMessage.trim() && !isAgentRunning) || isSending || !conversation}
>
{isAgentRunning ? (
<Square className="h-4 w-4" />
) : (
<Send className="h-4 w-4" />
)}
</Button>
</div>
</div>

View File

@ -2,11 +2,13 @@ import DashboardLayout from "@/components/layout/DashboardLayout";
import { createClient } from "@/lib/supabase/server";
import { redirect } from "next/navigation";
export default async function PersonalDashboardLayout({
children,
}: {
interface PersonalAccountLayoutProps {
children: React.ReactNode;
}) {
}
export default async function PersonalAccountLayout({
children,
}: PersonalAccountLayoutProps) {
// Get the current user via Supabase
const supabaseClient = createClient();
const { data: { user } } = await supabaseClient.auth.getUser();
@ -19,61 +21,19 @@ export default async function PersonalDashboardLayout({
// Get the personal account details
const { data: personalAccount } = await supabaseClient.rpc('get_personal_account');
// Define the navigation items for our agent-based UI
const navigation = [
{
name: "Home",
href: "/dashboard",
},
{
name: "Agents",
href: "/dashboard/agents",
},
{
name: "Devices",
href: "/dashboard/devices",
},
{
name: "Data",
href: "/dashboard/data",
},
{
name: "Settings",
href: "/dashboard/settings",
},
{ name: "Dashboard", href: "/dashboard" },
{ name: "Agents", href: "/dashboard/agents" },
{ name: "Settings", href: "/dashboard/settings" },
];
// Right panel content
const rightPanelContent = (
<div className="flex flex-col gap-4">
<div className="p-4 border rounded-lg">
<h3 className="font-medium mb-2">Account Status</h3>
<p className="text-sm text-muted-foreground">Active</p>
</div>
<div className="p-4 border rounded-lg">
<h3 className="font-medium mb-2">Recent Activity</h3>
<ul className="space-y-2 text-sm">
<li className="flex items-center justify-between">
<span>Login</span>
<span className="text-muted-foreground">1 hour ago</span>
</li>
<li className="flex items-center justify-between">
<span>Settings updated</span>
<span className="text-muted-foreground">3 days ago</span>
</li>
</ul>
</div>
</div>
);
return (
<DashboardLayout
navigation={navigation}
accountId={personalAccount?.account_id || user.id}
userName={personalAccount?.name || user.email?.split("@")[0] || "User"}
userEmail={personalAccount?.email || user.email}
rightPanelContent={rightPanelContent}
rightPanelTitle="Account Details"
userName={user?.user_metadata?.name || user.email?.split('@')[0] || 'User'}
userEmail={user.email}
rightPanelTitle="Suna's Computer"
>
{children}
</DashboardLayout>

View File

@ -1,19 +1,13 @@
import { Inter as FontSans } from "next/font/google"
import "./globals.css";
import { cn } from "@/lib/utils";
import { ThemeProvider } from "@/components/theme-provider";
import './globals.css';
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import { Providers } from './providers';
const defaultUrl = process.env.NEXT_PUBLIC_URL as string || "http://localhost:3000";
const inter = Inter({ subsets: ['latin'] });
const fontSans = FontSans({
subsets: ["latin"],
variable: "--font-sans",
})
export const metadata = {
metadataBase: new URL(defaultUrl),
title: "Suna | General AI Agent",
description: "Suna is a general AI agent that can help you with your tasks.",
export const metadata: Metadata = {
title: 'AgentPress',
description: 'Run AI agents in your company or personally',
};
export default function RootLayout({
@ -22,21 +16,9 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<html lang="en" suppressHydrationWarning className={cn(
"min-h-screen bg-background font-sans antialiased",
fontSans.variable
)}>
<body className="bg-background text-foreground">
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<main className="min-h-screen flex flex-col">
{children}
</main>
</ThemeProvider>
<html lang="en" suppressHydrationWarning>
<body className={inter.className}>
<Providers>{children}</Providers>
</body>
</html>
);

View File

@ -0,0 +1,27 @@
'use client';
import { ThemeProvider } from 'next-themes';
import { useState, createContext } from 'react';
import { ParsedTag } from '@/lib/types/tool-calls';
// Create the context here instead of importing it
export const ToolCallsContext = createContext<{
toolCalls: ParsedTag[];
setToolCalls: React.Dispatch<React.SetStateAction<ParsedTag[]>>;
}>({
toolCalls: [],
setToolCalls: () => {},
});
export function Providers({ children }: { children: React.ReactNode }) {
// Shared state for tool calls across the app
const [toolCalls, setToolCalls] = useState<ParsedTag[]>([]);
return (
<ToolCallsContext.Provider value={{ toolCalls, setToolCalls }}>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
</ToolCallsContext.Provider>
);
}

View File

@ -14,9 +14,11 @@ import {
MessagesSquare,
ArrowRight,
PanelLeft,
Wrench
} from "lucide-react";
import { useTheme } from "next-themes";
import { getProjects, getThreads } from "@/lib/api";
import { useToolsPanel } from "@/lib/hooks/use-tools-panel";
import UserAccountPanel from "@/components/dashboard/user-account-panel";
@ -81,6 +83,25 @@ export default function DashboardLayout({
const [rightSidebarCollapsed, setRightSidebarCollapsed] = useState(true);
const [showRightSidebar, setShowRightSidebar] = useState(false);
// Use our tools panel hook
const {
showPanel,
renderToolsPanel,
toolCalls
} = useToolsPanel();
// Use tool panel as the right panel if it's available
const effectiveRightPanelContent = showPanel ? renderToolsPanel() : rightPanelContent;
const effectiveRightPanelTitle = showPanel ? `Suna's Computer (${toolCalls.length})` : rightPanelTitle;
// Update right sidebar visibility based on panel visibility
useEffect(() => {
if (showPanel) {
setShowRightSidebar(true);
setRightSidebarCollapsed(false);
}
}, [showPanel]);
// State for draggable panel
const [position, setPosition] = useState({ x: 20, y: 400 });
const [isDragging, setIsDragging] = useState(false);
@ -129,8 +150,8 @@ export default function DashboardLayout({
// Set initial showRightSidebar based on rightPanelContent prop
useEffect(() => {
setShowRightSidebar(!!rightPanelContent);
}, [rightPanelContent]);
setShowRightSidebar(!!effectiveRightPanelContent);
}, [effectiveRightPanelContent]);
// Load layout state from localStorage on initial render
useEffect(() => {
@ -141,7 +162,7 @@ export default function DashboardLayout({
const layout = JSON.parse(savedLayout);
setLeftSidebarCollapsed(layout.leftSidebarCollapsed);
if (rightPanelContent) {
if (effectiveRightPanelContent) {
setRightSidebarCollapsed(layout.rightSidebarCollapsed);
setShowRightSidebar(layout.showRightSidebar);
}
@ -160,7 +181,7 @@ export default function DashboardLayout({
console.error("Error loading layout from localStorage:", error);
}
}
}, [rightPanelContent]);
}, [effectiveRightPanelContent]);
// Save layout state to localStorage whenever relevant state changes
useEffect(() => {
@ -465,12 +486,19 @@ export default function DashboardLayout({
>
<header className="h-12 px-4 flex items-center justify-end">
<div className="flex items-center gap-2">
{!showRightSidebar && rightPanelContent && (
{!showRightSidebar && effectiveRightPanelContent && (
<button
onClick={toggleRightSidebarVisibility}
className="px-3 py-1.5 rounded-full hover:bg-hover-bg dark:hover:bg-hover-bg-dark text-foreground/80 hover:text-foreground transition-all duration-200 text-xs border border-subtle dark:border-white/10"
>
{rightPanelTitle}
{showPanel ? (
<span className="flex items-center gap-1">
<Wrench className="h-3 w-3" />
Tool Calls
</span>
) : (
effectiveRightPanelTitle
)}
</button>
)}
<div className="relative">
@ -500,7 +528,7 @@ export default function DashboardLayout({
</div>
{/* Floating draggable right panel - only show when right sidebar is collapsed and visible */}
{showRightSidebar && rightSidebarCollapsed && rightPanelContent && (
{showRightSidebar && rightSidebarCollapsed && effectiveRightPanelContent && (
<div
ref={floatingPanelRef}
className={`absolute bg-card-bg dark:bg-background-secondary border border-subtle dark:border-white/10 rounded-lg shadow-custom flex flex-col w-72 overflow-hidden ${
@ -521,7 +549,7 @@ export default function DashboardLayout({
className="text-icon-color dark:text-icon-color-dark"
/>
<h3 className="font-medium text-foreground text-xs select-none">
{rightPanelTitle}
{effectiveRightPanelTitle}
</h3>
</div>
<div className="flex items-center gap-1">
@ -566,7 +594,7 @@ export default function DashboardLayout({
</div>
{/* Right Sidebar - only show when visible */}
{showRightSidebar && rightPanelContent && (
{showRightSidebar && effectiveRightPanelContent && (
<div
className={`h-screen border-l border-subtle dark:border-white/10 bg-background-secondary dark:bg-background-secondary transition-all duration-300 ease-in-out rounded-l-xl shadow-custom ${
rightSidebarCollapsed
@ -579,7 +607,9 @@ export default function DashboardLayout({
}}
>
<div className="h-12 p-2 flex items-center justify-between rounded-t-xl">
<h2 className="font-medium text-sm text-card-title">{rightPanelTitle}</h2>
<h2 className="font-medium text-sm text-card-title">
{effectiveRightPanelTitle}
</h2>
<div className="flex items-center gap-1">
<button
onClick={toggleRightSidebar}
@ -599,7 +629,7 @@ export default function DashboardLayout({
</div>
<div className="p-3 overflow-y-auto h-[calc(100vh-48px)]">
{rightPanelContent || (
{effectiveRightPanelContent || (
<div className="flex flex-col items-center justify-center h-full text-foreground/40">
<p className="text-sm">No content selected</p>
</div>

View File

@ -0,0 +1,369 @@
'use client';
import React from 'react';
import { ParsedTag, ToolComponentProps, ToolDisplayMode, ToolStatusLabels } from '@/lib/types/tool-calls';
import {
File, FileText, Terminal, FolderPlus, Folder, Code, Search as SearchIcon,
Bell, Replace, ChevronDown, ChevronRight, Plus, Minus
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { diffLines } from 'diff';
import { useTheme } from 'next-themes';
interface BaseToolProps {
tag: ParsedTag;
mode: ToolDisplayMode;
title: string;
children: React.ReactNode;
icon?: React.ReactNode;
}
/**
* Base Tool Component
*/
export const BaseTool: React.FC<BaseToolProps> = ({ tag, mode, title, children, icon }) => {
// Determine if we should use dark mode based on the display mode
const isDark = mode === 'detailed';
const isRunning = tag.status === 'running';
return (
<div className="border rounded-lg overflow-hidden border-subtle dark:border-white/10">
<div className={cn(
"flex items-center px-2 py-1 text-xs font-medium border-b border-subtle dark:border-white/10",
isDark
? "bg-background-secondary dark:bg-background-secondary text-foreground"
: "bg-background-secondary dark:bg-background-secondary text-foreground"
)}>
{icon}
<div className="flex-1">{title}</div>
{isRunning && (
<div className="flex items-center gap-2">
<span className="text-amber-500">Running</span>
<div className="h-2 w-2 rounded-full bg-amber-500 animate-pulse"></div>
</div>
)}
</div>
<div className={cn(
"p-3",
isDark ? "bg-card-bg dark:bg-background-secondary text-foreground" : "bg-card-bg dark:bg-background-secondary text-foreground"
)}>
{children}
</div>
</div>
);
};
/**
* Create File Tool Component
*/
export const CreateFileTool: React.FC<ToolComponentProps> = ({ tag, mode }) => {
const filePath = tag.attributes.file_path || '';
const fileContent = tag.content || '';
return (
<BaseTool
tag={tag}
mode={mode}
title={`Creating file: ${filePath}`}
icon={<FileText className="h-4 w-4 mr-2" />}
>
<div className="space-y-2">
<div className="flex items-center gap-1 text-xs text-muted-foreground mb-1">
<FileText className="h-3 w-3" />
<span className="font-mono">{filePath}</span>
</div>
<pre className="whitespace-pre-wrap bg-muted p-2 rounded-md text-xs overflow-x-auto max-h-60">
{fileContent}
</pre>
</div>
</BaseTool>
);
};
/**
* Full File Rewrite Tool Component
*/
export const FullFileRewriteTool: React.FC<ToolComponentProps> = ({ tag, mode }) => {
const filePath = tag.attributes.file_path || '';
const fileContent = tag.content || '';
return (
<BaseTool
tag={tag}
mode={mode}
title={`Full file rewrite: ${filePath}`}
icon={<FileText className="h-4 w-4 mr-2" />}
>
<div className="space-y-2">
<div className="flex items-center gap-1 text-xs text-muted-foreground mb-1">
<FileText className="h-3 w-3" />
<span className="font-mono">{filePath}</span>
</div>
<pre className="whitespace-pre-wrap bg-muted p-2 rounded-md text-xs overflow-x-auto max-h-60">
{fileContent}
</pre>
</div>
</BaseTool>
);
};
/**
);
};
/**
* Read File Tool Component
*/
export const ReadFileTool: React.FC<ToolComponentProps> = ({ tag, mode }) => {
const filePath = tag.attributes.file_path || '';
const fileContent = tag.content || '';
return (
<BaseTool
tag={tag}
mode={mode}
title={`Reading file: ${filePath}`}
icon={<File className="h-4 w-4 mr-2" />}
>
<div className="space-y-2">
<div className="flex items-center gap-1 text-xs text-muted-foreground mb-1">
<File className="h-3 w-3" />
<span className="font-mono">{filePath}</span>
</div>
<pre className="whitespace-pre-wrap bg-muted p-2 rounded-md text-xs overflow-x-auto max-h-60">
{fileContent}
</pre>
</div>
</BaseTool>
);
};
/**
* Execute Command Tool Component
*/
export const ExecuteCommandTool: React.FC<ToolComponentProps> = ({ tag, mode }) => {
const command = tag.attributes.command || '';
const output = tag.content || '';
const resultOutput = tag.resultTag?.content || '';
// Only show paired view if explicitly paired
const showPairedView = tag.isPaired && tag.resultTag?.content;
// Check status
const isRunning = tag.status === 'running';
return (
<BaseTool
tag={tag}
mode={mode}
title={isRunning ? `Executing: ${command}` : `Executed: ${command}`}
icon={<Terminal className="h-4 w-4 mr-2" />}
>
<div className="space-y-2">
<div className="flex items-start gap-1 text-xs">
<span className="text-green-500 font-mono">$</span>
<span className="font-mono text-green-500 dark:text-green-400">{command}</span>
</div>
{showPairedView ? (
<div className="space-y-3">
{output && (
<div>
<div className="text-xs font-medium mb-1 text-amber-500 dark:text-amber-400">Command Output</div>
<div className="bg-muted dark:bg-gray-900 rounded-md p-2 text-xs font-mono text-green-500 dark:text-green-400 overflow-x-auto max-h-60">
<pre className="whitespace-pre-wrap">{output}</pre>
</div>
</div>
)}
<div>
<div className="text-xs font-medium mb-1 text-green-500 dark:text-green-400">Result</div>
<div className="bg-muted dark:bg-gray-900 rounded-md p-2 text-xs font-mono text-green-500 dark:text-green-400 overflow-x-auto max-h-60">
<pre className="whitespace-pre-wrap">{resultOutput}</pre>
</div>
</div>
</div>
) : (
<div className="bg-muted dark:bg-gray-900 rounded-md p-2 text-xs font-mono text-green-500 dark:text-green-400 overflow-x-auto max-h-60">
<pre className="whitespace-pre-wrap">{output}</pre>
</div>
)}
</div>
</BaseTool>
);
};
/**
* String Replace Tool Component
*/
export const StringReplaceTool: React.FC<ToolComponentProps> = ({ tag, mode }) => {
const content = tag.content || '';
// Parse the old and new strings from the content
const oldStrMatch = content.match(/<old_str>([\s\S]*?)<\/old_str>/);
const newStrMatch = content.match(/<new_str>([\s\S]*?)<\/new_str>/);
const oldStr = oldStrMatch ? oldStrMatch[1] : '';
const newStr = newStrMatch ? newStrMatch[1] : '';
// Calculate the diff between old and new strings
const diff = diffLines(oldStr, newStr);
interface DiffPart {
added?: boolean;
removed?: boolean;
value: string;
}
return (
<BaseTool
tag={tag}
mode={mode}
title="File update"
icon={<Replace className="h-4 w-4 mr-2" />}
>
<div className="space-y-2">
<div className="border border-subtle dark:border-white/10 rounded-md overflow-hidden font-mono text-xs">
{diff.map((part: DiffPart, index: number) => (
<div
key={index}
className={cn(
"px-2 py-0.5 flex items-start",
part.added ? "bg-green-500/10 text-green-700 dark:text-green-400" :
part.removed ? "bg-red-500/10 text-red-700 dark:text-red-400" :
"text-foreground"
)}
>
<span className="mr-2">
{part.added ? <Plus className="h-3 w-3" /> :
part.removed ? <Minus className="h-3 w-3" /> :
<span className="w-3" />}
</span>
<pre className="whitespace-pre-wrap flex-1">{part.value}</pre>
</div>
))}
</div>
</div>
</BaseTool>
);
};
/**
* Notification Tool Component
*/
export const NotifyTool: React.FC<ToolComponentProps> = ({ tag, mode }) => {
const message = tag.attributes.message || '';
const type = tag.attributes.type || 'info';
return (
<BaseTool
tag={tag}
mode={mode}
title={`Notification: ${message.substring(0, 30)}${message.length > 30 ? '...' : ''}`}
icon={<Bell className="h-4 w-4 mr-2" />}
>
<div className={cn(
"p-2 rounded-md",
type === 'error' ? 'bg-red-500/10 text-red-600 dark:text-red-400' :
type === 'warning' ? 'bg-amber-500/10 text-amber-600 dark:text-amber-400' :
type === 'success' ? 'bg-green-500/10 text-green-600 dark:text-green-400' :
'bg-blue-500/10 text-blue-600 dark:text-blue-400'
)}>
{message}
</div>
</BaseTool>
);
};
/**
* Directory Tool Component (for create-directory and list-directory)
*/
export const DirectoryTool: React.FC<ToolComponentProps> = ({ tag, mode }) => {
const path = tag.attributes.path || '';
const content = tag.content || '';
const isListDirectory = tag.tagName === 'list-directory';
return (
<BaseTool
tag={tag}
mode={mode}
title={isListDirectory ? `Listing directory: ${path}` : `Creating directory: ${path}`}
icon={isListDirectory ? <Folder className="h-4 w-4 mr-2" /> : <FolderPlus className="h-4 w-4 mr-2" />}
>
<div className="space-y-2">
<div className="flex items-center gap-1 text-xs text-muted-foreground mb-1">
<Folder className="h-3 w-3" />
<span className="font-mono">{path}</span>
</div>
{isListDirectory && content && (
<pre className="whitespace-pre-wrap bg-muted p-2 rounded-md text-xs overflow-x-auto max-h-60">
{content}
</pre>
)}
</div>
</BaseTool>
);
};
/**
* Search Code Tool Component
*/
export const SearchCodeTool: React.FC<ToolComponentProps> = ({ tag, mode }) => {
const query = tag.attributes.query || '';
const content = tag.content || '';
return (
<BaseTool
tag={tag}
mode={mode}
title={`Searching code: ${query}`}
icon={<SearchIcon className="h-4 w-4 mr-2" />}
>
<div className="space-y-2">
<div className="flex items-center gap-1 text-xs bg-primary/10 p-1 rounded">
<SearchIcon className="h-3 w-3" />
<span className="font-mono">{query}</span>
</div>
<pre className="whitespace-pre-wrap bg-muted p-2 rounded-md text-xs overflow-x-auto max-h-60">
{content}
</pre>
</div>
</BaseTool>
);
};
// Tool component registry
export const ToolComponentRegistry: Record<string, React.FC<ToolComponentProps>> = {
'create-file': CreateFileTool,
'read-file': ReadFileTool,
'execute-command': ExecuteCommandTool,
'str-replace': StringReplaceTool,
'create-directory': DirectoryTool,
'list-directory': DirectoryTool,
'search-code': SearchCodeTool,
'notify': NotifyTool,
'ask': NotifyTool, // Handle ask similar to notify for now
'complete': NotifyTool, // Handle complete similar to notify for now
'full-file-rewrite': FullFileRewriteTool,
};
// Helper function to get the appropriate component for a tag
export function getComponentForTag(tag: ParsedTag): React.FC<ToolComponentProps> {
if (!ToolComponentRegistry[tag.tagName]) {
console.warn(`No component registered for tag type: ${tag.tagName}`);
}
return ToolComponentRegistry[tag.tagName] ||
// Fallback component for unknown tag types
(({tag, mode}) => {
console.log(`Rendering fallback component for tag: ${tag.tagName}`, tag);
return (
<BaseTool
tag={tag}
mode={mode}
title={`${tag.tagName} operation`}
icon={<Code className="h-4 w-4 mr-2" />}
>
<pre className="whitespace-pre-wrap bg-muted p-2 rounded-md text-xs">{tag.content || ''}</pre>
</BaseTool>
);
});
}

View File

@ -0,0 +1,203 @@
'use client';
import { useEffect, useState, useContext } from 'react';
import { ToolCallsContext } from '@/app/providers';
import { ParsedTag, ToolDisplayMode } from '@/lib/types/tool-calls';
import { Grid3X3 } from 'lucide-react';
import { getComponentForTag } from '@/components/tools/tool-components';
export function useToolsPanel() {
const { toolCalls, setToolCalls } = useContext(ToolCallsContext);
const [showPanel, setShowPanel] = useState(false);
const [currentToolIndex, setCurrentToolIndex] = useState(0);
const [viewMode, setViewMode] = useState<'single' | 'grid'>('single');
// Update panel visibility when tool calls change
useEffect(() => {
if (toolCalls.length > 0 && !showPanel) {
setShowPanel(true);
}
}, [toolCalls, showPanel]);
// Reset current tool index when tools change
useEffect(() => {
if (toolCalls.length > 0) {
console.log(`[TOOLS PANEL] Has ${toolCalls.length} tools to display:`,
toolCalls.map(t => `${t.tagName}${t.isClosing ? '(completed)' : '(running)'}`).join(', '));
setCurrentToolIndex(state => Math.min(state, toolCalls.length - 1));
} else {
setCurrentToolIndex(0);
}
}, [toolCalls]);
// Clear all tool calls
const clearToolCalls = () => {
setToolCalls([]);
setShowPanel(false);
};
// Navigate to next tool
const nextTool = () => {
if (toolCalls.length > 0) {
setCurrentToolIndex((currentToolIndex + 1) % toolCalls.length);
}
};
// Navigate to previous tool
const prevTool = () => {
if (toolCalls.length > 0) {
setCurrentToolIndex((currentToolIndex - 1 + toolCalls.length) % toolCalls.length);
}
};
// Toggle view mode between single and grid
const toggleViewMode = () => {
setViewMode(viewMode === 'single' ? 'grid' : 'single');
};
// Navigate to next/previous tool with keyboard support
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (viewMode === 'single' && toolCalls.length > 1) {
if (e.key === 'ArrowRight') {
nextTool();
} else if (e.key === 'ArrowLeft') {
prevTool();
}
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [viewMode, toolCalls.length, nextTool, prevTool]);
// Render the tools panel content
const renderToolsPanel = () => {
if (!toolCalls || toolCalls.length === 0) {
return (
<div className="flex flex-col items-center justify-center h-full text-foreground/40">
<p className="text-sm">No tool calls yet</p>
</div>
);
}
return (
<div className="flex flex-col h-full">
{/* Panel header */}
<div className="flex justify-between items-center border-b border-subtle p-3">
<h3 className="text-sm font-medium">Tools ({toolCalls.length})</h3>
<div className="flex items-center gap-2">
<button
onClick={toggleViewMode}
className="p-1 rounded hover:bg-background-hover"
title={viewMode === 'single' ? "Grid view" : "Single view"}
>
<Grid3X3 className="h-4 w-4 text-foreground/60" />
</button>
</div>
</div>
{/* Panel content */}
<div className="flex-grow overflow-y-auto p-3">
{viewMode === 'single' ? (
<>
{/* Single tool view */}
{toolCalls.length > 0 && (
<div className="h-full flex flex-col">
{renderToolComponent(toolCalls[currentToolIndex], 'detailed')}
</div>
)}
</>
) : (
<>
{/* Grid view */}
<div className="grid grid-cols-1 gap-4">
{toolCalls.map((tool, index) => (
<div
key={tool.id || index}
className={`cursor-pointer transition-all border rounded-md p-2 ${
index === currentToolIndex
? 'border-primary shadow-sm'
: 'border-subtle hover:border-primary/40'
}`}
onClick={() => {
setCurrentToolIndex(index);
setViewMode('single');
}}
>
{renderToolComponent(tool, 'compact')}
</div>
))}
</div>
</>
)}
</div>
{/* Gallery navigation controls (only in single view with multiple tools) */}
{viewMode === 'single' && toolCalls.length > 1 && (
<div className="border-t border-subtle p-3">
<div className="flex flex-col gap-2">
<div className="text-xs text-muted-foreground text-center">
Tool {currentToolIndex + 1} of {toolCalls.length}
</div>
<input
type="range"
min={0}
max={toolCalls.length - 1}
value={currentToolIndex}
onChange={(e) => setCurrentToolIndex(parseInt(e.target.value))}
className="w-full"
/>
</div>
</div>
)}
</div>
);
};
// Render a tool component based on the tool type
const renderToolComponent = (tag: ParsedTag, mode: ToolDisplayMode) => {
// Get the specialized component from the registry
const ToolComponent = getComponentForTag(tag);
return <ToolComponent tag={tag} mode={mode} />;
};
return {
toolCalls,
setToolCalls,
showPanel,
setShowPanel,
renderToolsPanel,
clearToolCalls,
currentToolIndex,
setCurrentToolIndex,
nextTool,
prevTool,
};
}
// Helper function to get a friendly title for a tool call
function getToolTitle(tag: ParsedTag): string {
switch (tag.tagName) {
case 'create-file':
return `Creating file: ${tag.attributes.file_path || ''}`;
case 'read-file':
return `Reading file: ${tag.attributes.file_path || ''}`;
case 'execute-command':
return `Executing: ${tag.attributes.command || ''}`;
case 'create-directory':
return `Creating directory: ${tag.attributes.path || ''}`;
case 'list-directory':
return `Listing directory: ${tag.attributes.path || ''}`;
case 'search-code':
return `Searching code: ${tag.attributes.query || ''}`;
case 'notify':
return `Notification: ${tag.attributes.message || ''}`;
case 'str-replace':
return `String replace: ${tag.attributes.pattern || ''}`;
case 'full-file-rewrite':
return `Full file rewrite: ${tag.attributes.file_path || ''}`;
default:
return `${tag.tagName} operation`;
}
}

View File

@ -0,0 +1,48 @@
// Define types for parsed XML tags to be used across the application
export interface ParsedTag {
tagName: string;
attributes: Record<string, string>;
content: string;
isClosing: boolean;
id: string; // Unique ID for each tool call instance
rawMatch?: string; // Raw XML match for deduplication
timestamp?: number; // Timestamp when the tag was created
// Pairing and completion status
resultTag?: ParsedTag; // Reference to the result tag if this is a tool call
isToolCall?: boolean; // Whether this is a tool call (vs a result)
isPaired?: boolean; // Whether this tag has been paired with its call/result
status?: 'running' | 'completed' | 'error'; // Status of the tool call
}
// Display mode for tool components
export type ToolDisplayMode = 'compact' | 'detailed';
// Props for tool components
export interface ToolComponentProps {
tag: ParsedTag;
mode: ToolDisplayMode;
children?: React.ReactNode; // Added to fix TypeScript errors in tool components
}
// List of supported XML tags
export const SUPPORTED_XML_TAGS = [
'ask',
'str-replace',
'notify',
'create-file',
'read-file',
'execute-command',
'create-directory',
'list-directory',
'search-code',
'complete',
'full-file-rewrite'
];
// Tool status labels
export const ToolStatusLabels = {
running: 'Running',
completed: 'Completed',
error: 'Failed'
};

View File

@ -719,6 +719,11 @@
"@swc/counter" "^0.1.3"
tslib "^2.4.0"
"@types/diff@^7.0.2":
version "7.0.2"
resolved "https://registry.npmjs.org/@types/diff/-/diff-7.0.2.tgz"
integrity sha512-JSWRMozjFKsGlEjiiKajUjIJVKuKdE3oVy2DNtK+fUo8q82nhFZ2CPQwicAIkXrofahDXrWJ7mjelvZphMS98Q==
"@types/node@*", "@types/node@20.11.5":
version "20.11.5"
resolved "https://registry.npmjs.org/@types/node/-/node-20.11.5.tgz"
@ -979,6 +984,11 @@ didyoumean@^1.2.2:
resolved "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz"
integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==
diff@^7.0.0:
version "7.0.0"
resolved "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz"
integrity sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==
dlv@^1.1.3:
version "1.1.3"
resolved "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz"
@ -1054,6 +1064,15 @@ fraction.js@^4.3.7:
resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz"
integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
framer-motion@^12.6.5:
version "12.6.5"
resolved "https://registry.npmjs.org/framer-motion/-/framer-motion-12.6.5.tgz"
integrity sha512-MKvnWov0paNjvRJuIy6x418w23tFqRfS6CXHhZrCiSEpXVlo/F+usr8v4/3G6O0u7CpsaO1qop+v4Ip7PRCBqQ==
dependencies:
motion-dom "^12.6.5"
motion-utils "^12.6.5"
tslib "^2.4.0"
fsevents@~2.3.2:
version "2.3.3"
resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz"
@ -1249,6 +1268,18 @@ minimatch@^9.0.1:
resolved "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz"
integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==
motion-dom@^12.6.5:
version "12.6.5"
resolved "https://registry.npmjs.org/motion-dom/-/motion-dom-12.6.5.tgz"
integrity sha512-jpM9TQLXzYMWMJ7Ec7sAj0iis8oIuu6WvjI3yNKJLdrZyrsI/b2cRInDVL8dCl683zQQq19DpL9cSMP+k8T1NA==
dependencies:
motion-utils "^12.6.5"
motion-utils@^12.6.5:
version "12.6.5"
resolved "https://registry.npmjs.org/motion-utils/-/motion-utils-12.6.5.tgz"
integrity sha512-IsOeKsOF+FWBhxQEDFBO6ZYC8/jlidmVbbLpe9/lXSA9j9kzGIMUuIBx2SZY+0reAS0DjZZ1i7dJp4NHrjocPw==
mz@^2.7.0:
version "2.7.0"
resolved "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz"
@ -1426,7 +1457,7 @@ ramda@^0.29.0:
resolved "https://registry.npmjs.org/ramda/-/ramda-0.29.1.tgz"
integrity sha512-OfxIeWzd4xdUNxlWhgFazxsA/nl3mS4/jGZI5n00uWOoSSFRhC1b6gl6xvmzUamgmqELraWp0J/qqVlXYPDPyA==
"react-dom@^16.8 || ^17 || ^18", "react-dom@^16.8 || ^17.0 || ^18.0", react-dom@^18.0.0, "react-dom@^18.0.0 || ^19.0.0 || ^19.0.0-rc", react-dom@^18.2.0, react-dom@>=16.8.0, react-dom@18.2.0:
"react-dom@^16.8 || ^17 || ^18", "react-dom@^16.8 || ^17.0 || ^18.0", react-dom@^18.0.0, "react-dom@^18.0.0 || ^19.0.0", "react-dom@^18.0.0 || ^19.0.0 || ^19.0.0-rc", react-dom@^18.2.0, react-dom@>=16.8.0, react-dom@18.2.0:
version "18.2.0"
resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz"
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
@ -1478,7 +1509,7 @@ react-style-singleton@^2.2.1:
invariant "^2.2.4"
tslib "^2.0.0"
"react@^16.11.0 || ^17.0.0 || ^18.0.0", "react@^16.5.1 || ^17.0.0 || ^18.0.0", "react@^16.8 || ^17 || ^18", "react@^16.8 || ^17.0 || ^18.0", "react@^16.8.0 || ^17 || ^18 || ^19", "react@^16.8.0 || ^17.0.0 || ^18.0.0", react@^18.0.0, "react@^18.0.0 || ^19.0.0 || ^19.0.0-rc", react@^18.2.0, "react@>= 16.8.0 || 17.x.x || ^18.0.0-0", react@>=16.8.0, react@18.2.0:
"react@^16.11.0 || ^17.0.0 || ^18.0.0", "react@^16.5.1 || ^17.0.0 || ^18.0.0", "react@^16.8 || ^17 || ^18", "react@^16.8 || ^17.0 || ^18.0", "react@^16.8.0 || ^17 || ^18 || ^19", "react@^16.8.0 || ^17.0.0 || ^18.0.0", react@^18.0.0, "react@^18.0.0 || ^19.0.0", "react@^18.0.0 || ^19.0.0 || ^19.0.0-rc", react@^18.2.0, "react@>= 16.8.0 || 17.x.x || ^18.0.0-0", react@>=16.8.0, react@18.2.0:
version "18.2.0"
resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz"
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==