fix: response procesor & billing modal and other fixes

This commit is contained in:
Vukasin 2025-06-01 17:50:09 +02:00
parent 8c74d35841
commit 82df42badf
8 changed files with 440 additions and 74 deletions

View File

@ -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

View File

@ -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>

View File

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

View File

@ -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}

View File

@ -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}

View File

@ -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

View File

@ -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)

View File

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