mirror of https://github.com/kortix-ai/suna.git
This commit is contained in:
parent
a6d2e5b18e
commit
06ca86c5c5
|
@ -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 "information access"</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'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'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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
}
|
|
@ -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`;
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
};
|
|
@ -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==
|
||||
|
|
Loading…
Reference in New Issue