mirror of https://github.com/kortix-ai/suna.git
Merge branch 'main' into mcp-5a-custom-1
This commit is contained in:
commit
f65cc3eba9
|
@ -1514,7 +1514,7 @@ class ResponseProcessor:
|
|||
# This allows the LLM to see the tool result in subsequent interactions
|
||||
result_message = {
|
||||
"role": result_role,
|
||||
"content": structured_result
|
||||
"content": json.dumps(structured_result)
|
||||
}
|
||||
message_obj = await self.add_message(
|
||||
thread_id=thread_id,
|
||||
|
@ -1619,6 +1619,9 @@ class ResponseProcessor:
|
|||
# return summary
|
||||
|
||||
summary_output = result.output if hasattr(result, 'output') else str(result)
|
||||
success_status = structured_result["tool_execution"]["result"]["success"]
|
||||
|
||||
# Create a more comprehensive summary for the LLM
|
||||
if xml_tag_name:
|
||||
status = "completed successfully" if structured_result_v1["tool_execution"]["result"]["success"] else "failed"
|
||||
summary = f"Tool '{xml_tag_name}' {status}. Output: {summary_output}"
|
||||
|
|
|
@ -6,7 +6,7 @@ from fastapi import FastAPI, UploadFile, File, HTTPException, APIRouter, Form, D
|
|||
from fastapi.responses import Response
|
||||
from pydantic import BaseModel
|
||||
|
||||
from sandbox.sandbox import get_or_start_sandbox
|
||||
from sandbox.sandbox import get_or_start_sandbox, delete_sandbox
|
||||
from utils.logger import logger
|
||||
from utils.auth_utils import get_optional_user_id
|
||||
from services.supabase import DBConnection
|
||||
|
@ -305,6 +305,28 @@ async def delete_file(
|
|||
logger.error(f"Error deleting file in sandbox {sandbox_id}: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.delete("/sandboxes/{sandbox_id}")
|
||||
async def delete_sandbox_route(
|
||||
sandbox_id: str,
|
||||
request: Request = None,
|
||||
user_id: Optional[str] = Depends(get_optional_user_id)
|
||||
):
|
||||
"""Delete an entire sandbox"""
|
||||
logger.info(f"Received sandbox delete request for sandbox {sandbox_id}, user_id: {user_id}")
|
||||
client = await db.client
|
||||
|
||||
# Verify the user has access to this sandbox
|
||||
await verify_sandbox_access(client, sandbox_id, user_id)
|
||||
|
||||
try:
|
||||
# Delete the sandbox using the sandbox module function
|
||||
await delete_sandbox(sandbox_id)
|
||||
|
||||
return {"status": "success", "deleted": True, "sandbox_id": sandbox_id}
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting sandbox {sandbox_id}: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
# Should happen on server-side fully
|
||||
@router.post("/project/{project_id}/sandbox/ensure-active")
|
||||
async def ensure_project_sandbox_active(
|
||||
|
|
|
@ -125,3 +125,20 @@ def create_sandbox(password: str, project_id: str = None):
|
|||
logger.debug(f"Sandbox environment successfully initialized")
|
||||
return sandbox
|
||||
|
||||
async def delete_sandbox(sandbox_id: str):
|
||||
"""Delete a sandbox by its ID."""
|
||||
logger.info(f"Deleting sandbox with ID: {sandbox_id}")
|
||||
|
||||
try:
|
||||
# Get the sandbox
|
||||
sandbox = daytona.get_current_sandbox(sandbox_id)
|
||||
|
||||
# Delete the sandbox
|
||||
daytona.remove(sandbox)
|
||||
|
||||
logger.info(f"Successfully deleted sandbox {sandbox_id}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting sandbox {sandbox_id}: {str(e)}")
|
||||
raise e
|
||||
|
||||
|
|
|
@ -1,13 +1,30 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
BarChart3,
|
||||
Bot,
|
||||
Briefcase,
|
||||
Settings,
|
||||
Sparkles,
|
||||
RefreshCw,
|
||||
TrendingUp,
|
||||
Users,
|
||||
Shield,
|
||||
Zap,
|
||||
Target,
|
||||
Brain,
|
||||
Globe,
|
||||
Heart,
|
||||
PenTool,
|
||||
Code,
|
||||
Camera,
|
||||
Calendar,
|
||||
DollarSign,
|
||||
Rocket,
|
||||
} from 'lucide-react';
|
||||
|
||||
type PromptExample = {
|
||||
|
@ -16,14 +33,14 @@ type PromptExample = {
|
|||
icon: React.ReactNode;
|
||||
};
|
||||
|
||||
const prompts: PromptExample[] = [
|
||||
const allPrompts: PromptExample[] = [
|
||||
{
|
||||
title: 'Market research dashboard',
|
||||
query: 'Create a comprehensive market research dashboard analyzing industry trends, customer segments, and competitive landscape. Include data visualization and actionable recommendations.',
|
||||
icon: <BarChart3 className="text-green-700 dark:text-green-400" size={16} />,
|
||||
},
|
||||
{
|
||||
title: 'Recommendation engine development',
|
||||
title: 'Recommendation engine',
|
||||
query: 'Develop a recommendation engine for personalized product suggestions. Include collaborative filtering, content-based filtering, and hybrid approaches with evaluation metrics.',
|
||||
icon: <Bot className="text-blue-700 dark:text-blue-400" size={16} />,
|
||||
},
|
||||
|
@ -37,32 +54,159 @@ const prompts: PromptExample[] = [
|
|||
query: 'Create an automated data pipeline for ETL processes. Include data validation, error handling, monitoring, and scalable architecture design.',
|
||||
icon: <Settings className="text-purple-700 dark:text-purple-400" size={16} />,
|
||||
},
|
||||
{
|
||||
title: 'Productivity system',
|
||||
query: 'Design a comprehensive personal productivity system including task management, goal tracking, habit formation, and time blocking. Create templates and workflows for daily, weekly, and monthly planning.',
|
||||
icon: <Target className="text-orange-700 dark:text-orange-400" size={16} />,
|
||||
},
|
||||
{
|
||||
title: 'Content marketing plan',
|
||||
query: 'Develop a 6-month content marketing strategy including blog posts, social media, email campaigns, and SEO optimization. Include content calendar and performance metrics.',
|
||||
icon: <PenTool className="text-indigo-700 dark:text-indigo-400" size={16} />,
|
||||
},
|
||||
{
|
||||
title: 'Portfolio analysis',
|
||||
query: 'Create a personal investment portfolio analysis tool with risk assessment, diversification recommendations, and performance tracking against market benchmarks.',
|
||||
icon: <DollarSign className="text-emerald-700 dark:text-emerald-400" size={16} />,
|
||||
},
|
||||
{
|
||||
title: 'Customer journey map',
|
||||
query: 'Map the complete customer journey from awareness to advocacy. Include touchpoints, pain points, emotions, and optimization opportunities at each stage.',
|
||||
icon: <Users className="text-cyan-700 dark:text-cyan-400" size={16} />,
|
||||
},
|
||||
{
|
||||
title: 'A/B testing framework',
|
||||
query: 'Design a comprehensive A/B testing framework including hypothesis formation, statistical significance calculations, and result interpretation guidelines.',
|
||||
icon: <TrendingUp className="text-teal-700 dark:text-teal-400" size={16} />,
|
||||
},
|
||||
{
|
||||
title: 'Code review automation',
|
||||
query: 'Create an automated code review system that checks for security vulnerabilities, performance issues, and coding standards. Include integration with CI/CD pipelines.',
|
||||
icon: <Code className="text-violet-700 dark:text-violet-400" size={16} />,
|
||||
},
|
||||
{
|
||||
title: 'Risk assessment matrix',
|
||||
query: 'Develop a comprehensive risk assessment framework for business operations including risk identification, probability analysis, impact evaluation, and mitigation strategies.',
|
||||
icon: <Shield className="text-red-700 dark:text-red-400" size={16} />,
|
||||
},
|
||||
{
|
||||
title: 'Learning path generator',
|
||||
query: 'Create a personalized learning path generator that adapts to individual goals, current skill level, and preferred learning style. Include progress tracking and resource recommendations.',
|
||||
icon: <Brain className="text-pink-700 dark:text-pink-400" size={16} />,
|
||||
},
|
||||
{
|
||||
title: 'Social media automation',
|
||||
query: 'Design a social media automation system including content scheduling, engagement tracking, hashtag optimization, and performance analytics across multiple platforms.',
|
||||
icon: <Globe className="text-blue-600 dark:text-blue-300" size={16} />,
|
||||
},
|
||||
{
|
||||
title: 'Health tracking dashboard',
|
||||
query: 'Build a comprehensive health tracking dashboard integrating fitness data, nutrition logging, sleep patterns, and medical records with actionable insights and goal setting.',
|
||||
icon: <Heart className="text-red-600 dark:text-red-300" size={16} />,
|
||||
},
|
||||
{
|
||||
title: 'Project automation',
|
||||
query: 'Create an intelligent project management system with automatic task assignment, deadline tracking, resource allocation, and team communication integration.',
|
||||
icon: <Calendar className="text-amber-700 dark:text-amber-400" size={16} />,
|
||||
},
|
||||
{
|
||||
title: 'Sales funnel optimizer',
|
||||
query: 'Analyze and optimize the entire sales funnel from lead generation to conversion. Include lead scoring, nurture sequences, and conversion rate optimization strategies.',
|
||||
icon: <Zap className="text-yellow-600 dark:text-yellow-300" size={16} />,
|
||||
},
|
||||
{
|
||||
title: 'Startup pitch deck',
|
||||
query: 'Generate a compelling startup pitch deck including problem statement, solution overview, market analysis, business model, financial projections, and funding requirements.',
|
||||
icon: <Rocket className="text-orange-600 dark:text-orange-300" size={16} />,
|
||||
},
|
||||
{
|
||||
title: 'Photography workflow',
|
||||
query: 'Design an end-to-end photography workflow including shoot planning, file organization, editing presets, client delivery, and portfolio management systems.',
|
||||
icon: <Camera className="text-slate-700 dark:text-slate-400" size={16} />,
|
||||
},
|
||||
{
|
||||
title: 'Supply chain analysis',
|
||||
query: 'Create a supply chain optimization analysis including vendor evaluation, cost reduction opportunities, risk mitigation, and inventory management strategies.',
|
||||
icon: <Briefcase className="text-stone-700 dark:text-stone-400" size={16} />,
|
||||
},
|
||||
{
|
||||
title: 'UX research framework',
|
||||
query: 'Develop a comprehensive UX research framework including user interviews, usability testing, persona development, and data-driven design recommendations.',
|
||||
icon: <Sparkles className="text-fuchsia-700 dark:text-fuchsia-400" size={16} />,
|
||||
},
|
||||
];
|
||||
|
||||
// Function to get random prompts
|
||||
const getRandomPrompts = (count: number = 3): PromptExample[] => {
|
||||
const shuffled = [...allPrompts].sort(() => 0.5 - Math.random());
|
||||
return shuffled.slice(0, count);
|
||||
};
|
||||
|
||||
export const Examples = ({
|
||||
onSelectPrompt,
|
||||
}: {
|
||||
onSelectPrompt?: (query: string) => void;
|
||||
}) => {
|
||||
return (
|
||||
<div className="w-full max-w-3xl mx-auto">
|
||||
<div className="grid grid-cols-2 md:grid-cols-2 xl:grid-cols-4 gap-4">
|
||||
{prompts.map((prompt, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
className="group cursor-pointer h-full shadow-none transition-all bg-sidebar hover:bg-neutral-100 dark:hover:bg-neutral-800/60"
|
||||
onClick={() => onSelectPrompt && onSelectPrompt(prompt.query)}
|
||||
>
|
||||
<CardHeader className="px-4">
|
||||
<div className="flex items-center gap-2">
|
||||
{prompt.icon}
|
||||
</div>
|
||||
<CardTitle className="font-normal group-hover:text-foreground transition-all text-muted-foreground text-sm line-clamp-3">
|
||||
{prompt.title}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
const [displayedPrompts, setDisplayedPrompts] = useState<PromptExample[]>([]);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
|
||||
</Card>
|
||||
// Initialize with random prompts on mount
|
||||
useEffect(() => {
|
||||
setDisplayedPrompts(getRandomPrompts(3));
|
||||
}, []);
|
||||
|
||||
const handleRefresh = () => {
|
||||
setIsRefreshing(true);
|
||||
setDisplayedPrompts(getRandomPrompts(3));
|
||||
setTimeout(() => setIsRefreshing(false), 500);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-3xl mx-auto px-4">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<span className="text-xs text-muted-foreground font-medium">Quick starts</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleRefresh}
|
||||
className="h-6 px-2 text-xs text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<motion.div
|
||||
animate={{ rotate: isRefreshing ? 360 : 0 }}
|
||||
transition={{ duration: 0.5, ease: "easeInOut" }}
|
||||
>
|
||||
<RefreshCw size={10} />
|
||||
</motion.div>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
{displayedPrompts.map((prompt, index) => (
|
||||
<motion.div
|
||||
key={`${prompt.title}-${index}`}
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
delay: index * 0.05,
|
||||
ease: "easeOut"
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
className="group cursor-pointer h-full shadow-none transition-all bg-sidebar hover:bg-neutral-100 dark:hover:bg-neutral-800/60 p-0 justify-center"
|
||||
onClick={() => onSelectPrompt && onSelectPrompt(prompt.query)}
|
||||
>
|
||||
<CardHeader className="p-2 grid-rows-1">
|
||||
<div className="flex items-start justify-center gap-1.5">
|
||||
<div className="flex-shrink-0 mt-0.5">
|
||||
{React.cloneElement(prompt.icon as React.ReactElement, { size: 14 })}
|
||||
</div>
|
||||
<CardTitle className="font-normal group-hover:text-foreground transition-all text-muted-foreground text-xs leading-relaxed line-clamp-3">
|
||||
{prompt.title}
|
||||
</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,140 @@
|
|||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { PricingSection } from '@/components/home/sections/pricing-section';
|
||||
import { isLocalMode } from '@/lib/config';
|
||||
import {
|
||||
getSubscription,
|
||||
createPortalSession,
|
||||
SubscriptionStatus,
|
||||
} from '@/lib/api';
|
||||
import { useAuth } from '@/components/AuthProvider';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { X } from 'lucide-react';
|
||||
|
||||
interface BillingModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
returnUrl?: string;
|
||||
}
|
||||
|
||||
export function BillingModal({ open, onOpenChange, returnUrl = window?.location?.href || '/' }: BillingModalProps) {
|
||||
const { session, isLoading: authLoading } = useAuth();
|
||||
const [subscriptionData, setSubscriptionData] = useState<SubscriptionStatus | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isManaging, setIsManaging] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchSubscription() {
|
||||
if (!open || authLoading || !session) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const data = await getSubscription();
|
||||
setSubscriptionData(data);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
console.error('Failed to get subscription:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to load subscription data');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
fetchSubscription();
|
||||
}, [open, session, authLoading]);
|
||||
|
||||
const handleManageSubscription = async () => {
|
||||
try {
|
||||
setIsManaging(true);
|
||||
const { url } = await createPortalSession({ return_url: returnUrl });
|
||||
window.location.href = url;
|
||||
} catch (err) {
|
||||
console.error('Failed to create portal session:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to create portal session');
|
||||
} finally {
|
||||
setIsManaging(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Local mode content
|
||||
if (isLocalMode()) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Billing & Subscription</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="p-4 bg-muted/30 border border-border rounded-lg text-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Running in local development mode - billing features are disabled
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
All premium features are available in this environment
|
||||
</p>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Upgrade Your Plan</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{isLoading || authLoading ? (
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-20 w-full" />
|
||||
<Skeleton className="h-40 w-full" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="p-4 bg-destructive/10 border border-destructive/20 rounded-lg text-center">
|
||||
<p className="text-sm text-destructive">Error loading billing status: {error}</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{subscriptionData && (
|
||||
<div className="mb-6">
|
||||
<div className="rounded-lg border bg-background p-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium text-foreground/90">
|
||||
Agent Usage This Month
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{subscriptionData.current_usage?.toFixed(2) || '0'} /{' '}
|
||||
{subscriptionData.minutes_limit || '0'} minutes
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<PricingSection returnUrl={returnUrl} showTitleAndTabs={false} />
|
||||
|
||||
{subscriptionData && (
|
||||
<Button
|
||||
onClick={handleManageSubscription}
|
||||
disabled={isManaging}
|
||||
className="w-full bg-primary hover:bg-primary/90 shadow-md hover:shadow-lg transition-all mt-4"
|
||||
>
|
||||
{isManaging ? 'Loading...' : 'Manage Subscription'}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
|
@ -225,10 +225,16 @@ export function NavAgents() {
|
|||
// Store threadToDelete in a local variable since it might be cleared
|
||||
const deletedThread = { ...threadToDelete };
|
||||
|
||||
// Get sandbox ID from projects data
|
||||
const thread = combinedThreads.find(t => t.threadId === threadId);
|
||||
const project = projects.find(p => p.id === thread?.projectId);
|
||||
const sandboxId = project?.sandbox?.id;
|
||||
|
||||
// Log operation start
|
||||
console.log('DELETION - Starting thread deletion process', {
|
||||
threadId: deletedThread.id,
|
||||
isCurrentThread: isActive,
|
||||
sandboxId
|
||||
});
|
||||
|
||||
// Use the centralized deletion system with completion callback
|
||||
|
@ -236,9 +242,9 @@ export function NavAgents() {
|
|||
threadId,
|
||||
isActive,
|
||||
async () => {
|
||||
// Delete the thread using the mutation
|
||||
// Delete the thread using the mutation with sandbox ID
|
||||
deleteThreadMutation(
|
||||
{ threadId },
|
||||
{ threadId, sandboxId },
|
||||
{
|
||||
onSuccess: () => {
|
||||
// Invalidate queries to refresh the list
|
||||
|
@ -282,6 +288,13 @@ export function NavAgents() {
|
|||
deleteMultipleThreadsMutation(
|
||||
{
|
||||
threadIds: threadIdsToDelete,
|
||||
threadSandboxMap: Object.fromEntries(
|
||||
threadIdsToDelete.map(threadId => {
|
||||
const thread = combinedThreads.find(t => t.threadId === threadId);
|
||||
const project = projects.find(p => p.id === thread?.projectId);
|
||||
return [threadId, project?.sandbox?.id || ''];
|
||||
}).filter(([, sandboxId]) => sandboxId)
|
||||
),
|
||||
onProgress: handleDeletionProgress
|
||||
},
|
||||
{
|
||||
|
|
|
@ -27,6 +27,7 @@ import {
|
|||
MODELS // Import the centralized MODELS constant
|
||||
} from './_use-model-selection';
|
||||
import { PaywallDialog } from '@/components/payment/paywall-dialog';
|
||||
import { BillingModal } from '@/components/billing/billing-modal';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { isLocalMode } from '@/lib/config';
|
||||
|
@ -56,6 +57,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
|||
refreshCustomModels,
|
||||
}) => {
|
||||
const [paywallOpen, setPaywallOpen] = useState(false);
|
||||
const [billingModalOpen, setBillingModalOpen] = useState(false);
|
||||
const [lockedModel, setLockedModel] = useState<string | null>(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
@ -195,7 +197,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
|||
};
|
||||
|
||||
const handleUpgradeClick = () => {
|
||||
router.push('/settings/billing');
|
||||
setBillingModalOpen(true);
|
||||
};
|
||||
|
||||
const closeDialog = () => {
|
||||
|
@ -756,6 +758,13 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
|||
mode={dialogMode}
|
||||
/>
|
||||
|
||||
{/* Billing Modal */}
|
||||
<BillingModal
|
||||
open={billingModalOpen}
|
||||
onOpenChange={setBillingModalOpen}
|
||||
returnUrl={typeof window !== 'undefined' ? window.location.href : '/'}
|
||||
/>
|
||||
|
||||
{paywallOpen && (
|
||||
<PaywallDialog
|
||||
open={true}
|
||||
|
|
|
@ -203,7 +203,34 @@ export function ToolCallSidePanel({
|
|||
currentToolCall?.assistantCall?.name || 'unknown',
|
||||
);
|
||||
const isStreaming = displayToolCall?.toolResult?.content === 'STREAMING';
|
||||
const isSuccess = displayToolCall?.toolResult?.isSuccess ?? true;
|
||||
|
||||
// Extract actual success value from tool content with fallbacks
|
||||
const getActualSuccess = (toolCall: any): boolean => {
|
||||
const content = toolCall?.toolResult?.content;
|
||||
if (!content) return toolCall?.toolResult?.isSuccess ?? true;
|
||||
|
||||
const safeParse = (data: any) => {
|
||||
try { return typeof data === 'string' ? JSON.parse(data) : data; }
|
||||
catch { return null; }
|
||||
};
|
||||
|
||||
const parsed = safeParse(content);
|
||||
if (!parsed) return toolCall?.toolResult?.isSuccess ?? true;
|
||||
|
||||
if (parsed.content) {
|
||||
const inner = safeParse(parsed.content);
|
||||
if (inner?.tool_execution?.result?.success !== undefined) {
|
||||
return inner.tool_execution.result.success;
|
||||
}
|
||||
}
|
||||
const success = parsed.tool_execution?.result?.success ??
|
||||
parsed.result?.success ??
|
||||
parsed.success;
|
||||
|
||||
return success !== undefined ? success : (toolCall?.toolResult?.isSuccess ?? true);
|
||||
};
|
||||
|
||||
const isSuccess = isStreaming ? true : getActualSuccess(displayToolCall);
|
||||
|
||||
const internalNavigate = React.useCallback((newIndex: number, source: string = 'internal') => {
|
||||
if (newIndex < 0 || newIndex >= totalCalls) return;
|
||||
|
@ -520,7 +547,7 @@ export function ToolCallSidePanel({
|
|||
toolContent={displayToolCall.toolResult?.content}
|
||||
assistantTimestamp={displayToolCall.assistantCall.timestamp}
|
||||
toolTimestamp={displayToolCall.toolResult?.timestamp}
|
||||
isSuccess={isStreaming ? true : (displayToolCall.toolResult?.isSuccess ?? true)}
|
||||
isSuccess={isSuccess}
|
||||
isStreaming={isStreaming}
|
||||
project={project}
|
||||
messages={messages}
|
||||
|
|
|
@ -39,25 +39,25 @@ export function GenericToolView({
|
|||
|
||||
// Use the new parser for backwards compatibility
|
||||
const { toolResult } = extractToolData(content);
|
||||
|
||||
|
||||
if (toolResult) {
|
||||
// Format the structured content nicely
|
||||
const formatted: any = {
|
||||
tool: toolResult.xmlTagName || toolResult.functionName,
|
||||
};
|
||||
|
||||
|
||||
if (toolResult.arguments && Object.keys(toolResult.arguments).length > 0) {
|
||||
formatted.parameters = toolResult.arguments;
|
||||
}
|
||||
|
||||
|
||||
if (toolResult.toolOutput) {
|
||||
formatted.output = toolResult.toolOutput;
|
||||
}
|
||||
|
||||
|
||||
if (toolResult.isSuccess !== undefined) {
|
||||
formatted.success = toolResult.isSuccess;
|
||||
}
|
||||
|
||||
|
||||
return JSON.stringify(formatted, null, 2);
|
||||
}
|
||||
|
||||
|
@ -68,18 +68,18 @@ export function GenericToolView({
|
|||
const formatted: any = {
|
||||
tool: content.tool_name || content.xml_tag_name || 'unknown',
|
||||
};
|
||||
|
||||
|
||||
if (content.parameters && Object.keys(content.parameters).length > 0) {
|
||||
formatted.parameters = content.parameters;
|
||||
}
|
||||
|
||||
|
||||
if (content.result) {
|
||||
formatted.result = content.result;
|
||||
}
|
||||
|
||||
|
||||
return JSON.stringify(formatted, null, 2);
|
||||
}
|
||||
|
||||
|
||||
// Check if it has a content field that might contain the structured data (legacy)
|
||||
if ('content' in content && typeof content.content === 'object') {
|
||||
const innerContent = content.content;
|
||||
|
@ -87,26 +87,26 @@ export function GenericToolView({
|
|||
const formatted: any = {
|
||||
tool: innerContent.tool_name || innerContent.xml_tag_name || 'unknown',
|
||||
};
|
||||
|
||||
|
||||
if (innerContent.parameters && Object.keys(innerContent.parameters).length > 0) {
|
||||
formatted.parameters = innerContent.parameters;
|
||||
}
|
||||
|
||||
|
||||
if (innerContent.result) {
|
||||
formatted.result = innerContent.result;
|
||||
}
|
||||
|
||||
|
||||
return JSON.stringify(formatted, null, 2);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Fall back to old format handling
|
||||
if (content.content && typeof content.content === 'string') {
|
||||
return content.content;
|
||||
}
|
||||
return JSON.stringify(content, null, 2);
|
||||
}
|
||||
|
||||
|
||||
if (typeof content === 'string') {
|
||||
try {
|
||||
const parsedJson = JSON.parse(content);
|
||||
|
@ -142,13 +142,13 @@ export function GenericToolView({
|
|||
</CardTitle>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{!isStreaming && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={
|
||||
isSuccess
|
||||
? "bg-gradient-to-b from-emerald-200 to-emerald-100 text-emerald-700 dark:from-emerald-800/50 dark:to-emerald-900/60 dark:text-emerald-300"
|
||||
isSuccess
|
||||
? "bg-gradient-to-b from-emerald-200 to-emerald-100 text-emerald-700 dark:from-emerald-800/50 dark:to-emerald-900/60 dark:text-emerald-300"
|
||||
: "bg-gradient-to-b from-rose-200 to-rose-100 text-rose-700 dark:from-rose-800/50 dark:to-rose-900/60 dark:text-rose-300"
|
||||
}
|
||||
>
|
||||
|
@ -165,7 +165,7 @@ export function GenericToolView({
|
|||
|
||||
<CardContent className="p-0 h-full flex-1 overflow-hidden relative">
|
||||
{isStreaming ? (
|
||||
<LoadingState
|
||||
<LoadingState
|
||||
icon={Wrench}
|
||||
iconColor="text-orange-500 dark:text-orange-400"
|
||||
bgColor="bg-gradient-to-b from-orange-100 to-orange-50 shadow-inner dark:from-orange-800/40 dark:to-orange-900/60 dark:shadow-orange-950/20"
|
||||
|
@ -223,7 +223,7 @@ export function GenericToolView({
|
|||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
|
||||
<div className="px-4 py-2 h-10 bg-gradient-to-r from-zinc-50/90 to-zinc-100/90 dark:from-zinc-900/90 dark:to-zinc-800/90 backdrop-blur-sm border-t border-zinc-200 dark:border-zinc-800 flex justify-between items-center gap-4">
|
||||
<div className="h-full flex items-center gap-2 text-sm text-zinc-500 dark:text-zinc-400">
|
||||
{!isStreaming && (formattedAssistantContent || formattedToolContent) && (
|
||||
|
@ -233,7 +233,7 @@ export function GenericToolView({
|
|||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
<div className="text-xs text-zinc-500 dark:text-zinc-400 flex items-center gap-2">
|
||||
<Clock className="h-3.5 w-3.5" />
|
||||
{toolTimestamp && !isStreaming
|
||||
|
|
|
@ -37,8 +37,8 @@ const UnifiedDiffView: React.FC<{ lineDiff: LineDiff[] }> = ({ lineDiff }) => (
|
|||
<table className="w-full border-collapse">
|
||||
<tbody>
|
||||
{lineDiff.map((line, i) => (
|
||||
<tr
|
||||
key={i}
|
||||
<tr
|
||||
key={i}
|
||||
className={cn(
|
||||
"hover:bg-zinc-50 dark:hover:bg-zinc-900",
|
||||
line.type === 'removed' && "bg-red-50 dark:bg-red-950/30",
|
||||
|
@ -78,9 +78,9 @@ const SplitDiffView: React.FC<{ lineDiff: LineDiff[] }> = ({ lineDiff }) => (
|
|||
<tbody>
|
||||
{lineDiff.map((line, i) => (
|
||||
<tr key={i}>
|
||||
<td
|
||||
<td
|
||||
className={cn(
|
||||
"p-2 align-top",
|
||||
"p-2 align-top",
|
||||
line.type === 'removed' ? 'bg-red-50 dark:bg-red-950/30 text-red-700 dark:text-red-400' : '',
|
||||
line.oldLine === null ? 'bg-zinc-100 dark:bg-zinc-900' : ''
|
||||
)}
|
||||
|
@ -90,7 +90,7 @@ const SplitDiffView: React.FC<{ lineDiff: LineDiff[] }> = ({ lineDiff }) => (
|
|||
<div className="w-8 text-right pr-2 select-none text-xs text-zinc-500 dark:text-zinc-400">
|
||||
{line.lineNumber}
|
||||
</div>
|
||||
{line.type === 'removed' &&
|
||||
{line.type === 'removed' &&
|
||||
<Minus className="h-3.5 w-3.5 text-red-500 mt-0.5 mr-2 flex-shrink-0" />
|
||||
}
|
||||
<div className="overflow-x-auto">
|
||||
|
@ -99,7 +99,7 @@ const SplitDiffView: React.FC<{ lineDiff: LineDiff[] }> = ({ lineDiff }) => (
|
|||
</div>
|
||||
) : null}
|
||||
</td>
|
||||
<td
|
||||
<td
|
||||
className={cn(
|
||||
"p-2 align-top",
|
||||
line.type === 'added' ? 'bg-emerald-50 dark:bg-emerald-950/30 text-emerald-700 dark:text-emerald-400' : '',
|
||||
|
@ -111,7 +111,7 @@ const SplitDiffView: React.FC<{ lineDiff: LineDiff[] }> = ({ lineDiff }) => (
|
|||
<div className="w-8 text-right pr-2 select-none text-xs text-zinc-500 dark:text-zinc-400">
|
||||
{line.lineNumber}
|
||||
</div>
|
||||
{line.type === 'added' &&
|
||||
{line.type === 'added' &&
|
||||
<Plus className="h-3.5 w-3.5 text-emerald-500 mt-0.5 mr-2 flex-shrink-0" />
|
||||
}
|
||||
<div className="overflow-x-auto">
|
||||
|
@ -152,7 +152,7 @@ export function StrReplaceToolView({
|
|||
}: ToolViewProps): JSX.Element {
|
||||
const [expanded, setExpanded] = useState<boolean>(true);
|
||||
const [viewMode, setViewMode] = useState<'unified' | 'split'>('unified');
|
||||
|
||||
|
||||
let filePath: string | null = null;
|
||||
let oldStr: string | null = null;
|
||||
let newStr: string | null = null;
|
||||
|
@ -198,7 +198,7 @@ export function StrReplaceToolView({
|
|||
if (!filePath) {
|
||||
filePath = extractFilePath(assistantContent) || extractFilePath(toolContent);
|
||||
}
|
||||
|
||||
|
||||
if (!oldStr || !newStr) {
|
||||
const assistantStrReplace = extractStrReplaceContent(assistantContent);
|
||||
const toolStrReplace = extractStrReplaceContent(toolContent);
|
||||
|
@ -211,7 +211,7 @@ export function StrReplaceToolView({
|
|||
// Generate diff data (only if we have both strings)
|
||||
const lineDiff = oldStr && newStr ? generateLineDiff(oldStr, newStr) : [];
|
||||
const charDiff = oldStr && newStr ? generateCharDiff(oldStr, newStr) : [];
|
||||
|
||||
|
||||
// Calculate stats on changes
|
||||
const stats: DiffStats = calculateDiffStats(lineDiff);
|
||||
|
||||
|
@ -230,13 +230,13 @@ export function StrReplaceToolView({
|
|||
{toolTitle}
|
||||
</CardTitle>
|
||||
</div>
|
||||
|
||||
|
||||
{!isStreaming && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={
|
||||
actualIsSuccess
|
||||
? "bg-gradient-to-b from-emerald-200 to-emerald-100 text-emerald-700 dark:from-emerald-800/50 dark:to-emerald-900/60 dark:text-emerald-300"
|
||||
actualIsSuccess
|
||||
? "bg-gradient-to-b from-emerald-200 to-emerald-100 text-emerald-700 dark:from-emerald-800/50 dark:to-emerald-900/60 dark:text-emerald-300"
|
||||
: "bg-gradient-to-b from-rose-200 to-rose-100 text-rose-700 dark:from-rose-800/50 dark:to-rose-900/60 dark:text-rose-300"
|
||||
}
|
||||
>
|
||||
|
@ -260,7 +260,7 @@ export function StrReplaceToolView({
|
|||
|
||||
<CardContent className="p-0 h-full flex-1 overflow-hidden relative">
|
||||
{isStreaming ? (
|
||||
<LoadingState
|
||||
<LoadingState
|
||||
icon={FileDiff}
|
||||
iconColor="text-purple-500 dark:text-purple-400"
|
||||
bgColor="bg-gradient-to-b from-purple-100 to-purple-50 shadow-inner dark:from-purple-800/40 dark:to-purple-900/60 dark:shadow-purple-950/20"
|
||||
|
@ -282,7 +282,7 @@ export function StrReplaceToolView({
|
|||
{filePath || 'Unknown file'}
|
||||
</code>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center text-xs text-zinc-500 dark:text-zinc-400 gap-3">
|
||||
<div className="flex items-center">
|
||||
|
@ -294,7 +294,7 @@ export function StrReplaceToolView({
|
|||
<span>{stats.deletions}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
|
@ -314,7 +314,7 @@ export function StrReplaceToolView({
|
|||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{expanded && (
|
||||
<div>
|
||||
<Tabs value={viewMode} onValueChange={(v) => setViewMode(v as 'unified' | 'split')} className="w-auto">
|
||||
|
@ -324,11 +324,11 @@ export function StrReplaceToolView({
|
|||
<TabsTrigger value="split" className="text-xs h-6 px-2">Split</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
|
||||
<TabsContent value="unified" className="m-0 pb-4">
|
||||
<UnifiedDiffView lineDiff={lineDiff} />
|
||||
</TabsContent>
|
||||
|
||||
|
||||
<TabsContent value="split" className="m-0">
|
||||
<SplitDiffView lineDiff={lineDiff} />
|
||||
</TabsContent>
|
||||
|
@ -340,7 +340,7 @@ export function StrReplaceToolView({
|
|||
</ScrollArea>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
|
||||
<div className="px-4 py-2 h-10 bg-gradient-to-r from-zinc-50/90 to-zinc-100/90 dark:from-zinc-900/90 dark:to-zinc-800/90 backdrop-blur-sm border-t border-zinc-200 dark:border-zinc-800 flex justify-between items-center">
|
||||
<div className="h-full flex items-center gap-2 text-xs text-zinc-500 dark:text-zinc-400">
|
||||
{!isStreaming && (
|
||||
|
@ -365,7 +365,7 @@ export function StrReplaceToolView({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
<div className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
{actualToolTimestamp && !isStreaming
|
||||
? formatTimestamp(actualToolTimestamp)
|
||||
|
|
|
@ -27,7 +27,32 @@ export interface ExtractedData {
|
|||
|
||||
|
||||
export const extractFromNewFormat = (content: any): ExtractedData => {
|
||||
if (!content || typeof content !== 'object') return { filePath: null, oldStr: null, newStr: null };
|
||||
if (!content) {
|
||||
return { filePath: null, oldStr: null, newStr: null };
|
||||
}
|
||||
|
||||
if (typeof content === 'string') {
|
||||
// Only try to parse if it looks like JSON
|
||||
const trimmed = content.trim();
|
||||
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||
try {
|
||||
console.debug('StrReplaceToolView: Attempting to parse JSON string:', content.substring(0, 100) + '...');
|
||||
const parsed = JSON.parse(content);
|
||||
console.debug('StrReplaceToolView: Successfully parsed JSON:', parsed);
|
||||
return extractFromNewFormat(parsed);
|
||||
} catch (error) {
|
||||
console.error('StrReplaceToolView: JSON parse error:', error, 'Content:', content.substring(0, 200));
|
||||
return { filePath: null, oldStr: null, newStr: null };
|
||||
}
|
||||
} else {
|
||||
console.debug('StrReplaceToolView: String content does not look like JSON, skipping parse');
|
||||
return { filePath: null, oldStr: null, newStr: null };
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof content !== 'object') {
|
||||
return { filePath: null, oldStr: null, newStr: null };
|
||||
}
|
||||
|
||||
if ('tool_execution' in content && typeof content.tool_execution === 'object') {
|
||||
const toolExecution = content.tool_execution;
|
||||
|
@ -49,7 +74,13 @@ export const extractFromNewFormat = (content: any): ExtractedData => {
|
|||
};
|
||||
}
|
||||
|
||||
if ('role' in content && 'content' in content && typeof content.content === 'string') {
|
||||
console.debug('StrReplaceToolView: Found role/content structure with string content, parsing...');
|
||||
return extractFromNewFormat(content.content);
|
||||
}
|
||||
|
||||
if ('role' in content && 'content' in content && typeof content.content === 'object') {
|
||||
console.debug('StrReplaceToolView: Found role/content structure with object content');
|
||||
return extractFromNewFormat(content.content);
|
||||
}
|
||||
|
||||
|
|
|
@ -33,12 +33,13 @@ export const useThreads = createQueryHook(
|
|||
|
||||
interface DeleteThreadVariables {
|
||||
threadId: string;
|
||||
sandboxId?: string;
|
||||
isNavigateAway?: boolean;
|
||||
}
|
||||
|
||||
export const useDeleteThread = createMutationHook(
|
||||
async ({ threadId }: DeleteThreadVariables) => {
|
||||
return await deleteThread(threadId);
|
||||
async ({ threadId, sandboxId }: DeleteThreadVariables) => {
|
||||
return await deleteThread(threadId, sandboxId);
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
|
@ -48,16 +49,18 @@ export const useDeleteThread = createMutationHook(
|
|||
|
||||
interface DeleteMultipleThreadsVariables {
|
||||
threadIds: string[];
|
||||
threadSandboxMap?: Record<string, string>;
|
||||
onProgress?: (completed: number, total: number) => void;
|
||||
}
|
||||
|
||||
export const useDeleteMultipleThreads = createMutationHook(
|
||||
async ({ threadIds, onProgress }: DeleteMultipleThreadsVariables) => {
|
||||
async ({ threadIds, threadSandboxMap, onProgress }: DeleteMultipleThreadsVariables) => {
|
||||
let completedCount = 0;
|
||||
const results = await Promise.all(
|
||||
threadIds.map(async (threadId) => {
|
||||
try {
|
||||
const result = await deleteThread(threadId);
|
||||
const sandboxId = threadSandboxMap?.[threadId];
|
||||
const result = await deleteThread(threadId, sandboxId);
|
||||
completedCount++;
|
||||
onProgress?.(completedCount, threadIds.length);
|
||||
return { success: true, threadId };
|
||||
|
|
|
@ -74,41 +74,99 @@ export const toggleThreadPublicStatus = async (
|
|||
return updateThread(threadId, { is_public: isPublic });
|
||||
};
|
||||
|
||||
export const deleteThread = async (threadId: string): Promise<void> => {
|
||||
const deleteSandbox = async (sandboxId: string): Promise<void> => {
|
||||
try {
|
||||
const supabase = createClient();
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession();
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
if (session?.access_token) {
|
||||
headers['Authorization'] = `Bearer ${session.access_token}`;
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_URL}/sandboxes/${sandboxId}`, {
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('Failed to delete sandbox, continuing with thread deletion');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Error deleting sandbox, continuing with thread deletion:', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteThread = async (threadId: string, sandboxId?: string): Promise<void> => {
|
||||
try {
|
||||
const supabase = createClient();
|
||||
|
||||
|
||||
// If sandbox ID is provided, delete it directly
|
||||
if (sandboxId) {
|
||||
await deleteSandbox(sandboxId);
|
||||
} else {
|
||||
// Otherwise, get the thread to find its project and sandbox
|
||||
const { data: thread, error: threadError } = await supabase
|
||||
.from('threads')
|
||||
.select('project_id')
|
||||
.eq('thread_id', threadId)
|
||||
.single();
|
||||
|
||||
if (threadError) {
|
||||
console.error('Error fetching thread:', threadError);
|
||||
throw new Error(`Error fetching thread: ${threadError.message}`);
|
||||
}
|
||||
|
||||
// If thread has a project, get sandbox ID and delete it
|
||||
if (thread?.project_id) {
|
||||
const { data: project } = await supabase
|
||||
.from('projects')
|
||||
.select('sandbox')
|
||||
.eq('project_id', thread.project_id)
|
||||
.single();
|
||||
|
||||
if (project?.sandbox?.id) {
|
||||
await deleteSandbox(project.sandbox.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Deleting all agent runs for thread ${threadId}`);
|
||||
const { error: agentRunsError } = await supabase
|
||||
.from('agent_runs')
|
||||
.delete()
|
||||
.eq('thread_id', threadId);
|
||||
|
||||
|
||||
if (agentRunsError) {
|
||||
console.error('Error deleting agent runs:', agentRunsError);
|
||||
throw new Error(`Error deleting agent runs: ${agentRunsError.message}`);
|
||||
}
|
||||
|
||||
|
||||
console.log(`Deleting all messages for thread ${threadId}`);
|
||||
const { error: messagesError } = await supabase
|
||||
.from('messages')
|
||||
.delete()
|
||||
.eq('thread_id', threadId);
|
||||
|
||||
|
||||
if (messagesError) {
|
||||
console.error('Error deleting messages:', messagesError);
|
||||
throw new Error(`Error deleting messages: ${messagesError.message}`);
|
||||
}
|
||||
|
||||
console.log(`Deleting thread ${threadId}`);
|
||||
const { error: threadError } = await supabase
|
||||
const { error: threadError2 } = await supabase
|
||||
.from('threads')
|
||||
.delete()
|
||||
.eq('thread_id', threadId);
|
||||
|
||||
if (threadError) {
|
||||
console.error('Error deleting thread:', threadError);
|
||||
throw new Error(`Error deleting thread: ${threadError.message}`);
|
||||
if (threadError2) {
|
||||
console.error('Error deleting thread:', threadError2);
|
||||
throw new Error(`Error deleting thread: ${threadError2.message}`);
|
||||
}
|
||||
|
||||
console.log(
|
||||
|
|
Loading…
Reference in New Issue