mirror of https://github.com/kortix-ai/suna.git
fix: response procesor & billing modal and other fixes
This commit is contained in:
parent
8c74d35841
commit
82df42badf
|
@ -1512,7 +1512,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,
|
||||
|
@ -1597,14 +1597,29 @@ class ResponseProcessor:
|
|||
# For backwards compatibility with LLM, also include a human-readable summary
|
||||
# Use the original string output for the summary to avoid complex object representation
|
||||
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:
|
||||
# For XML tools, create a readable summary
|
||||
status = "completed successfully" if structured_result["tool_execution"]["result"]["success"] else "failed"
|
||||
summary = f"Tool '{xml_tag_name}' {status}. Output: {summary_output}"
|
||||
# For XML tools, create a detailed readable summary
|
||||
status = "completed successfully" if success_status else "failed"
|
||||
summary = f"""[Tool Execution Result]
|
||||
Tool: {xml_tag_name} ({function_name})
|
||||
Status: {status}
|
||||
Arguments: {json.dumps(arguments)}
|
||||
Output: {summary_output}"""
|
||||
if not success_status and structured_result["tool_execution"]["result"].get("error"):
|
||||
summary += f"\nError: {structured_result['tool_execution']['result']['error']}"
|
||||
else:
|
||||
# For native tools, create a readable summary
|
||||
status = "completed successfully" if structured_result["tool_execution"]["result"]["success"] else "failed"
|
||||
summary = f"Function '{function_name}' {status}. Output: {summary_output}"
|
||||
# For native tools, create a detailed readable summary
|
||||
status = "completed successfully" if success_status else "failed"
|
||||
summary = f"""[Function Call Result]
|
||||
Function: {function_name}
|
||||
Status: {status}
|
||||
Arguments: {json.dumps(arguments)}
|
||||
Output: {summary_output}"""
|
||||
if not success_status and structured_result["tool_execution"]["result"].get("error"):
|
||||
summary += f"\nError: {structured_result['tool_execution']['result']['error']}"
|
||||
|
||||
structured_result["summary"] = summary
|
||||
|
||||
|
|
|
@ -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;
|
||||
}) => {
|
||||
const [displayedPrompts, setDisplayedPrompts] = useState<PromptExample[]>([]);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
|
||||
// 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="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)}
|
||||
<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" }}
|
||||
>
|
||||
<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">
|
||||
<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>
|
||||
</CardHeader>
|
||||
|
||||
</Card>
|
||||
</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>
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue