From 82df42badfdaa33e69b533016f04842b65333d96 Mon Sep 17 00:00:00 2001 From: Vukasin Date: Sun, 1 Jun 2025 17:50:09 +0200 Subject: [PATCH] fix: response procesor & billing modal and other fixes --- backend/agentpress/response_processor.py | 29 ++- .../_components/suggestions/examples.tsx | 180 ++++++++++++++++-- .../src/components/billing/billing-modal.tsx | 140 ++++++++++++++ .../thread/chat-input/model-selector.tsx | 11 +- .../thread/tool-call-side-panel.tsx | 31 ++- .../thread/tool-views/GenericToolView.tsx | 44 ++--- .../str-replace/StrReplaceToolView.tsx | 46 ++--- .../thread/tool-views/str-replace/_utils.ts | 33 +++- 8 files changed, 440 insertions(+), 74 deletions(-) create mode 100644 frontend/src/components/billing/billing-modal.tsx diff --git a/backend/agentpress/response_processor.py b/backend/agentpress/response_processor.py index 47fdeebd..b2b4a443 100644 --- a/backend/agentpress/response_processor.py +++ b/backend/agentpress/response_processor.py @@ -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 diff --git a/frontend/src/app/(dashboard)/dashboard/_components/suggestions/examples.tsx b/frontend/src/app/(dashboard)/dashboard/_components/suggestions/examples.tsx index 0dd4eeb4..0ad0ffd5 100644 --- a/frontend/src/app/(dashboard)/dashboard/_components/suggestions/examples.tsx +++ b/frontend/src/app/(dashboard)/dashboard/_components/suggestions/examples.tsx @@ -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: , }, { - 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: , }, @@ -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: , }, + { + 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: , + }, + { + 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: , + }, + { + title: 'Portfolio analysis', + query: 'Create a personal investment portfolio analysis tool with risk assessment, diversification recommendations, and performance tracking against market benchmarks.', + icon: , + }, + { + 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: , + }, + { + title: 'A/B testing framework', + query: 'Design a comprehensive A/B testing framework including hypothesis formation, statistical significance calculations, and result interpretation guidelines.', + icon: , + }, + { + 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: , + }, + { + 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: , + }, + { + 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: , + }, + { + 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: , + }, + { + 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: , + }, + { + title: 'Project automation', + query: 'Create an intelligent project management system with automatic task assignment, deadline tracking, resource allocation, and team communication integration.', + icon: , + }, + { + 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: , + }, + { + 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: , + }, + { + 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: , + }, + { + title: 'Supply chain analysis', + query: 'Create a supply chain optimization analysis including vendor evaluation, cost reduction opportunities, risk mitigation, and inventory management strategies.', + icon: , + }, + { + title: 'UX research framework', + query: 'Develop a comprehensive UX research framework including user interviews, usability testing, persona development, and data-driven design recommendations.', + icon: , + }, ]; +// 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([]); + 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 (
-
- {prompts.map((prompt, index) => ( - onSelectPrompt && onSelectPrompt(prompt.query)} +
+ Quick starts + +
+
+ {displayedPrompts.map((prompt, index) => ( + + onSelectPrompt && onSelectPrompt(prompt.query)} + > + +
+
+ {React.cloneElement(prompt.icon as React.ReactElement, { size: 14 })} +
+ {prompt.title} - - - - + +
+
+
+
))}
diff --git a/frontend/src/components/billing/billing-modal.tsx b/frontend/src/components/billing/billing-modal.tsx new file mode 100644 index 00000000..839e102a --- /dev/null +++ b/frontend/src/components/billing/billing-modal.tsx @@ -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(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(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 ( + + + + Billing & Subscription + +
+

+ Running in local development mode - billing features are disabled +

+

+ All premium features are available in this environment +

+
+
+
+ ); + } + + return ( + + + + Upgrade Your Plan + + + {isLoading || authLoading ? ( +
+ + + +
+ ) : error ? ( +
+

Error loading billing status: {error}

+
+ ) : ( + <> + {subscriptionData && ( +
+
+
+ + Agent Usage This Month + + + {subscriptionData.current_usage?.toFixed(2) || '0'} /{' '} + {subscriptionData.minutes_limit || '0'} minutes + +
+
+
+ )} + + + + {subscriptionData && ( + + )} + + )} +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/thread/chat-input/model-selector.tsx b/frontend/src/components/thread/chat-input/model-selector.tsx index 29c0ac4a..45308556 100644 --- a/frontend/src/components/thread/chat-input/model-selector.tsx +++ b/frontend/src/components/thread/chat-input/model-selector.tsx @@ -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 = ({ refreshCustomModels, }) => { const [paywallOpen, setPaywallOpen] = useState(false); + const [billingModalOpen, setBillingModalOpen] = useState(false); const [lockedModel, setLockedModel] = useState(null); const [isOpen, setIsOpen] = useState(false); const [searchQuery, setSearchQuery] = useState(''); @@ -195,7 +197,7 @@ export const ModelSelector: React.FC = ({ }; const handleUpgradeClick = () => { - router.push('/settings/billing'); + setBillingModalOpen(true); }; const closeDialog = () => { @@ -756,6 +758,13 @@ export const ModelSelector: React.FC = ({ mode={dialogMode} /> + {/* Billing Modal */} + + {paywallOpen && ( { + 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} diff --git a/frontend/src/components/thread/tool-views/GenericToolView.tsx b/frontend/src/components/thread/tool-views/GenericToolView.tsx index 45152444..52da784f 100644 --- a/frontend/src/components/thread/tool-views/GenericToolView.tsx +++ b/frontend/src/components/thread/tool-views/GenericToolView.tsx @@ -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({
- + {!isStreaming && ( - @@ -165,7 +165,7 @@ export function GenericToolView({ {isStreaming ? ( - )} - +
{!isStreaming && (formattedAssistantContent || formattedToolContent) && ( @@ -233,7 +233,7 @@ export function GenericToolView({ )}
- +
{toolTimestamp && !isStreaming diff --git a/frontend/src/components/thread/tool-views/str-replace/StrReplaceToolView.tsx b/frontend/src/components/thread/tool-views/str-replace/StrReplaceToolView.tsx index c1686abd..a2767047 100644 --- a/frontend/src/components/thread/tool-views/str-replace/StrReplaceToolView.tsx +++ b/frontend/src/components/thread/tool-views/str-replace/StrReplaceToolView.tsx @@ -37,8 +37,8 @@ const UnifiedDiffView: React.FC<{ lineDiff: LineDiff[] }> = ({ lineDiff }) => ( {lineDiff.map((line, i) => ( - = ({ lineDiff }) => ( {lineDiff.map((line, i) => ( - -
= ({ lineDiff }) => (
{line.lineNumber}
- {line.type === 'removed' && + {line.type === 'removed' && }
@@ -99,7 +99,7 @@ const SplitDiffView: React.FC<{ lineDiff: LineDiff[] }> = ({ lineDiff }) => (
) : null}
= ({ lineDiff }) => (
{line.lineNumber}
- {line.type === 'added' && + {line.type === 'added' && }
@@ -152,7 +152,7 @@ export function StrReplaceToolView({ }: ToolViewProps): JSX.Element { const [expanded, setExpanded] = useState(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}
- + {!isStreaming && ( - @@ -260,7 +260,7 @@ export function StrReplaceToolView({ {isStreaming ? ( - - +
@@ -294,7 +294,7 @@ export function StrReplaceToolView({ {stats.deletions}
- + @@ -314,7 +314,7 @@ export function StrReplaceToolView({
- + {expanded && (
setViewMode(v as 'unified' | 'split')} className="w-auto"> @@ -324,11 +324,11 @@ export function StrReplaceToolView({ Split
- + - + @@ -340,7 +340,7 @@ export function StrReplaceToolView({ )}
- +
{!isStreaming && ( @@ -365,7 +365,7 @@ export function StrReplaceToolView({
)}
- +
{actualToolTimestamp && !isStreaming ? formatTimestamp(actualToolTimestamp) diff --git a/frontend/src/components/thread/tool-views/str-replace/_utils.ts b/frontend/src/components/thread/tool-views/str-replace/_utils.ts index 199cc97a..6917a115 100644 --- a/frontend/src/components/thread/tool-views/str-replace/_utils.ts +++ b/frontend/src/components/thread/tool-views/str-replace/_utils.ts @@ -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); }