From 8220ee2e0cc6603aab6a7a5c158f7f6eef626342 Mon Sep 17 00:00:00 2001 From: Gemini AI Date: Mon, 29 Dec 2025 13:51:51 +0400 Subject: [PATCH] feat: add Google Ads export (CSV/HTML), Qwen auth UI, improve change log stripping --- components/AIAssist.tsx | 56 ++++++++++- components/GoogleAdsGenerator.tsx | 159 ++++++++++++++++++++++++++++-- 2 files changed, 206 insertions(+), 9 deletions(-) diff --git a/components/AIAssist.tsx b/components/AIAssist.tsx index f00df3d..5d3fc72 100644 --- a/components/AIAssist.tsx +++ b/components/AIAssist.tsx @@ -4,7 +4,7 @@ import React, { useState, useEffect, useRef, memo } from "react"; import { MessageSquare, Send, Code2, Palette, Search, Trash2, Copy, Monitor, StopCircle, X, Zap, Ghost, - Wand2, LayoutPanelLeft, Play, Orbit, Plus + Wand2, LayoutPanelLeft, Play, Orbit, Plus, Key } from "lucide-react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; @@ -390,7 +390,17 @@ function parseStreamingContent(text: string, currentAgent: string) { // CRITICAL: Strip any change log that leaked into preview data // The change log should be in chat, not rendered in canvas preview.data = preview.data + // Strip closing tags followed by change logs .replace(/\[\/[a-z]+:[a-z]+\]\s*\*?\*?Change\s*Log:?\*?\*?[\s\S]*$/i, '') + // Strip change log sections at end (various formats) + .replace(/\n\s*\*?\*?\s*Change\s*Log\s*:?\s*\*?\*?\s*\n[\s\S]*$/i, '') + .replace(/\n\s*##+\s*Change\s*Log[\s\S]*$/i, '') + .replace(/\n\s*---+\s*\n\s*\*?\*?Change\s*Log[\s\S]*$/i, '') + // Strip "Changes Made:" or similar sections + .replace(/\n\s*\*?\*?\s*Changes\s*(Made|Applied|Implemented)\s*:?\s*\*?\*?\s*\n[\s\S]*$/i, '') + // Strip update/modification logs + .replace(/\n\s*\*?\*?\s*Updates?\s*:?\s*\*?\*?\s*\n\s*[-*]\s+[\s\S]*$/i, '') + // Final catch - any markdown list at the very end that looks like changes .replace(/\*?\*?Change\s*Log:?\*?\*?[\s\S]*$/i, function (match) { // Only strip if it's at the end and looks like a change log section if (match.length > 500) return match; // Probably part of the app, not a log @@ -460,6 +470,11 @@ export default function AIAssist() { // Agentic States const [assistStep, setAssistStep] = useState<"idle" | "plan" | "generating" | "preview">("idle"); const [aiPlan, setAiPlan] = useState(null); + const [isAuthenticatingQwen, setIsAuthenticatingQwen] = useState(false); + const [qwenAuthError, setQwenAuthError] = useState(null); + + // Check if Qwen is authenticated + const isQwenAuthed = modelAdapter.hasQwenAuth(); // Sync local state when tab changes - FULL ISOLATION useEffect(() => { @@ -686,6 +701,20 @@ export default function AIAssist() { setAiPlan(null); }; + const handleQwenAuth = async () => { + setIsAuthenticatingQwen(true); + setQwenAuthError(null); + try { + await modelAdapter.startQwenOAuth(); + // Force re-render to update auth state + window.location.reload(); + } catch (err) { + setQwenAuthError(err instanceof Error ? err.message : "Failed to authenticate with Qwen"); + } finally { + setIsAuthenticatingQwen(false); + } + }; + return (
{/* --- Chat Panel --- */} @@ -835,6 +864,31 @@ export default function AIAssist() {
+ {/* Qwen Auth Banner */} + {selectedProvider === "qwen" && !isQwenAuthed && ( +
+
+
+ +
+
+

Qwen Authentication Required

+

Sign in with Qwen to use AI Assist with this provider

+ {qwenAuthError && ( +

{qwenAuthError}

+ )} +
+ +
+
+ )} + {aiAssistHistory.length === 0 && (
diff --git a/components/GoogleAdsGenerator.tsx b/components/GoogleAdsGenerator.tsx index 7bb35e6..c999bb0 100644 --- a/components/GoogleAdsGenerator.tsx +++ b/components/GoogleAdsGenerator.tsx @@ -7,7 +7,7 @@ import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import useStore from "@/lib/store"; import modelAdapter from "@/lib/services/adapter-instance"; -import { Megaphone, Copy, Loader2, CheckCircle2, Settings, Plus, X, ChevronDown, ChevronUp, Wand2, Target, TrendingUp, ShieldAlert, BarChart3, Users, Rocket } from "lucide-react"; +import { Megaphone, Copy, Loader2, CheckCircle2, Settings, Plus, X, ChevronDown, ChevronUp, Wand2, Target, TrendingUp, ShieldAlert, BarChart3, Users, Rocket, Download, FileSpreadsheet } from "lucide-react"; import { cn } from "@/lib/utils"; import { GoogleAdsResult } from "@/types"; import { translations } from "@/lib/i18n/translations"; @@ -304,6 +304,141 @@ export default function GoogleAdsGenerator() { } }; + const exportCSV = () => { + if (!googleAdsResult && !magicWandResult) return; + + let csvContent = "data:text/csv;charset=utf-8,"; + + if (googleAdsResult) { + // Keywords section + csvContent += "KEYWORDS RESEARCH\n"; + csvContent += "Type,Keyword,CPC\n"; + googleAdsResult.keywords?.primary?.forEach(k => { + csvContent += `Primary,"${k.keyword}","${k.cpc || 'N/A'}"\n`; + }); + googleAdsResult.keywords?.longTail?.forEach(k => { + csvContent += `Long-tail,"${k.keyword}","${k.cpc || 'N/A'}"\n`; + }); + googleAdsResult.keywords?.negative?.forEach(k => { + csvContent += `Negative,"${k.keyword}",""\n`; + }); + + // Ad Copies section + csvContent += "\nAD COPIES\n"; + csvContent += "Headlines,Descriptions,CTA\n"; + googleAdsResult.adCopies?.forEach(ad => { + csvContent += `"${ad.headlines?.join(' | ') || ''}","${ad.descriptions?.join(' | ') || ''}","${ad.callToAction || ''}"\n`; + }); + } + + if (magicWandResult) { + csvContent += "\nMARKET ANALYSIS\n"; + csvContent += `Growth Rate,"${magicWandResult.marketAnalysis?.growthRate || 'N/A'}"\n`; + csvContent += `Top Competitors,"${magicWandResult.marketAnalysis?.topCompetitors?.join(', ') || 'N/A'}"\n`; + csvContent += `Market Trends,"${magicWandResult.marketAnalysis?.marketTrends?.join(', ') || 'N/A'}"\n`; + } + + const encodedUri = encodeURI(csvContent); + const link = document.createElement("a"); + link.setAttribute("href", encodedUri); + link.setAttribute("download", `google-ads-report-${new Date().toISOString().split('T')[0]}.csv`); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; + + const exportHTML = () => { + if (!googleAdsResult && !magicWandResult) return; + + let htmlContent = ` + + + + + Google Ads Report - ${new Date().toLocaleDateString()} + + + +
+

📊 Google Ads Report

+

Generated on ${new Date().toLocaleString()}

`; + + if (googleAdsResult) { + // Keywords + if (googleAdsResult.keywords?.primary?.length) { + htmlContent += ` +
+
🎯 Primary Keywords
+ ${googleAdsResult.keywords.primary.map(k => `${k.keyword}${k.cpc ? `${k.cpc}` : ''}`).join('')} +
`; + } + + // Ad Copies + if (googleAdsResult.adCopies?.length) { + htmlContent += ` +
+
📝 Ad Variations
+ ${googleAdsResult.adCopies.map(ad => ` +
+
${ad.headlines?.[0] || ''}
+

${ad.descriptions?.[0] || ''}

+ ${ad.callToAction ? `${ad.callToAction}` : ''} +
`).join('')} +
`; + } + } + + if (magicWandResult) { + const ma = magicWandResult.marketAnalysis; + htmlContent += ` +
+
📈 Market Intelligence
+
+
${ma?.growthRate || 'N/A'}
Market Growth
+
+ ${ma?.topCompetitors?.length ? `

Top Competitors: ${ma.topCompetitors.join(', ')}

` : ''} + ${ma?.marketTrends?.length ? `

Market Trends: ${ma.marketTrends.join(', ')}

` : ''} +
`; + } + + htmlContent += ` + +
+ +`; + + const blob = new Blob([htmlContent], { type: 'text/html' }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.setAttribute("href", url); + link.setAttribute("download", `google-ads-report-${new Date().toISOString().split('T')[0]}.html`); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + }; + const sections = [ { id: "keywords", title: t.keywordsResearch }, { id: "adcopies", title: t.adCopyVariations }, @@ -974,13 +1109,21 @@ export default function GoogleAdsGenerator() { {magicWandResult ? t.strategicDirections : t.generatedCampaign} {(googleAdsResult || magicWandResult) && ( - +
+ + + +
)}