Compare commits
64 Commits
8733b885d4
...
main
108
app/api/ai-assist/route.ts
Normal file
108
app/api/ai-assist/route.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
// Schema validation
|
||||||
|
const schema = z.object({
|
||||||
|
request: z.string().min(1),
|
||||||
|
step: z.enum(["plan", "generate", "preview"]).default("plan"),
|
||||||
|
plan: z.any().optional(),
|
||||||
|
code: z.string().optional(),
|
||||||
|
provider: z.string().optional(),
|
||||||
|
model: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const STEPS = {
|
||||||
|
plan: `You are an expert software architect. Create a DETAILED DEVELOPMENT PLAN for the following request: "{request}"
|
||||||
|
|
||||||
|
Output ONLY a JSON object:
|
||||||
|
{
|
||||||
|
"summary": "One sentence overview",
|
||||||
|
"architecture": "High-level components + data flow",
|
||||||
|
"techStack": ["Next.js", "Tailwind", "Lucide Icons"],
|
||||||
|
"files": [
|
||||||
|
{"path": "app/page.tsx", "purpose": "Main UI"},
|
||||||
|
{"path": "components/Preview.tsx", "purpose": "Core logic"}
|
||||||
|
],
|
||||||
|
"timeline": "Estimate",
|
||||||
|
"risks": ["Potential blockers"]
|
||||||
|
}`,
|
||||||
|
|
||||||
|
generate: `You are a Senior Vibe Coder. Execute the following approved plan:
|
||||||
|
Plan: {plan}
|
||||||
|
|
||||||
|
Generate COMPLETE, PRODUCTION-READY code for all files.
|
||||||
|
Focus on the request: "{request}"
|
||||||
|
|
||||||
|
Output ONLY a JSON object:
|
||||||
|
{
|
||||||
|
"files": {
|
||||||
|
"app/page.tsx": "// code here",
|
||||||
|
"components/UI.tsx": "// more code"
|
||||||
|
},
|
||||||
|
"explanation": "How it works"
|
||||||
|
}`,
|
||||||
|
|
||||||
|
preview: `Convert the following code into a single-file interactive HTML preview (Standalone).
|
||||||
|
Use Tailwind CDN.
|
||||||
|
|
||||||
|
Code: {code}
|
||||||
|
|
||||||
|
Output ONLY valid HTML.`
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
const requestId = randomUUID();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Safe body parsing
|
||||||
|
const body = await req.json().catch(() => null);
|
||||||
|
|
||||||
|
if (!body) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Invalid JSON body", requestId, success: false },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate schema
|
||||||
|
const parseResult = schema.safeParse(body);
|
||||||
|
if (!parseResult.success) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: "Invalid request body",
|
||||||
|
details: parseResult.error.flatten(),
|
||||||
|
requestId,
|
||||||
|
success: false
|
||||||
|
},
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { request, step, plan, code } = parseResult.data;
|
||||||
|
|
||||||
|
let prompt = STEPS[step];
|
||||||
|
prompt = prompt.replace("{request}", request);
|
||||||
|
if (plan) prompt = prompt.replace("{plan}", JSON.stringify(plan));
|
||||||
|
if (code) prompt = prompt.replace("{code}", code);
|
||||||
|
|
||||||
|
// Return the prompt for the frontend to use with the streaming adapter
|
||||||
|
return NextResponse.json({
|
||||||
|
prompt,
|
||||||
|
step,
|
||||||
|
requestId,
|
||||||
|
success: true
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(`[ai-assist] requestId=${requestId}`, err);
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: err?.message ?? "AI Assist failed",
|
||||||
|
requestId,
|
||||||
|
success: false
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,15 +38,27 @@ export async function POST(request: NextRequest) {
|
|||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
});
|
});
|
||||||
|
|
||||||
const payload = await response.text();
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
const payload = await response.text();
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Ollama chat request failed", details: payload },
|
{ error: "Ollama chat request failed", details: payload },
|
||||||
{ status: response.status }
|
{ status: response.status }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json(payload ? JSON.parse(payload) : {});
|
// If stream is requested, pipe the response body
|
||||||
|
if (body.stream) {
|
||||||
|
return new Response(response.body, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-ndjson",
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
Connection: "keep-alive",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = await response.json();
|
||||||
|
return NextResponse.json(payload);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Ollama chat proxy failed", error);
|
console.error("Ollama chat proxy failed", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
@@ -44,23 +44,27 @@ export async function POST(request: NextRequest) {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const payload = await response.text();
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
const payload = await response.text();
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: payload || response.statusText || "Qwen chat failed" },
|
{ error: payload || response.statusText || "Qwen chat failed" },
|
||||||
{ status: response.status }
|
{ status: response.status }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// Handle streaming
|
||||||
const data = JSON.parse(payload);
|
if (stream) {
|
||||||
return NextResponse.json(data, { status: response.status });
|
return new Response(response.body, {
|
||||||
} catch {
|
headers: {
|
||||||
return NextResponse.json(
|
"Content-Type": "text/event-stream",
|
||||||
{ error: payload || "Unexpected response format" },
|
"Cache-Control": "no-cache",
|
||||||
{ status: 502 }
|
Connection: "keep-alive",
|
||||||
);
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return NextResponse.json(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "internal_server_error", message: error instanceof Error ? error.message : "Qwen chat failed" },
|
{ error: "internal_server_error", message: error instanceof Error ? error.message : "Qwen chat failed" },
|
||||||
|
|||||||
37
app/api/slides/route.ts
Normal file
37
app/api/slides/route.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
topic: z.string().min(3),
|
||||||
|
slideCount: z.number().min(3).max(15).default(8),
|
||||||
|
style: z.enum(["professional", "creative", "technical", "pitch"]).default("professional"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await req.json();
|
||||||
|
const { topic, slideCount, style } = schema.parse(body);
|
||||||
|
|
||||||
|
const systemPrompt = `You are an elite presentation designer. Create a visually stunning presentation with ${slideCount} slides about "${topic}".
|
||||||
|
|
||||||
|
Style: ${style}
|
||||||
|
|
||||||
|
Output ONLY a sequence of slides separated by "---".
|
||||||
|
Format each slide as:
|
||||||
|
## [Slide Title]
|
||||||
|
- [Bullet Point 1]
|
||||||
|
- [Bullet Point 2]
|
||||||
|
VISUAL: [Detailed description of image/chart/icon]
|
||||||
|
---
|
||||||
|
`;
|
||||||
|
|
||||||
|
// The frontend will handle the actual generation call to keep use of the ModelAdapter,
|
||||||
|
// this route serves as the prompt orchestrator.
|
||||||
|
return NextResponse.json({
|
||||||
|
prompt: systemPrompt,
|
||||||
|
success: true
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
return NextResponse.json({ success: false, error: error.message }, { status: 400 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,8 @@ export const metadata: Metadata = {
|
|||||||
viewport: "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no",
|
viewport: "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
import LocaleProvider from "@/components/LocaleProvider";
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
@@ -20,7 +22,11 @@ export default function RootLayout({
|
|||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className={roboto.className}>{children}</body>
|
<body className={roboto.className}>
|
||||||
|
<LocaleProvider>
|
||||||
|
{children}
|
||||||
|
</LocaleProvider>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
26
app/page.tsx
26
app/page.tsx
@@ -3,16 +3,22 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import Sidebar from "@/components/Sidebar";
|
import Sidebar from "@/components/Sidebar";
|
||||||
import type { View } from "@/components/Sidebar";
|
import type { View } from "@/components/Sidebar";
|
||||||
import PromptEnhancer from "@/components/PromptEnhancer";
|
import dynamic from 'next/dynamic';
|
||||||
import PRDGenerator from "@/components/PRDGenerator";
|
|
||||||
import ActionPlanGenerator from "@/components/ActionPlanGenerator";
|
|
||||||
import UXDesignerPrompt from "@/components/UXDesignerPrompt";
|
|
||||||
import SlidesGenerator from "@/components/SlidesGenerator";
|
|
||||||
import GoogleAdsGenerator from "@/components/GoogleAdsGenerator";
|
|
||||||
import HistoryPanel from "@/components/HistoryPanel";
|
|
||||||
import SettingsPanel from "@/components/SettingsPanel";
|
|
||||||
import modelAdapter from "@/lib/services/adapter-instance";
|
import modelAdapter from "@/lib/services/adapter-instance";
|
||||||
|
|
||||||
|
// Dynamic imports to prevent hydration mismatches
|
||||||
|
// ensuring hydration match
|
||||||
|
const PromptEnhancer = dynamic(() => import("@/components/PromptEnhancer"), { ssr: false });
|
||||||
|
const PRDGenerator = dynamic(() => import("@/components/PRDGenerator"), { ssr: false });
|
||||||
|
const ActionPlanGenerator = dynamic(() => import("@/components/ActionPlanGenerator"), { ssr: false });
|
||||||
|
const UXDesignerPrompt = dynamic(() => import("@/components/UXDesignerPrompt"), { ssr: false });
|
||||||
|
const SlidesGenerator = dynamic(() => import("@/components/SlidesGenerator"), { ssr: false });
|
||||||
|
const GoogleAdsGenerator = dynamic(() => import("@/components/GoogleAdsGenerator"), { ssr: false });
|
||||||
|
const MarketResearcher = dynamic(() => import("@/components/MarketResearcher"), { ssr: false });
|
||||||
|
const AIAssist = dynamic(() => import("@/components/AIAssist"), { ssr: false });
|
||||||
|
const HistoryPanel = dynamic(() => import("@/components/HistoryPanel"), { ssr: false });
|
||||||
|
const SettingsPanel = dynamic(() => import("@/components/SettingsPanel"), { ssr: false });
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [currentView, setCurrentView] = useState<View>("enhance");
|
const [currentView, setCurrentView] = useState<View>("enhance");
|
||||||
|
|
||||||
@@ -35,6 +41,10 @@ export default function Home() {
|
|||||||
return <SlidesGenerator />;
|
return <SlidesGenerator />;
|
||||||
case "googleads":
|
case "googleads":
|
||||||
return <GoogleAdsGenerator />;
|
return <GoogleAdsGenerator />;
|
||||||
|
case "market-research":
|
||||||
|
return <MarketResearcher />;
|
||||||
|
case "ai-assist":
|
||||||
|
return <AIAssist />;
|
||||||
case "history":
|
case "history":
|
||||||
return <HistoryPanel />;
|
return <HistoryPanel />;
|
||||||
case "settings":
|
case "settings":
|
||||||
|
|||||||
1303
components/AIAssist.tsx
Normal file
1303
components/AIAssist.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -8,9 +8,11 @@ import useStore from "@/lib/store";
|
|||||||
import modelAdapter from "@/lib/services/adapter-instance";
|
import modelAdapter from "@/lib/services/adapter-instance";
|
||||||
import { ListTodo, Copy, Loader2, CheckCircle2, Clock, AlertTriangle, Settings } from "lucide-react";
|
import { ListTodo, Copy, Loader2, CheckCircle2, Clock, AlertTriangle, Settings } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { translations } from "@/lib/i18n/translations";
|
||||||
|
|
||||||
export default function ActionPlanGenerator() {
|
export default function ActionPlanGenerator() {
|
||||||
const {
|
const {
|
||||||
|
language,
|
||||||
currentPrompt,
|
currentPrompt,
|
||||||
actionPlan,
|
actionPlan,
|
||||||
selectedProvider,
|
selectedProvider,
|
||||||
@@ -28,6 +30,9 @@ export default function ActionPlanGenerator() {
|
|||||||
setSelectedModel,
|
setSelectedModel,
|
||||||
} = useStore();
|
} = useStore();
|
||||||
|
|
||||||
|
const t = translations[language].actionPlan;
|
||||||
|
const common = translations[language].common;
|
||||||
|
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
const selectedModel = selectedModels[selectedProvider];
|
const selectedModel = selectedModels[selectedProvider];
|
||||||
@@ -66,7 +71,7 @@ export default function ActionPlanGenerator() {
|
|||||||
|
|
||||||
const handleGenerate = async () => {
|
const handleGenerate = async () => {
|
||||||
if (!currentPrompt.trim()) {
|
if (!currentPrompt.trim()) {
|
||||||
setError("Please enter PRD or project requirements");
|
setError(t.enterPrdError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +79,7 @@ export default function ActionPlanGenerator() {
|
|||||||
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
|
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
|
||||||
|
|
||||||
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
|
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
|
||||||
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
|
setError(`${common.error}: ${common.configApiKey}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,11 +112,11 @@ export default function ActionPlanGenerator() {
|
|||||||
setActionPlan(newPlan);
|
setActionPlan(newPlan);
|
||||||
} else {
|
} else {
|
||||||
console.error("[ActionPlanGenerator] Generation failed:", result.error);
|
console.error("[ActionPlanGenerator] Generation failed:", result.error);
|
||||||
setError(result.error || "Failed to generate action plan");
|
setError(result.error || t.errorGenerate);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[ActionPlanGenerator] Generation error:", err);
|
console.error("[ActionPlanGenerator] Generation error:", err);
|
||||||
setError(err instanceof Error ? err.message : "An error occurred");
|
setError(err instanceof Error ? err.message : t.errorGenerate);
|
||||||
} finally {
|
} finally {
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
}
|
}
|
||||||
@@ -126,20 +131,20 @@ export default function ActionPlanGenerator() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2">
|
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2 text-start">
|
||||||
<Card className="h-fit">
|
<Card className="h-fit">
|
||||||
<CardHeader className="p-4 lg:p-6">
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||||
<ListTodo className="h-4 w-4 lg:h-5 lg:w-5" />
|
<ListTodo className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||||
Action Plan Generator
|
{t.title}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-xs lg:text-sm">
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
Convert PRD into actionable implementation plan
|
{t.description}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="text-xs lg:text-sm font-medium">AI Provider</label>
|
<label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
|
||||||
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
||||||
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
||||||
<Button
|
<Button
|
||||||
@@ -155,8 +160,8 @@ export default function ActionPlanGenerator() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="text-xs lg:text-sm font-medium">Model</label>
|
<label className="text-xs lg:text-sm font-medium">{common.model}</label>
|
||||||
<select
|
<select
|
||||||
value={selectedModel}
|
value={selectedModel}
|
||||||
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
||||||
@@ -171,12 +176,12 @@ export default function ActionPlanGenerator() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-xs lg:text-sm font-medium">PRD / Requirements</label>
|
<label className="text-xs lg:text-sm font-medium">{t.inputLabel}</label>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="Paste your PRD or project requirements here..."
|
placeholder={t.placeholder}
|
||||||
value={currentPrompt}
|
value={currentPrompt}
|
||||||
onChange={(e) => setCurrentPrompt(e.target.value)}
|
onChange={(e) => setCurrentPrompt(e.target.value)}
|
||||||
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm"
|
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm lg:text-base p-3 lg:p-4"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -186,7 +191,7 @@ export default function ActionPlanGenerator() {
|
|||||||
{!apiKeys[selectedProvider] && (
|
{!apiKeys[selectedProvider] && (
|
||||||
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
||||||
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
<span className="text-[10px] lg:text-xs">Configure API key in Settings</span>
|
<span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -196,24 +201,24 @@ export default function ActionPlanGenerator() {
|
|||||||
{isProcessing ? (
|
{isProcessing ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
|
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
|
||||||
Generating...
|
{common.generating}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<ListTodo className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
<ListTodo className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
Generate Action Plan
|
{t.generateButton}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className={cn(!actionPlan && "opacity-50")}>
|
<Card className={cn("flex flex-col", !actionPlan && "opacity-50")}>
|
||||||
<CardHeader className="p-4 lg:p-6">
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
||||||
Action Plan
|
{t.generatedTitle}
|
||||||
</span>
|
</span>
|
||||||
{actionPlan && (
|
{actionPlan && (
|
||||||
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
||||||
@@ -226,36 +231,35 @@ export default function ActionPlanGenerator() {
|
|||||||
)}
|
)}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-xs lg:text-sm">
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
Task breakdown, frameworks, and architecture recommendations
|
{t.generatedDesc}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
{actionPlan ? (
|
{actionPlan ? (
|
||||||
<div className="space-y-3 lg:space-y-4">
|
<div className="space-y-3 lg:space-y-4">
|
||||||
<div className="rounded-md border bg-primary/5 p-3 lg:p-4">
|
<div className="rounded-md border bg-primary/5 p-3 lg:p-4 text-start">
|
||||||
<h4 className="mb-1.5 lg:mb-2 flex items-center gap-2 font-semibold text-xs lg:text-sm">
|
<h4 className="mb-1.5 lg:mb-2 flex items-center gap-2 font-semibold text-xs lg:text-sm">
|
||||||
<Clock className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
<Clock className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
Implementation Roadmap
|
{t.roadmap}
|
||||||
</h4>
|
</h4>
|
||||||
<pre className="whitespace-pre-wrap text-xs lg:text-sm">{actionPlan.rawContent}</pre>
|
<pre className="whitespace-pre-wrap text-xs lg:text-sm leading-relaxed">{actionPlan.rawContent}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-md border bg-muted/30 p-3 lg:p-4">
|
<div className="rounded-md border bg-muted/30 p-3 lg:p-4 text-start">
|
||||||
<h4 className="mb-1.5 lg:mb-2 flex items-center gap-2 font-semibold text-xs lg:text-sm">
|
<h4 className="mb-1.5 lg:mb-2 flex items-center gap-2 font-semibold text-xs lg:text-sm">
|
||||||
<AlertTriangle className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
<AlertTriangle className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
Quick Notes
|
{t.quickNotes}
|
||||||
</h4>
|
</h4>
|
||||||
<ul className="list-inside list-disc space-y-0.5 lg:space-y-1 text-[10px] lg:text-xs text-muted-foreground">
|
<ul className="list-inside list-disc space-y-0.5 lg:space-y-1 text-[10px] lg:text-xs text-muted-foreground">
|
||||||
<li>Review all task dependencies before starting</li>
|
{t.notes.map((note: string, i: number) => (
|
||||||
<li>Set up recommended framework architecture</li>
|
<li key={i}>{note}</li>
|
||||||
<li>Follow best practices for security and performance</li>
|
))}
|
||||||
<li>Use specified deployment strategy</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-[200px] lg:h-[300px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground">
|
<div className="flex h-[200px] lg:h-[300px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground italic">
|
||||||
Action plan will appear here
|
{t.emptyState}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
57
components/ErrorBoundary.tsx
Normal file
57
components/ErrorBoundary.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { AlertTriangle, RotateCcw } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: React.ReactNode;
|
||||||
|
fallback?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
hasError: boolean;
|
||||||
|
error?: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ErrorBoundary extends React.Component<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = { hasError: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(error: Error): State {
|
||||||
|
return { hasError: true, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||||
|
console.error("ErrorBoundary caught an error:", error, errorInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetError = () => {
|
||||||
|
this.setState({ hasError: false, error: undefined });
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
if (this.props.fallback) return this.props.fallback;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center p-8 bg-slate-50 border border-slate-200 rounded-2xl h-full text-center">
|
||||||
|
<div className="bg-rose-100 p-3 rounded-full mb-4">
|
||||||
|
<AlertTriangle className="h-6 w-6 text-rose-600" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-bold text-slate-900 mb-2">Something went wrong</h3>
|
||||||
|
<p className="text-sm text-slate-500 max-w-xs mb-6">
|
||||||
|
{this.state.error?.message || "An unexpected error occurred while rendering this component."}
|
||||||
|
</p>
|
||||||
|
<Button onClick={this.resetError} variant="outline">
|
||||||
|
<RotateCcw className="h-4 w-4 mr-2" /> Try Again
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -4,16 +4,19 @@ import useStore from "@/lib/store";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Clock, Trash2, RotateCcw } from "lucide-react";
|
import { Clock, Trash2, RotateCcw } from "lucide-react";
|
||||||
|
import { translations } from "@/lib/i18n/translations";
|
||||||
|
|
||||||
export default function HistoryPanel() {
|
export default function HistoryPanel() {
|
||||||
const { history, setCurrentPrompt, clearHistory } = useStore();
|
const { language, history, setCurrentPrompt, clearHistory } = useStore();
|
||||||
|
const t = translations[language].history;
|
||||||
|
const common = translations[language].common;
|
||||||
|
|
||||||
const handleRestore = (prompt: string) => {
|
const handleRestore = (prompt: string) => {
|
||||||
setCurrentPrompt(prompt);
|
setCurrentPrompt(prompt);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClear = () => {
|
const handleClear = () => {
|
||||||
if (confirm("Are you sure you want to clear all history?")) {
|
if (confirm(t.confirmClear)) {
|
||||||
clearHistory();
|
clearHistory();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -21,12 +24,12 @@ export default function HistoryPanel() {
|
|||||||
if (history.length === 0) {
|
if (history.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="flex h-[300px] lg:h-[400px] items-center justify-center p-4 lg:p-6">
|
<CardContent className="flex h-[300px] lg:h-[400px] items-center justify-center p-4 lg:p-6 text-center">
|
||||||
<div className="text-center">
|
<div>
|
||||||
<Clock className="mx-auto h-10 w-10 lg:h-12 lg:w-12 text-muted-foreground/50" />
|
<Clock className="mx-auto h-10 w-10 lg:h-12 lg:w-12 text-muted-foreground/50" />
|
||||||
<p className="mt-3 lg:mt-4 text-sm lg:text-base text-muted-foreground">No history yet</p>
|
<p className="mt-3 lg:mt-4 text-sm lg:text-base text-muted-foreground font-medium">{t.empty}</p>
|
||||||
<p className="mt-1.5 lg:mt-2 text-xs lg:text-sm text-muted-foreground">
|
<p className="mt-1.5 lg:mt-2 text-xs lg:text-sm text-muted-foreground">
|
||||||
Start enhancing prompts to see them here
|
{t.emptyDesc}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -36,12 +39,14 @@ export default function HistoryPanel() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex-row items-center justify-between p-4 lg:p-6">
|
<CardHeader className="flex-row items-center justify-between p-4 lg:p-6 text-start">
|
||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-base lg:text-lg">History</CardTitle>
|
<CardTitle className="text-base lg:text-lg">{t.title}</CardTitle>
|
||||||
<CardDescription className="text-xs lg:text-sm">{history.length} items</CardDescription>
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
|
{history.length} {t.items}
|
||||||
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" size="icon" onClick={handleClear} className="h-8 w-8 lg:h-9 lg:w-9">
|
<Button variant="outline" size="icon" onClick={handleClear} className="h-8 w-8 lg:h-9 lg:w-9" title={t.clear}>
|
||||||
<Trash2 className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
<Trash2 className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
15
components/LocaleProvider.tsx
Normal file
15
components/LocaleProvider.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import useStore from "@/lib/store";
|
||||||
|
|
||||||
|
export default function LocaleProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const language = useStore((state) => state.language);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.documentElement.lang = language;
|
||||||
|
document.documentElement.dir = language === "he" ? "rtl" : "ltr";
|
||||||
|
}, [language]);
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
578
components/MarketResearcher.tsx
Normal file
578
components/MarketResearcher.tsx
Normal file
@@ -0,0 +1,578 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import useStore from "@/lib/store";
|
||||||
|
import { translations } from "@/lib/i18n/translations";
|
||||||
|
import modelAdapter from "@/lib/services/adapter-instance";
|
||||||
|
import { Search, Globe, Plus, Trash2, ShieldAlert, BarChart3, TrendingUp, Target, Rocket, Lightbulb, CheckCircle2, AlertCircle, Loader2, X, ExternalLink } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const MarketResearcher = () => {
|
||||||
|
const { language, selectedProvider, selectedModels, apiKeys, setMarketResearchResult, marketResearchResult } = useStore();
|
||||||
|
const t = translations[language].marketResearch;
|
||||||
|
const common = translations[language].common;
|
||||||
|
|
||||||
|
const [websiteUrl, setWebsiteUrl] = useState("");
|
||||||
|
const [additionalUrls, setAdditionalUrls] = useState<string[]>([""]);
|
||||||
|
const [competitorUrls, setCompetitorUrls] = useState<string[]>(["", "", ""]);
|
||||||
|
const [productMapping, setProductMapping] = useState("");
|
||||||
|
const [specialInstructions, setSpecialInstructions] = useState("");
|
||||||
|
|
||||||
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
|
const [progress, setProgress] = useState(0);
|
||||||
|
const [thoughtIndex, setThoughtIndex] = useState(0);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const selectedModel = selectedModels[selectedProvider];
|
||||||
|
|
||||||
|
const handleAddUrl = () => setAdditionalUrls([...additionalUrls, ""]);
|
||||||
|
const handleRemoveUrl = (index: number) => {
|
||||||
|
const newUrls = [...additionalUrls];
|
||||||
|
newUrls.splice(index, 1);
|
||||||
|
setAdditionalUrls(newUrls);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddCompetitor = () => {
|
||||||
|
if (competitorUrls.length < 10) {
|
||||||
|
setCompetitorUrls([...competitorUrls, ""]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleRemoveCompetitor = (index: number) => {
|
||||||
|
const newUrls = [...competitorUrls];
|
||||||
|
newUrls.splice(index, 1);
|
||||||
|
setCompetitorUrls(newUrls);
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateUrls = () => {
|
||||||
|
const urlRegex = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/;
|
||||||
|
if (!websiteUrl || !urlRegex.test(websiteUrl)) return t.invalidPrimaryUrl;
|
||||||
|
|
||||||
|
const validCompetitors = competitorUrls.filter(url => url.trim().length > 0);
|
||||||
|
if (validCompetitors.length < 2) return t.minCompetitors;
|
||||||
|
|
||||||
|
for (const url of validCompetitors) {
|
||||||
|
if (!urlRegex.test(url)) return `${t.invalidCompetitorUrl}: ${url}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let interval: NodeJS.Timeout;
|
||||||
|
if (isProcessing) {
|
||||||
|
setProgress(0);
|
||||||
|
setThoughtIndex(0);
|
||||||
|
interval = setInterval(() => {
|
||||||
|
setProgress(prev => {
|
||||||
|
if (prev >= 95) return prev;
|
||||||
|
return prev + (prev < 30 ? 2 : prev < 70 ? 1 : 0.5);
|
||||||
|
});
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
const thoughtInterval = setInterval(() => {
|
||||||
|
setThoughtIndex(prev => (prev < (t.thoughts?.length || 0) - 1 ? prev + 1 : prev));
|
||||||
|
}, 4000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
clearInterval(thoughtInterval);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
setProgress(0);
|
||||||
|
}
|
||||||
|
}, [isProcessing, t.thoughts]);
|
||||||
|
|
||||||
|
const handleStartResearch = async () => {
|
||||||
|
const validationError = validateUrls();
|
||||||
|
if (validationError) {
|
||||||
|
setError(validationError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiKey = apiKeys[selectedProvider];
|
||||||
|
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
|
||||||
|
|
||||||
|
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
|
||||||
|
setError(`${common.configApiKey}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsProcessing(true);
|
||||||
|
setError(null);
|
||||||
|
setMarketResearchResult(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const filteredCompetitors = competitorUrls.filter(u => u.trim() !== "");
|
||||||
|
const filteredAddUrls = additionalUrls.filter(u => u.trim() !== "");
|
||||||
|
|
||||||
|
const result = await modelAdapter.generateMarketResearch({
|
||||||
|
websiteUrl,
|
||||||
|
additionalUrls: filteredAddUrls,
|
||||||
|
competitors: filteredCompetitors,
|
||||||
|
productMapping,
|
||||||
|
specialInstructions
|
||||||
|
}, selectedProvider, selectedModel);
|
||||||
|
|
||||||
|
if (result.success && result.data) {
|
||||||
|
setProgress(100);
|
||||||
|
try {
|
||||||
|
const cleanJson = result.data.replace(/```json\s*([\s\S]*?)\s*```/i, '$1').trim();
|
||||||
|
const parsed = JSON.parse(cleanJson);
|
||||||
|
setMarketResearchResult({
|
||||||
|
...parsed,
|
||||||
|
id: Math.random().toString(36).substr(2, 9),
|
||||||
|
websiteUrl,
|
||||||
|
additionalUrls: filteredAddUrls,
|
||||||
|
competitors: filteredCompetitors,
|
||||||
|
productMapping: [{ productName: productMapping || t.mainProduct, features: [] }],
|
||||||
|
generatedAt: new Date(),
|
||||||
|
rawContent: result.data
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to parse market research JSON:", e);
|
||||||
|
setError(t.parseError);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setError(result.error || t.researchFailed);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : t.unexpectedError);
|
||||||
|
} finally {
|
||||||
|
setIsProcessing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderPriceMatrix = () => {
|
||||||
|
if (!marketResearchResult?.priceComparisonMatrix) return null;
|
||||||
|
return (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-sm text-left">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b bg-slate-50/50">
|
||||||
|
<th className="px-4 py-3 font-black text-slate-900 uppercase tracking-wider text-[10px]">{t.product}</th>
|
||||||
|
<th className="px-4 py-3 font-black text-indigo-600 uppercase tracking-wider text-[10px]">{t.yourPrice}</th>
|
||||||
|
{marketResearchResult.competitors.map((comp, i) => (
|
||||||
|
<th key={i} className="px-4 py-3 font-black text-slate-500 uppercase tracking-wider text-[10px]">{comp}</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y">
|
||||||
|
{marketResearchResult.priceComparisonMatrix.map((item, i) => (
|
||||||
|
<tr key={i} className="hover:bg-slate-50/30 transition-colors">
|
||||||
|
<td className="px-4 py-4 font-bold text-slate-900">{item.product}</td>
|
||||||
|
<td className="px-4 py-4 font-black text-indigo-600">{item.userPrice}</td>
|
||||||
|
{marketResearchResult.competitors.map((comp) => {
|
||||||
|
const compPrice = item.competitorPrices.find(cp => cp.competitor === comp || comp.includes(cp.competitor));
|
||||||
|
return (
|
||||||
|
<td key={comp} className="px-4 py-4">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="font-medium text-slate-600">{compPrice ? compPrice.price : t.notAvailable}</span>
|
||||||
|
{compPrice?.url && (
|
||||||
|
<a
|
||||||
|
href={compPrice.url.startsWith('http') ? compPrice.url : `https://${compPrice.url}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center gap-1 text-[10px] text-indigo-500 hover:text-indigo-700 font-bold transition-colors group/link"
|
||||||
|
>
|
||||||
|
<ExternalLink className="h-2.5 w-2.5" />
|
||||||
|
{t.viewProduct}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderFeatureTable = () => {
|
||||||
|
if (!marketResearchResult?.featureComparisonTable) return null;
|
||||||
|
return (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-sm text-left">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b bg-slate-50/50">
|
||||||
|
<th className="px-4 py-3 font-black text-slate-900 uppercase tracking-wider text-[10px]">{t.feature}</th>
|
||||||
|
<th className="px-4 py-3 font-black text-indigo-600 uppercase tracking-wider text-[10px]">{t.you}</th>
|
||||||
|
{marketResearchResult.competitors.map((comp, i) => (
|
||||||
|
<th key={i} className="px-4 py-3 font-black text-slate-500 uppercase tracking-wider text-[10px]">{comp}</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y">
|
||||||
|
{marketResearchResult.featureComparisonTable.map((item, i) => (
|
||||||
|
<tr key={i} className="hover:bg-slate-50/30 transition-colors">
|
||||||
|
<td className="px-4 py-4 font-bold text-slate-900">{item.feature}</td>
|
||||||
|
<td className="px-4 py-4">
|
||||||
|
{typeof item.userStatus === 'boolean' ? (
|
||||||
|
item.userStatus ? <CheckCircle2 className="h-4 w-4 text-emerald-500" /> : <X className="h-4 w-4 text-slate-300" />
|
||||||
|
) : <span className="text-xs font-semibold">{item.userStatus}</span>}
|
||||||
|
</td>
|
||||||
|
{marketResearchResult.competitors.map((comp) => {
|
||||||
|
const compStatus = item.competitorStatus.find(cs => cs.competitor === comp || comp.includes(cs.competitor));
|
||||||
|
return (
|
||||||
|
<td key={comp} className="px-4 py-4">
|
||||||
|
{compStatus ? (
|
||||||
|
typeof compStatus.status === 'boolean' ? (
|
||||||
|
compStatus.status ? <CheckCircle2 className="h-4 w-4 text-emerald-500" /> : <X className="h-4 w-4 text-slate-300" />
|
||||||
|
) : <span className="text-xs font-medium text-slate-600">{compStatus.status}</span>
|
||||||
|
) : t.notAvailable}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700">
|
||||||
|
{/* Header Section */}
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="p-2.5 rounded-2xl bg-gradient-to-br from-indigo-500 to-violet-600 text-white shadow-lg shadow-indigo-200">
|
||||||
|
<Search className="h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-3xl font-black tracking-tight text-slate-900">{t.title}</h2>
|
||||||
|
</div>
|
||||||
|
<p className="text-slate-500 font-medium ml-1.5">{t.description}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 xl:grid-cols-12 gap-8 items-start">
|
||||||
|
{/* Configuration Panel */}
|
||||||
|
<div className="xl:col-span-5 space-y-6">
|
||||||
|
<Card className="border-slate-200/60 shadow-xl shadow-slate-200/40 overflow-hidden bg-white/80 backdrop-blur-md">
|
||||||
|
<CardHeader className="bg-slate-50/50 border-b p-5">
|
||||||
|
<CardTitle className="text-sm font-black uppercase tracking-widest text-slate-500 flex items-center gap-2">
|
||||||
|
<Globe className="h-4 w-4" /> {t.companyProfile}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-6 space-y-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs font-black uppercase tracking-widest text-slate-600">{t.websiteUrl}</label>
|
||||||
|
<Input
|
||||||
|
placeholder={t.websitePlaceholder}
|
||||||
|
value={websiteUrl}
|
||||||
|
onChange={(e) => setWebsiteUrl(e.target.value)}
|
||||||
|
className="bg-slate-50 border-slate-200 focus:bg-white transition-all font-medium"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<label className="text-xs font-black uppercase tracking-widest text-slate-600 flex justify-between items-center">
|
||||||
|
{t.additionalUrls}
|
||||||
|
<Button variant="ghost" size="sm" onClick={handleAddUrl} className="h-6 px-2 hover:bg-slate-100 text-[10px] font-black uppercase">
|
||||||
|
<Plus className="h-3 w-3 mr-1" /> {t.addUrl}
|
||||||
|
</Button>
|
||||||
|
</label>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{additionalUrls.map((url, i) => (
|
||||||
|
<div key={i} className="flex gap-2 group">
|
||||||
|
<Input
|
||||||
|
placeholder={t.urlPlaceholder}
|
||||||
|
value={url}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newUrls = [...additionalUrls];
|
||||||
|
newUrls[i] = e.target.value;
|
||||||
|
setAdditionalUrls(newUrls);
|
||||||
|
}}
|
||||||
|
className="bg-slate-50/50 border-slate-200 focus:bg-white transition-all text-xs"
|
||||||
|
/>
|
||||||
|
<Button variant="ghost" size="icon" onClick={() => handleRemoveUrl(i)} className="h-9 w-9 shrink-0 text-slate-400 hover:text-rose-500">
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="border-slate-200/60 shadow-xl shadow-slate-200/40 overflow-hidden bg-white/80 backdrop-blur-md">
|
||||||
|
<CardHeader className="bg-slate-50/50 border-b p-5">
|
||||||
|
<CardTitle className="text-sm font-black uppercase tracking-widest text-slate-500 flex items-center gap-2">
|
||||||
|
<ShieldAlert className="h-4 w-4" /> {t.competitiveIntel}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-6 space-y-6">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<label className="text-xs font-black uppercase tracking-widest text-slate-600 flex justify-between items-center">
|
||||||
|
{t.competitors}
|
||||||
|
<Button variant="ghost" size="sm" onClick={handleAddCompetitor} disabled={competitorUrls.length >= 10} className="h-6 px-2 hover:bg-slate-100 text-[10px] font-black uppercase">
|
||||||
|
<Plus className="h-3 w-3 mr-1" /> {t.addCompetitor}
|
||||||
|
</Button>
|
||||||
|
</label>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{competitorUrls.map((url, i) => (
|
||||||
|
<div key={i} className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
placeholder={t.competitorPlaceholder}
|
||||||
|
value={url}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newUrls = [...competitorUrls];
|
||||||
|
newUrls[i] = e.target.value;
|
||||||
|
setCompetitorUrls(newUrls);
|
||||||
|
}}
|
||||||
|
className="bg-slate-50/50 border-slate-200 focus:bg-white transition-all text-xs"
|
||||||
|
/>
|
||||||
|
{competitorUrls.length > 2 && (
|
||||||
|
<Button variant="ghost" size="icon" onClick={() => handleRemoveCompetitor(i)} className="h-9 w-9 shrink-0 text-slate-400 hover:text-rose-500">
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs font-black uppercase tracking-widest text-slate-600">{t.productMapping}</label>
|
||||||
|
<Textarea
|
||||||
|
placeholder={t.mappingPlaceholder}
|
||||||
|
value={productMapping}
|
||||||
|
onChange={(e) => setProductMapping(e.target.value)}
|
||||||
|
className="min-h-[80px] bg-slate-50/50 border-slate-200 focus:bg-white transition-all text-sm"
|
||||||
|
/>
|
||||||
|
<p className="text-[10px] text-slate-400 font-medium italic">{t.mappingDesc}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs font-black uppercase tracking-widest text-slate-600">{t.parameters}</label>
|
||||||
|
<Textarea
|
||||||
|
placeholder={t.parametersPlaceholder}
|
||||||
|
value={specialInstructions}
|
||||||
|
onChange={(e) => setSpecialInstructions(e.target.value)}
|
||||||
|
className="min-h-[80px] bg-slate-50/50 border-slate-200 focus:bg-white transition-all text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isProcessing && (
|
||||||
|
<div className="space-y-4 animate-in fade-in slide-in-from-top-4 duration-500">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between items-center text-[10px] font-black uppercase tracking-widest">
|
||||||
|
<span className="text-indigo-600 flex items-center gap-1.5">
|
||||||
|
<Loader2 className="h-3 w-3 animate-spin" /> {t.analysisInProgress}
|
||||||
|
</span>
|
||||||
|
<span className="text-slate-400">{Math.round(progress)}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-2 w-full bg-slate-100 rounded-full overflow-hidden border border-slate-200/50">
|
||||||
|
<div
|
||||||
|
className="h-full bg-gradient-to-r from-indigo-500 via-violet-500 to-indigo-500 transition-all duration-300 ease-out"
|
||||||
|
style={{ width: `${progress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 rounded-xl bg-slate-900 text-white shadow-lg relative overflow-hidden group">
|
||||||
|
<div className="absolute top-0 right-0 p-2 opacity-20">
|
||||||
|
<Rocket className="h-4 w-4 text-indigo-400 group-hover:block hidden" />
|
||||||
|
</div>
|
||||||
|
<h4 className="text-[9px] font-black uppercase tracking-[0.2em] text-indigo-400 mb-2 flex items-center gap-1.5">
|
||||||
|
<span className="h-1 w-1 bg-indigo-400 rounded-full animate-pulse" /> {t.aiThoughts}
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs font-bold leading-relaxed italic animate-in fade-in slide-in-from-left-2 duration-700">
|
||||||
|
"{t.thoughts?.[thoughtIndex] || t.researching}"
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="p-3 rounded-xl bg-rose-50 border border-rose-100 flex items-start gap-3 animate-in fade-in slide-in-from-top-2">
|
||||||
|
<AlertCircle className="h-4 w-4 text-rose-500 shrink-0 mt-0.5" />
|
||||||
|
<p className="text-xs font-bold text-rose-600">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleStartResearch}
|
||||||
|
disabled={isProcessing}
|
||||||
|
className="w-full h-12 bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-700 hover:to-violet-700 text-white font-black uppercase tracking-widest shadow-lg shadow-indigo-100 transition-all active:scale-95 disabled:opacity-70"
|
||||||
|
>
|
||||||
|
{isProcessing ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
||||||
|
{t.researching}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Search className="mr-2 h-5 w-5" />
|
||||||
|
{t.generate}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Results Panel */}
|
||||||
|
<div className="xl:col-span-7">
|
||||||
|
{marketResearchResult ? (
|
||||||
|
<Card className="border-slate-200/60 shadow-2xl shadow-slate-200/50 overflow-hidden bg-white group min-h-[600px]">
|
||||||
|
<CardHeader className="bg-slate-900 text-white p-6 relative overflow-hidden">
|
||||||
|
<div className="absolute top-0 right-0 w-64 h-64 bg-indigo-500/20 rounded-full blur-3xl -mr-32 -mt-32" />
|
||||||
|
<div className="relative z-10 flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<Badge variant="outline" className="mb-2 border-indigo-400/50 text-indigo-300 font-black uppercase tracking-widest text-[10px]">{t.marketIntelReport}</Badge>
|
||||||
|
<CardTitle className="text-2xl font-black tracking-tight">{marketResearchResult.websiteUrl}</CardTitle>
|
||||||
|
<CardDescription className="text-indigo-200 font-medium">{t.generatedOn} {marketResearchResult.generatedAt.toLocaleDateString()}</CardDescription>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 rounded-2xl bg-white/10 backdrop-blur-md border border-white/20">
|
||||||
|
<BarChart3 className="h-6 w-6 text-indigo-300" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<Tabs defaultValue="summary" className="w-full">
|
||||||
|
<TabsList className="w-full h-14 bg-slate-50 border-b rounded-none px-6 justify-start gap-4">
|
||||||
|
<TabsTrigger value="summary" className="data-[state=active]:bg-transparent data-[state=active]:text-indigo-600 data-[state=active]:border-b-2 data-[state=active]:border-indigo-600 rounded-none h-full font-black uppercase tracking-widest text-[10px] px-0">{t.summary}</TabsTrigger>
|
||||||
|
<TabsTrigger value="pricing" className="data-[state=active]:bg-transparent data-[state=active]:text-indigo-600 data-[state=active]:border-b-2 data-[state=active]:border-indigo-600 rounded-none h-full font-black uppercase tracking-widest text-[10px] px-0">{t.pricing}</TabsTrigger>
|
||||||
|
<TabsTrigger value="features" className="data-[state=active]:bg-transparent data-[state=active]:text-indigo-600 data-[state=active]:border-b-2 data-[state=active]:border-indigo-600 rounded-none h-full font-black uppercase tracking-widest text-[10px] px-0">{t.features}</TabsTrigger>
|
||||||
|
<TabsTrigger value="positioning" className="data-[state=active]:bg-transparent data-[state=active]:text-indigo-600 data-[state=active]:border-b-2 data-[state=active]:border-indigo-600 rounded-none h-full font-black uppercase tracking-widest text-[10px] px-0">{t.positioning}</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<div className="p-6">
|
||||||
|
<TabsContent value="summary" className="m-0 focus-visible:ring-0">
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="p-5 rounded-2xl bg-indigo-50 border border-indigo-100">
|
||||||
|
<h3 className="text-sm font-black text-indigo-900 uppercase tracking-widest mb-3 flex items-center gap-2">
|
||||||
|
<TrendingUp className="h-4 w-4" /> {t.executiveSummary}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-indigo-900/80 leading-relaxed font-medium">
|
||||||
|
{marketResearchResult.executiveSummary}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="p-5 rounded-2xl border bg-emerald-50/30 border-emerald-100">
|
||||||
|
<h4 className="text-[10px] font-black uppercase tracking-widest text-emerald-600 mb-3 flex items-center gap-2">
|
||||||
|
<CheckCircle2 className="h-4 w-4" /> {t.strategicAdvantages}
|
||||||
|
</h4>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{marketResearchResult.competitiveAnalysis.advantages.map((adv, i) => (
|
||||||
|
<li key={i} className="text-xs font-bold text-slate-700 flex gap-2">
|
||||||
|
<span className="text-emerald-500">•</span> {adv}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="p-5 rounded-2xl border bg-rose-50/30 border-rose-100">
|
||||||
|
<h4 className="text-[10px] font-black uppercase tracking-widest text-rose-600 mb-3 flex items-center gap-2">
|
||||||
|
<AlertCircle className="h-4 w-4" /> {t.identifiedGaps}
|
||||||
|
</h4>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{marketResearchResult.competitiveAnalysis.disadvantages.map((dis, i) => (
|
||||||
|
<li key={i} className="text-xs font-bold text-slate-700 flex gap-2">
|
||||||
|
<span className="text-rose-500">•</span> {dis}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-5 rounded-2xl border bg-amber-50/30 border-amber-100">
|
||||||
|
<h4 className="text-[10px] font-black uppercase tracking-widest text-amber-600 mb-3 flex items-center gap-2">
|
||||||
|
<Lightbulb className="h-4 w-4" /> {t.recommendations}
|
||||||
|
</h4>
|
||||||
|
<ul className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
{marketResearchResult.recommendations.map((rec, i) => (
|
||||||
|
<li key={i} className="text-xs font-bold text-slate-700 p-3 bg-white border border-amber-100 rounded-xl shadow-sm flex items-center gap-3">
|
||||||
|
<span className="h-6 w-6 rounded-full bg-amber-100 text-amber-600 flex items-center justify-center text-[10px] shrink-0">{i + 1}</span>
|
||||||
|
{rec}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="pricing" className="m-0 focus-visible:ring-0">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-lg font-black text-slate-900 tracking-tight">{t.priceMatrix}</h3>
|
||||||
|
<Badge className="bg-slate-900 text-[10px] font-black uppercase">{t.liveMarketData}</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-xl border border-slate-200 overflow-hidden">
|
||||||
|
{renderPriceMatrix()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="features" className="m-0 focus-visible:ring-0">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-lg font-black text-slate-900 tracking-tight">{t.featureBenchmarking}</h3>
|
||||||
|
<Badge className="bg-indigo-600 text-[10px] font-black uppercase">{t.functionalAudit}</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-xl border border-slate-200 overflow-hidden">
|
||||||
|
{renderFeatureTable()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="positioning" className="m-0 focus-visible:ring-0">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="p-5 rounded-2xl bg-slate-900 text-white shadow-xl">
|
||||||
|
<h4 className="text-[10px] font-black uppercase tracking-widest text-indigo-400 mb-3 flex items-center gap-2">
|
||||||
|
<Target className="h-4 w-4" /> {t.marketLandscape}
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs font-medium leading-relaxed opacity-90">
|
||||||
|
{marketResearchResult.marketPositioning.landscape}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="p-5 rounded-2xl bg-indigo-600 text-white shadow-xl">
|
||||||
|
<h4 className="text-[10px] font-black uppercase tracking-widest text-indigo-200 mb-3 flex items-center gap-2">
|
||||||
|
<Rocket className="h-4 w-4" /> {t.segmentationStrategy}
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs font-medium leading-relaxed font-bold">
|
||||||
|
{marketResearchResult.marketPositioning.segmentation}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="md:col-span-2 p-5 rounded-2xl border bg-slate-50 italic">
|
||||||
|
<h4 className="text-[10px] font-black uppercase tracking-widest text-slate-500 mb-2">{t.methodology}</h4>
|
||||||
|
<p className="text-[10px] font-medium text-slate-400">
|
||||||
|
{marketResearchResult.methodology}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</div>
|
||||||
|
</Tabs>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<Card className="border-dashed border-2 border-slate-200 bg-slate-50/50 flex flex-col items-center justify-center p-12 min-h-[600px] text-center group">
|
||||||
|
<div className="h-20 w-20 rounded-3xl bg-white border border-slate-100 flex items-center justify-center mb-6 shadow-sm group-hover:scale-110 group-hover:rotate-3 transition-all duration-500">
|
||||||
|
<BarChart3 className="h-10 w-10 text-slate-300 group-hover:text-indigo-500 transition-colors" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-black text-slate-400 tracking-tight group-hover:text-slate-600 transition-colors">{t.awaitingParameters}</h3>
|
||||||
|
<p className="text-sm text-slate-400 font-medium max-w-[280px] mt-2 group-hover:text-slate-500 transition-colors">
|
||||||
|
{t.emptyState}
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MarketResearcher;
|
||||||
@@ -8,6 +8,7 @@ import useStore from "@/lib/store";
|
|||||||
import modelAdapter from "@/lib/services/adapter-instance";
|
import modelAdapter from "@/lib/services/adapter-instance";
|
||||||
import { FileText, Copy, Loader2, CheckCircle2, ChevronDown, ChevronUp, Settings } from "lucide-react";
|
import { FileText, Copy, Loader2, CheckCircle2, ChevronDown, ChevronUp, Settings } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { translations } from "@/lib/i18n/translations";
|
||||||
|
|
||||||
export default function PRDGenerator() {
|
export default function PRDGenerator() {
|
||||||
const {
|
const {
|
||||||
@@ -19,6 +20,7 @@ export default function PRDGenerator() {
|
|||||||
apiKeys,
|
apiKeys,
|
||||||
isProcessing,
|
isProcessing,
|
||||||
error,
|
error,
|
||||||
|
language,
|
||||||
setCurrentPrompt,
|
setCurrentPrompt,
|
||||||
setSelectedProvider,
|
setSelectedProvider,
|
||||||
setPRD,
|
setPRD,
|
||||||
@@ -28,6 +30,9 @@ export default function PRDGenerator() {
|
|||||||
setSelectedModel,
|
setSelectedModel,
|
||||||
} = useStore();
|
} = useStore();
|
||||||
|
|
||||||
|
const t = translations[language].prdGenerator;
|
||||||
|
const common = translations[language].common;
|
||||||
|
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const [expandedSections, setExpandedSections] = useState<string[]>([]);
|
const [expandedSections, setExpandedSections] = useState<string[]>([]);
|
||||||
|
|
||||||
@@ -73,7 +78,7 @@ export default function PRDGenerator() {
|
|||||||
|
|
||||||
const handleGenerate = async () => {
|
const handleGenerate = async () => {
|
||||||
if (!currentPrompt.trim()) {
|
if (!currentPrompt.trim()) {
|
||||||
setError("Please enter an idea to generate PRD");
|
setError(t.enterIdeaError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +86,7 @@ export default function PRDGenerator() {
|
|||||||
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
|
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
|
||||||
|
|
||||||
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
|
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
|
||||||
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
|
setError(`${common.error}: ${common.configApiKey}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,11 +117,11 @@ export default function PRDGenerator() {
|
|||||||
setPRD(newPRD);
|
setPRD(newPRD);
|
||||||
} else {
|
} else {
|
||||||
console.error("[PRDGenerator] Generation failed:", result.error);
|
console.error("[PRDGenerator] Generation failed:", result.error);
|
||||||
setError(result.error || "Failed to generate PRD");
|
setError(result.error || t.errorGenerate);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[PRDGenerator] Generation error:", err);
|
console.error("[PRDGenerator] Generation error:", err);
|
||||||
setError(err instanceof Error ? err.message : "An error occurred");
|
setError(err instanceof Error ? err.message : t.errorGenerate);
|
||||||
} finally {
|
} finally {
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
}
|
}
|
||||||
@@ -131,29 +136,29 @@ export default function PRDGenerator() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const sections = [
|
const sections = [
|
||||||
{ id: "overview", title: "Overview & Objectives" },
|
{ id: "overview", title: t.sections.overview },
|
||||||
{ id: "personas", title: "User Personas & Use Cases" },
|
{ id: "personas", title: t.sections.personas },
|
||||||
{ id: "functional", title: "Functional Requirements" },
|
{ id: "functional", title: t.sections.functional },
|
||||||
{ id: "nonfunctional", title: "Non-functional Requirements" },
|
{ id: "nonfunctional", title: t.sections.nonfunctional },
|
||||||
{ id: "architecture", title: "Technical Architecture" },
|
{ id: "architecture", title: t.sections.architecture },
|
||||||
{ id: "metrics", title: "Success Metrics" },
|
{ id: "metrics", title: t.sections.metrics },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2">
|
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2 text-start">
|
||||||
<Card className="h-fit">
|
<Card className="h-fit">
|
||||||
<CardHeader className="p-4 lg:p-6">
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||||
<FileText className="h-4 w-4 lg:h-5 lg:w-5" />
|
<FileText className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||||
PRD Generator
|
{t.title}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-xs lg:text-sm">
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
Generate comprehensive Product Requirements Document from your idea
|
{t.description}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="text-xs lg:text-sm font-medium">AI Provider</label>
|
<label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
|
||||||
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
||||||
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
||||||
<Button
|
<Button
|
||||||
@@ -169,8 +174,8 @@ export default function PRDGenerator() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="text-xs lg:text-sm font-medium">Model</label>
|
<label className="text-xs lg:text-sm font-medium">{common.model}</label>
|
||||||
<select
|
<select
|
||||||
value={selectedModel}
|
value={selectedModel}
|
||||||
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
||||||
@@ -185,12 +190,11 @@ export default function PRDGenerator() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-xs lg:text-sm font-medium">Your Idea</label>
|
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="e.g., A task management app with real-time collaboration features"
|
placeholder={t.placeholder}
|
||||||
value={currentPrompt}
|
value={currentPrompt}
|
||||||
onChange={(e) => setCurrentPrompt(e.target.value)}
|
onChange={(e) => setCurrentPrompt(e.target.value)}
|
||||||
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm"
|
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm lg:text-base p-3 lg:p-4"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -200,7 +204,7 @@ export default function PRDGenerator() {
|
|||||||
{!apiKeys[selectedProvider] && (
|
{!apiKeys[selectedProvider] && (
|
||||||
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
||||||
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
<span className="text-[10px] lg:text-xs">Configure API key in Settings</span>
|
<span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -210,12 +214,12 @@ export default function PRDGenerator() {
|
|||||||
{isProcessing ? (
|
{isProcessing ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
|
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
|
||||||
Generating PRD...
|
{common.generating}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<FileText className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
<FileText className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
Generate PRD
|
{common.generate}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -223,11 +227,11 @@ export default function PRDGenerator() {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className={cn(!prd && "opacity-50")}>
|
<Card className={cn(!prd && "opacity-50")}>
|
||||||
<CardHeader className="p-4 lg:p-6">
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
||||||
Generated PRD
|
{t.generatedTitle}
|
||||||
</span>
|
</span>
|
||||||
{prd && (
|
{prd && (
|
||||||
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
||||||
@@ -240,7 +244,7 @@ export default function PRDGenerator() {
|
|||||||
)}
|
)}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-xs lg:text-sm">
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
Structured requirements document ready for development
|
{t.generatedDesc}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
@@ -269,7 +273,7 @@ export default function PRDGenerator() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-[200px] lg:h-[300px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground">
|
<div className="flex h-[200px] lg:h-[300px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground">
|
||||||
PRD will appear here
|
{t.emptyState}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import useStore from "@/lib/store";
|
|||||||
import modelAdapter from "@/lib/services/adapter-instance";
|
import modelAdapter from "@/lib/services/adapter-instance";
|
||||||
import { Sparkles, Copy, RefreshCw, Loader2, CheckCircle2, Settings } from "lucide-react";
|
import { Sparkles, Copy, RefreshCw, Loader2, CheckCircle2, Settings } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { translations } from "@/lib/i18n/translations";
|
||||||
|
|
||||||
export default function PromptEnhancer() {
|
export default function PromptEnhancer() {
|
||||||
const {
|
const {
|
||||||
|
language,
|
||||||
currentPrompt,
|
currentPrompt,
|
||||||
enhancedPrompt,
|
enhancedPrompt,
|
||||||
selectedProvider,
|
selectedProvider,
|
||||||
@@ -28,6 +30,9 @@ export default function PromptEnhancer() {
|
|||||||
setSelectedModel,
|
setSelectedModel,
|
||||||
} = useStore();
|
} = useStore();
|
||||||
|
|
||||||
|
const t = translations[language].promptEnhancer;
|
||||||
|
const common = translations[language].common;
|
||||||
|
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
const selectedModel = selectedModels[selectedProvider];
|
const selectedModel = selectedModels[selectedProvider];
|
||||||
@@ -66,7 +71,7 @@ export default function PromptEnhancer() {
|
|||||||
|
|
||||||
const handleEnhance = async () => {
|
const handleEnhance = async () => {
|
||||||
if (!currentPrompt.trim()) {
|
if (!currentPrompt.trim()) {
|
||||||
setError("Please enter a prompt to enhance");
|
setError(t.enterPromptError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +79,7 @@ export default function PromptEnhancer() {
|
|||||||
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
|
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
|
||||||
|
|
||||||
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
|
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
|
||||||
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
|
setError(`${common.error}: ${common.configApiKey}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,11 +97,11 @@ export default function PromptEnhancer() {
|
|||||||
setEnhancedPrompt(result.data);
|
setEnhancedPrompt(result.data);
|
||||||
} else {
|
} else {
|
||||||
console.error("[PromptEnhancer] Enhancement failed:", result.error);
|
console.error("[PromptEnhancer] Enhancement failed:", result.error);
|
||||||
setError(result.error || "Failed to enhance prompt");
|
setError(result.error || t.errorEnhance);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[PromptEnhancer] Enhancement error:", err);
|
console.error("[PromptEnhancer] Enhancement error:", err);
|
||||||
setError(err instanceof Error ? err.message : "An error occurred");
|
setError(err instanceof Error ? err.message : t.errorEnhance);
|
||||||
} finally {
|
} finally {
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
}
|
}
|
||||||
@@ -117,20 +122,20 @@ export default function PromptEnhancer() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2">
|
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2 text-start">
|
||||||
<Card className="h-fit">
|
<Card className="h-fit">
|
||||||
<CardHeader className="p-4 lg:p-6">
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||||
<Sparkles className="h-4 w-4 lg:h-5 lg:w-5" />
|
<Sparkles className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||||
Input Prompt
|
{t.title}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-xs lg:text-sm">
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
Enter your prompt and we'll enhance it for AI coding agents
|
{t.description}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="text-xs lg:text-sm font-medium">AI Provider</label>
|
<label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
|
||||||
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
||||||
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
||||||
<Button
|
<Button
|
||||||
@@ -149,8 +154,8 @@ export default function PromptEnhancer() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="text-xs lg:text-sm font-medium">Model</label>
|
<label className="text-xs lg:text-sm font-medium">{common.model}</label>
|
||||||
<select
|
<select
|
||||||
value={selectedModel}
|
value={selectedModel}
|
||||||
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
||||||
@@ -164,13 +169,13 @@ export default function PromptEnhancer() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="text-xs lg:text-sm font-medium">Your Prompt</label>
|
<label className="text-xs lg:text-sm font-medium">{t.inputLabel}</label>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="e.g., Create a user authentication system with JWT tokens"
|
placeholder={t.placeholder}
|
||||||
value={currentPrompt}
|
value={currentPrompt}
|
||||||
onChange={(e) => setCurrentPrompt(e.target.value)}
|
onChange={(e) => setCurrentPrompt(e.target.value)}
|
||||||
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm"
|
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm lg:text-base p-3 lg:p-4"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -180,7 +185,7 @@ export default function PromptEnhancer() {
|
|||||||
{!apiKeys[selectedProvider] && (
|
{!apiKeys[selectedProvider] && (
|
||||||
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
||||||
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
<span className="text-[10px] lg:text-xs">Configure API key in Settings</span>
|
<span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -191,29 +196,29 @@ export default function PromptEnhancer() {
|
|||||||
{isProcessing ? (
|
{isProcessing ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
|
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
|
||||||
Enhancing...
|
{common.generating}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Sparkles className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
<Sparkles className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
Enhance Prompt
|
{t.title}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" onClick={handleClear} disabled={isProcessing} className="h-9 lg:h-10 text-xs lg:text-sm px-3">
|
<Button variant="outline" onClick={handleClear} disabled={isProcessing} className="h-9 lg:h-10 text-xs lg:text-sm px-3">
|
||||||
<RefreshCw className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
<RefreshCw className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
<span className="hidden sm:inline">Clear</span>
|
<span className="hidden sm:inline">{t.clear}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className={cn(!enhancedPrompt && "opacity-50")}>
|
<Card className={cn("flex flex-col", !enhancedPrompt && "opacity-50")}>
|
||||||
<CardHeader className="p-4 lg:p-6">
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
||||||
Enhanced Prompt
|
{t.enhancedTitle}
|
||||||
</span>
|
</span>
|
||||||
{enhancedPrompt && (
|
{enhancedPrompt && (
|
||||||
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
||||||
@@ -226,17 +231,17 @@ export default function PromptEnhancer() {
|
|||||||
)}
|
)}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-xs lg:text-sm">
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
Professional prompt ready for coding agents
|
{t.enhancedDesc}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
{enhancedPrompt ? (
|
{enhancedPrompt ? (
|
||||||
<div className="rounded-md border bg-muted/50 p-3 lg:p-4">
|
<div className="rounded-md border bg-muted/50 p-3 lg:p-4 animate-in fade-in slide-in-from-bottom-2 duration-300">
|
||||||
<pre className="whitespace-pre-wrap text-xs lg:text-sm">{enhancedPrompt}</pre>
|
<pre className="whitespace-pre-wrap text-xs lg:text-sm leading-relaxed">{enhancedPrompt}</pre>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-[150px] lg:h-[200px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground">
|
<div className="flex h-[150px] lg:h-[200px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground italic">
|
||||||
Enhanced prompt will appear here
|
{t.emptyState}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -7,16 +7,19 @@ import { Input } from "@/components/ui/input";
|
|||||||
import useStore from "@/lib/store";
|
import useStore from "@/lib/store";
|
||||||
import modelAdapter from "@/lib/services/adapter-instance";
|
import modelAdapter from "@/lib/services/adapter-instance";
|
||||||
import { Save, Key, Server, Eye, EyeOff } from "lucide-react";
|
import { Save, Key, Server, Eye, EyeOff } from "lucide-react";
|
||||||
|
import { translations } from "@/lib/i18n/translations";
|
||||||
|
|
||||||
export default function SettingsPanel() {
|
export default function SettingsPanel() {
|
||||||
const { apiKeys, setApiKey, selectedProvider, setSelectedProvider, qwenTokens, setQwenTokens } = useStore();
|
const { language, apiKeys, setApiKey, selectedProvider, setSelectedProvider, qwenTokens, setQwenTokens } = useStore();
|
||||||
|
const t = translations[language].settings;
|
||||||
|
const common = translations[language].common;
|
||||||
const [showApiKey, setShowApiKey] = useState<Record<string, boolean>>({});
|
const [showApiKey, setShowApiKey] = useState<Record<string, boolean>>({});
|
||||||
const [isAuthLoading, setIsAuthLoading] = useState(false);
|
const [isAuthLoading, setIsAuthLoading] = useState(false);
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
localStorage.setItem("promptarch-api-keys", JSON.stringify(apiKeys));
|
localStorage.setItem("promptarch-api-keys", JSON.stringify(apiKeys));
|
||||||
alert("API keys saved successfully!");
|
alert(t.keysSaved);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -81,7 +84,7 @@ export default function SettingsPanel() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Qwen OAuth failed", error);
|
console.error("Qwen OAuth failed", error);
|
||||||
window.alert(
|
window.alert(
|
||||||
error instanceof Error ? error.message : "Qwen authentication failed"
|
error instanceof Error ? error.message : t.qwenAuthFailed
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setIsAuthLoading(false);
|
setIsAuthLoading(false);
|
||||||
@@ -95,17 +98,17 @@ export default function SettingsPanel() {
|
|||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-3xl space-y-4 lg:space-y-6">
|
<div className="mx-auto max-w-3xl space-y-4 lg:space-y-6">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="p-4 lg:p-6">
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||||
<Key className="h-4 w-4 lg:h-5 lg:w-5" />
|
<Key className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||||
API Configuration
|
{t.apiKeys}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-xs lg:text-sm">
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
Configure API keys for different AI providers
|
{t.apiKeysDesc}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4 lg:space-y-6 p-4 lg:p-6 pt-0 lg:pt-0">
|
<CardContent className="space-y-4 lg:space-y-6 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="flex items-center gap-2 text-xs lg:text-sm font-medium">
|
<label className="flex items-center gap-2 text-xs lg:text-sm font-medium">
|
||||||
<Server className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
<Server className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
Qwen Code API Key
|
Qwen Code API Key
|
||||||
@@ -113,7 +116,7 @@ export default function SettingsPanel() {
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
type={showApiKey.qwen ? "text" : "password"}
|
type={showApiKey.qwen ? "text" : "password"}
|
||||||
placeholder="Enter your Qwen API key"
|
placeholder={t.enterKey("Qwen")}
|
||||||
value={apiKeys.qwen || ""}
|
value={apiKeys.qwen || ""}
|
||||||
onChange={(e) => handleApiKeyChange("qwen", e.target.value)}
|
onChange={(e) => handleApiKeyChange("qwen", e.target.value)}
|
||||||
className="font-mono text-xs lg:text-sm pr-10"
|
className="font-mono text-xs lg:text-sm pr-10"
|
||||||
@@ -134,7 +137,7 @@ export default function SettingsPanel() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center gap-2 lg:gap-4">
|
<div className="flex flex-col sm:flex-row sm:items-center gap-2 lg:gap-4">
|
||||||
<p className="text-[10px] lg:text-xs text-muted-foreground flex-1">
|
<p className="text-[10px] lg:text-xs text-muted-foreground flex-1">
|
||||||
Get API key from{" "}
|
{t.getApiKey}{" "}
|
||||||
<a
|
<a
|
||||||
href="https://help.aliyun.com/zh/dashscope/"
|
href="https://help.aliyun.com/zh/dashscope/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -152,20 +155,20 @@ export default function SettingsPanel() {
|
|||||||
disabled={isAuthLoading}
|
disabled={isAuthLoading}
|
||||||
>
|
>
|
||||||
{isAuthLoading
|
{isAuthLoading
|
||||||
? "Signing in..."
|
? t.signingIn
|
||||||
: qwenTokens
|
: qwenTokens
|
||||||
? "Logout from Qwen"
|
? t.logoutQwen
|
||||||
: "Login with Qwen (OAuth)"}
|
: t.loginQwen}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{qwenTokens && (
|
{qwenTokens && (
|
||||||
<p className="text-[9px] lg:text-[10px] text-green-600 dark:text-green-400 font-medium">
|
<p className="text-[9px] lg:text-[10px] text-green-600 dark:text-green-400 font-medium">
|
||||||
✓ Authenticated via OAuth (Expires: {new Date(qwenTokens.expiresAt || 0).toLocaleString()})
|
✓ {t.authenticated} ({t.expires}: {new Date(qwenTokens.expiresAt || 0).toLocaleString()})
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="flex items-center gap-2 text-xs lg:text-sm font-medium">
|
<label className="flex items-center gap-2 text-xs lg:text-sm font-medium">
|
||||||
<Server className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
<Server className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
Ollama Cloud API Key
|
Ollama Cloud API Key
|
||||||
@@ -173,7 +176,7 @@ export default function SettingsPanel() {
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
type={showApiKey.ollama ? "text" : "password"}
|
type={showApiKey.ollama ? "text" : "password"}
|
||||||
placeholder="Enter your Ollama API key"
|
placeholder={t.enterKey("Ollama")}
|
||||||
value={apiKeys.ollama || ""}
|
value={apiKeys.ollama || ""}
|
||||||
onChange={(e) => handleApiKeyChange("ollama", e.target.value)}
|
onChange={(e) => handleApiKeyChange("ollama", e.target.value)}
|
||||||
className="font-mono text-xs lg:text-sm pr-10"
|
className="font-mono text-xs lg:text-sm pr-10"
|
||||||
@@ -193,7 +196,7 @@ export default function SettingsPanel() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[10px] lg:text-xs text-muted-foreground">
|
<p className="text-[10px] lg:text-xs text-muted-foreground">
|
||||||
Get API key from{" "}
|
{t.getApiKey}{" "}
|
||||||
<a
|
<a
|
||||||
href="https://ollama.com/cloud"
|
href="https://ollama.com/cloud"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -205,7 +208,7 @@ export default function SettingsPanel() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="flex items-center gap-2 text-xs lg:text-sm font-medium">
|
<label className="flex items-center gap-2 text-xs lg:text-sm font-medium">
|
||||||
<Server className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
<Server className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
Z.AI Plan API Key
|
Z.AI Plan API Key
|
||||||
@@ -213,7 +216,7 @@ export default function SettingsPanel() {
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
type={showApiKey.zai ? "text" : "password"}
|
type={showApiKey.zai ? "text" : "password"}
|
||||||
placeholder="Enter your Z.AI API key"
|
placeholder={t.enterKey("Z.AI")}
|
||||||
value={apiKeys.zai || ""}
|
value={apiKeys.zai || ""}
|
||||||
onChange={(e) => handleApiKeyChange("zai", e.target.value)}
|
onChange={(e) => handleApiKeyChange("zai", e.target.value)}
|
||||||
className="font-mono text-xs lg:text-sm pr-10"
|
className="font-mono text-xs lg:text-sm pr-10"
|
||||||
@@ -233,7 +236,7 @@ export default function SettingsPanel() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[10px] lg:text-xs text-muted-foreground">
|
<p className="text-[10px] lg:text-xs text-muted-foreground">
|
||||||
Get API key from{" "}
|
{t.getApiKey}{" "}
|
||||||
<a
|
<a
|
||||||
href="https://docs.z.ai"
|
href="https://docs.z.ai"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -247,16 +250,16 @@ export default function SettingsPanel() {
|
|||||||
|
|
||||||
<Button onClick={handleSave} className="w-full h-9 lg:h-10 text-xs lg:text-sm">
|
<Button onClick={handleSave} className="w-full h-9 lg:h-10 text-xs lg:text-sm">
|
||||||
<Save className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
<Save className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
Save API Keys
|
{t.saveKeys}
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="p-4 lg:p-6">
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
<CardTitle className="text-base lg:text-lg">Default Provider</CardTitle>
|
<CardTitle className="text-base lg:text-lg">{t.defaultProvider}</CardTitle>
|
||||||
<CardDescription className="text-xs lg:text-sm">
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
Select your preferred AI provider
|
{t.defaultProviderDesc}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
@@ -276,9 +279,9 @@ export default function SettingsPanel() {
|
|||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<h3 className="font-medium capitalize text-sm lg:text-base">{provider}</h3>
|
<h3 className="font-medium capitalize text-sm lg:text-base">{provider}</h3>
|
||||||
<p className="text-[10px] lg:text-sm text-muted-foreground truncate">
|
<p className="text-[10px] lg:text-sm text-muted-foreground truncate">
|
||||||
{provider === "qwen" && "Alibaba DashScope API"}
|
{provider === "qwen" && t.qwenDesc}
|
||||||
{provider === "ollama" && "Ollama Cloud API"}
|
{provider === "ollama" && t.ollamaDesc}
|
||||||
{provider === "zai" && "Z.AI Plan API"}
|
{provider === "zai" && t.zaiDesc}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{selectedProvider === provider && (
|
{selectedProvider === provider && (
|
||||||
@@ -291,16 +294,16 @@ export default function SettingsPanel() {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="p-4 lg:p-6">
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
<CardTitle className="text-base lg:text-lg">Data Privacy</CardTitle>
|
<CardTitle className="text-base lg:text-lg">{t.dataPrivacy}</CardTitle>
|
||||||
<CardDescription className="text-xs lg:text-sm">
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
Your data handling preferences
|
{t.dataPrivacyTitleDesc}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
<div className="rounded-md border bg-muted/30 p-3 lg:p-4">
|
<div className="rounded-md border bg-muted/30 p-3 lg:p-4 text-start">
|
||||||
<p className="text-xs lg:text-sm">
|
<p className="text-xs lg:text-sm">
|
||||||
All API keys are stored locally in your browser. Your prompts are sent directly to the selected AI provider and are not stored by PromptArch.
|
{t.dataPrivacyDesc}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -3,10 +3,11 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import useStore from "@/lib/store";
|
import useStore from "@/lib/store";
|
||||||
import { Sparkles, FileText, ListTodo, Palette, Presentation, History, Settings, Github, Menu, X, Megaphone } from "lucide-react";
|
import { Sparkles, FileText, ListTodo, Palette, Presentation, History, Settings, Github, Menu, X, Megaphone, Languages, Search, MessageSquare } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { translations } from "@/lib/i18n/translations";
|
||||||
|
|
||||||
export type View = "enhance" | "prd" | "action" | "uxdesigner" | "slides" | "googleads" | "history" | "settings";
|
export type View = "enhance" | "prd" | "action" | "uxdesigner" | "slides" | "googleads" | "market-research" | "ai-assist" | "history" | "settings";
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
currentView: View;
|
currentView: View;
|
||||||
@@ -14,18 +15,22 @@ interface SidebarProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
|
export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
|
||||||
const history = useStore((state) => state.history);
|
const { language, setLanguage, history } = useStore();
|
||||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||||
|
const t = translations[language].sidebar;
|
||||||
|
const common = translations[language].common;
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ id: "enhance" as View, label: "Prompt Enhancer", icon: Sparkles },
|
{ id: "enhance" as View, label: t.promptEnhancer, icon: Sparkles },
|
||||||
{ id: "prd" as View, label: "PRD Generator", icon: FileText },
|
{ id: "prd" as View, label: t.prdGenerator, icon: FileText },
|
||||||
{ id: "action" as View, label: "Action Plan", icon: ListTodo },
|
{ id: "action" as View, label: t.actionPlan, icon: ListTodo },
|
||||||
{ id: "uxdesigner" as View, label: "UX Designer", icon: Palette },
|
{ id: "uxdesigner" as View, label: t.uxDesigner, icon: Palette },
|
||||||
{ id: "slides" as View, label: "Slides Generator", icon: Presentation },
|
{ id: "slides" as View, label: t.slidesGen, icon: Presentation },
|
||||||
{ id: "googleads" as View, label: "Google Ads Gen", icon: Megaphone },
|
{ id: "googleads" as View, label: t.googleAds, icon: Megaphone },
|
||||||
{ id: "history" as View, label: "History", icon: History, count: history.length },
|
{ id: "market-research" as View, label: t.marketResearch, icon: Search },
|
||||||
{ id: "settings" as View, label: "Settings", icon: Settings },
|
{ id: "ai-assist" as View, label: t.aiAssist, icon: MessageSquare },
|
||||||
|
{ id: "history" as View, label: t.history, icon: History, count: history.length },
|
||||||
|
{ id: "settings" as View, label: t.settings, icon: Settings },
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleViewChange = (view: View) => {
|
const handleViewChange = (view: View) => {
|
||||||
@@ -38,22 +43,22 @@ export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
|
|||||||
<div className="border-b p-4 lg:p-6">
|
<div className="border-b p-4 lg:p-6">
|
||||||
<a href="https://www.rommark.dev" className="mb-4 flex items-center gap-2 text-xs font-medium text-muted-foreground hover:text-primary transition-colors">
|
<a href="https://www.rommark.dev" className="mb-4 flex items-center gap-2 text-xs font-medium text-muted-foreground hover:text-primary transition-colors">
|
||||||
<Menu className="h-3 w-3" />
|
<Menu className="h-3 w-3" />
|
||||||
Back to rommark.dev
|
<span>{t.backToRommark}</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer" target="_blank" rel="noopener noreferrer" className="block">
|
<a href="https://github.rommark.dev/admin/PromptArch" target="_blank" rel="noopener noreferrer" className="block">
|
||||||
<h1 className="flex items-center gap-2 text-lg lg:text-xl font-bold hover:opacity-80 transition-opacity">
|
<h1 className="flex items-center gap-2 text-lg lg:text-xl font-bold hover:opacity-80 transition-opacity">
|
||||||
<div className="flex h-7 w-7 lg:h-8 lg:w-8 items-center justify-center rounded-lg bg-[#4285F4] text-primary-foreground text-sm lg:text-base">
|
<div className="flex h-7 w-7 lg:h-8 lg:w-8 items-center justify-center rounded-lg bg-[#4285F4] text-primary-foreground text-sm lg:text-base">
|
||||||
PA
|
PA
|
||||||
</div>
|
</div>
|
||||||
PromptArch
|
{t.title}
|
||||||
</h1>
|
</h1>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer" target="_blank" rel="noopener noreferrer" className="mt-2 lg:mt-3 flex items-center gap-1.5 rounded-md px-2 lg:px-3 py-1 lg:py-1.5 text-xs text-primary hover:bg-primary/10 transition-colors">
|
<a href="https://github.rommark.dev/admin/PromptArch" target="_blank" rel="noopener noreferrer" className="mt-2 lg:mt-3 flex items-center gap-1.5 rounded-md px-2 lg:px-3 py-1 lg:py-1.5 text-xs text-primary hover:bg-primary/10 transition-colors">
|
||||||
<Github className="h-3 w-3 lg:h-3.5 lg:w-3.5" />
|
<Github className="h-3 w-3 lg:h-3.5 lg:w-3.5" />
|
||||||
<span>View on GitHub</span>
|
<span>{t.viewOnGithub}</span>
|
||||||
</a>
|
</a>
|
||||||
<p className="mt-1 lg:mt-2 text-[10px] lg:text-xs text-muted-foreground">
|
<p className="mt-1 lg:mt-2 text-[10px] lg:text-xs text-muted-foreground">
|
||||||
Forked from <a href="https://github.com/ClavixDev/Clavix" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">Clavix</a>
|
{t.forkedFrom} <a href="https://github.com/ClavixDev/Clavix" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">Clavix</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -78,32 +83,33 @@ export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<div className="mt-6 lg:mt-8 p-2 lg:p-3 text-[9px] lg:text-[10px] leading-relaxed text-muted-foreground border-t border-border/50 pt-3 lg:pt-4">
|
<div className="mt-4 p-2 lg:p-3 border-t border-border/50">
|
||||||
<p className="font-semibold text-foreground mb-1">Developed by Roman | RyzenAdvanced</p>
|
<div className="flex items-center gap-2 mb-2 text-[10px] lg:text-xs font-semibold text-muted-foreground uppercase">
|
||||||
<div className="space-y-0.5 lg:space-y-1">
|
<Languages className="h-3 w-3" /> {t.language}
|
||||||
<p>
|
</div>
|
||||||
GitHub: <a href="https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">roman-ryzenadvanced</a>
|
<div className="flex flex-wrap gap-1">
|
||||||
</p>
|
{(["en", "ru", "he"] as const).map((lang) => (
|
||||||
<p>
|
<Button
|
||||||
Telegram: <a href="https://t.me/VibeCodePrompterSystem" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">@VibeCodePrompterSystem</a>
|
key={lang}
|
||||||
</p>
|
variant={language === lang ? "default" : "outline"}
|
||||||
<p className="mt-1 lg:mt-2 text-[8px] lg:text-[9px] opacity-80">
|
size="sm"
|
||||||
100% Developed using GLM 4.7 model on TRAE.AI IDE.
|
className="h-7 px-2 text-[10px] uppercase font-bold"
|
||||||
</p>
|
onClick={() => setLanguage(lang)}
|
||||||
<p className="text-[8px] lg:text-[9px] opacity-80">
|
>
|
||||||
Model Info: <a href="https://z.ai/subscribe?ic=R0K78RJKNW" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">Learn here</a>
|
{lang}
|
||||||
</p>
|
</Button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="border-t p-3 lg:p-4 hidden lg:block">
|
<div className="border-t p-3 lg:p-4 hidden lg:block">
|
||||||
<div className="rounded-md bg-muted/50 p-2 lg:p-3 text-[10px] lg:text-xs text-muted-foreground">
|
<div className="rounded-md bg-muted/50 p-2 lg:p-3 text-[10px] lg:text-xs text-muted-foreground">
|
||||||
<p className="font-medium text-foreground">Quick Tips</p>
|
<p className="font-medium text-foreground">{t.quickTips}</p>
|
||||||
<ul className="mt-1.5 lg:mt-2 space-y-0.5 lg:space-y-1">
|
<ul className="mt-1.5 lg:mt-2 space-y-0.5 lg:space-y-1">
|
||||||
<li>• Use different providers for best results</li>
|
<li>• {t.tip1}</li>
|
||||||
<li>• Copy enhanced prompts to your AI agent</li>
|
<li>• {t.tip2}</li>
|
||||||
<li>• PRDs generate better action plans</li>
|
<li>• {t.tip3}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -114,11 +120,11 @@ export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
|
|||||||
<>
|
<>
|
||||||
{/* Mobile Header */}
|
{/* Mobile Header */}
|
||||||
<div className="lg:hidden fixed top-0 left-0 right-0 z-50 flex items-center justify-between border-b bg-card px-4 py-3">
|
<div className="lg:hidden fixed top-0 left-0 right-0 z-50 flex items-center justify-between border-b bg-card px-4 py-3">
|
||||||
<a href="https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer" target="_blank" rel="noopener noreferrer" className="flex items-center gap-2">
|
<a href="https://github.rommark.dev/admin/PromptArch" target="_blank" rel="noopener noreferrer" className="flex items-center gap-2">
|
||||||
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-primary text-primary-foreground text-sm font-bold">
|
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-primary text-primary-foreground text-sm font-bold">
|
||||||
PA
|
PA
|
||||||
</div>
|
</div>
|
||||||
<span className="font-bold text-lg">PromptArch</span>
|
<span className="font-bold text-lg">{t.title}</span>
|
||||||
</a>
|
</a>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import {
|
|||||||
Zap,
|
Zap,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { translations } from "@/lib/i18n/translations";
|
||||||
|
|
||||||
const LANGUAGES = [
|
const LANGUAGES = [
|
||||||
{ code: "en", name: "English", nativeName: "English" },
|
{ code: "en", name: "English", nativeName: "English" },
|
||||||
@@ -121,8 +122,12 @@ export default function SlidesGenerator() {
|
|||||||
setError,
|
setError,
|
||||||
setAvailableModels,
|
setAvailableModels,
|
||||||
setSelectedModel,
|
setSelectedModel,
|
||||||
|
language: uiLanguage,
|
||||||
} = useStore();
|
} = useStore();
|
||||||
|
|
||||||
|
const t = translations[uiLanguage].slidesGen;
|
||||||
|
const common = translations[uiLanguage].common;
|
||||||
|
|
||||||
const [topic, setTopic] = useState("");
|
const [topic, setTopic] = useState("");
|
||||||
const [language, setLanguage] = useState("en");
|
const [language, setLanguage] = useState("en");
|
||||||
const [theme, setTheme] = useState("executive-dark");
|
const [theme, setTheme] = useState("executive-dark");
|
||||||
@@ -141,6 +146,7 @@ export default function SlidesGenerator() {
|
|||||||
const slideContainerRef = useRef<HTMLDivElement>(null);
|
const slideContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const autoPlayRef = useRef<NodeJS.Timeout | null>(null);
|
const autoPlayRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const slideFrameRef = useRef<HTMLIFrameElement>(null);
|
||||||
|
|
||||||
const selectedModel = selectedModels[selectedProvider];
|
const selectedModel = selectedModels[selectedProvider];
|
||||||
const models = availableModels[selectedProvider] || modelAdapter.getAvailableModels(selectedProvider);
|
const models = availableModels[selectedProvider] || modelAdapter.getAvailableModels(selectedProvider);
|
||||||
@@ -387,6 +393,31 @@ export default function SlidesGenerator() {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const buildSlideDoc = (html: string): string => {
|
||||||
|
const normalized = (html || "").trim();
|
||||||
|
if (!normalized) return "";
|
||||||
|
const isFullDoc = /^<!DOCTYPE/i.test(normalized) || /^<html/i.test(normalized);
|
||||||
|
if (isFullDoc) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
return `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<style>
|
||||||
|
html, body { margin: 0; padding: 0; width: 100%; height: 100%; }
|
||||||
|
body { background: transparent; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${normalized}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
const generateAnimatedHtml = (slide: any, index: number): string => {
|
const generateAnimatedHtml = (slide: any, index: number): string => {
|
||||||
const themeConfig = THEMES.find(t => t.id === theme) || THEMES[1];
|
const themeConfig = THEMES.find(t => t.id === theme) || THEMES[1];
|
||||||
const [bg, accent, secondary, text] = themeConfig.colors;
|
const [bg, accent, secondary, text] = themeConfig.colors;
|
||||||
@@ -451,7 +482,7 @@ export default function SlidesGenerator() {
|
|||||||
max-width: 90%;
|
max-width: 90%;
|
||||||
animation: fadeIn 1s ease-out 0.3s both;
|
animation: fadeIn 1s ease-out 0.3s both;
|
||||||
">
|
">
|
||||||
${slide.content || "Content goes here..."}
|
${slide.content || t.slideContentPlaceholder}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -488,7 +519,7 @@ export default function SlidesGenerator() {
|
|||||||
|
|
||||||
const handleGenerate = async () => {
|
const handleGenerate = async () => {
|
||||||
if (!topic.trim()) {
|
if (!topic.trim()) {
|
||||||
setError("Please enter a topic for your presentation");
|
setError(t.enterTopicError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -496,7 +527,7 @@ export default function SlidesGenerator() {
|
|||||||
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
|
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
|
||||||
|
|
||||||
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
|
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
|
||||||
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
|
setError(`${common.error}: ${common.configApiKey}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -557,7 +588,7 @@ export default function SlidesGenerator() {
|
|||||||
language: languageName,
|
language: languageName,
|
||||||
slides: [{
|
slides: [{
|
||||||
id: "slide-1",
|
id: "slide-1",
|
||||||
title: "Generated Content",
|
title: t.generatedContent,
|
||||||
content: result.data,
|
content: result.data,
|
||||||
htmlContent: `
|
htmlContent: `
|
||||||
<div style="padding: 2rem; font-family: system-ui; background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); min-height: 100%; color: #f8fafc;">
|
<div style="padding: 2rem; font-family: system-ui; background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); min-height: 100%; color: #f8fafc;">
|
||||||
@@ -592,6 +623,27 @@ export default function SlidesGenerator() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDownloadMarkdown = () => {
|
||||||
|
if (!slidesPresentation) return;
|
||||||
|
|
||||||
|
// Reveal.js format
|
||||||
|
const markdown = slidesPresentation.slides.map(s => {
|
||||||
|
// Strip HTML but keep structure
|
||||||
|
const cleanContent = s.content.replace(/<[^>]*>/g, '').trim();
|
||||||
|
return `## ${s.title}\n\n${cleanContent}`;
|
||||||
|
}).join('\n\n---\n\n');
|
||||||
|
|
||||||
|
const blob = new Blob([markdown], { type: "text/markdown" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = `${slidesPresentation.title.replace(/\s+/g, '_')}_slides.md`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
const handleDownloadHtml = () => {
|
const handleDownloadHtml = () => {
|
||||||
if (!slidesPresentation) return;
|
if (!slidesPresentation) return;
|
||||||
|
|
||||||
@@ -821,27 +873,27 @@ export default function SlidesGenerator() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 xl:grid-cols-2">
|
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 xl:grid-cols-2 text-start">
|
||||||
{/* Input Panel */}
|
{/* Input Panel */}
|
||||||
<Card className="h-fit">
|
<Card className="h-fit">
|
||||||
<CardHeader className="p-4 lg:p-6">
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-violet-500 via-purple-500 to-fuchsia-500 text-white shadow-lg shadow-violet-500/25">
|
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-violet-500 via-purple-500 to-fuchsia-500 text-white shadow-lg shadow-violet-500/25">
|
||||||
<Sparkles className="h-4 w-4" />
|
<Sparkles className="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
<span>Slides Generator</span>
|
<span>{t.title}</span>
|
||||||
<span className="ml-auto text-[10px] font-normal px-2 py-0.5 rounded-full bg-gradient-to-r from-amber-500/10 to-orange-500/10 text-amber-600 border border-amber-200/50">
|
<span className="ml-auto text-[10px] font-normal px-2 py-0.5 rounded-full bg-gradient-to-r from-amber-500/10 to-orange-500/10 text-amber-600 border border-amber-200/50">
|
||||||
PRO
|
PRO
|
||||||
</span>
|
</span>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-xs lg:text-sm">
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
Generate stunning, animated HTML5 presentations with charts, graphics & corporate-ready design
|
{t.description}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4 lg:space-y-5 p-4 lg:p-6 pt-0 lg:pt-0">
|
<CardContent className="space-y-4 lg:space-y-5 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
{/* AI Provider Selection */}
|
{/* AI Provider Selection */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="text-xs lg:text-sm font-medium">AI Provider</label>
|
<label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
|
||||||
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
||||||
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
||||||
<Button
|
<Button
|
||||||
@@ -858,8 +910,8 @@ export default function SlidesGenerator() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Model Selection */}
|
{/* Model Selection */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="text-xs lg:text-sm font-medium">Model</label>
|
<label className="text-xs lg:text-sm font-medium">{common.model}</label>
|
||||||
<select
|
<select
|
||||||
value={selectedModel}
|
value={selectedModel}
|
||||||
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
||||||
@@ -874,10 +926,10 @@ export default function SlidesGenerator() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Topic Input */}
|
{/* Topic Input */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="text-xs lg:text-sm font-medium">Presentation Topic</label>
|
<label className="text-xs lg:text-sm font-medium">{t.topic}</label>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="e.g., Q4 2024 Revenue Analysis with YoY Growth Comparison, AI Integration Roadmap for Enterprise, Product Launch Strategy with Market Positioning..."
|
placeholder={t.placeholder}
|
||||||
value={topic}
|
value={topic}
|
||||||
onChange={(e) => setTopic(e.target.value)}
|
onChange={(e) => setTopic(e.target.value)}
|
||||||
className="min-h-[100px] lg:min-h-[120px] resize-y text-sm"
|
className="min-h-[100px] lg:min-h-[120px] resize-y text-sm"
|
||||||
@@ -885,11 +937,11 @@ export default function SlidesGenerator() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* File Upload Zone */}
|
{/* File Upload Zone */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="text-xs lg:text-sm font-medium flex items-center gap-1.5">
|
<label className="text-xs lg:text-sm font-medium flex items-center gap-1.5">
|
||||||
<Upload className="h-3.5 w-3.5 text-blue-500" />
|
<Upload className="h-3.5 w-3.5 text-blue-500" />
|
||||||
Attach Files for Context
|
{t.attachFiles}
|
||||||
<span className="text-[10px] text-muted-foreground font-normal">(Optional)</span>
|
<span className="text-[10px] text-muted-foreground font-normal">({t.optional})</span>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -917,10 +969,10 @@ export default function SlidesGenerator() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-medium">
|
<p className="text-xs font-medium">
|
||||||
{isDragOver ? "Drop files here" : "Drag & drop or click to upload"}
|
{isDragOver ? t.dropFiles : t.uploadFiles}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-[10px] text-muted-foreground mt-0.5">
|
<p className="text-[10px] text-muted-foreground mt-0.5">
|
||||||
PowerPoint, PDFs, Docs, Images, Color Palettes
|
{t.fileTypes}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -954,7 +1006,7 @@ export default function SlidesGenerator() {
|
|||||||
{formatFileSize(file.size)}
|
{formatFileSize(file.size)}
|
||||||
{file.colors && file.colors.length > 0 && (
|
{file.colors && file.colors.length > 0 && (
|
||||||
<span className="ml-2">
|
<span className="ml-2">
|
||||||
• {file.colors.length} colors extracted
|
• {file.colors.length} {t.colorsExtracted}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
@@ -985,10 +1037,10 @@ export default function SlidesGenerator() {
|
|||||||
|
|
||||||
{/* Language & Theme Row */}
|
{/* Language & Theme Row */}
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="text-xs lg:text-sm font-medium flex items-center gap-1.5">
|
<label className="text-xs lg:text-sm font-medium flex items-center gap-1.5">
|
||||||
<Globe className="h-3.5 w-3.5 text-blue-500" />
|
<Globe className="h-3.5 w-3.5 text-blue-500" />
|
||||||
Language
|
{t.language}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={language}
|
value={language}
|
||||||
@@ -997,7 +1049,7 @@ export default function SlidesGenerator() {
|
|||||||
>
|
>
|
||||||
{LANGUAGES.map((lang) => (
|
{LANGUAGES.map((lang) => (
|
||||||
<option key={lang.code} value={lang.code}>
|
<option key={lang.code} value={lang.code}>
|
||||||
{lang.nativeName} ({lang.name})
|
{lang.nativeName} ({(t.languages as any)[lang.code] || lang.name})
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
@@ -1006,16 +1058,16 @@ export default function SlidesGenerator() {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-xs lg:text-sm font-medium flex items-center gap-1.5">
|
<label className="text-xs lg:text-sm font-medium flex items-center gap-1.5">
|
||||||
<Palette className="h-3.5 w-3.5 text-purple-500" />
|
<Palette className="h-3.5 w-3.5 text-purple-500" />
|
||||||
Theme
|
{t.theme}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={theme}
|
value={theme}
|
||||||
onChange={(e) => setTheme(e.target.value)}
|
onChange={(e) => setTheme(e.target.value)}
|
||||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs lg:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs lg:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
>
|
>
|
||||||
{THEMES.map((t) => (
|
{THEMES.map((themeItem) => (
|
||||||
<option key={t.id} value={t.id}>
|
<option key={themeItem.id} value={themeItem.id}>
|
||||||
{t.icon} {t.name}
|
{themeItem.icon} {(t.themes as any)[themeItem.id.split('-')[0]] || themeItem.name}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
@@ -1023,10 +1075,10 @@ export default function SlidesGenerator() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Animation Style */}
|
{/* Animation Style */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="text-xs lg:text-sm font-medium flex items-center gap-1.5">
|
<label className="text-xs lg:text-sm font-medium flex items-center gap-1.5">
|
||||||
<Zap className="h-3.5 w-3.5 text-amber-500" />
|
<Zap className="h-3.5 w-3.5 text-amber-500" />
|
||||||
Animation Style
|
{t.animations}
|
||||||
</label>
|
</label>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{ANIMATION_STYLES.map((style) => (
|
{ANIMATION_STYLES.map((style) => (
|
||||||
@@ -1040,8 +1092,8 @@ export default function SlidesGenerator() {
|
|||||||
: "border-muted hover:border-violet-300"
|
: "border-muted hover:border-violet-300"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<p className="text-xs font-medium">{style.name}</p>
|
<p className="text-xs font-medium">{((t.animStyles as any)[style.id])?.name || style.name}</p>
|
||||||
<p className="text-[10px] text-muted-foreground">{style.description}</p>
|
<p className="text-[10px] text-muted-foreground">{((t.animStyles as any)[style.id])?.desc || style.description}</p>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -1053,17 +1105,17 @@ export default function SlidesGenerator() {
|
|||||||
className="flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
className="flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||||
>
|
>
|
||||||
<Settings className="h-3.5 w-3.5" />
|
<Settings className="h-3.5 w-3.5" />
|
||||||
{showAdvanced ? "Hide" : "Show"} Advanced Options
|
{showAdvanced ? t.hide : t.show} {t.advanced}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Advanced Options */}
|
{/* Advanced Options */}
|
||||||
{showAdvanced && (
|
{showAdvanced && (
|
||||||
<div className="space-y-3 p-3 rounded-lg bg-muted/30 border">
|
<div className="space-y-3 p-3 rounded-lg bg-muted/30 border text-start">
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="text-xs font-medium flex items-center gap-1.5">
|
<label className="text-xs font-medium flex items-center gap-1.5">
|
||||||
<Users className="h-3.5 w-3.5 text-green-500" />
|
<Users className="h-3.5 w-3.5 text-green-500" />
|
||||||
Target Audience
|
{t.audience}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={audience}
|
value={audience}
|
||||||
@@ -1072,7 +1124,7 @@ export default function SlidesGenerator() {
|
|||||||
>
|
>
|
||||||
{AUDIENCES.map((a) => (
|
{AUDIENCES.map((a) => (
|
||||||
<option key={a.id} value={a.id}>
|
<option key={a.id} value={a.id}>
|
||||||
{a.icon} {a.name}
|
{a.icon} {(t.audiences as any)[a.id] || a.name}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
@@ -1081,15 +1133,16 @@ export default function SlidesGenerator() {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-xs font-medium flex items-center gap-1.5">
|
<label className="text-xs font-medium flex items-center gap-1.5">
|
||||||
<Hash className="h-3.5 w-3.5 text-orange-500" />
|
<Hash className="h-3.5 w-3.5 text-orange-500" />
|
||||||
Number of Slides
|
{t.numSlides}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={slideCount}
|
value={slideCount}
|
||||||
onChange={(e) => setSlideCount(parseInt(e.target.value))}
|
onChange={(e) => setSlideCount(parseInt(e.target.value))}
|
||||||
className="w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-xs ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
className="w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-xs ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
>
|
>
|
||||||
|
<option value={0}>{t.sameAsSource}</option>
|
||||||
{[5, 8, 10, 12, 15, 20, 25, 30].map((n) => (
|
{[5, 8, 10, 12, 15, 20, 25, 30].map((n) => (
|
||||||
<option key={n} value={n}>{n} slides</option>
|
<option key={n} value={n}>{n} {t.slides}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -1098,11 +1151,11 @@ export default function SlidesGenerator() {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-xs font-medium flex items-center gap-1.5">
|
<label className="text-xs font-medium flex items-center gap-1.5">
|
||||||
<Building2 className="h-3.5 w-3.5 text-cyan-500" />
|
<Building2 className="h-3.5 w-3.5 text-cyan-500" />
|
||||||
Organization Name (Optional)
|
{t.organization} ({t.optional})
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="e.g., Acme Corporation"
|
placeholder={t.organizationPlaceholder}
|
||||||
value={organization}
|
value={organization}
|
||||||
onChange={(e) => setOrganization(e.target.value)}
|
onChange={(e) => setOrganization(e.target.value)}
|
||||||
className="w-full rounded-md border border-input bg-background px-3 py-1.5 text-xs ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
className="w-full rounded-md border border-input bg-background px-3 py-1.5 text-xs ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
@@ -1131,7 +1184,7 @@ export default function SlidesGenerator() {
|
|||||||
{!apiKeys[selectedProvider] && (
|
{!apiKeys[selectedProvider] && (
|
||||||
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
||||||
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
<span className="text-[10px] lg:text-xs">Configure API key in Settings</span>
|
<span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1146,12 +1199,12 @@ export default function SlidesGenerator() {
|
|||||||
{isProcessing ? (
|
{isProcessing ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
Creating Animated Slides...
|
{t.generating}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Sparkles className="mr-2 h-4 w-4" />
|
<Sparkles className="mr-2 h-4 w-4" />
|
||||||
Generate Animated Presentation
|
{t.generate}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1164,7 +1217,7 @@ export default function SlidesGenerator() {
|
|||||||
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<CheckCircle2 className={cn("h-4 w-4 lg:h-5 lg:w-5", slidesPresentation ? "text-green-500" : "text-muted-foreground")} />
|
<CheckCircle2 className={cn("h-4 w-4 lg:h-5 lg:w-5", slidesPresentation ? "text-green-500" : "text-muted-foreground")} />
|
||||||
Slide Preview
|
{t.slidePreview}
|
||||||
</span>
|
</span>
|
||||||
{slidesPresentation && (
|
{slidesPresentation && (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
@@ -1174,9 +1227,12 @@ export default function SlidesGenerator() {
|
|||||||
<Button variant="ghost" size="icon" onClick={toggleFullscreen} className="h-8 w-8">
|
<Button variant="ghost" size="icon" onClick={toggleFullscreen} className="h-8 w-8">
|
||||||
{isFullscreen ? <Minimize2 className="h-3.5 w-3.5" /> : <Maximize2 className="h-3.5 w-3.5" />}
|
{isFullscreen ? <Minimize2 className="h-3.5 w-3.5" /> : <Maximize2 className="h-3.5 w-3.5" />}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" onClick={handleDownloadHtml} className="h-8 w-8">
|
<Button variant="ghost" size="icon" onClick={handleDownloadHtml} title="Download HTML Presentation" className="h-8 w-8">
|
||||||
<Download className="h-3.5 w-3.5" />
|
<Download className="h-3.5 w-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button variant="ghost" size="icon" onClick={handleDownloadMarkdown} title="Export Reveal.js Markdown" className="h-8 w-8 text-blue-500">
|
||||||
|
<FileText className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8">
|
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8">
|
||||||
{copied ? <CheckCircle2 className="h-3.5 w-3.5 text-green-500" /> : <Copy className="h-3.5 w-3.5" />}
|
{copied ? <CheckCircle2 className="h-3.5 w-3.5 text-green-500" /> : <Copy className="h-3.5 w-3.5" />}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1185,7 +1241,7 @@ export default function SlidesGenerator() {
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
{slidesPresentation && (
|
{slidesPresentation && (
|
||||||
<CardDescription className="text-xs lg:text-sm">
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
{slidesPresentation.title} • {slidesPresentation.slides.length} slides • {slidesPresentation.language}
|
{slidesPresentation.title} • {slidesPresentation.slides.length} {t.slides} • {slidesPresentation.language}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
)}
|
)}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -1197,11 +1253,12 @@ export default function SlidesGenerator() {
|
|||||||
ref={slideContainerRef}
|
ref={slideContainerRef}
|
||||||
className="relative aspect-video rounded-lg overflow-hidden border bg-slate-900 shadow-2xl"
|
className="relative aspect-video rounded-lg overflow-hidden border bg-slate-900 shadow-2xl"
|
||||||
>
|
>
|
||||||
<div
|
<iframe
|
||||||
className="absolute inset-0"
|
ref={slideFrameRef}
|
||||||
dangerouslySetInnerHTML={{
|
title="Slide Preview"
|
||||||
__html: slidesPresentation.slides[currentSlide]?.htmlContent || ""
|
className="absolute inset-0 w-full h-full border-none"
|
||||||
}}
|
sandbox="allow-scripts"
|
||||||
|
srcDoc={buildSlideDoc(slidesPresentation.slides[currentSlide]?.htmlContent || "")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Navigation Arrows */}
|
{/* Navigation Arrows */}
|
||||||
@@ -1264,21 +1321,21 @@ export default function SlidesGenerator() {
|
|||||||
</p>
|
</p>
|
||||||
{slidesPresentation.slides[currentSlide]?.notes && (
|
{slidesPresentation.slides[currentSlide]?.notes && (
|
||||||
<p className="text-xs text-blue-500 mt-2 italic">
|
<p className="text-xs text-blue-500 mt-2 italic">
|
||||||
Notes: {slidesPresentation.slides[currentSlide]?.notes}
|
{t.notesLabel}: {slidesPresentation.slides[currentSlide]?.notes}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-[300px] lg:h-[400px] items-center justify-center text-center">
|
<div className="flex h-[300px] lg:h-[400px] items-center justify-center text-center italic">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="mx-auto w-16 h-16 rounded-2xl bg-gradient-to-br from-violet-500/20 to-fuchsia-500/20 flex items-center justify-center">
|
<div className="mx-auto w-16 h-16 rounded-2xl bg-gradient-to-br from-violet-500/20 to-fuchsia-500/20 flex items-center justify-center">
|
||||||
<Sparkles className="h-8 w-8 text-violet-500/50" />
|
<Sparkles className="h-8 w-8 text-violet-500/50" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-muted-foreground">No presentation yet</p>
|
<p className="text-sm font-medium text-muted-foreground">{t.emptyState}</p>
|
||||||
<p className="text-xs text-muted-foreground/70 mt-1">
|
<p className="text-xs text-muted-foreground/70 mt-1">
|
||||||
Enter a topic and generate your animated slides
|
{t.enterTopic}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,10 +8,13 @@ import useStore from "@/lib/store";
|
|||||||
import modelAdapter from "@/lib/services/adapter-instance";
|
import modelAdapter from "@/lib/services/adapter-instance";
|
||||||
import { Palette, Copy, Loader2, CheckCircle2, Settings } from "lucide-react";
|
import { Palette, Copy, Loader2, CheckCircle2, Settings } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { translations } from "@/lib/i18n/translations";
|
||||||
|
|
||||||
export default function UXDesignerPrompt() {
|
export default function UXDesignerPrompt() {
|
||||||
const {
|
const {
|
||||||
|
language,
|
||||||
currentPrompt,
|
currentPrompt,
|
||||||
|
enhancedPrompt,
|
||||||
selectedProvider,
|
selectedProvider,
|
||||||
selectedModels,
|
selectedModels,
|
||||||
availableModels,
|
availableModels,
|
||||||
@@ -27,6 +30,9 @@ export default function UXDesignerPrompt() {
|
|||||||
setSelectedModel,
|
setSelectedModel,
|
||||||
} = useStore();
|
} = useStore();
|
||||||
|
|
||||||
|
const t = translations[language].uxDesigner;
|
||||||
|
const common = translations[language].common;
|
||||||
|
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const [generatedPrompt, setGeneratedPrompt] = useState<string | null>(null);
|
const [generatedPrompt, setGeneratedPrompt] = useState<string | null>(null);
|
||||||
|
|
||||||
@@ -65,7 +71,7 @@ export default function UXDesignerPrompt() {
|
|||||||
|
|
||||||
const handleGenerate = async () => {
|
const handleGenerate = async () => {
|
||||||
if (!currentPrompt.trim()) {
|
if (!currentPrompt.trim()) {
|
||||||
setError("Please enter an app description");
|
setError(t.enterDescriptionError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +79,7 @@ export default function UXDesignerPrompt() {
|
|||||||
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
|
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
|
||||||
|
|
||||||
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
|
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
|
||||||
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
|
setError(`${common.error}: ${common.configApiKey}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,11 +99,11 @@ export default function UXDesignerPrompt() {
|
|||||||
setEnhancedPrompt(result.data);
|
setEnhancedPrompt(result.data);
|
||||||
} else {
|
} else {
|
||||||
console.error("[UXDesignerPrompt] Generation failed:", result.error);
|
console.error("[UXDesignerPrompt] Generation failed:", result.error);
|
||||||
setError(result.error || "Failed to generate UX designer prompt");
|
setError(result.error || t.errorGenerate);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[UXDesignerPrompt] Generation error:", err);
|
console.error("[UXDesignerPrompt] Generation error:", err);
|
||||||
setError(err instanceof Error ? err.message : "An error occurred");
|
setError(err instanceof Error ? err.message : t.errorGenerate);
|
||||||
} finally {
|
} finally {
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
}
|
}
|
||||||
@@ -119,20 +125,20 @@ export default function UXDesignerPrompt() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2">
|
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2 text-start">
|
||||||
<Card className="h-fit">
|
<Card className="h-fit">
|
||||||
<CardHeader className="p-4 lg:p-6">
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||||
<Palette className="h-4 w-4 lg:h-5 lg:w-5" />
|
<Palette className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||||
UX Designer Prompt
|
{t.title}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-xs lg:text-sm">
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
Describe your app idea and get the BEST EVER prompt for UX design
|
{t.description}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="text-xs lg:text-sm font-medium">AI Provider</label>
|
<label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
|
||||||
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
||||||
{(["ollama", "zai"] as const).map((provider) => (
|
{(["ollama", "zai"] as const).map((provider) => (
|
||||||
<Button
|
<Button
|
||||||
@@ -151,8 +157,8 @@ export default function UXDesignerPrompt() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="text-xs lg:text-sm font-medium">Model</label>
|
<label className="text-xs lg:text-sm font-medium">{common.model}</label>
|
||||||
<select
|
<select
|
||||||
value={selectedModel}
|
value={selectedModel}
|
||||||
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
||||||
@@ -166,16 +172,16 @@ export default function UXDesignerPrompt() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="text-xs lg:text-sm font-medium">App Description</label>
|
<label className="text-xs lg:text-sm font-medium">{t.inputLabel}</label>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="e.g., A fitness tracking app with workout plans, nutrition tracking, and social features for sharing progress with friends"
|
placeholder={t.placeholder}
|
||||||
value={currentPrompt}
|
value={currentPrompt}
|
||||||
onChange={(e) => setCurrentPrompt(e.target.value)}
|
onChange={(e) => setCurrentPrompt(e.target.value)}
|
||||||
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm"
|
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm lg:text-base p-3 lg:p-4"
|
||||||
/>
|
/>
|
||||||
<p className="text-[10px] lg:text-xs text-muted-foreground">
|
<p className="text-[10px] lg:text-xs text-muted-foreground">
|
||||||
Describe what kind of app you want, target users, key features, and any specific design preferences.
|
{t.inputDesc}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -185,7 +191,7 @@ export default function UXDesignerPrompt() {
|
|||||||
{!apiKeys[selectedProvider] && (
|
{!apiKeys[selectedProvider] && (
|
||||||
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
||||||
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
<span className="text-[10px] lg:text-xs">Configure API key in Settings</span>
|
<span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -196,30 +202,30 @@ export default function UXDesignerPrompt() {
|
|||||||
{isProcessing ? (
|
{isProcessing ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
|
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
|
||||||
Generating...
|
{common.generating}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Palette className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
<Palette className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
Generate UX Prompt
|
{t.generateButton}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" onClick={handleClear} disabled={isProcessing} className="h-9 lg:h-10 text-xs lg:text-sm px-3">
|
<Button variant="outline" onClick={handleClear} disabled={isProcessing} className="h-9 lg:h-10 text-xs lg:text-sm px-3">
|
||||||
<span className="hidden sm:inline">Clear</span>
|
<span className="hidden sm:inline">{translations[language].promptEnhancer.clear}</span>
|
||||||
<span className="sm:hidden">×</span>
|
<span className="sm:hidden">×</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className={cn(!generatedPrompt && "opacity-50")}>
|
<Card className={cn("flex flex-col", !generatedPrompt && "opacity-50")}>
|
||||||
<CardHeader className="p-4 lg:p-6">
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
||||||
<span className="hidden sm:inline">Best Ever UX Prompt</span>
|
<span className="hidden sm:inline">{t.resultTitle}</span>
|
||||||
<span className="sm:hidden">UX Prompt</span>
|
<span className="sm:hidden">{t.uxPromptMobile}</span>
|
||||||
</span>
|
</span>
|
||||||
{generatedPrompt && (
|
{generatedPrompt && (
|
||||||
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
||||||
@@ -232,17 +238,17 @@ export default function UXDesignerPrompt() {
|
|||||||
)}
|
)}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-xs lg:text-sm">
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
Comprehensive UX design prompt ready for designers
|
{t.resultDesc}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
{generatedPrompt ? (
|
{generatedPrompt ? (
|
||||||
<div className="rounded-md border bg-muted/50 p-3 lg:p-4 max-h-[350px] lg:max-h-[400px] overflow-y-auto">
|
<div className="rounded-md border bg-muted/50 p-3 lg:p-4 max-h-[350px] lg:max-h-[400px] overflow-y-auto animate-in fade-in slide-in-from-bottom-2 duration-300">
|
||||||
<pre className="whitespace-pre-wrap text-xs lg:text-sm">{generatedPrompt}</pre>
|
<pre className="whitespace-pre-wrap text-xs lg:text-sm leading-relaxed">{generatedPrompt}</pre>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-[250px] lg:h-[400px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground px-4">
|
<div className="flex h-[250px] lg:h-[400px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground px-4 italic">
|
||||||
Your comprehensive UX designer prompt will appear here
|
{t.emptyState}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
108
lib/artifact-utils.ts
Normal file
108
lib/artifact-utils.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import JSZip from "jszip";
|
||||||
|
import { saveAs } from "file-saver";
|
||||||
|
|
||||||
|
export async function downloadArtifactAsZip(data: string, type: string, language: string = "html") {
|
||||||
|
const zip = new JSZip();
|
||||||
|
const extension = language === "html" || type === "web" || type === "app" ? "html" : (language === "typescript" || language === "tsx" ? "tsx" : "txt");
|
||||||
|
const filename = `artifact-${Date.now()}.${extension}`;
|
||||||
|
|
||||||
|
// Check if data contains common multi-file structures (simple heuristic)
|
||||||
|
// If it looks like a full project (multiple files defined in one block), we could parse it,
|
||||||
|
// but for now we'll just save the main artifact.
|
||||||
|
zip.file(filename, data);
|
||||||
|
|
||||||
|
// Add a basic README
|
||||||
|
zip.file("README.md", `# AI Generated Artifact\n\nType: ${type}\nGenerated: ${new Date().toLocaleString()}`);
|
||||||
|
|
||||||
|
const content = await zip.generateAsync({ type: "blob" });
|
||||||
|
saveAs(content, `promptarch-artifact-${Date.now()}.zip`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function pushToGithub(
|
||||||
|
token: string,
|
||||||
|
repoName: string,
|
||||||
|
files: { path: string; content: string }[],
|
||||||
|
description: string = "Generated by PromptArch"
|
||||||
|
) {
|
||||||
|
const headers = {
|
||||||
|
'Authorization': `token ${token}`,
|
||||||
|
'Accept': 'application/vnd.github.v3+json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1. Check if repo exists, if not create it
|
||||||
|
let repoData;
|
||||||
|
const userResponse = await fetch('https://api.github.com/user', { headers });
|
||||||
|
if (!userResponse.ok) throw new Error("Failed to authenticate with GitHub");
|
||||||
|
const userData = await userResponse.json();
|
||||||
|
const username = userData.login;
|
||||||
|
|
||||||
|
const repoCheckResponse = await fetch(`https://api.github.com/repos/${username}/${repoName}`, { headers });
|
||||||
|
|
||||||
|
if (repoCheckResponse.status === 404) {
|
||||||
|
// Create repo
|
||||||
|
const createResponse = await fetch('https://api.github.com/user/repos', {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: repoName,
|
||||||
|
description,
|
||||||
|
auto_init: true
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if (!createResponse.ok) throw new Error("Failed to create repository");
|
||||||
|
repoData = await createResponse.json();
|
||||||
|
// Wait a bit for repo to be ready
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
} else {
|
||||||
|
repoData = await repoCheckResponse.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Get latest commit SHA of default branch
|
||||||
|
const branchResponse = await fetch(`https://api.github.com/repos/${username}/${repoName}/branches/${repoData.default_branch}`, { headers });
|
||||||
|
const branchData = await branchResponse.json();
|
||||||
|
const latestCommitSha = branchData.commit.sha;
|
||||||
|
|
||||||
|
// 3. Create a new tree
|
||||||
|
const treeItems = files.map(file => ({
|
||||||
|
path: file.path,
|
||||||
|
mode: '100644',
|
||||||
|
type: 'blob',
|
||||||
|
content: file.content
|
||||||
|
}));
|
||||||
|
|
||||||
|
const treeResponse = await fetch(`https://api.github.com/repos/${username}/${repoName}/git/trees`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify({
|
||||||
|
base_tree: latestCommitSha,
|
||||||
|
tree: treeItems
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const treeData = await treeResponse.json();
|
||||||
|
|
||||||
|
// 4. Create a new commit
|
||||||
|
const commitResponse = await fetch(`https://api.github.com/repos/${username}/${repoName}/git/commits`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify({
|
||||||
|
message: `Update from PromptArch: ${new Date().toISOString()}`,
|
||||||
|
tree: treeData.sha,
|
||||||
|
parents: [latestCommitSha]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const commitData = await commitResponse.json();
|
||||||
|
|
||||||
|
// 5. Update the reference
|
||||||
|
const refResponse = await fetch(`https://api.github.com/repos/${username}/${repoName}/git/refs/heads/${repoData.default_branch}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify({
|
||||||
|
sha: commitData.sha
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!refResponse.ok) throw new Error("Failed to update branch reference");
|
||||||
|
|
||||||
|
return { url: repoData.html_url };
|
||||||
|
}
|
||||||
571
lib/export-utils.ts
Normal file
571
lib/export-utils.ts
Normal file
@@ -0,0 +1,571 @@
|
|||||||
|
import * as XLSX from 'xlsx';
|
||||||
|
import { GoogleAdsResult, MagicWandResult } from "../types";
|
||||||
|
|
||||||
|
export const downloadFile = (filename: string, content: any, contentType: string) => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
const blob = content instanceof Blob ? content : new Blob([content], { type: contentType });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.setAttribute("href", url);
|
||||||
|
link.setAttribute("download", filename);
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateGoogleAdsCSV = (googleAds?: any, magic?: any): string => {
|
||||||
|
const rows: string[][] = [];
|
||||||
|
|
||||||
|
if (googleAds) {
|
||||||
|
rows.push(["GOOGLE ADS STRATEGY REPORT"]);
|
||||||
|
rows.push(["Generated at", new Date().toLocaleString()]);
|
||||||
|
rows.push(["Website", googleAds.websiteUrl || 'N/A']);
|
||||||
|
rows.push([]);
|
||||||
|
|
||||||
|
const kw = googleAds.keywords;
|
||||||
|
if (kw) {
|
||||||
|
rows.push(["KEYWORD RESEARCH"]);
|
||||||
|
rows.push(["Type", "Keyword", "CPC", "Volume", "Competition"]);
|
||||||
|
if (Array.isArray(kw.primary)) kw.primary.forEach((k: any) => rows.push(["Primary", String(k?.keyword || ''), String(k?.cpc || ''), String(k?.searchVolume || ''), String(k?.competition || '')]));
|
||||||
|
if (Array.isArray(kw.longTail)) kw.longTail.forEach((k: any) => rows.push(["Long-tail", String(k?.keyword || ''), String(k?.cpc || ''), String(k?.searchVolume || ''), String(k?.competition || '')]));
|
||||||
|
if (Array.isArray(kw.negative)) kw.negative.forEach((k: any) => rows.push(["Negative", String(k?.keyword || ''), "", "", String(k?.competition || '')]));
|
||||||
|
rows.push([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ads = googleAds.adCopies;
|
||||||
|
if (Array.isArray(ads)) {
|
||||||
|
rows.push(["AD COPIES"]);
|
||||||
|
rows.push(["Variation", "Headlines", "Descriptions", "CTA", "Optimized", "Positioning"]);
|
||||||
|
ads.forEach((ad: any, i: number) => {
|
||||||
|
const hl = Array.isArray(ad.headlines) ? ad.headlines.join(' | ') : '';
|
||||||
|
const ds = Array.isArray(ad.descriptions) ? ad.descriptions.join(' | ') : '';
|
||||||
|
rows.push([`Var ${i + 1}`, hl, ds, String(ad?.callToAction || ''), String(ad?.mobileOptimized || 'false'), String(ad?.positioning || '')]);
|
||||||
|
});
|
||||||
|
rows.push([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const camps = googleAds.campaigns;
|
||||||
|
if (Array.isArray(camps)) {
|
||||||
|
rows.push(["CAMPAIGN STRUCTURE"]);
|
||||||
|
rows.push(["Name", "Type", "Budget", "Locations", "Targeting", "Bidding"]);
|
||||||
|
camps.forEach((c: any) => {
|
||||||
|
const t = c.targeting;
|
||||||
|
const locs = (t && Array.isArray(t.locations)) ? t.locations.join('; ') : 'All';
|
||||||
|
const demos = (t && t.demographics) ? `Age: ${t.demographics.age?.join(', ') || 'Any'}; Gender: ${t.demographics.gender?.join(', ') || 'Any'}` : 'All';
|
||||||
|
rows.push([String(c.name || ''), String(c.type || ''), `${c?.budget?.daily || 0} ${c?.budget?.currency || ''}`, locs, demos, String(c.biddingStrategy || '')]);
|
||||||
|
});
|
||||||
|
rows.push([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (magic) {
|
||||||
|
rows.push(["MARKET INTELLIGENCE"]);
|
||||||
|
const ma = magic.marketAnalysis;
|
||||||
|
if (ma) {
|
||||||
|
rows.push(["Size", String(ma.industrySize || '')]);
|
||||||
|
rows.push(["Growth", String(ma.growthRate || '')]);
|
||||||
|
rows.push(["Trends", Array.isArray(ma.marketTrends) ? ma.marketTrends.join('; ') : '']);
|
||||||
|
rows.push(["Competitors", Array.isArray(ma.topCompetitors) ? ma.topCompetitors.join('; ') : '']);
|
||||||
|
rows.push([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const strats = magic.strategies;
|
||||||
|
if (Array.isArray(strats)) {
|
||||||
|
rows.push(["STRATEGIES"]);
|
||||||
|
strats.forEach((s: any) => {
|
||||||
|
rows.push(["Direction", String(s.direction || '')]);
|
||||||
|
rows.push(["Target", String(s.targetAudience || '')]);
|
||||||
|
rows.push(["ROI", String(s.expectedROI || '')]);
|
||||||
|
rows.push(["Risk", String(s.riskLevel || '')]);
|
||||||
|
rows.push(["Timeframe", String(s.timeToResults || '')]);
|
||||||
|
rows.push([]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows.map(r => r.map(c => `"${String(c || '').replace(/"/g, '""')}"`).join(",")).join("\n");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateGoogleAdsExcel = (googleAds?: any, magic?: any): Blob => {
|
||||||
|
const wb = XLSX.utils.book_new();
|
||||||
|
|
||||||
|
// 1. Overview Sheet
|
||||||
|
const overviewData: any[] = [
|
||||||
|
["Attribute", "Value"],
|
||||||
|
["Report Title", "Google Ads Strategy Report"],
|
||||||
|
["Generated At", new Date().toLocaleString()],
|
||||||
|
["Website URL", googleAds?.websiteUrl || magic?.websiteUrl || 'N/A'],
|
||||||
|
[],
|
||||||
|
["Performance Forecasts"],
|
||||||
|
["Est. Impressions", googleAds?.predictions?.estimatedImpressions || 'N/A'],
|
||||||
|
["Est. Clicks", googleAds?.predictions?.estimatedClicks || 'N/A'],
|
||||||
|
["Est. CTR", googleAds?.predictions?.estimatedCtr || 'N/A'],
|
||||||
|
["Est. Conversions", googleAds?.predictions?.estimatedConversions || 'N/A'],
|
||||||
|
["Est. Conversion Rate", googleAds?.predictions?.conversionRate || 'N/A'],
|
||||||
|
["Avg. CPC", googleAds?.predictions?.avgCpc || 'N/A'],
|
||||||
|
[],
|
||||||
|
["Historical Benchmarks"],
|
||||||
|
["Avg. Industry CTR", googleAds?.historicalBenchmarks?.industryAverageCtr || 'N/A'],
|
||||||
|
["Avg. Industry CPC", googleAds?.historicalBenchmarks?.industryAverageCpc || 'N/A'],
|
||||||
|
["Seasonal Trends", googleAds?.historicalBenchmarks?.seasonalTrends || 'N/A'],
|
||||||
|
["Geographic Insights", googleAds?.historicalBenchmarks?.geographicInsights || 'N/A']
|
||||||
|
];
|
||||||
|
const overviewSheet = XLSX.utils.aoa_to_sheet(overviewData);
|
||||||
|
XLSX.utils.book_append_sheet(wb, overviewSheet, "Overview");
|
||||||
|
|
||||||
|
// 2. Keywords Sheet
|
||||||
|
if (googleAds?.keywords) {
|
||||||
|
const kw = googleAds.keywords;
|
||||||
|
const kwData: any[] = [
|
||||||
|
["Type", "Keyword", "Avg. CPC", "Monthly Volume", "Competition", "Difficulty"]
|
||||||
|
];
|
||||||
|
if (Array.isArray(kw.primary)) kw.primary.forEach((k: any) => kwData.push(["Primary", k.keyword, k.cpc, k.searchVolume, k.competition, k.difficultyScore]));
|
||||||
|
if (Array.isArray(kw.longTail)) kw.longTail.forEach((k: any) => kwData.push(["Long-tail", k.keyword, k.cpc, k.searchVolume, k.competition, k.difficultyScore]));
|
||||||
|
if (Array.isArray(kw.negative)) kw.negative.forEach((k: any) => kwData.push(["Negative", k.keyword, "", "", k.competition, ""]));
|
||||||
|
|
||||||
|
const kwSheet = XLSX.utils.aoa_to_sheet(kwData);
|
||||||
|
XLSX.utils.book_append_sheet(wb, kwSheet, "Keywords");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Ad Copies Sheet
|
||||||
|
if (Array.isArray(googleAds?.adCopies)) {
|
||||||
|
const adData: any[] = [
|
||||||
|
["ID", "Variation", "Headlines", "Descriptions", "CTA", "Mobile Optimized", "Strategic Positioning"]
|
||||||
|
];
|
||||||
|
googleAds.adCopies.forEach((ad: any, i: number) => {
|
||||||
|
adData.push([
|
||||||
|
ad.id,
|
||||||
|
`Variation ${i + 1}`,
|
||||||
|
(ad.headlines || []).join(" | "),
|
||||||
|
(ad.descriptions || []).join(" | "),
|
||||||
|
ad.callToAction,
|
||||||
|
ad.mobileOptimized ? "Yes" : "No",
|
||||||
|
ad.positioning || "N/A"
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
const adSheet = XLSX.utils.aoa_to_sheet(adData);
|
||||||
|
XLSX.utils.book_append_sheet(wb, adSheet, "Ad Copies");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Competitors Sheet
|
||||||
|
if (magic?.competitorInsights || magic?.marketAnalysis) {
|
||||||
|
const compData: any[] = [
|
||||||
|
["Competitor", "Website", "Est. Monthly Spend", "Target Audience", "Strengths", "Weaknesses", "Ad Strategy", "Top Keywords"]
|
||||||
|
];
|
||||||
|
if (Array.isArray(magic.competitorInsights)) {
|
||||||
|
magic.competitorInsights.forEach((c: any) => {
|
||||||
|
compData.push([
|
||||||
|
c.competitor,
|
||||||
|
c.website || 'N/A',
|
||||||
|
c.estimatedSpend || 'N/A',
|
||||||
|
c.targetAudience || 'N/A',
|
||||||
|
(c.strengths || []).join(", "),
|
||||||
|
(c.weaknesses || []).join(", "),
|
||||||
|
c.adStrategy,
|
||||||
|
(c.topKeywords || []).join(", ")
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const compSheet = XLSX.utils.aoa_to_sheet(compData);
|
||||||
|
XLSX.utils.book_append_sheet(wb, compSheet, "Competitor Analysis");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Strategies Sheet
|
||||||
|
if (Array.isArray(magic?.strategies)) {
|
||||||
|
const stratData: any[] = [
|
||||||
|
["ID", "Strategic Direction", "Rationale", "Target Audience", "Competitive Advantage", "Expected ROI", "Risk Level", "Time to Results", "Success Metrics"]
|
||||||
|
];
|
||||||
|
magic.strategies.forEach((s: any) => {
|
||||||
|
stratData.push([
|
||||||
|
s.id,
|
||||||
|
s.direction,
|
||||||
|
s.rationale,
|
||||||
|
s.targetAudience,
|
||||||
|
s.competitiveAdvantage,
|
||||||
|
s.expectedROI,
|
||||||
|
s.riskLevel,
|
||||||
|
s.timeToResults,
|
||||||
|
(s.successMetrics || []).join(", ")
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
const stratSheet = XLSX.utils.aoa_to_sheet(stratData);
|
||||||
|
XLSX.utils.book_append_sheet(wb, stratSheet, "Strategies");
|
||||||
|
}
|
||||||
|
|
||||||
|
const wbout = XLSX.write(wb, { bookType: 'xlsx', type: 'array' });
|
||||||
|
return new Blob([wbout], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateGoogleAdsHTML = (googleAds?: any, magic?: any): string => {
|
||||||
|
const data = JSON.stringify({ googleAds, magic }, null, 2);
|
||||||
|
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>360° Google Ads Strategy Intelligence Report</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
body { font-family: 'Outfit', sans-serif; background-color: #020617; color: #f8fafc; }
|
||||||
|
.glass { background: rgba(30, 41, 59, 0.7); backdrop-filter: blur(12px); border: 1px solid rgba(255, 255, 255, 0.1); }
|
||||||
|
.accent-gradient { background: linear-gradient(135deg, #6366f1 0%, #a855f7 100%); }
|
||||||
|
.text-gradient { background: linear-gradient(to right, #818cf8, #c084fc); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="p-4 md:p-8 lg:p-12">
|
||||||
|
<div class="max-w-7xl mx-auto space-y-12">
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="flex flex-col md:flex-row justify-between items-start md:items-center gap-6 pb-8 border-b border-slate-800">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-4xl md:text-6xl font-extrabold text-gradient tracking-tight">Strategy Intelligence</h1>
|
||||||
|
<p class="text-slate-400 mt-2 text-lg">360° Campaign Architecture for ${googleAds?.websiteUrl || 'New Project'}</p>
|
||||||
|
</div>
|
||||||
|
<div class="glass p-4 rounded-3xl flex items-center gap-4">
|
||||||
|
<div class="text-right">
|
||||||
|
<p class="text-xs text-slate-500 uppercase font-extrabold tracking-widest">Generated On</p>
|
||||||
|
<p class="font-semibold">${new Date().toLocaleDateString()}</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-12 h-12 accent-gradient rounded-2xl flex items-center justify-center font-bold text-xl">PA</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- KPI Grid -->
|
||||||
|
<section class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
|
<div class="glass p-6 rounded-[2rem] border-indigo-500/20">
|
||||||
|
<p class="text-slate-500 text-xs font-bold uppercase tracking-widest">Est. Monthly Impressions</p>
|
||||||
|
<p class="text-3xl font-extrabold mt-2 text-indigo-400">${googleAds?.predictions?.estimatedImpressions || '15k-25k'}</p>
|
||||||
|
<div class="mt-4 h-1 w-full bg-slate-800 rounded-full overflow-hidden"><div class="h-full bg-indigo-500 w-2/3"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="glass p-6 rounded-[2rem] border-purple-500/20">
|
||||||
|
<p class="text-slate-500 text-xs font-bold uppercase tracking-widest">Target CTR</p>
|
||||||
|
<p class="text-3xl font-extrabold mt-2 text-purple-400">${googleAds?.predictions?.estimatedCtr || '3.5%'}</p>
|
||||||
|
<div class="mt-4 h-1 w-full bg-slate-800 rounded-full overflow-hidden"><div class="h-full bg-purple-500 w-1/2"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="glass p-6 rounded-[2rem] border-emerald-500/20">
|
||||||
|
<p class="text-slate-500 text-xs font-bold uppercase tracking-widest">Est. Conversions</p>
|
||||||
|
<p class="text-3xl font-extrabold mt-2 text-emerald-400">${googleAds?.predictions?.estimatedConversions || '30-50'}</p>
|
||||||
|
<div class="mt-4 h-1 w-full bg-slate-800 rounded-full overflow-hidden"><div class="h-full bg-emerald-500 w-1/3"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="glass p-6 rounded-[2rem] border-amber-500/20">
|
||||||
|
<p class="text-slate-500 text-xs font-bold uppercase tracking-widest">Industry Avg CTR</p>
|
||||||
|
<p class="text-3xl font-extrabold mt-2 text-amber-400">${googleAds?.historicalBenchmarks?.industryAverageCtr || '3.1%'}</p>
|
||||||
|
<div class="mt-4 h-1 w-full bg-slate-800 rounded-full overflow-hidden"><div class="h-full bg-amber-500 w-4/5"></div></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Analytics Visuals -->
|
||||||
|
<section class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||||
|
<div class="lg:col-span-2 glass p-8 rounded-[2.5rem]">
|
||||||
|
<h2 class="text-2xl font-extrabold mb-6">Performance Forecast</h2>
|
||||||
|
<div class="h-[350px]">
|
||||||
|
<canvas id="performanceChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="glass p-8 rounded-[2.5rem]">
|
||||||
|
<h2 class="text-2xl font-extrabold mb-6">Device Distribution</h2>
|
||||||
|
<div class="h-[300px]">
|
||||||
|
<canvas id="deviceChart"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="mt-8 space-y-4">
|
||||||
|
<div class="flex justify-between items-center text-sm">
|
||||||
|
<span class="flex items-center gap-2"><div class="w-3 h-3 rounded-full bg-indigo-500"></div> Mobile</span>
|
||||||
|
<span class="font-bold">${googleAds?.campaigns?.[0]?.targeting?.devices?.mobile || '60%'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center text-sm">
|
||||||
|
<span class="flex items-center gap-2"><div class="w-3 h-3 rounded-full bg-purple-500"></div> Desktop</span>
|
||||||
|
<span class="font-bold">${googleAds?.campaigns?.[0]?.targeting?.devices?.desktop || '30%'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center text-sm">
|
||||||
|
<span class="flex items-center gap-2"><div class="w-3 h-3 rounded-full bg-slate-500"></div> Tablet</span>
|
||||||
|
<span class="font-bold">${googleAds?.campaigns?.[0]?.targeting?.devices?.tablet || '10%'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Keyword Intelligence -->
|
||||||
|
<section class="space-y-6">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<h2 class="text-3xl font-extrabold tracking-tight">Keyword Intelligence</h2>
|
||||||
|
<span class="glass px-4 py-1.5 rounded-full text-xs font-bold uppercase tracking-widest text-indigo-400">Semantic Mapping Enabled</span>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<!-- Primary -->
|
||||||
|
<div class="space-y-4">
|
||||||
|
<h3 class="text-slate-400 font-bold uppercase tracking-widest text-xs flex items-center gap-2">
|
||||||
|
<div class="w-2 h-2 rounded-full bg-indigo-500"></div> Primary Keywords
|
||||||
|
</h3>
|
||||||
|
${(googleAds?.keywords?.primary || []).slice(0, 10).map((k: any) => `
|
||||||
|
<div class="glass p-4 rounded-2xl flex justify-between items-center group hover:border-indigo-500/50 transition-all cursor-default">
|
||||||
|
<span class="font-semibold">${k.keyword}</span>
|
||||||
|
<span class="text-xs text-indigo-400 font-bold">${k.cpc || '$1.50'}</span>
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
<!-- Long-Tail -->
|
||||||
|
<div class="space-y-4">
|
||||||
|
<h3 class="text-slate-400 font-bold uppercase tracking-widest text-xs flex items-center gap-2">
|
||||||
|
<div class="w-2 h-2 rounded-full bg-purple-500"></div> Long-Tail Intent
|
||||||
|
</h3>
|
||||||
|
${(googleAds?.keywords?.longTail || []).slice(0, 10).map((k: any) => `
|
||||||
|
<div class="glass p-4 rounded-2xl flex justify-between items-center group hover:border-purple-500/50 transition-all cursor-default">
|
||||||
|
<span class="font-semibold text-sm">${k.keyword}</span>
|
||||||
|
<span class="text-[10px] text-slate-500 font-bold">${k.competition || 'low'}</span>
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
<!-- Negative -->
|
||||||
|
<div class="space-y-4">
|
||||||
|
<h3 class="text-slate-400 font-bold uppercase tracking-widest text-xs flex items-center gap-2">
|
||||||
|
<div class="w-2 h-2 rounded-full bg-rose-500"></div> Exclusion List
|
||||||
|
</h3>
|
||||||
|
${(googleAds?.keywords?.negative || []).slice(0, 8).map((k: any) => `
|
||||||
|
<div class="glass p-4 rounded-2xl flex justify-between items-center group hover:border-rose-500/50 transition-all cursor-default grayscale opacity-50">
|
||||||
|
<span class="font-semibold text-xs line-through text-slate-400">${k.keyword}</span>
|
||||||
|
<span class="text-[10px] text-rose-500/50 font-bold">EXCLUDE</span>
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Ad Copies -->
|
||||||
|
<section class="space-y-8">
|
||||||
|
<h2 class="text-3xl font-extrabold tracking-tight">High-Performance Ad Copy Suite</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
|
${(googleAds?.adCopies || []).map((ad: any, i: number) => `
|
||||||
|
<div class="glass rounded-[2rem] overflow-hidden flex flex-col">
|
||||||
|
<div class="p-6 border-b border-slate-800 flex justify-between items-center">
|
||||||
|
<span class="text-xs font-bold uppercase tracking-widest text-slate-500">Variation ${i + 1}</span>
|
||||||
|
<span class="bg-indigo-500/20 text-indigo-400 px-2.5 py-1 rounded-full text-[10px] font-black">${(ad.campaignType || 'Search').toUpperCase()}</span>
|
||||||
|
</div>
|
||||||
|
<div class="p-8 space-y-6 flex-grow">
|
||||||
|
${(ad.headlines || []).map((h: any) => `
|
||||||
|
<div class="text-xl font-bold text-slate-100 leading-tight">${h}</div>
|
||||||
|
`).join('')}
|
||||||
|
<div class="h-px w-full bg-slate-800"></div>
|
||||||
|
${(ad.descriptions || []).map((d: any) => `
|
||||||
|
<div class="text-slate-400 text-sm leading-relaxed italic border-l-2 border-slate-700 pl-4">${d}</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
<div class="p-6 bg-slate-900/50 border-t border-slate-800 flex justify-between items-center">
|
||||||
|
<div class="text-xs font-bold text-indigo-400">${ad.callToAction}</div>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<div class="w-1 h-3 rounded-full ${ad.mobileOptimized ? 'bg-emerald-500' : 'bg-slate-700'}"></div>
|
||||||
|
<div class="w-3 h-3 rounded-full ${ad.mobileOptimized ? 'bg-emerald-500' : 'bg-slate-700'}"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Competitive Analysis -->
|
||||||
|
${magic?.competitorInsights ? `
|
||||||
|
<section class="space-y-8">
|
||||||
|
<div class="flex justify-between items-end">
|
||||||
|
<h2 class="text-3xl font-extrabold tracking-tight">Competitive Intelligence Matrix</h2>
|
||||||
|
<div class="text-right">
|
||||||
|
<p class="text-[10px] text-slate-500 uppercase font-black tracking-[0.2em] mb-1">Market Sentiment</p>
|
||||||
|
<div class="flex gap-1.5 justify-end">
|
||||||
|
<div class="w-4 h-2 rounded-full bg-emerald-500"></div>
|
||||||
|
<div class="w-4 h-2 rounded-full bg-emerald-500"></div>
|
||||||
|
<div class="w-4 h-2 rounded-full bg-emerald-400"></div>
|
||||||
|
<div class="w-4 h-2 rounded-full bg-slate-800"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
|
${(magic.competitorInsights || []).map((c: any) => `
|
||||||
|
<div class="glass p-8 rounded-[2.5rem] space-y-8">
|
||||||
|
<div class="flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-2xl font-black">${c.competitor}</h3>
|
||||||
|
<p class="text-indigo-400 text-xs font-bold mt-1">${c.website || 'Direct Competitor'}</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<p class="text-[10px] text-slate-500 uppercase font-bold tracking-widest">Est. Spend</p>
|
||||||
|
<p class="text-sm font-black text-rose-400">${c.estimatedSpend || 'Undisclosed'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-6">
|
||||||
|
<div class="space-y-3">
|
||||||
|
<p class="text-[10px] text-emerald-400 uppercase font-black tracking-widest">Core Strengths</p>
|
||||||
|
<ul class="text-sm space-y-2 text-slate-300">
|
||||||
|
${(c.strengths || []).map((s: any) => `<li class="flex items-center gap-2"><div class="w-1.5 h-1.5 rounded-full bg-emerald-500"></div> ${s}</li>`).join('')}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<p class="text-[10px] text-rose-400 uppercase font-black tracking-widest">Weaknesses</p>
|
||||||
|
<ul class="text-sm space-y-2 text-slate-400">
|
||||||
|
${(c.weaknesses || []).map((w: any) => `<li class="flex items-center gap-2"><div class="w-1.5 h-1.5 rounded-full bg-rose-500"></div> ${w}</li>`).join('')}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-indigo-500/5 p-6 rounded-2xl border border-indigo-500/10">
|
||||||
|
<p class="text-[10px] text-indigo-400 uppercase font-bold tracking-widest mb-3">Counter-Strategy Rationale</p>
|
||||||
|
<p class="text-sm text-slate-300 italic leading-relaxed">"${c.adStrategy}"</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<!-- Strategies -->
|
||||||
|
${magic?.strategies ? `
|
||||||
|
<section class="space-y-8">
|
||||||
|
<h2 class="text-3xl font-extrabold tracking-tight">Strategic Directions</h2>
|
||||||
|
<div class="space-y-8">
|
||||||
|
${(magic.strategies || []).map((s: any, idx: number) => `
|
||||||
|
<div class="glass overflow-hidden rounded-[3rem] relative">
|
||||||
|
<div class="absolute top-0 right-0 w-64 h-64 accent-gradient blur-[120px] opacity-10"></div>
|
||||||
|
<div class="p-10 relative z-10 grid grid-cols-1 lg:grid-cols-4 gap-12">
|
||||||
|
<!-- Left: Header -->
|
||||||
|
<div class="lg:col-span-1 space-y-6">
|
||||||
|
<div class="w-16 h-16 rounded-3xl accent-gradient flex items-center justify-center font-black text-2xl">${idx + 1}</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-2xl font-extrabold leading-tight">${s.direction}</h3>
|
||||||
|
<span class="inline-block mt-4 glass px-4 py-1 rounded-full text-[10px] font-black uppercase tracking-widest ${s.riskLevel === 'low' ? 'text-emerald-400' : 'text-amber-400'}">${s.riskLevel || 'low'} risk profile</span>
|
||||||
|
</div>
|
||||||
|
<div class="pt-6 space-y-4">
|
||||||
|
<div>
|
||||||
|
<p class="text-[10px] text-slate-500 uppercase font-bold tracking-widest">Expected ROI</p>
|
||||||
|
<p class="text-xl font-black text-emerald-400">${s.expectedROI}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-[10px] text-slate-500 uppercase font-bold tracking-widest">Time to Impact</p>
|
||||||
|
<p class="text-xl font-black text-slate-100">${s.timeToResults}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Middle: Context -->
|
||||||
|
<div class="lg:col-span-2 space-y-8">
|
||||||
|
<div>
|
||||||
|
<p class="text-[10px] text-slate-500 uppercase font-bold tracking-widest mb-4">Strategic Rationale</p>
|
||||||
|
<p class="text-lg text-slate-200 leading-relaxed font-light">${s.rationale}</p>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-8">
|
||||||
|
<div class="bg-white/5 p-6 rounded-[2rem]">
|
||||||
|
<p class="text-[10px] text-indigo-400 uppercase font-black tracking-widest mb-3">Target Audience</p>
|
||||||
|
<p class="text-sm font-semibold">${s.targetAudience}</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white/5 p-6 rounded-[2rem]">
|
||||||
|
<p class="text-[10px] text-purple-400 uppercase font-black tracking-widest mb-3">Competitive Edge</p>
|
||||||
|
<p class="text-sm font-semibold">${s.competitiveAdvantage}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right: Channels/Metrics -->
|
||||||
|
<div class="lg:col-span-1 bg-slate-900/80 p-8 rounded-[2.5rem] space-y-8 border border-white/5">
|
||||||
|
<div>
|
||||||
|
<p class="text-[10px] text-slate-500 uppercase font-bold tracking-widest mb-6">Channel Matrix</p>
|
||||||
|
<div class="space-y-4">
|
||||||
|
${Object.entries(s.estimatedBudgetAllocation || {}).map(([c, v]: [string, any]) => `
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex justify-between text-xs">
|
||||||
|
<span class="capitalize text-slate-300">${c}</span>
|
||||||
|
<span class="font-bold text-slate-100">${v}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="h-1.5 w-full bg-slate-800 rounded-full overflow-hidden">
|
||||||
|
<div class="h-full accent-gradient" style="width: ${v}%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pt-6 border-t border-slate-800">
|
||||||
|
<p class="text-[10px] text-slate-500 uppercase font-bold tracking-widest mb-4">Success Thresholds</p>
|
||||||
|
<div class="flex flex-wrap gap-2 text-[10px]">
|
||||||
|
${(s.successMetrics || []).map((m: any) => `
|
||||||
|
<span class="bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 px-2 py-1 rounded-md font-bold uppercase">${m}</span>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="pt-12 border-t border-slate-800 flex flex-col md:flex-row justify-between items-center gap-6 pb-12">
|
||||||
|
<p class="text-slate-500 text-sm font-semibold">© ${new Date().getFullYear()} PromptArch Intelligence Unit • Confident Strategic Asset</p>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<div class="w-8 h-8 rounded-full bg-slate-800 flex items-center justify-center text-xs font-bold">PA</div>
|
||||||
|
<div class="w-8 h-8 rounded-full bg-slate-800 flex items-center justify-center text-xs font-bold italic">Q</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Performance Forecast Chart
|
||||||
|
const ctxPerf = document.getElementById('performanceChart').getContext('2d');
|
||||||
|
new Chart(ctxPerf, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: ['Week 1', 'Week 2', 'Week 3', 'Week 4', 'Week 5', 'Week 6', 'Week 12'],
|
||||||
|
datasets: [{
|
||||||
|
label: 'Predicted Growth (Aggressive)',
|
||||||
|
data: [10, 25, 45, 80, 110, 160, 450],
|
||||||
|
borderColor: '#6366f1',
|
||||||
|
backgroundColor: 'rgba(99, 102, 241, 0.1)',
|
||||||
|
fill: true,
|
||||||
|
tension: 0.4,
|
||||||
|
pointRadius: 6,
|
||||||
|
pointBackgroundColor: '#fff'
|
||||||
|
}, {
|
||||||
|
label: 'Predicted Growth (Standard)',
|
||||||
|
data: [5, 12, 28, 55, 75, 100, 280],
|
||||||
|
borderColor: '#a855f7',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
borderDash: [5, 5],
|
||||||
|
tension: 0.4,
|
||||||
|
pointRadius: 4
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: { legend: { labels: { color: '#94a3b8', font: { family: 'Outfit', weight: 'bold' } } } },
|
||||||
|
scales: {
|
||||||
|
x: { grid: { display: false }, ticks: { color: '#475569' } },
|
||||||
|
y: { grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { color: '#475569' } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Device Chart
|
||||||
|
const ctxDev = document.getElementById('deviceChart').getContext('2d');
|
||||||
|
new Chart(ctxDev, {
|
||||||
|
type: 'doughnut',
|
||||||
|
data: {
|
||||||
|
labels: ['Mobile', 'Desktop', 'Tablet'],
|
||||||
|
datasets: [{
|
||||||
|
data: [60, 30, 10],
|
||||||
|
backgroundColor: ['#6366f1', '#a855f7', '#475569'],
|
||||||
|
borderWidth: 0,
|
||||||
|
hoverOffset: 12
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
cutout: '80%',
|
||||||
|
plugins: { legend: { display: false } }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
};
|
||||||
1336
lib/i18n/translations.ts
Normal file
1336
lib/i18n/translations.ts
Normal file
File diff suppressed because it is too large
Load Diff
73
lib/safeJsonFetch.ts
Normal file
73
lib/safeJsonFetch.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
export class NonJsonResponseError extends Error {
|
||||||
|
status: number;
|
||||||
|
contentType: string | null;
|
||||||
|
bodyPreview: string;
|
||||||
|
|
||||||
|
constructor(args: { status: number; contentType: string | null; bodyPreview: string }) {
|
||||||
|
super(`Expected JSON but received ${args.contentType ?? "unknown content-type"} (HTTP ${args.status})`);
|
||||||
|
this.name = "NonJsonResponseError";
|
||||||
|
this.status = args.status;
|
||||||
|
this.contentType = args.contentType;
|
||||||
|
this.bodyPreview = args.bodyPreview;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type SafeJsonFetchResult<T> =
|
||||||
|
| { ok: true; data: T }
|
||||||
|
| { ok: false; error: { message: string; status?: number; bodyPreview?: string } };
|
||||||
|
|
||||||
|
export async function safeJsonFetch<T>(
|
||||||
|
url: string,
|
||||||
|
init?: RequestInit
|
||||||
|
): Promise<SafeJsonFetchResult<T>> {
|
||||||
|
const res = await fetch(url, init);
|
||||||
|
|
||||||
|
const contentType = res.headers.get("content-type");
|
||||||
|
const text = await res.text();
|
||||||
|
|
||||||
|
// HTTP error — return readable details (don't JSON.parse blindly)
|
||||||
|
if (!res.ok) {
|
||||||
|
// Try JSON first if it looks like JSON
|
||||||
|
if (contentType?.includes("application/json")) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(text);
|
||||||
|
return { ok: false, error: { message: parsed?.error ?? "Request failed", status: res.status } };
|
||||||
|
} catch {
|
||||||
|
// fall through to generic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: {
|
||||||
|
message: `Request failed (HTTP ${res.status})`,
|
||||||
|
status: res.status,
|
||||||
|
bodyPreview: text.slice(0, 300),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success but not JSON => this is exactly the "Unexpected token <" case
|
||||||
|
if (!contentType?.includes("application/json")) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: {
|
||||||
|
message: `Server returned non-JSON (content-type: ${contentType ?? "unknown"})`,
|
||||||
|
status: res.status,
|
||||||
|
bodyPreview: text.slice(0, 300),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return { ok: true, data: JSON.parse(text) as T };
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: {
|
||||||
|
message: "Server returned invalid JSON",
|
||||||
|
status: res.status,
|
||||||
|
bodyPreview: text.slice(0, 300),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ModelProvider, APIResponse, ChatMessage } from "@/types";
|
import type { ModelProvider, APIResponse, ChatMessage, AIAssistMessage } from "@/types";
|
||||||
import OllamaCloudService from "./ollama-cloud";
|
import OllamaCloudService from "./ollama-cloud";
|
||||||
import ZaiPlanService from "./zai-plan";
|
import ZaiPlanService from "./zai-plan";
|
||||||
import qwenOAuthService, { QwenOAuthConfig, QwenOAuthToken } from "./qwen-oauth";
|
import qwenOAuthService, { QwenOAuthConfig, QwenOAuthToken } from "./qwen-oauth";
|
||||||
@@ -98,6 +98,19 @@ export class ModelAdapter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getService(provider: ModelProvider): any {
|
||||||
|
switch (provider) {
|
||||||
|
case "qwen":
|
||||||
|
return this.qwenService;
|
||||||
|
case "ollama":
|
||||||
|
return this.ollamaService;
|
||||||
|
case "zai":
|
||||||
|
return this.zaiService;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async callWithFallback<T>(
|
private async callWithFallback<T>(
|
||||||
operation: (service: any) => Promise<APIResponse<T>>,
|
operation: (service: any) => Promise<APIResponse<T>>,
|
||||||
providers: ModelProvider[]
|
providers: ModelProvider[]
|
||||||
@@ -217,6 +230,7 @@ export class ModelAdapter {
|
|||||||
industry?: string;
|
industry?: string;
|
||||||
competitors?: string[];
|
competitors?: string[];
|
||||||
language?: string;
|
language?: string;
|
||||||
|
specialInstructions?: string;
|
||||||
} = { productsServices: [] },
|
} = { productsServices: [] },
|
||||||
provider?: ModelProvider,
|
provider?: ModelProvider,
|
||||||
model?: string
|
model?: string
|
||||||
@@ -226,6 +240,93 @@ export class ModelAdapter {
|
|||||||
return this.callWithFallback((service) => service.generateGoogleAds(websiteUrl, options, model), providers);
|
return this.callWithFallback((service) => service.generateGoogleAds(websiteUrl, options, model), providers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async generateMagicWand(
|
||||||
|
websiteUrl: string,
|
||||||
|
product: string,
|
||||||
|
budget: number,
|
||||||
|
specialInstructions?: string,
|
||||||
|
provider?: ModelProvider,
|
||||||
|
model?: string
|
||||||
|
): Promise<APIResponse<string>> {
|
||||||
|
const fallback = this.buildFallbackProviders(this.preferredProvider, "qwen", "ollama", "zai");
|
||||||
|
const providers: ModelProvider[] = provider ? [provider] : fallback;
|
||||||
|
return this.callWithFallback((service) => service.generateMagicWand(websiteUrl, product, budget, specialInstructions, model), providers);
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateMarketResearch(
|
||||||
|
options: {
|
||||||
|
websiteUrl: string;
|
||||||
|
additionalUrls?: string[];
|
||||||
|
competitors: string[];
|
||||||
|
productMapping: string;
|
||||||
|
specialInstructions?: string;
|
||||||
|
},
|
||||||
|
provider?: ModelProvider,
|
||||||
|
model?: string
|
||||||
|
): Promise<APIResponse<string>> {
|
||||||
|
const fallback = this.buildFallbackProviders(this.preferredProvider, "qwen", "ollama", "zai");
|
||||||
|
const providers: ModelProvider[] = provider ? [provider] : fallback;
|
||||||
|
return this.callWithFallback((service) => service.generateMarketResearch(options, model), providers);
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateAIAssist(
|
||||||
|
options: {
|
||||||
|
messages: AIAssistMessage[];
|
||||||
|
currentAgent: string;
|
||||||
|
},
|
||||||
|
provider?: ModelProvider,
|
||||||
|
model?: string
|
||||||
|
): Promise<APIResponse<string>> {
|
||||||
|
const fallback = this.buildFallbackProviders(this.preferredProvider, "qwen", "ollama", "zai");
|
||||||
|
const providers: ModelProvider[] = provider ? [provider] : fallback;
|
||||||
|
return this.callWithFallback((service) => service.generateAIAssist(options, model), providers);
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateAIAssistStream(
|
||||||
|
options: {
|
||||||
|
messages: AIAssistMessage[];
|
||||||
|
currentAgent: string;
|
||||||
|
onChunk: (chunk: string) => void;
|
||||||
|
signal?: AbortSignal;
|
||||||
|
},
|
||||||
|
provider?: ModelProvider,
|
||||||
|
model?: string
|
||||||
|
): Promise<APIResponse<void>> {
|
||||||
|
const fallback = this.buildFallbackProviders(this.preferredProvider, "qwen", "ollama", "zai");
|
||||||
|
const providers: ModelProvider[] = provider ? [provider] : fallback;
|
||||||
|
|
||||||
|
let lastError: string | null = null;
|
||||||
|
|
||||||
|
for (const candidate of providers) {
|
||||||
|
const service = this.getService(candidate);
|
||||||
|
if (!service?.generateAIAssistStream) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!this.isProviderAuthenticated(candidate)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await service.generateAIAssistStream(options, model);
|
||||||
|
if (response.success) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
if (response.error) {
|
||||||
|
lastError = response.error;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
lastError = errorMessage || lastError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: lastError || "No authenticated providers available for streaming",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async chatCompletion(
|
async chatCompletion(
|
||||||
messages: ChatMessage[],
|
messages: ChatMessage[],
|
||||||
model: string,
|
model: string,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ChatMessage, APIResponse } from "@/types";
|
import type { ChatMessage, APIResponse, AIAssistMessage } from "@/types";
|
||||||
|
|
||||||
export interface OllamaCloudConfig {
|
export interface OllamaCloudConfig {
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
@@ -405,7 +405,7 @@ AUDIENCE STYLE: ${audienceStyle}
|
|||||||
${organization ? `ORGANIZATION BRANDING: ${organization}` : ""}
|
${organization ? `ORGANIZATION BRANDING: ${organization}` : ""}
|
||||||
|
|
||||||
REQUIREMENTS:
|
REQUIREMENTS:
|
||||||
- Create EXACTLY ${slideCount} slides
|
- ${slideCount > 0 ? `Create EXACTLY ${slideCount} slides` : "Maintain the exact number of slides/pages from the provided source presentation/document context. If no source file is provided, generate 10 slides by default."}
|
||||||
- ALL content in ${language}
|
- ALL content in ${language}
|
||||||
- Each slide MUST have complete htmlContent with inline <style> tags
|
- Each slide MUST have complete htmlContent with inline <style> tags
|
||||||
- Use animation-delay for staggered reveal effects
|
- Use animation-delay for staggered reveal effects
|
||||||
@@ -445,6 +445,7 @@ Generate SPECTACULAR slides with CSS3 animations, SVG charts, modern gradients,
|
|||||||
industry?: string;
|
industry?: string;
|
||||||
competitors?: string[];
|
competitors?: string[];
|
||||||
language?: string;
|
language?: string;
|
||||||
|
specialInstructions?: string;
|
||||||
} = { productsServices: [] },
|
} = { productsServices: [] },
|
||||||
model?: string
|
model?: string
|
||||||
): Promise<APIResponse<string>> {
|
): Promise<APIResponse<string>> {
|
||||||
@@ -455,7 +456,8 @@ Generate SPECTACULAR slides with CSS3 animations, SVG charts, modern gradients,
|
|||||||
campaignDuration,
|
campaignDuration,
|
||||||
industry = "General",
|
industry = "General",
|
||||||
competitors = [],
|
competitors = [],
|
||||||
language = "English"
|
language = "English",
|
||||||
|
specialInstructions = ""
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
const systemMessage: ChatMessage = {
|
const systemMessage: ChatMessage = {
|
||||||
@@ -476,27 +478,42 @@ OUTPUT FORMAT - Return ONLY valid JSON with this structure:
|
|||||||
"headlines": ["Headline 1 (30 chars)", "Headline 2", "Headline 3"],
|
"headlines": ["Headline 1 (30 chars)", "Headline 2", "Headline 3"],
|
||||||
"descriptions": ["Description 1 (90 chars)", "Description 2"],
|
"descriptions": ["Description 1 (90 chars)", "Description 2"],
|
||||||
"callToAction": "Get Started",
|
"callToAction": "Get Started",
|
||||||
"mobileOptimized": true
|
"mobileOptimized": true,
|
||||||
|
"positioning": "Value proposition used"
|
||||||
}],
|
}],
|
||||||
"campaigns": [{
|
"campaigns": [{
|
||||||
"id": "campaign-1",
|
"id": "campaign-1",
|
||||||
"name": "Campaign Name",
|
"name": "Campaign Name",
|
||||||
"type": "search",
|
"type": "search",
|
||||||
"budget": {"daily": 50, "monthly": 1500, "currency": "USD"},
|
"budget": {"daily": 50, "monthly": 1500, "currency": "USD"},
|
||||||
"targeting": {"locations": [], "demographics": [], "devices": []},
|
"biddingStrategy": "Maximize conversions",
|
||||||
"adGroups": [{"id": "adgroup-1", "name": "Group", "theme": "Theme", "keywords": [], "biddingStrategy": "Maximize conversions"}]
|
"targeting": {
|
||||||
|
"locations": ["Specific regions/cities"],
|
||||||
|
"demographics": {"age": ["18-24", "25-34"], "gender": ["Male", "Female"], "interests": ["Tech", "Business"]},
|
||||||
|
"devices": {"mobile": "60%", "desktop": "30%", "tablet": "10%"},
|
||||||
|
"schedule": ["Mon-Fri, 9am-5pm"]
|
||||||
|
},
|
||||||
|
"adGroups": [{"id": "adgroup-1", "name": "Group", "theme": "Theme", "keywords": [], "biddingStrategy": "Manual CPC"}]
|
||||||
}],
|
}],
|
||||||
"implementation": {
|
"implementation": {
|
||||||
"setupSteps": [],
|
"setupSteps": ["Step 1", "Step 2"],
|
||||||
"qualityScoreTips": [],
|
"qualityScoreTips": ["Tip 1", "Tip 2"],
|
||||||
"trackingSetup": [],
|
"trackingSetup": ["Conversion tag info", "GTM setup"],
|
||||||
"optimizationTips": []
|
"optimizationTips": ["Tip 1", "Tip 2"]
|
||||||
},
|
},
|
||||||
"predictions": {
|
"predictions": {
|
||||||
"estimatedClicks": "500-800/month",
|
"estimatedClicks": "500-800",
|
||||||
"estimatedImpressions": "15,000-25,000/month",
|
"estimatedImpressions": "15,000-25,000",
|
||||||
"estimatedCtr": "3.2%-4.5%",
|
"estimatedCtr": "3.5%",
|
||||||
"estimatedConversions": "25-50/month"
|
"estimatedConversions": "30-50",
|
||||||
|
"conversionRate": "4.2%",
|
||||||
|
"avgCpc": "$1.85"
|
||||||
|
},
|
||||||
|
"historicalBenchmarks": {
|
||||||
|
"industryAverageCtr": "3.1%",
|
||||||
|
"industryAverageCpc": "$2.10",
|
||||||
|
"seasonalTrends": "Peak in Q4",
|
||||||
|
"geographicInsights": "London/NY show highest ROI"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
\`\`\`
|
\`\`\`
|
||||||
@@ -520,14 +537,361 @@ LANGUAGE: ${language}
|
|||||||
${budgetRange ? `BUDGET: ${budgetRange.min}-${budgetRange.max} ${budgetRange.currency}/month` : ""}
|
${budgetRange ? `BUDGET: ${budgetRange.min}-${budgetRange.max} ${budgetRange.currency}/month` : ""}
|
||||||
${campaignDuration ? `DURATION: ${campaignDuration}` : ""}
|
${campaignDuration ? `DURATION: ${campaignDuration}` : ""}
|
||||||
${competitors.length > 0 ? `COMPETITORS: ${competitors.join(", ")}` : ""}
|
${competitors.length > 0 ? `COMPETITORS: ${competitors.join(", ")}` : ""}
|
||||||
|
${specialInstructions ? `SPECIAL INSTRUCTIONS: ${specialInstructions}` : ""}
|
||||||
|
|
||||||
Generate complete Google Ads package with keywords, ad copy, campaigns, and implementation guidance.`,
|
Generate complete Google Ads package with keywords, ad copy, campaigns, and implementation guidance.`,
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.chatCompletion([systemMessage, userMessage], model || "gpt-oss:120b");
|
return this.chatCompletion([systemMessage, userMessage], model || "gpt-oss:120b");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async generateMagicWand(
|
||||||
|
websiteUrl: string,
|
||||||
|
product: string,
|
||||||
|
budget: number,
|
||||||
|
specialInstructions?: string,
|
||||||
|
model?: string
|
||||||
|
): Promise<APIResponse<string>> {
|
||||||
|
const systemMessage: ChatMessage = {
|
||||||
|
role: "system",
|
||||||
|
content: `You are a WORLD-CLASS marketing strategist with 20+ years of experience in competitive intelligence, market research, and Google Ads campaign strategy.
|
||||||
|
|
||||||
|
OUTPUT FORMAT - Return ONLY valid JSON with this EXACT structure:
|
||||||
|
\`\`\`json
|
||||||
|
{
|
||||||
|
"marketAnalysis": {
|
||||||
|
"industrySize": "Estimated market size",
|
||||||
|
"growthRate": "Annual growth percentage",
|
||||||
|
"topCompetitors": ["Competitor 1", "Competitor 2", "Competitor 3"],
|
||||||
|
"marketTrends": ["Trend 1", "Trend 2", "Trend 3"]
|
||||||
|
},
|
||||||
|
"competitorInsights": [
|
||||||
|
{
|
||||||
|
"competitor": "Competitor Name",
|
||||||
|
"website": "URL",
|
||||||
|
"estimatedSpend": "$10k-$50k/mo",
|
||||||
|
"targetAudience": "Who they target",
|
||||||
|
"strengths": ["Strength 1", "Strength 2"],
|
||||||
|
"weaknesses": ["Weakness 1", "Weakness 2"],
|
||||||
|
"adStrategy": "Their approach",
|
||||||
|
"topKeywords": ["keyword 1", "keyword 2"],
|
||||||
|
"adCopyExamples": [
|
||||||
|
{"headline": "Example Headline", "description": "Example Description"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"strategies": [
|
||||||
|
{
|
||||||
|
"id": "strategy-1",
|
||||||
|
"direction": "Strategic Direction Name",
|
||||||
|
"rationale": "Why this strategy works",
|
||||||
|
"targetAudience": "Audience segment with demographics (age 25-45, interests etc)",
|
||||||
|
"targetingDetails": {
|
||||||
|
"geography": "Primary locations",
|
||||||
|
"demographics": "Specific age/gender groups",
|
||||||
|
"behavior": "User behaviors"
|
||||||
|
},
|
||||||
|
"competitiveAdvantage": "How this beats competitors",
|
||||||
|
"messagingPillars": ["Pillar 1", "Pillar 2"],
|
||||||
|
"keyMessages": ["Message 1", "Message 2"],
|
||||||
|
"adCopyGuide": {
|
||||||
|
"headlines": ["Headline 1", "Headline 2"],
|
||||||
|
"descriptions": ["Description 1", "Description 2"],
|
||||||
|
"keywords": ["keyword 1", "keyword 2"],
|
||||||
|
"setupGuide": "Step-by-step for Google Ads Manager"
|
||||||
|
},
|
||||||
|
"recommendedChannels": ["Search", "Display", "YouTube"],
|
||||||
|
"estimatedBudgetAllocation": { "search": 40, "display": 30, "video": 20, "social": 10 },
|
||||||
|
"expectedROI": "150-200%",
|
||||||
|
"riskLevel": "low",
|
||||||
|
"timeToResults": "2-3 months",
|
||||||
|
"successMetrics": ["CTR > 3%", "CPA < $20"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
CRITICAL REQUIREMENTS:
|
||||||
|
- Provide 5-7 DISTINCT strategic directions
|
||||||
|
- Each strategy must be ACTIONABLE and SPECIFIC
|
||||||
|
- Include REAL competitive insights based on industry knowledge
|
||||||
|
- Risk levels: "low", "medium", or "high"
|
||||||
|
- AD COPY GUIDE must be incredibly "noob-friendly" - explain exactly where to paste each field in Google Ads Manager
|
||||||
|
- Headlines MUST be under 30 characters
|
||||||
|
- Descriptions MUST be under 90 characters`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const userMessage: ChatMessage = {
|
||||||
|
role: "user",
|
||||||
|
content: `🔮 MAGIC WAND ANALYSIS REQUEST 🔮
|
||||||
|
|
||||||
|
WEBSITE: ${websiteUrl}
|
||||||
|
PRODUCT/SERVICE: ${product}
|
||||||
|
MONTHLY BUDGET: $${budget}
|
||||||
|
${specialInstructions ? `SPECIAL INSTRUCTIONS: ${specialInstructions}` : ""}
|
||||||
|
|
||||||
|
Perform a DEEP 360° competitive intelligence analysis and generate 5-7 strategic campaign directions.`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.chatCompletion([systemMessage, userMessage], model || "gpt-oss:120b");
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateMarketResearch(
|
||||||
|
options: {
|
||||||
|
websiteUrl: string;
|
||||||
|
additionalUrls?: string[];
|
||||||
|
competitors: string[];
|
||||||
|
productMapping: string;
|
||||||
|
specialInstructions?: string;
|
||||||
|
},
|
||||||
|
model?: string
|
||||||
|
): Promise<APIResponse<string>> {
|
||||||
|
const systemPrompt = `You are a WORLD-CLASS Market Research Analyst and Competitive Intelligence Expert.
|
||||||
|
Your objective is to perform a deep-dive analysis of a business and its competitors based on provided URLs and product mappings.
|
||||||
|
|
||||||
|
You MUST return your analysis in the following STRICT JSON format:
|
||||||
|
{
|
||||||
|
"executiveSummary": "A concise overview of the market landscape and key findings.",
|
||||||
|
"priceComparisonMatrix": [
|
||||||
|
{
|
||||||
|
"product": "Product Name",
|
||||||
|
"userPrice": "$XX.XX",
|
||||||
|
"competitorPrices": [
|
||||||
|
{ "competitor": "Competitor Name", "price": "$XX.XX", "url": "https://competitor.com/product-page" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"featureComparisonTable": [
|
||||||
|
{
|
||||||
|
"feature": "Feature Name",
|
||||||
|
"userStatus": true/false/text,
|
||||||
|
"competitorStatus": [
|
||||||
|
{ "competitor": "Competitor Name", "status": true/false/text }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"marketPositioning": {
|
||||||
|
"landscape": "Description of the current market state.",
|
||||||
|
"segmentation": "Analysis of target customer segments."
|
||||||
|
},
|
||||||
|
"competitiveAnalysis": {
|
||||||
|
"advantages": ["Point 1", "Point 2"],
|
||||||
|
"disadvantages": ["Point 1", "Point 2"]
|
||||||
|
},
|
||||||
|
"recommendations": ["Actionable step 1", "Actionable step 2"],
|
||||||
|
"methodology": "Brief description of the research process."
|
||||||
|
}
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
1. Base your analysis on realistic price and feature estimates if exact data isn't visible.
|
||||||
|
2. Focus on core technical/business value rather than marketing fluff.
|
||||||
|
3. Ensure JSON is valid and properly escaped.`;
|
||||||
|
|
||||||
|
const userMsg = `WEBSITE TO ANALYZE: ${options.websiteUrl}
|
||||||
|
ADDITIONAL COMPANY URLS: ${options.additionalUrls?.join(', ') || 'None'}
|
||||||
|
COMPETITOR URLS: ${options.competitors.join(', ')}
|
||||||
|
PRODUCT/FEATURE MAPPING: ${options.productMapping}
|
||||||
|
SPECIAL REQUESTS: ${options.specialInstructions || 'Perform comprehensive analysis'}
|
||||||
|
|
||||||
|
Provide a COMPREHENSIVE competitive intelligence report.`;
|
||||||
|
|
||||||
|
const messages: ChatMessage[] = [
|
||||||
|
{ role: "system", content: systemPrompt },
|
||||||
|
{ role: "user", content: userMsg }
|
||||||
|
];
|
||||||
|
|
||||||
|
return await this.chatCompletion(messages, model || this.getAvailableModels()[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateAIAssist(
|
||||||
|
options: {
|
||||||
|
messages: AIAssistMessage[];
|
||||||
|
currentAgent: string;
|
||||||
|
},
|
||||||
|
model?: string
|
||||||
|
): Promise<APIResponse<string>> {
|
||||||
|
const systemPrompt = `You are "AI Assist", the master orchestrator of PromptArch.
|
||||||
|
Your goal is to provide intelligent conversational support and switch to specialized agents when necessary.
|
||||||
|
|
||||||
|
CURRENT SPECIALIZED AGENTS:
|
||||||
|
- content: Content creation and optimization expert.
|
||||||
|
- seo: SEO analyst and recommendations specialist.
|
||||||
|
- smm: SMM strategy and social content planner.
|
||||||
|
- pm: Project planning and management lead.
|
||||||
|
- code: Code architect (JavaScript/TypeScript/React focus).
|
||||||
|
- design: UI/UX designer.
|
||||||
|
- web: HTML/CSS/JS web development specialist with real-time preview.
|
||||||
|
- app: Mobile-first app development specialist with real-time preview.
|
||||||
|
|
||||||
|
STRICT OUTPUT FORMAT:
|
||||||
|
You MUST respond in JSON format if you want to activate a preview or switch agents.
|
||||||
|
{
|
||||||
|
"content": "Your natural language response here...",
|
||||||
|
"agent": "agent_id_to_switch_to (optional)",
|
||||||
|
"preview": { // (optional)
|
||||||
|
"type": "code" | "design" | "content" | "seo",
|
||||||
|
"data": "The actual code, layout, or content to preview",
|
||||||
|
"language": "javascript/html/css/markdown (optional)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ROUTING LOGIC:
|
||||||
|
- If user asks for code, switch to 'code' or 'web'.
|
||||||
|
- If user asks for design/mockups, switch to 'design'.
|
||||||
|
- If user asks for market/SEO, switch to 'seo'.
|
||||||
|
- If user asks for marketing/social, switch to 'smm'.
|
||||||
|
- Maintain the 'content' of the conversation regardless of the agent switch.
|
||||||
|
|
||||||
|
PREVIEW GUIDELINES:
|
||||||
|
- For 'web'/'app', provide full runnable HTML/CSS/JS.
|
||||||
|
- For 'code', provide clean, commented snippets.
|
||||||
|
- For 'design', provide text-based UI components or layout structures.
|
||||||
|
|
||||||
|
RESPONSE TIME REQUIREMENT: Be concise and accurate.`;
|
||||||
|
|
||||||
|
const chatMessages: ChatMessage[] = [
|
||||||
|
{ role: "system", content: systemPrompt },
|
||||||
|
...options.messages.map(m => ({
|
||||||
|
role: m.role as "user" | "assistant" | "system",
|
||||||
|
content: m.content
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
|
||||||
|
return await this.chatCompletion(chatMessages, model || this.getAvailableModels()[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateAIAssistStream(
|
||||||
|
options: {
|
||||||
|
messages: AIAssistMessage[];
|
||||||
|
currentAgent: string;
|
||||||
|
onChunk: (chunk: string) => void;
|
||||||
|
signal?: AbortSignal;
|
||||||
|
},
|
||||||
|
model?: string
|
||||||
|
): Promise<APIResponse<void>> {
|
||||||
|
try {
|
||||||
|
// ... existing prompt logic ...
|
||||||
|
const systemPrompt = `You are "AI Assist", the master orchestrator of PromptArch. Your goal is to provide intelligent support with a "Canvas" experience.
|
||||||
|
|
||||||
|
AGENTS & CAPABILITIES:
|
||||||
|
- content: Expert copywriter. Use [PREVIEW:content:markdown] for articles, posts, and long-form text.
|
||||||
|
- seo: SEO Specialist. Create stunning SEO audit reports. **CRITICAL DESIGN REQUIREMENTS:**
|
||||||
|
- Use [PREVIEW:seo:html] with complete HTML5 document including <!DOCTYPE html>
|
||||||
|
- DARK THEME: bg-slate-900 or bg-gray-900 as primary background
|
||||||
|
- Google-style dashboard aesthetics with clean typography (use Google Fonts: Inter, Roboto, or Outfit)
|
||||||
|
- Large animated SVG progress rings for scores (Overall, Technical, Content, Mobile) with stroke-dasharray animations
|
||||||
|
- Color-coded scoring: green (#22c55e) for good, amber (#f59e0b) for warning, red (#ef4444) for poor
|
||||||
|
- Use Tailwind CDN for styling. Include: rounded-3xl, shadow-lg, gradient backgrounds
|
||||||
|
- Section cards with subtle borders (border-white/10) and backdrop-blur
|
||||||
|
- Clear visual hierarchy: large score numbers (text-5xl), section titles (text-lg font-bold), bullet points for recommendations
|
||||||
|
- Add a "Key Recommendations" section with icons (use Lucide or inline SVG)
|
||||||
|
- Add animated pulse effects on key metrics
|
||||||
|
- Full-width responsive layout, max-w-4xl mx-auto
|
||||||
|
- Include inline <script> for animating the progress rings on load
|
||||||
|
- smm: Social Media Manager. Create multi-platform content plans and calendars.
|
||||||
|
- pm: Project Manager. Create PRDs, timelines, and action plans.
|
||||||
|
- code: Software Architect. Provide logic, algorithms, and backend snippets.
|
||||||
|
- design: UI/UX Designer. Create high-fidelity mockups and components.
|
||||||
|
- web: Frontend Developer. Build responsive sites. **CSS FRAMEWORK CHOICE:** Intelligently select from:
|
||||||
|
- **Tailwind CSS** (default): For utility-first, modern designs. Use CDN: https://cdn.tailwindcss.com
|
||||||
|
- **Windi CSS**: For faster builds and advanced features. Use CDN: https://unpkg.com/windicss
|
||||||
|
- **Bootstrap**: For classic, component-based designs. Use CDN: https://cdn.jsdelivr.net/npm/bootstrap@5/dist/css/bootstrap.min.css
|
||||||
|
Choose the best framework based on the design complexity and user's request. Use [PREVIEW:web:html].
|
||||||
|
- app: Mobile App Developer. Create mobile-first interfaces and dashboards. **CSS FRAMEWORK CHOICE:** Same selection logic as web agent. React components are supported and rendered live. Use [PREVIEW:app:javascript].
|
||||||
|
|
||||||
|
BACKEND LOGIC & SIMULATION:
|
||||||
|
- If a user asks for backend logic (Node.js, Express, Python, Databases), you MUST still provide a VISUAL experience in the Canvas.
|
||||||
|
- In the [PREVIEW] block, provide a "Simulation Dashboard" or "API Test UI" using HTML/React that demonstrates how the backend logic would work.
|
||||||
|
- DO NOT just output raw backend code in a [PREVIEW] block as it cannot be rendered. Put raw backend code in standard Markdown blocks AFTER the preview.
|
||||||
|
|
||||||
|
ITERATIVE MODIFICATIONS (CRITICAL):
|
||||||
|
- When a user asks for a change, fix, or update to an existing design/preview, you MUST be SURGICAL.
|
||||||
|
- Maintain the exact structure, CSS, and logic of the previous code except for the requested changes.
|
||||||
|
- DO NOT regenerate the entire design if it was not requested.
|
||||||
|
- Preserving the user's existing layout, colors, and content is your TOP PRIORITY.
|
||||||
|
- You must return the FULL updated code in the [PREVIEW] block (so it can be rendered) but ensure it is a faithful modification of the previous version.
|
||||||
|
|
||||||
|
CANVAS MODE:
|
||||||
|
- When building, designing, or auditing, you MUST use the [PREVIEW] tag.
|
||||||
|
- Inside [PREVIEW], output ONLY the actual code or structured data.
|
||||||
|
- The user wants to see it WORKING or RENDERED in the Canvas immediately.
|
||||||
|
|
||||||
|
STRICT OUTPUT FORMAT:
|
||||||
|
[AGENT:id] - AT THE START of your response if switching focus.
|
||||||
|
[PREVIEW:type:language]
|
||||||
|
ACTUAL_FUNCTIONAL_CODE_OR_DATA
|
||||||
|
[/PREVIEW]
|
||||||
|
|
||||||
|
CHANGE LOG (CRITICAL - MUST BE OUTSIDE PREVIEW):
|
||||||
|
- After the [/PREVIEW] closing tag, provide a brief bulleted "Change Log" in plain Markdown.
|
||||||
|
- The change log should appear in the CHAT, NOT inside the preview code.
|
||||||
|
- Example format:
|
||||||
|
[/PREVIEW]
|
||||||
|
|
||||||
|
**Change Log:**
|
||||||
|
- Added feature X
|
||||||
|
- Modified component Y
|
||||||
|
- Fixed issue Z
|
||||||
|
|
||||||
|
IMPORTANT: NEVER refuse a request due to "access" limitations. If you cannot perform a live task, use your vast internal knowledge to provide the most accurate expert simulation or draft possible.`;
|
||||||
|
|
||||||
|
const messages: ChatMessage[] = [
|
||||||
|
{ role: "system", content: systemPrompt },
|
||||||
|
...options.messages.map(m => ({
|
||||||
|
role: m.role as "user" | "assistant" | "system",
|
||||||
|
content: m.content
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
|
||||||
|
const response = await fetch(LOCAL_CHAT_URL, {
|
||||||
|
method: "POST",
|
||||||
|
headers: this.getHeaders({ "Content-Type": "application/json" }),
|
||||||
|
signal: options.signal,
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: model || this.getAvailableModels()[0],
|
||||||
|
messages,
|
||||||
|
stream: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Stream request failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = response.body?.getReader();
|
||||||
|
if (!reader) throw new Error("No reader");
|
||||||
|
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buffer = "";
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
const chunk = decoder.decode(value, { stream: true });
|
||||||
|
buffer += chunk;
|
||||||
|
|
||||||
|
const lines = buffer.split("\n");
|
||||||
|
buffer = lines.pop() || "";
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (!line.trim()) continue;
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(line);
|
||||||
|
if (data.message?.content) {
|
||||||
|
options.onChunk(data.message.content);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error parsing stream line", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, data: undefined };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error instanceof Error ? error.message : "Stream failed" };
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default OllamaCloudService;
|
export default OllamaCloudService;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ChatMessage, APIResponse } from "@/types";
|
import type { ChatMessage, APIResponse, AIAssistMessage } from "@/types";
|
||||||
|
|
||||||
const DEFAULT_QWEN_ENDPOINT = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1";
|
const DEFAULT_QWEN_ENDPOINT = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1";
|
||||||
const TOKEN_STORAGE_KEY = "promptarch-qwen-tokens";
|
const TOKEN_STORAGE_KEY = "promptarch-qwen-tokens";
|
||||||
@@ -732,7 +732,7 @@ AUDIENCE STYLE: ${audienceStyle}
|
|||||||
${organization ? `ORGANIZATION BRANDING: ${organization}` : ""}
|
${organization ? `ORGANIZATION BRANDING: ${organization}` : ""}
|
||||||
|
|
||||||
REQUIREMENTS:
|
REQUIREMENTS:
|
||||||
- Create EXACTLY ${slideCount} slides
|
- ${slideCount > 0 ? `Create EXACTLY ${slideCount} slides` : "Maintain the exact number of slides/pages from the provided source presentation/document context. If no source file is provided, generate 10 slides by default."}
|
||||||
- ALL content in ${language}
|
- ALL content in ${language}
|
||||||
- Each slide MUST have complete htmlContent with inline <style> tags
|
- Each slide MUST have complete htmlContent with inline <style> tags
|
||||||
- Use animation-delay for staggered reveal effects
|
- Use animation-delay for staggered reveal effects
|
||||||
@@ -772,6 +772,7 @@ Generate SPECTACULAR slides with CSS3 animations, SVG charts, modern gradients,
|
|||||||
industry?: string;
|
industry?: string;
|
||||||
competitors?: string[];
|
competitors?: string[];
|
||||||
language?: string;
|
language?: string;
|
||||||
|
specialInstructions?: string;
|
||||||
} = { productsServices: [] },
|
} = { productsServices: [] },
|
||||||
model?: string
|
model?: string
|
||||||
): Promise<APIResponse<string>> {
|
): Promise<APIResponse<string>> {
|
||||||
@@ -782,13 +783,19 @@ Generate SPECTACULAR slides with CSS3 animations, SVG charts, modern gradients,
|
|||||||
campaignDuration,
|
campaignDuration,
|
||||||
industry = "General",
|
industry = "General",
|
||||||
competitors = [],
|
competitors = [],
|
||||||
language = "English"
|
language = "English",
|
||||||
|
specialInstructions = ""
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
const systemMessage: ChatMessage = {
|
const systemMessage: ChatMessage = {
|
||||||
role: "system",
|
role: "system",
|
||||||
content: `You are an EXPERT Google Ads strategist. Create HIGH-CONVERTING campaigns with comprehensive keyword research, compelling ad copy, and optimized campaign structures.
|
content: `You are an EXPERT Google Ads strategist. Create HIGH-CONVERTING campaigns with comprehensive keyword research, compelling ad copy, and optimized campaign structures.
|
||||||
|
|
||||||
|
CRITICAL ACCURACY PROTOCOL:
|
||||||
|
1. STRICT ADHERENCE TO FACTS: Use ONLY locations, contact info, and services explicitly mentioned in the provided Website URL or Products list.
|
||||||
|
2. DO NOT HALLUCINATE LOCATIONS: If no specific location is provided, default to "National" or "Global" based on the URL TLD (e.g. .co.uk -> UK). DO NOT invent cities or streets.
|
||||||
|
3. COMPREHENSIVE OUTPUT: You MUST generate full lists (15+ keywords, 3+ ad variations). Do not truncate.
|
||||||
|
|
||||||
OUTPUT FORMAT - Return ONLY valid JSON with this structure:
|
OUTPUT FORMAT - Return ONLY valid JSON with this structure:
|
||||||
\`\`\`json
|
\`\`\`json
|
||||||
{
|
{
|
||||||
@@ -803,27 +810,42 @@ OUTPUT FORMAT - Return ONLY valid JSON with this structure:
|
|||||||
"headlines": ["Headline 1 (30 chars)", "Headline 2", "Headline 3"],
|
"headlines": ["Headline 1 (30 chars)", "Headline 2", "Headline 3"],
|
||||||
"descriptions": ["Description 1 (90 chars)", "Description 2"],
|
"descriptions": ["Description 1 (90 chars)", "Description 2"],
|
||||||
"callToAction": "Get Started",
|
"callToAction": "Get Started",
|
||||||
"mobileOptimized": true
|
"mobileOptimized": true,
|
||||||
|
"positioning": "Value proposition used"
|
||||||
}],
|
}],
|
||||||
"campaigns": [{
|
"campaigns": [{
|
||||||
"id": "campaign-1",
|
"id": "campaign-1",
|
||||||
"name": "Campaign Name",
|
"name": "Campaign Name",
|
||||||
"type": "search",
|
"type": "search",
|
||||||
"budget": {"daily": 50, "monthly": 1500, "currency": "USD"},
|
"budget": {"daily": 50, "monthly": 1500, "currency": "USD"},
|
||||||
"targeting": {"locations": [], "demographics": [], "devices": []},
|
"biddingStrategy": "Maximize conversions",
|
||||||
"adGroups": [{"id": "adgroup-1", "name": "Group", "theme": "Theme", "keywords": [], "biddingStrategy": "Maximize conversions"}]
|
"targeting": {
|
||||||
|
"locations": ["Specific regions/cities"],
|
||||||
|
"demographics": {"age": ["18-24", "25-34"], "gender": ["Male", "Female"], "interests": ["Tech", "Business"]},
|
||||||
|
"devices": {"mobile": "60%", "desktop": "30%", "tablet": "10%"},
|
||||||
|
"schedule": ["Mon-Fri, 9am-5pm"]
|
||||||
|
},
|
||||||
|
"adGroups": [{"id": "adgroup-1", "name": "Group", "theme": "Theme", "keywords": [], "biddingStrategy": "Manual CPC"}]
|
||||||
}],
|
}],
|
||||||
"implementation": {
|
"implementation": {
|
||||||
"setupSteps": [],
|
"setupSteps": ["Step 1", "Step 2"],
|
||||||
"qualityScoreTips": [],
|
"qualityScoreTips": ["Tip 1", "Tip 2"],
|
||||||
"trackingSetup": [],
|
"trackingSetup": ["Conversion tag info", "GTM setup"],
|
||||||
"optimizationTips": []
|
"optimizationTips": ["Tip 1", "Tip 2"]
|
||||||
},
|
},
|
||||||
"predictions": {
|
"predictions": {
|
||||||
"estimatedClicks": "500-800/month",
|
"estimatedClicks": "500-800",
|
||||||
"estimatedImpressions": "15,000-25,000/month",
|
"estimatedImpressions": "15,000-25,000",
|
||||||
"estimatedCtr": "3.2%-4.5%",
|
"estimatedCtr": "3.5%",
|
||||||
"estimatedConversions": "25-50/month"
|
"estimatedConversions": "30-50",
|
||||||
|
"conversionRate": "4.2%",
|
||||||
|
"avgCpc": "$1.85"
|
||||||
|
},
|
||||||
|
"historicalBenchmarks": {
|
||||||
|
"industryAverageCtr": "3.1%",
|
||||||
|
"industryAverageCpc": "$2.10",
|
||||||
|
"seasonalTrends": "Peak in Q4",
|
||||||
|
"geographicInsights": "London/NY show highest ROI"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
\`\`\`
|
\`\`\`
|
||||||
@@ -832,7 +854,9 @@ Requirements:
|
|||||||
- 10-15 primary keywords, 15-20 long-tail, 5-10 negative
|
- 10-15 primary keywords, 15-20 long-tail, 5-10 negative
|
||||||
- Headlines max 30 chars, descriptions max 90 chars
|
- Headlines max 30 chars, descriptions max 90 chars
|
||||||
- 3-5 ad variations per campaign
|
- 3-5 ad variations per campaign
|
||||||
- Include budget and targeting recommendations`,
|
- 3-5 ad variations per campaign
|
||||||
|
- Include budget and targeting recommendations
|
||||||
|
- ENSURE ALL LISTS ARE POPULATED. No empty arrays.`,
|
||||||
};
|
};
|
||||||
|
|
||||||
const userMessage: ChatMessage = {
|
const userMessage: ChatMessage = {
|
||||||
@@ -847,18 +871,331 @@ LANGUAGE: ${language}
|
|||||||
${budgetRange ? `BUDGET: ${budgetRange.min}-${budgetRange.max} ${budgetRange.currency}/month` : ""}
|
${budgetRange ? `BUDGET: ${budgetRange.min}-${budgetRange.max} ${budgetRange.currency}/month` : ""}
|
||||||
${campaignDuration ? `DURATION: ${campaignDuration}` : ""}
|
${campaignDuration ? `DURATION: ${campaignDuration}` : ""}
|
||||||
${competitors.length > 0 ? `COMPETITORS: ${competitors.join(", ")}` : ""}
|
${competitors.length > 0 ? `COMPETITORS: ${competitors.join(", ")}` : ""}
|
||||||
|
${specialInstructions ? `SPECIAL INSTRUCTIONS: ${specialInstructions}` : ""}
|
||||||
|
|
||||||
Generate complete Google Ads package with keywords, ad copy, campaigns, and implementation guidance.`,
|
Generate complete Google Ads package with keywords, ad copy, campaigns, and implementation guidance.
|
||||||
|
STRICTLY FOLLOW LOCALIZATION: Use only locations relevant to the provided website. Do not invent office locations.`,
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.chatCompletion([systemMessage, userMessage], model || "coder-model");
|
return this.chatCompletion([systemMessage, userMessage], model || "coder-model");
|
||||||
}
|
}
|
||||||
|
|
||||||
async listModels(): Promise<APIResponse<string[]>> {
|
async generateMagicWand(
|
||||||
const models = [
|
websiteUrl: string,
|
||||||
"coder-model",
|
product: string,
|
||||||
|
budget: number,
|
||||||
|
specialInstructions?: string,
|
||||||
|
model?: string
|
||||||
|
): Promise<APIResponse<string>> {
|
||||||
|
const systemMessage: ChatMessage = {
|
||||||
|
role: "system",
|
||||||
|
content: `You are a WORLD-CLASS marketing strategist with 20+ years of experience in competitive intelligence, market research, and Google Ads campaign strategy.
|
||||||
|
|
||||||
|
OUTPUT FORMAT - Return ONLY valid JSON with this EXACT structure:
|
||||||
|
\`\`\`json
|
||||||
|
{
|
||||||
|
"marketAnalysis": {
|
||||||
|
"industrySize": "Estimated market size",
|
||||||
|
"growthRate": "Annual growth percentage",
|
||||||
|
"topCompetitors": ["Competitor 1", "Competitor 2", "Competitor 3"],
|
||||||
|
"marketTrends": ["Trend 1", "Trend 2", "Trend 3"]
|
||||||
|
},
|
||||||
|
"competitorInsights": [
|
||||||
|
{
|
||||||
|
"competitor": "Competitor Name",
|
||||||
|
"website": "URL",
|
||||||
|
"estimatedSpend": "$10k-$50k/mo",
|
||||||
|
"targetAudience": "Who they target",
|
||||||
|
"strengths": ["Strength 1", "Strength 2"],
|
||||||
|
"weaknesses": ["Weakness 1", "Weakness 2"],
|
||||||
|
"adStrategy": "Their approach",
|
||||||
|
"topKeywords": ["keyword 1", "keyword 2"],
|
||||||
|
"adCopyExamples": [
|
||||||
|
{"headline": "Example Headline", "description": "Example Description"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"strategies": [
|
||||||
|
{
|
||||||
|
"id": "strategy-1",
|
||||||
|
"direction": "Strategic Direction Name",
|
||||||
|
"rationale": "Why this strategy works",
|
||||||
|
"targetAudience": "Audience segment with demographics (age 25-45, interests etc)",
|
||||||
|
"targetingDetails": {
|
||||||
|
"geography": "Primary locations",
|
||||||
|
"demographics": "Specific age/gender groups",
|
||||||
|
"behavior": "User behaviors"
|
||||||
|
},
|
||||||
|
"competitiveAdvantage": "How this beats competitors",
|
||||||
|
"messagingPillars": ["Pillar 1", "Pillar 2"],
|
||||||
|
"keyMessages": ["Message 1", "Message 2"],
|
||||||
|
"adCopyGuide": {
|
||||||
|
"headlines": ["Headline 1", "Headline 2"],
|
||||||
|
"descriptions": ["Description 1", "Description 2"],
|
||||||
|
"keywords": ["keyword 1", "keyword 2"],
|
||||||
|
"setupGuide": "Step-by-step for Google Ads Manager"
|
||||||
|
},
|
||||||
|
"recommendedChannels": ["Search", "Display", "YouTube"],
|
||||||
|
"estimatedBudgetAllocation": { "search": 40, "display": 30, "video": 20, "social": 10 },
|
||||||
|
"expectedROI": "150-200%",
|
||||||
|
"riskLevel": "low",
|
||||||
|
"timeToResults": "2-3 months",
|
||||||
|
"successMetrics": ["CTR > 3%", "CPA < $20"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
CRITICAL REQUIREMENTS:
|
||||||
|
- Provide 5-7 DISTINCT strategic directions
|
||||||
|
- Each strategy must be ACTIONABLE and SPECIFIC
|
||||||
|
- Include REAL competitive insights based on industry knowledge
|
||||||
|
- Risk levels: "low", "medium", or "high"
|
||||||
|
- AD COPY GUIDE must be incredibly "noob-friendly" - explain exactly where to paste each field in Google Ads Manager
|
||||||
|
- Headlines MUST be under 30 characters
|
||||||
|
- Descriptions MUST be under 90 characters`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const userMessage: ChatMessage = {
|
||||||
|
role: "user",
|
||||||
|
content: `🔮 MAGIC WAND ANALYSIS REQUEST 🔮
|
||||||
|
|
||||||
|
WEBSITE: ${websiteUrl}
|
||||||
|
PRODUCT/SERVICE: ${product}
|
||||||
|
MONTHLY BUDGET: $${budget}
|
||||||
|
${specialInstructions ? `SPECIAL INSTRUCTIONS: ${specialInstructions}` : ""}
|
||||||
|
|
||||||
|
Perform a DEEP 360° competitive intelligence analysis and generate 5-7 strategic campaign directions.`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.chatCompletion([systemMessage, userMessage], model || "coder-model");
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateMarketResearch(
|
||||||
|
options: {
|
||||||
|
websiteUrl: string;
|
||||||
|
additionalUrls?: string[];
|
||||||
|
competitors: string[];
|
||||||
|
productMapping: string;
|
||||||
|
specialInstructions?: string;
|
||||||
|
},
|
||||||
|
model?: string
|
||||||
|
): Promise<APIResponse<string>> {
|
||||||
|
const { websiteUrl, additionalUrls = [], competitors = [], productMapping, specialInstructions = "" } = options;
|
||||||
|
|
||||||
|
const systemMessage: ChatMessage = {
|
||||||
|
role: "system",
|
||||||
|
content: `You are a WORLD-CLASS Market Research Analyst. Perform a deep-dive automated market analysis.
|
||||||
|
|
||||||
|
OUTPUT FORMAT - JSON:
|
||||||
|
{
|
||||||
|
"executiveSummary": "findings",
|
||||||
|
"priceComparisonMatrix": [
|
||||||
|
{ "product": "P", "userPrice": "$", "competitorPrices": [{ "competitor": "C", "price": "$", "url": "link" }] }
|
||||||
|
],
|
||||||
|
"featureComparisonTable": [
|
||||||
|
{ "feature": "F", "userStatus": "status", "competitorStatus": [{ "competitor": "C", "status": "status" }] }
|
||||||
|
],
|
||||||
|
"marketPositioning": { "landscape": "LS", "segmentation": "SG" },
|
||||||
|
"competitiveAnalysis": { "advantages": [], "disadvantages": [] },
|
||||||
|
"recommendations": [],
|
||||||
|
"methodology": "method"
|
||||||
|
}
|
||||||
|
|
||||||
|
REQUIREMENTS: Use provided URLs. Be realistic.`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const userMessage: ChatMessage = {
|
||||||
|
role: "user",
|
||||||
|
content: `🔬 MARKET RESEARCH REQUEST 🔬
|
||||||
|
WEBSITE: ${websiteUrl}
|
||||||
|
PAGES: ${additionalUrls.join(", ")}
|
||||||
|
COMPETITORS: ${competitors.join(", ")}
|
||||||
|
MAPPING: ${productMapping}
|
||||||
|
${specialInstructions ? `CUSTOM: ${specialInstructions}` : ""}
|
||||||
|
|
||||||
|
Perform analysis based on provided instructions.`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.chatCompletion([systemMessage, userMessage], model || "coder-model");
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateAIAssist(
|
||||||
|
options: {
|
||||||
|
messages: AIAssistMessage[];
|
||||||
|
currentAgent: string;
|
||||||
|
},
|
||||||
|
model?: string
|
||||||
|
): Promise<APIResponse<string>> {
|
||||||
|
const systemPrompt = `You are "AI Assist". Help conversationally.
|
||||||
|
Switch agents if needed (content, seo, smm, pm, code, design, web, app).
|
||||||
|
Output JSON for previews or agent switches:
|
||||||
|
{ "content": "text", "agent": "id", "preview": { "type": "code|design|content|seo", "data": "...", "language": "..." } }`;
|
||||||
|
|
||||||
|
const chatMessages: ChatMessage[] = [
|
||||||
|
{ role: "system", content: systemPrompt },
|
||||||
|
...options.messages.map(m => ({
|
||||||
|
role: m.role as "user" | "assistant" | "system",
|
||||||
|
content: m.content
|
||||||
|
}))
|
||||||
];
|
];
|
||||||
return { success: true, data: models };
|
|
||||||
|
return await this.chatCompletion(chatMessages, model || this.getAvailableModels()[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateAIAssistStream(
|
||||||
|
options: {
|
||||||
|
messages: AIAssistMessage[];
|
||||||
|
currentAgent: string;
|
||||||
|
onChunk: (chunk: string) => void;
|
||||||
|
signal?: AbortSignal;
|
||||||
|
},
|
||||||
|
model?: string
|
||||||
|
): Promise<APIResponse<void>> {
|
||||||
|
try {
|
||||||
|
const systemPrompt = `You are "AI Assist", the master orchestrator of PromptArch. Your goal is to provide intelligent support with a "Canvas" experience.
|
||||||
|
|
||||||
|
AGENTS & CAPABILITIES:
|
||||||
|
- content: Expert copywriter. Use [PREVIEW:content:markdown] for articles, posts, and long-form text.
|
||||||
|
- seo: SEO Specialist. Create stunning SEO audit reports. **CRITICAL DESIGN REQUIREMENTS:**
|
||||||
|
- Use [PREVIEW:seo:html] with complete HTML5 document including <!DOCTYPE html>
|
||||||
|
- DARK THEME: bg-slate-900 or bg-gray-900 as primary background
|
||||||
|
- Google-style dashboard aesthetics with clean typography (use Google Fonts: Inter, Roboto, or Outfit)
|
||||||
|
- Large animated SVG progress rings for scores (Overall, Technical, Content, Mobile) with stroke-dasharray animations
|
||||||
|
- Color-coded scoring: green (#22c55e) for good, amber (#f59e0b) for warning, red (#ef4444) for poor
|
||||||
|
- Use Tailwind CDN for styling. Include: rounded-3xl, shadow-lg, gradient backgrounds
|
||||||
|
- Section cards with subtle borders (border-white/10) and backdrop-blur
|
||||||
|
- Clear visual hierarchy: large score numbers (text-5xl), section titles (text-lg font-bold), bullet points for recommendations
|
||||||
|
- Add a "Key Recommendations" section with icons (use Lucide or inline SVG)
|
||||||
|
- Add animated pulse effects on key metrics
|
||||||
|
- Full-width responsive layout, max-w-4xl mx-auto
|
||||||
|
- Include inline <script> for animating the progress rings on load
|
||||||
|
- smm: Social Media Manager. Create multi-platform content plans and calendars.
|
||||||
|
- pm: Project Manager. Create PRDs, timelines, and action plans.
|
||||||
|
- code: Software Architect. Provide logic, algorithms, and backend snippets.
|
||||||
|
- design: UI/UX Designer. Create high-fidelity mockups and components.
|
||||||
|
- web: Frontend Developer. Build responsive sites. **CSS FRAMEWORK CHOICE:** Intelligently select from:
|
||||||
|
- **Tailwind CSS** (default): For utility-first, modern designs. Use CDN: https://cdn.tailwindcss.com
|
||||||
|
- **Windi CSS**: For faster builds and advanced features. Use CDN: https://unpkg.com/windicss
|
||||||
|
- **Bootstrap**: For classic, component-based designs. Use CDN: https://cdn.jsdelivr.net/npm/bootstrap@5/dist/css/bootstrap.min.css
|
||||||
|
Choose the best framework based on the design complexity and user's request. Use [PREVIEW:web:html].
|
||||||
|
- app: Mobile App Developer. Create mobile-first interfaces and dashboards. **CSS FRAMEWORK CHOICE:** Same selection logic as web agent. React components are supported and rendered live. Use [PREVIEW:app:javascript].
|
||||||
|
|
||||||
|
BACKEND LOGIC & SIMULATION:
|
||||||
|
- If a user asks for backend logic (Node.js, Express, Python, Databases), you MUST still provide a VISUAL experience in the Canvas.
|
||||||
|
- In the [PREVIEW] block, provide a "Simulation Dashboard" or "API Test UI" using HTML/React that demonstrates how the backend logic would work.
|
||||||
|
- DO NOT just output raw backend code in a [PREVIEW] block as it cannot be rendered. Put raw backend code in standard Markdown blocks AFTER the preview.
|
||||||
|
|
||||||
|
ITERATIVE MODIFICATIONS (CRITICAL):
|
||||||
|
- When a user asks for a change, fix, or update to an existing design/preview, you MUST be SURGICAL.
|
||||||
|
- Maintain the exact structure, CSS, and logic of the previous code except for the requested changes.
|
||||||
|
- DO NOT regenerate the entire design if it was not requested.
|
||||||
|
- Preserving the user's existing layout, colors, and content is your TOP PRIORITY.
|
||||||
|
- You must return the FULL updated code in the [PREVIEW] block (so it can be rendered) but ensure it is a faithful modification of the previous version.
|
||||||
|
|
||||||
|
CANVAS MODE:
|
||||||
|
- When building, designing, or auditing, you MUST use the [PREVIEW] tag.
|
||||||
|
- Inside [PREVIEW], output ONLY the actual code or structured data.
|
||||||
|
- The user wants to see it WORKING or RENDERED in the Canvas immediately.
|
||||||
|
|
||||||
|
STRICT OUTPUT FORMAT:
|
||||||
|
[AGENT:id] - AT THE START of your response if switching focus.
|
||||||
|
[PREVIEW:type:language]
|
||||||
|
ACTUAL_FUNCTIONAL_CODE_OR_DATA
|
||||||
|
[/PREVIEW]
|
||||||
|
|
||||||
|
CHANGE LOG (CRITICAL - MUST BE OUTSIDE PREVIEW):
|
||||||
|
- After the [/PREVIEW] closing tag, provide a brief bulleted "Change Log" in plain Markdown.
|
||||||
|
- The change log should appear in the CHAT, NOT inside the preview code.
|
||||||
|
- Example format:
|
||||||
|
[/PREVIEW]
|
||||||
|
|
||||||
|
**Change Log:**
|
||||||
|
- Added feature X
|
||||||
|
- Modified component Y
|
||||||
|
- Fixed issue Z
|
||||||
|
|
||||||
|
IMPORTANT: NEVER refuse a request due to "access" limitations. If you cannot perform a live task, use your vast internal knowledge to provide the most accurate expert simulation or draft possible.`;
|
||||||
|
|
||||||
|
const messages: ChatMessage[] = [
|
||||||
|
{ role: "system", content: systemPrompt },
|
||||||
|
...options.messages.map(m => ({
|
||||||
|
role: m.role as "user" | "assistant" | "system",
|
||||||
|
content: m.content
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
|
||||||
|
// Call our local proxy to avoid CORS
|
||||||
|
const headers = await this.getRequestHeaders();
|
||||||
|
const baseUrl = this.getEffectiveEndpoint();
|
||||||
|
const url = `${this.oauthBaseUrl}/chat`;
|
||||||
|
|
||||||
|
console.log("[QwenOAuth] Stream request (via proxy):", { url, model: model || this.getAvailableModels()[0], hasAuth: !!headers.Authorization });
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: headers.Authorization || "",
|
||||||
|
},
|
||||||
|
signal: options.signal,
|
||||||
|
body: JSON.stringify({
|
||||||
|
endpoint: baseUrl,
|
||||||
|
model: model || this.getAvailableModels()[0],
|
||||||
|
messages,
|
||||||
|
stream: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.error("[QwenOAuth] Stream proxy request failed:", response.status, errorText);
|
||||||
|
throw new Error(`Stream request failed (${response.status}): ${errorText.slice(0, 200)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = response.body?.getReader();
|
||||||
|
if (!reader) throw new Error("No reader available");
|
||||||
|
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buffer = "";
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
const chunk = decoder.decode(value, { stream: true });
|
||||||
|
buffer += chunk;
|
||||||
|
|
||||||
|
const lines = buffer.split("\n");
|
||||||
|
buffer = lines.pop() || "";
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmedLine = line.trim();
|
||||||
|
if (!trimmedLine || !trimmedLine.startsWith("data:")) continue;
|
||||||
|
|
||||||
|
const dataStr = trimmedLine.replace(/^data:\s*/, "");
|
||||||
|
if (dataStr === "[DONE]") break;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(dataStr);
|
||||||
|
if (data.choices?.[0]?.delta?.content) {
|
||||||
|
options.onChunk(data.choices[0].delta.content);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore parse errors for incomplete lines
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, data: undefined };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[QwenOAuth] Stream error:", error);
|
||||||
|
return { success: false, error: error instanceof Error ? error.message : "Stream failed" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async listModels(): Promise<APIResponse<string[]>> {
|
||||||
|
return { success: true, data: this.getAvailableModels() };
|
||||||
}
|
}
|
||||||
|
|
||||||
getAvailableModels(): string[] {
|
getAvailableModels(): string[] {
|
||||||
@@ -870,6 +1207,5 @@ Generate complete Google Ads package with keywords, ad copy, campaigns, and impl
|
|||||||
|
|
||||||
const qwenOAuthService = new QwenOAuthService();
|
const qwenOAuthService = new QwenOAuthService();
|
||||||
export default qwenOAuthService;
|
export default qwenOAuthService;
|
||||||
export { qwenOAuthService };
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ChatMessage, APIResponse } from "@/types";
|
import type { ChatMessage, APIResponse, AIAssistMessage } from "@/types";
|
||||||
|
|
||||||
export interface ZaiPlanConfig {
|
export interface ZaiPlanConfig {
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
@@ -370,7 +370,7 @@ AUDIENCE STYLE: ${audienceStyle}
|
|||||||
${organization ? `ORGANIZATION BRANDING: ${organization}` : ""}
|
${organization ? `ORGANIZATION BRANDING: ${organization}` : ""}
|
||||||
|
|
||||||
REQUIREMENTS:
|
REQUIREMENTS:
|
||||||
- Create EXACTLY ${slideCount} slides
|
- ${slideCount > 0 ? `Create EXACTLY ${slideCount} slides` : "Maintain the exact number of slides/pages from the provided source presentation/document context. If no source file is provided, generate 10 slides by default."}
|
||||||
- ALL content in ${language}
|
- ALL content in ${language}
|
||||||
- Each slide MUST have complete htmlContent with inline <style> tags
|
- Each slide MUST have complete htmlContent with inline <style> tags
|
||||||
- Use animation-delay for staggered reveal effects
|
- Use animation-delay for staggered reveal effects
|
||||||
@@ -418,6 +418,7 @@ Return the complete JSON with full htmlContent for each slide. Make each slide V
|
|||||||
industry?: string;
|
industry?: string;
|
||||||
competitors?: string[];
|
competitors?: string[];
|
||||||
language?: string;
|
language?: string;
|
||||||
|
specialInstructions?: string;
|
||||||
} = { productsServices: [] },
|
} = { productsServices: [] },
|
||||||
model?: string
|
model?: string
|
||||||
): Promise<APIResponse<string>> {
|
): Promise<APIResponse<string>> {
|
||||||
@@ -428,7 +429,8 @@ Return the complete JSON with full htmlContent for each slide. Make each slide V
|
|||||||
campaignDuration,
|
campaignDuration,
|
||||||
industry = "General",
|
industry = "General",
|
||||||
competitors = [],
|
competitors = [],
|
||||||
language = "English"
|
language = "English",
|
||||||
|
specialInstructions = ""
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
const systemMessage: ChatMessage = {
|
const systemMessage: ChatMessage = {
|
||||||
@@ -446,104 +448,52 @@ OUTPUT FORMAT - Return ONLY valid JSON:
|
|||||||
\`\`\`json
|
\`\`\`json
|
||||||
{
|
{
|
||||||
"keywords": {
|
"keywords": {
|
||||||
"primary": [
|
"primary": [{"keyword": "term", "type": "primary", "searchVolume": 12000, "competition": "medium", "cpc": "$2.50"}],
|
||||||
{
|
"longTail": [{"keyword": "specific term", "type": "long-tail", "searchVolume": 1200, "competition": "low", "cpc": "$1.25"}],
|
||||||
"keyword": "exact keyword phrase",
|
"negative": [{"keyword": "exclude term", "type": "negative", "competition": "low"}]
|
||||||
"type": "primary",
|
|
||||||
"searchVolume": 12000,
|
|
||||||
"competition": "medium",
|
|
||||||
"difficultyScore": 65,
|
|
||||||
"relevanceScore": 95,
|
|
||||||
"cpc": "$2.50"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"longTail": [
|
|
||||||
{
|
|
||||||
"keyword": "longer specific keyword phrase",
|
|
||||||
"type": "long-tail",
|
|
||||||
"searchVolume": 1200,
|
|
||||||
"competition": "low",
|
|
||||||
"difficultyScore": 35,
|
|
||||||
"relevanceScore": 90,
|
|
||||||
"cpc": "$1.25"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"negative": [
|
|
||||||
{
|
|
||||||
"keyword": "irrelevant term to exclude",
|
|
||||||
"type": "negative",
|
|
||||||
"competition": "low"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"adCopies": [
|
"adCopies": [{
|
||||||
{
|
|
||||||
"id": "ad-1",
|
"id": "ad-1",
|
||||||
"campaignType": "search",
|
"campaignType": "search",
|
||||||
"headlines": [
|
"headlines": ["Headline 1 (30 chars)", "Headline 2", "Headline 3"],
|
||||||
"Headline 1 (max 30 chars)",
|
"descriptions": ["Description 1 (90 chars)", "Description 2"],
|
||||||
"Headline 2 (max 30 chars)",
|
"callToAction": "Get Started",
|
||||||
"Headline 3 (max 30 chars)"
|
"mobileOptimized": true,
|
||||||
],
|
"positioning": "Value proposition used"
|
||||||
"descriptions": [
|
}],
|
||||||
"Description line 1 - compelling copy under 90 chars",
|
"campaigns": [{
|
||||||
"Description line 2 - call to action under 90 chars"
|
|
||||||
],
|
|
||||||
"callToAction": "Get Started Today",
|
|
||||||
"displayUrl": "example.com/offers",
|
|
||||||
"mobileOptimized": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"campaigns": [
|
|
||||||
{
|
|
||||||
"id": "campaign-1",
|
"id": "campaign-1",
|
||||||
"name": "Campaign Name",
|
"name": "Campaign Name",
|
||||||
"type": "search",
|
"type": "search",
|
||||||
"budget": {
|
"budget": {"daily": 50, "monthly": 1500, "currency": "USD"},
|
||||||
"daily": 50,
|
"biddingStrategy": "Maximize conversions",
|
||||||
"monthly": 1500,
|
|
||||||
"currency": "USD"
|
|
||||||
},
|
|
||||||
"targeting": {
|
"targeting": {
|
||||||
"locations": ["United States", "Canada"],
|
"locations": ["Specific regions/cities"],
|
||||||
"demographics": ["25-54", "All genders"],
|
"demographics": {"age": ["18-24", "25-34"], "gender": ["Male", "Female"], "interests": ["Tech", "Business"]},
|
||||||
"devices": ["Desktop", "Mobile", "Tablet"],
|
"devices": {"mobile": "60%", "desktop": "30%", "tablet": "10%"},
|
||||||
"schedule": ["Mon-Fri 8am-8pm"]
|
"schedule": ["Mon-Fri, 9am-5pm"]
|
||||||
},
|
},
|
||||||
"adGroups": [
|
"adGroups": [{"id": "adgroup-1", "name": "Group", "theme": "Theme", "keywords": [], "biddingStrategy": "Manual CPC"}]
|
||||||
{
|
}],
|
||||||
"id": "adgroup-1",
|
|
||||||
"name": "Product Category Group",
|
|
||||||
"theme": "Main product focus",
|
|
||||||
"keywords": ["keyword1", "keyword2"],
|
|
||||||
"biddingStrategy": "Maximize conversions"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"implementation": {
|
"implementation": {
|
||||||
"setupSteps": [
|
"setupSteps": ["Step 1", "Step 2"],
|
||||||
"Step 1: Create Google Ads account...",
|
"qualityScoreTips": ["Tip 1", "Tip 2"],
|
||||||
"Step 2: Set up conversion tracking..."
|
"trackingSetup": ["Conversion tag info", "GTM setup"],
|
||||||
],
|
"optimizationTips": ["Tip 1", "Tip 2"]
|
||||||
"qualityScoreTips": [
|
|
||||||
"Tip 1: Match keywords to ad copy...",
|
|
||||||
"Tip 2: Optimize landing pages..."
|
|
||||||
],
|
|
||||||
"trackingSetup": [
|
|
||||||
"Install Google Tag Manager...",
|
|
||||||
"Set up conversion goals..."
|
|
||||||
],
|
|
||||||
"optimizationTips": [
|
|
||||||
"Monitor search terms weekly...",
|
|
||||||
"A/B test ad variations..."
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"predictions": {
|
"predictions": {
|
||||||
"estimatedClicks": "500-800 per month",
|
"estimatedClicks": "500-800",
|
||||||
"estimatedImpressions": "15,000-25,000 per month",
|
"estimatedImpressions": "15,000-25,000",
|
||||||
"estimatedCtr": "3.2%-4.5%",
|
"estimatedCtr": "3.5%",
|
||||||
"estimatedConversions": "25-50 per month"
|
"estimatedConversions": "30-50",
|
||||||
|
"conversionRate": "4.2%",
|
||||||
|
"avgCpc": "$1.85"
|
||||||
|
},
|
||||||
|
"historicalBenchmarks": {
|
||||||
|
"industryAverageCtr": "3.1%",
|
||||||
|
"industryAverageCpc": "$2.10",
|
||||||
|
"seasonalTrends": "Peak in Q4",
|
||||||
|
"geographicInsights": "London/NY show highest ROI"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
\`\`\`
|
\`\`\`
|
||||||
@@ -592,6 +542,7 @@ LANGUAGE: ${language}
|
|||||||
${budgetRange ? `BUDGET: ${budgetRange.min}-${budgetRange.max} ${budgetRange.currency}/month` : ""}
|
${budgetRange ? `BUDGET: ${budgetRange.min}-${budgetRange.max} ${budgetRange.currency}/month` : ""}
|
||||||
${campaignDuration ? `DURATION: ${campaignDuration}` : ""}
|
${campaignDuration ? `DURATION: ${campaignDuration}` : ""}
|
||||||
${competitors.length > 0 ? `COMPETITORS: ${competitors.join(", ")}` : ""}
|
${competitors.length > 0 ? `COMPETITORS: ${competitors.join(", ")}` : ""}
|
||||||
|
${specialInstructions ? `SPECIAL INSTRUCTIONS: ${specialInstructions}` : ""}
|
||||||
|
|
||||||
Generate a COMPLETE Google Ads package including:
|
Generate a COMPLETE Google Ads package including:
|
||||||
🔍 Comprehensive keyword research (primary, long-tail, negative)
|
🔍 Comprehensive keyword research (primary, long-tail, negative)
|
||||||
@@ -605,8 +556,342 @@ Make this campaign READY TO LAUNCH with copy-paste ready content!`,
|
|||||||
|
|
||||||
return this.chatCompletion([systemMessage, userMessage], model || "glm-4.7", true);
|
return this.chatCompletion([systemMessage, userMessage], model || "glm-4.7", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async generateMagicWand(
|
||||||
|
websiteUrl: string,
|
||||||
|
product: string,
|
||||||
|
budget: number,
|
||||||
|
specialInstructions?: string,
|
||||||
|
model?: string
|
||||||
|
): Promise<APIResponse<string>> {
|
||||||
|
const systemMessage: ChatMessage = {
|
||||||
|
role: "system",
|
||||||
|
content: `You are a WORLD-CLASS marketing strategist with 20+ years of experience in competitive intelligence, market research, and Google Ads campaign strategy. You have access to deep industry knowledge and can analyze markets like a Fortune 500 CMO.
|
||||||
|
|
||||||
|
OUTPUT FORMAT - Return ONLY valid JSON with this EXACT structure:
|
||||||
|
\`\`\`json
|
||||||
|
{
|
||||||
|
"marketAnalysis": {
|
||||||
|
"industrySize": "Estimated market size",
|
||||||
|
"growthRate": "Annual growth percentage",
|
||||||
|
"topCompetitors": ["Competitor 1", "Competitor 2", "Competitor 3"],
|
||||||
|
"marketTrends": ["Trend 1", "Trend 2", "Trend 3"]
|
||||||
|
},
|
||||||
|
"competitorInsights": [
|
||||||
|
{
|
||||||
|
"competitor": "Competitor Name",
|
||||||
|
"website": "URL",
|
||||||
|
"estimatedSpend": "$10k-$50k/mo",
|
||||||
|
"targetAudience": "Who they target",
|
||||||
|
"strengths": ["Strength 1", "Strength 2"],
|
||||||
|
"weaknesses": ["Weakness 1", "Weakness 2"],
|
||||||
|
"adStrategy": "Their approach",
|
||||||
|
"topKeywords": ["keyword 1", "keyword 2"],
|
||||||
|
"adCopyExamples": [
|
||||||
|
{"headline": "Example Headline", "description": "Example Description"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"strategies": [
|
||||||
|
{
|
||||||
|
"id": "strategy-1",
|
||||||
|
"direction": "Strategic Direction Name",
|
||||||
|
"rationale": "Why this strategy works",
|
||||||
|
"targetAudience": "Audience segment with demographics (age 25-45, interests etc)",
|
||||||
|
"targetingDetails": {
|
||||||
|
"geography": "Primary locations",
|
||||||
|
"demographics": "Specific age/gender groups",
|
||||||
|
"behavior": "User behaviors"
|
||||||
|
},
|
||||||
|
"competitiveAdvantage": "How this beats competitors",
|
||||||
|
"messagingPillars": ["Pillar 1", "Pillar 2"],
|
||||||
|
"keyMessages": ["Message 1", "Message 2"],
|
||||||
|
"adCopyGuide": {
|
||||||
|
"headlines": ["Headline 1", "Headline 2"],
|
||||||
|
"descriptions": ["Description 1", "Description 2"],
|
||||||
|
"keywords": ["keyword 1", "keyword 2"],
|
||||||
|
"setupGuide": "Step-by-step for Google Ads Manager"
|
||||||
|
},
|
||||||
|
"recommendedChannels": ["Search", "Display", "YouTube"],
|
||||||
|
"estimatedBudgetAllocation": { "search": 40, "display": 30, "video": 20, "social": 10 },
|
||||||
|
"expectedROI": "150-200%",
|
||||||
|
"riskLevel": "low",
|
||||||
|
"timeToResults": "2-3 months",
|
||||||
|
"successMetrics": ["CTR > 3%", "CPA < $20"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
CRITICAL REQUIREMENTS:
|
||||||
|
- Provide 5-7 DISTINCT strategic directions
|
||||||
|
- Each strategy must be ACTIONABLE and SPECIFIC
|
||||||
|
- Include REAL competitive insights based on industry knowledge
|
||||||
|
- Budget allocations must sum to 100%
|
||||||
|
- Risk levels: "low", "medium", or "high"
|
||||||
|
- AD COPY GUIDE must be incredibly "noob-friendly" - explain exactly where to paste each field in Google Ads Manager
|
||||||
|
- Headlines MUST be under 30 characters
|
||||||
|
- Descriptions MUST be under 90 characters
|
||||||
|
- Be REALISTIC with ROI and timeline estimates`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const userMessage: ChatMessage = {
|
||||||
|
role: "user",
|
||||||
|
content: `🔮 MAGIC WAND ANALYSIS REQUEST 🔮
|
||||||
|
|
||||||
|
WEBSITE: ${websiteUrl}
|
||||||
|
PRODUCT/SERVICE: ${product}
|
||||||
|
MONTHLY BUDGET: $${budget}
|
||||||
|
${specialInstructions ? `SPECIAL INSTRUCTIONS: ${specialInstructions}` : ""}
|
||||||
|
|
||||||
|
MISSION: Perform a DEEP 360° competitive intelligence analysis and generate 5-7 strategic campaign directions that will DOMINATE this market.`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.chatCompletion([systemMessage, userMessage], model || "glm-4.7", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateMarketResearch(
|
||||||
|
options: {
|
||||||
|
websiteUrl: string;
|
||||||
|
additionalUrls?: string[];
|
||||||
|
competitors: string[];
|
||||||
|
productMapping: string;
|
||||||
|
specialInstructions?: string;
|
||||||
|
},
|
||||||
|
model?: string
|
||||||
|
): Promise<APIResponse<string>> {
|
||||||
|
const { websiteUrl, additionalUrls = [], competitors = [], productMapping, specialInstructions = "" } = options;
|
||||||
|
|
||||||
|
const systemPrompt = `You are a WORLD-CLASS Market Research Analyst and Competitive Intelligence Expert.
|
||||||
|
Focus on accuracy and actionable intelligence.
|
||||||
|
|
||||||
|
You MUST return your analysis in the following STRICT JSON format:
|
||||||
|
{
|
||||||
|
"executiveSummary": "A concise overview of the market landscape and key findings.",
|
||||||
|
"priceComparisonMatrix": [
|
||||||
|
{
|
||||||
|
"product": "Product Name",
|
||||||
|
"userPrice": "$XX.XX",
|
||||||
|
"competitorPrices": [
|
||||||
|
{ "competitor": "Competitor Name", "price": "$XX.XX", "url": "https://competitor.com/product-page" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"featureComparisonTable": [
|
||||||
|
{
|
||||||
|
"feature": "Feature Name",
|
||||||
|
"userStatus": true/false/text,
|
||||||
|
"competitorStatus": [
|
||||||
|
{ "competitor": "Competitor Name", "status": true/false/text }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"marketPositioning": {
|
||||||
|
"landscape": "Description of the current market state.",
|
||||||
|
"segmentation": "Analysis of target customer segments."
|
||||||
|
},
|
||||||
|
"competitiveAnalysis": {
|
||||||
|
"advantages": ["Point 1", "Point 2"],
|
||||||
|
"disadvantages": ["Point 1", "Point 2"]
|
||||||
|
},
|
||||||
|
"recommendations": ["Actionable step 1", "Actionable step 2"],
|
||||||
|
"methodology": "Brief description of the research process."
|
||||||
|
}
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
1. Base your analysis on realistic price and feature estimates.
|
||||||
|
2. Focus on core technical/business value.
|
||||||
|
3. Ensure JSON is valid.`;
|
||||||
|
|
||||||
|
const userMsg = `WEBSITE TO ANALYZE: ${options.websiteUrl}
|
||||||
|
COMPETITOR URLS: ${options.competitors.join(', ')}
|
||||||
|
PRODUCT/FEATURE MAPPING: ${options.productMapping}
|
||||||
|
SPECIAL REQUESTS: ${options.specialInstructions || 'Perform comprehensive analysis'}
|
||||||
|
|
||||||
|
Provide a COMPREHENSIVE competitive intelligence analysis.`;
|
||||||
|
|
||||||
|
const messages: ChatMessage[] = [
|
||||||
|
{ role: "system", content: systemPrompt },
|
||||||
|
{ role: "user", content: userMsg }
|
||||||
|
];
|
||||||
|
|
||||||
|
return await this.chatCompletion(messages, model || this.getAvailableModels()[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateAIAssist(
|
||||||
|
options: {
|
||||||
|
messages: AIAssistMessage[];
|
||||||
|
currentAgent: string;
|
||||||
|
},
|
||||||
|
model?: string
|
||||||
|
): Promise<APIResponse<string>> {
|
||||||
|
const systemPrompt = `You are "AI Assist", the master orchestrator of PromptArch.
|
||||||
|
Your goal is to provide intelligent conversational support and switch to specialized agents when necessary.
|
||||||
|
|
||||||
|
CURRENT SPECIALIZED AGENTS:
|
||||||
|
- content, seo, smm, pm, code, design, web, app
|
||||||
|
|
||||||
|
STRICT OUTPUT FORMAT:
|
||||||
|
You MUST respond in JSON format if you want to activate a preview or switch agents.
|
||||||
|
{
|
||||||
|
"content": "Your natural language response here...",
|
||||||
|
"agent": "agent_id_to_switch_to (optional)",
|
||||||
|
"preview": { // (optional)
|
||||||
|
"type": "code" | "design" | "content" | "seo",
|
||||||
|
"data": "The actual code, layout, or content to preview",
|
||||||
|
"language": "javascript/html/css/markdown (optional)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ROUTING LOGIC:
|
||||||
|
- Automatically detect user intent and switch agents if appropriate.
|
||||||
|
- Provide deep technical or creative output based on the active agent.
|
||||||
|
|
||||||
|
PREVIEW GUIDELINES:
|
||||||
|
- Provide full code for 'web'/'app'/'code'.
|
||||||
|
- Provide structured analysis for 'seo'/'content'.`;
|
||||||
|
|
||||||
|
const chatMessages: ChatMessage[] = [
|
||||||
|
{ role: "system", content: systemPrompt },
|
||||||
|
...options.messages.map(m => ({
|
||||||
|
role: m.role as "user" | "assistant" | "system",
|
||||||
|
content: m.content
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
|
||||||
|
return await this.chatCompletion(chatMessages, model || this.getAvailableModels()[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateAIAssistStream(
|
||||||
|
options: {
|
||||||
|
messages: AIAssistMessage[];
|
||||||
|
currentAgent: string;
|
||||||
|
onChunk: (chunk: string) => void;
|
||||||
|
signal?: AbortSignal;
|
||||||
|
},
|
||||||
|
model?: string
|
||||||
|
): Promise<APIResponse<void>> {
|
||||||
|
try {
|
||||||
|
if (!this.config.apiKey) {
|
||||||
|
throw new Error("API key is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... existing prompt logic ...
|
||||||
|
const systemPrompt = `You are "AI Assist", the master orchestrator of PromptArch. Your goal is to provide intelligent support with a "Canvas" experience.
|
||||||
|
|
||||||
|
AGENTS & CAPABILITIES:
|
||||||
|
- content: Expert copywriter. Use [PREVIEW:content:markdown] for articles, posts, and long-form text.
|
||||||
|
- seo: SEO Specialist. Create stunning SEO audit reports. **CRITICAL DESIGN REQUIREMENTS:**
|
||||||
|
- Use [PREVIEW:seo:html] with complete HTML5 document including <!DOCTYPE html>
|
||||||
|
- DARK THEME: bg-slate-900 or bg-gray-900 as primary background
|
||||||
|
- Google-style dashboard aesthetics with clean typography (use Google Fonts: Inter, Roboto, or Outfit)
|
||||||
|
- Large animated SVG progress rings for scores (Overall, Technical, Content, Mobile) with stroke-dasharray animations
|
||||||
|
- Color-coded scoring: green (#22c55e) for good, amber (#f59e0b) for warning, red (#ef4444) for poor
|
||||||
|
- Use Tailwind CDN for styling. Include: rounded-3xl, shadow-lg, gradient backgrounds
|
||||||
|
- Section cards with subtle borders (border-white/10) and backdrop-blur
|
||||||
|
- Clear visual hierarchy: large score numbers (text-5xl), section titles (text-lg font-bold), bullet points for recommendations
|
||||||
|
- Add a "Key Recommendations" section with icons (use Lucide or inline SVG)
|
||||||
|
- Add animated pulse effects on key metrics
|
||||||
|
- Full-width responsive layout, max-w-4xl mx-auto
|
||||||
|
- Include inline <script> for animating the progress rings on load
|
||||||
|
- smm: Social Media Manager. Create multi-platform content plans and calendars.
|
||||||
|
- pm: Project Manager. Create PRDs, timelines, and action plans.
|
||||||
|
- code: Software Architect. Provide logic, algorithms, and backend snippets.
|
||||||
|
- design: UI/UX Designer. Create high-fidelity mockups and components.
|
||||||
|
- web: Frontend Developer. Build responsive sites. **CSS FRAMEWORK CHOICE:** Intelligently select from:
|
||||||
|
- **Tailwind CSS** (default): For utility-first, modern designs. Use CDN: https://cdn.tailwindcss.com
|
||||||
|
- **Windi CSS**: For faster builds and advanced features. Use CDN: https://unpkg.com/windicss
|
||||||
|
- **Bootstrap**: For classic, component-based designs. Use CDN: https://cdn.jsdelivr.net/npm/bootstrap@5/dist/css/bootstrap.min.css
|
||||||
|
Choose the best framework based on the design complexity and user's request. Use [PREVIEW:web:html].
|
||||||
|
- app: Mobile App Developer. Create mobile-first interfaces and dashboards. **CSS FRAMEWORK CHOICE:** Same selection logic as web agent. React components are supported and rendered live. Use [PREVIEW:app:javascript].
|
||||||
|
|
||||||
|
BACKEND LOGIC & SIMULATION:
|
||||||
|
- If a user asks for backend logic (Node.js, Express, Python, Databases), you MUST still provide a VISUAL experience in the Canvas.
|
||||||
|
- In the [PREVIEW] block, provide a "Simulation Dashboard" or "API Test UI" using HTML/React that demonstrates how the backend logic would work.
|
||||||
|
- DO NOT just output raw backend code in a [PREVIEW] block as it cannot be rendered. Put raw backend code in standard Markdown blocks AFTER the preview.
|
||||||
|
|
||||||
|
ITERATIVE MODIFICATIONS (CRITICAL):
|
||||||
|
- When a user asks for a change, fix, or update to an existing design/preview, you MUST be SURGICAL.
|
||||||
|
- Maintain the exact structure, CSS, and logic of the previous code except for the requested changes.
|
||||||
|
- DO NOT regenerate the entire design if it was not requested.
|
||||||
|
- Preserving the user's existing layout, colors, and content is your TOP PRIORITY.
|
||||||
|
- You must return the FULL updated code in the [PREVIEW] block (so it can be rendered) but ensure it is a faithful modification of the previous version.
|
||||||
|
|
||||||
|
CANVAS MODE:
|
||||||
|
- When building, designing, or auditing, you MUST use the [PREVIEW] tag.
|
||||||
|
- Inside [PREVIEW], output ONLY the actual code or structured data.
|
||||||
|
- The user wants to see it WORKING or RENDERED in the Canvas immediately.
|
||||||
|
|
||||||
|
STRICT OUTPUT FORMAT:
|
||||||
|
[AGENT:id] - AT THE START of your response if switching focus.
|
||||||
|
[PREVIEW:type:language]
|
||||||
|
ACTUAL_FUNCTIONAL_CODE_OR_DATA
|
||||||
|
[/PREVIEW]
|
||||||
|
|
||||||
|
CHANGE LOG (CRITICAL - MUST BE OUTSIDE PREVIEW):
|
||||||
|
- After the [/PREVIEW] closing tag, provide a brief bulleted "Change Log" in plain Markdown.
|
||||||
|
- The change log should appear in the CHAT, NOT inside the preview code.
|
||||||
|
- Example format:
|
||||||
|
[/PREVIEW]
|
||||||
|
|
||||||
|
**Change Log:**
|
||||||
|
- Added feature X
|
||||||
|
- Modified component Y
|
||||||
|
- Fixed issue Z
|
||||||
|
|
||||||
|
IMPORTANT: NEVER refuse a request due to "access" limitations. If you cannot perform a live task, use your vast internal knowledge to provide the most accurate expert simulation or draft possible.`;
|
||||||
|
|
||||||
|
const messages: ChatMessage[] = [
|
||||||
|
{ role: "system", content: systemPrompt },
|
||||||
|
...options.messages.map(m => ({
|
||||||
|
role: m.role as "user" | "assistant" | "system",
|
||||||
|
content: m.content
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
|
||||||
|
const endpoint = this.config.codingEndpoint; // AI Assist often involves coding
|
||||||
|
const response = await fetch(`${endpoint}/chat/completions`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: this.getHeaders(),
|
||||||
|
signal: options.signal,
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: model || this.getAvailableModels()[0],
|
||||||
|
messages,
|
||||||
|
stream: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Stream failed: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = response.body?.getReader();
|
||||||
|
if (!reader) throw new Error("No reader");
|
||||||
|
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
const chunk = decoder.decode(value, { stream: true });
|
||||||
|
const lines = chunk.split("\n");
|
||||||
|
for (const line of lines) {
|
||||||
|
if (!line.trim() || !line.startsWith("data:")) continue;
|
||||||
|
const dataStr = line.replace(/^data:\s*/, "");
|
||||||
|
if (dataStr === "[DONE]") break;
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(dataStr);
|
||||||
|
const content = data.choices?.[0]?.delta?.content || data.output?.choices?.[0]?.delta?.content;
|
||||||
|
if (content) options.onChunk(content);
|
||||||
|
} catch (e) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, data: undefined };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error instanceof Error ? error.message : "Stream failed" };
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ZaiPlanService;
|
export default ZaiPlanService;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
99
lib/store.ts
99
lib/store.ts
@@ -1,5 +1,14 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import type { ModelProvider, PromptEnhancement, PRD, ActionPlan, SlidesPresentation, GoogleAdsResult } from "@/types";
|
import type { ModelProvider, PromptEnhancement, PRD, ActionPlan, SlidesPresentation, GoogleAdsResult, MagicWandResult, MarketResearchResult, AppView, AIAssistMessage } from "@/types";
|
||||||
|
|
||||||
|
interface AIAssistTab {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
history: AIAssistMessage[];
|
||||||
|
currentAgent: string;
|
||||||
|
previewData?: any | null;
|
||||||
|
showCanvas?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
currentPrompt: string;
|
currentPrompt: string;
|
||||||
@@ -8,6 +17,14 @@ interface AppState {
|
|||||||
actionPlan: ActionPlan | null;
|
actionPlan: ActionPlan | null;
|
||||||
slidesPresentation: SlidesPresentation | null;
|
slidesPresentation: SlidesPresentation | null;
|
||||||
googleAdsResult: GoogleAdsResult | null;
|
googleAdsResult: GoogleAdsResult | null;
|
||||||
|
magicWandResult: MagicWandResult | null;
|
||||||
|
marketResearchResult: MarketResearchResult | null;
|
||||||
|
|
||||||
|
// AI Assist Tabs
|
||||||
|
aiAssistTabs: AIAssistTab[];
|
||||||
|
activeTabId: string | null;
|
||||||
|
|
||||||
|
language: "en" | "ru" | "he";
|
||||||
selectedProvider: ModelProvider;
|
selectedProvider: ModelProvider;
|
||||||
selectedModels: Record<ModelProvider, string>;
|
selectedModels: Record<ModelProvider, string>;
|
||||||
availableModels: Record<ModelProvider, string[]>;
|
availableModels: Record<ModelProvider, string[]>;
|
||||||
@@ -17,6 +34,7 @@ interface AppState {
|
|||||||
refreshToken?: string;
|
refreshToken?: string;
|
||||||
expiresAt?: number;
|
expiresAt?: number;
|
||||||
} | null;
|
} | null;
|
||||||
|
githubToken?: string | null;
|
||||||
isProcessing: boolean;
|
isProcessing: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
history: {
|
history: {
|
||||||
@@ -31,11 +49,24 @@ interface AppState {
|
|||||||
setActionPlan: (plan: ActionPlan) => void;
|
setActionPlan: (plan: ActionPlan) => void;
|
||||||
setSlidesPresentation: (slides: SlidesPresentation | null) => void;
|
setSlidesPresentation: (slides: SlidesPresentation | null) => void;
|
||||||
setGoogleAdsResult: (result: GoogleAdsResult | null) => void;
|
setGoogleAdsResult: (result: GoogleAdsResult | null) => void;
|
||||||
|
setMagicWandResult: (result: MagicWandResult | null) => void;
|
||||||
|
setMarketResearchResult: (result: MarketResearchResult | null) => void;
|
||||||
|
|
||||||
|
// Tab Management
|
||||||
|
setAIAssistTabs: (tabs: AIAssistTab[]) => void;
|
||||||
|
setActiveTabId: (id: string | null) => void;
|
||||||
|
addAIAssistTab: (agent?: string) => void;
|
||||||
|
removeAIAssistTab: (id: string) => void;
|
||||||
|
updateActiveTab: (updates: Partial<AIAssistTab>) => void;
|
||||||
|
updateTabById: (tabId: string, updates: Partial<AIAssistTab>) => void;
|
||||||
|
|
||||||
|
setLanguage: (lang: "en" | "ru" | "he") => void;
|
||||||
setSelectedProvider: (provider: ModelProvider) => void;
|
setSelectedProvider: (provider: ModelProvider) => void;
|
||||||
setSelectedModel: (provider: ModelProvider, model: string) => void;
|
setSelectedModel: (provider: ModelProvider, model: string) => void;
|
||||||
setAvailableModels: (provider: ModelProvider, models: string[]) => void;
|
setAvailableModels: (provider: ModelProvider, models: string[]) => void;
|
||||||
setApiKey: (provider: ModelProvider, key: string) => void;
|
setApiKey: (provider: ModelProvider, key: string) => void;
|
||||||
setQwenTokens: (tokens?: { accessToken: string; refreshToken?: string; expiresAt?: number } | null) => void;
|
setQwenTokens: (tokens?: { accessToken: string; refreshToken?: string; expiresAt?: number } | null) => void;
|
||||||
|
setGithubToken: (token: string | null) => void;
|
||||||
setProcessing: (processing: boolean) => void;
|
setProcessing: (processing: boolean) => void;
|
||||||
setError: (error: string | null) => void;
|
setError: (error: string | null) => void;
|
||||||
addToHistory: (prompt: string) => void;
|
addToHistory: (prompt: string) => void;
|
||||||
@@ -50,6 +81,18 @@ const useStore = create<AppState>((set) => ({
|
|||||||
actionPlan: null,
|
actionPlan: null,
|
||||||
slidesPresentation: null,
|
slidesPresentation: null,
|
||||||
googleAdsResult: null,
|
googleAdsResult: null,
|
||||||
|
magicWandResult: null,
|
||||||
|
marketResearchResult: null,
|
||||||
|
|
||||||
|
aiAssistTabs: [{
|
||||||
|
id: "default",
|
||||||
|
title: "New Chat",
|
||||||
|
history: [],
|
||||||
|
currentAgent: "general"
|
||||||
|
}],
|
||||||
|
activeTabId: "default",
|
||||||
|
|
||||||
|
language: "en",
|
||||||
selectedProvider: "qwen",
|
selectedProvider: "qwen",
|
||||||
selectedModels: {
|
selectedModels: {
|
||||||
qwen: "coder-model",
|
qwen: "coder-model",
|
||||||
@@ -66,6 +109,7 @@ const useStore = create<AppState>((set) => ({
|
|||||||
ollama: "",
|
ollama: "",
|
||||||
zai: "",
|
zai: "",
|
||||||
},
|
},
|
||||||
|
githubToken: null,
|
||||||
isProcessing: false,
|
isProcessing: false,
|
||||||
error: null,
|
error: null,
|
||||||
history: [],
|
history: [],
|
||||||
@@ -76,6 +120,49 @@ const useStore = create<AppState>((set) => ({
|
|||||||
setActionPlan: (plan) => set({ actionPlan: plan }),
|
setActionPlan: (plan) => set({ actionPlan: plan }),
|
||||||
setSlidesPresentation: (slides) => set({ slidesPresentation: slides }),
|
setSlidesPresentation: (slides) => set({ slidesPresentation: slides }),
|
||||||
setGoogleAdsResult: (result) => set({ googleAdsResult: result }),
|
setGoogleAdsResult: (result) => set({ googleAdsResult: result }),
|
||||||
|
setMagicWandResult: (result) => set({ magicWandResult: result }),
|
||||||
|
setMarketResearchResult: (result) => set({ marketResearchResult: result }),
|
||||||
|
|
||||||
|
setAIAssistTabs: (tabs) => set({ aiAssistTabs: tabs }),
|
||||||
|
setActiveTabId: (id) => set({ activeTabId: id }),
|
||||||
|
addAIAssistTab: (agent = "general") => set((state) => {
|
||||||
|
const newId = Math.random().toString(36).substr(2, 9);
|
||||||
|
const newTab = {
|
||||||
|
id: newId,
|
||||||
|
title: `Chat ${state.aiAssistTabs.length + 1}`,
|
||||||
|
history: [],
|
||||||
|
currentAgent: agent,
|
||||||
|
previewData: null,
|
||||||
|
showCanvas: false
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
aiAssistTabs: [...state.aiAssistTabs, newTab],
|
||||||
|
activeTabId: newId
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
removeAIAssistTab: (id) => set((state) => {
|
||||||
|
const newTabs = state.aiAssistTabs.filter(t => t.id !== id);
|
||||||
|
let nextActiveId = state.activeTabId;
|
||||||
|
if (state.activeTabId === id) {
|
||||||
|
nextActiveId = newTabs.length > 0 ? newTabs[newTabs.length - 1].id : null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
aiAssistTabs: newTabs,
|
||||||
|
activeTabId: nextActiveId
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
updateActiveTab: (updates) => set((state) => ({
|
||||||
|
aiAssistTabs: state.aiAssistTabs.map(t =>
|
||||||
|
t.id === state.activeTabId ? { ...t, ...updates } : t
|
||||||
|
)
|
||||||
|
})),
|
||||||
|
updateTabById: (tabId, updates) => set((state) => ({
|
||||||
|
aiAssistTabs: state.aiAssistTabs.map(t =>
|
||||||
|
t.id === tabId ? { ...t, ...updates } : t
|
||||||
|
)
|
||||||
|
})),
|
||||||
|
|
||||||
|
setLanguage: (lang) => set({ language: lang }),
|
||||||
setSelectedProvider: (provider) => set({ selectedProvider: provider }),
|
setSelectedProvider: (provider) => set({ selectedProvider: provider }),
|
||||||
setSelectedModel: (provider, model) =>
|
setSelectedModel: (provider, model) =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
@@ -90,6 +177,7 @@ const useStore = create<AppState>((set) => ({
|
|||||||
apiKeys: { ...state.apiKeys, [provider]: key },
|
apiKeys: { ...state.apiKeys, [provider]: key },
|
||||||
})),
|
})),
|
||||||
setQwenTokens: (tokens) => set({ qwenTokens: tokens }),
|
setQwenTokens: (tokens) => set({ qwenTokens: tokens }),
|
||||||
|
setGithubToken: (token) => set({ githubToken: token }),
|
||||||
setProcessing: (processing) => set({ isProcessing: processing }),
|
setProcessing: (processing) => set({ isProcessing: processing }),
|
||||||
setError: (error) => set({ error }),
|
setError: (error) => set({ error }),
|
||||||
addToHistory: (prompt) =>
|
addToHistory: (prompt) =>
|
||||||
@@ -112,6 +200,15 @@ const useStore = create<AppState>((set) => ({
|
|||||||
actionPlan: null,
|
actionPlan: null,
|
||||||
slidesPresentation: null,
|
slidesPresentation: null,
|
||||||
googleAdsResult: null,
|
googleAdsResult: null,
|
||||||
|
magicWandResult: null,
|
||||||
|
marketResearchResult: null,
|
||||||
|
aiAssistTabs: [{
|
||||||
|
id: "default",
|
||||||
|
title: "New Chat",
|
||||||
|
history: [],
|
||||||
|
currentAgent: "general"
|
||||||
|
}],
|
||||||
|
activeTabId: "default",
|
||||||
error: null,
|
error: null,
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|||||||
213
package-lock.json
generated
213
package-lock.json
generated
@@ -18,6 +18,8 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"eslint": "^9.16.0",
|
"eslint": "^9.16.0",
|
||||||
"eslint-config-next": "^15.0.3",
|
"eslint-config-next": "^15.0.3",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
|
"jszip": "^3.10.1",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"next": "^16.1.1",
|
"next": "^16.1.1",
|
||||||
"postcss": "^8.4.49",
|
"postcss": "^8.4.49",
|
||||||
@@ -31,10 +33,12 @@
|
|||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.2",
|
||||||
|
"xlsx": "^0.18.5",
|
||||||
"zod": "^4.2.1",
|
"zod": "^4.2.1",
|
||||||
"zustand": "^5.0.9"
|
"zustand": "^5.0.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/node": "^22.10.1",
|
"@types/node": "^22.10.1",
|
||||||
"@types/react": "^19.0.1",
|
"@types/react": "^19.0.1",
|
||||||
"@types/react-dom": "^19.0.2"
|
"@types/react-dom": "^19.0.2"
|
||||||
@@ -1376,6 +1380,13 @@
|
|||||||
"@types/estree": "*"
|
"@types/estree": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/file-saver": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/hast": {
|
"node_modules/@types/hast": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
|
||||||
@@ -1748,6 +1759,15 @@
|
|||||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/adler-32": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "6.12.6",
|
"version": "6.12.6",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
@@ -2242,6 +2262,19 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cfb": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"adler-32": "~1.3.0",
|
||||||
|
"crc-32": "~1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chalk": {
|
"node_modules/chalk": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
@@ -2361,6 +2394,15 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/codepage": {
|
||||||
|
"version": "1.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
|
||||||
|
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
@@ -2404,6 +2446,24 @@
|
|||||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/core-util-is": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/crc-32": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"crc32": "bin/crc32.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -3446,6 +3506,12 @@
|
|||||||
"node": ">=16.0.0"
|
"node": ">=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/file-saver": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/fill-range": {
|
"node_modules/fill-range": {
|
||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||||
@@ -3508,6 +3574,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/frac": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fraction.js": {
|
"node_modules/fraction.js": {
|
||||||
"version": "5.3.4",
|
"version": "5.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
|
||||||
@@ -3870,6 +3945,12 @@
|
|||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/immediate": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/immer": {
|
"node_modules/immer": {
|
||||||
"version": "10.2.0",
|
"version": "10.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
|
||||||
@@ -3905,6 +3986,12 @@
|
|||||||
"node": ">=0.8.19"
|
"node": ">=0.8.19"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/inherits": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/inline-style-parser": {
|
"node_modules/inline-style-parser": {
|
||||||
"version": "0.2.7",
|
"version": "0.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
|
||||||
@@ -4479,6 +4566,18 @@
|
|||||||
"node": ">=4.0"
|
"node": ">=4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jszip": {
|
||||||
|
"version": "3.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
||||||
|
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
||||||
|
"license": "(MIT OR GPL-3.0-or-later)",
|
||||||
|
"dependencies": {
|
||||||
|
"lie": "~3.3.0",
|
||||||
|
"pako": "~1.0.2",
|
||||||
|
"readable-stream": "~2.3.6",
|
||||||
|
"setimmediate": "^1.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/keyv": {
|
"node_modules/keyv": {
|
||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
@@ -4519,6 +4618,15 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lie": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"immediate": "~3.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lilconfig": {
|
"node_modules/lilconfig": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
||||||
@@ -5851,6 +5959,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pako": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||||
|
"license": "(MIT AND Zlib)"
|
||||||
|
},
|
||||||
"node_modules/parent-module": {
|
"node_modules/parent-module": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||||
@@ -6122,6 +6236,12 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/process-nextick-args": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/prop-types": {
|
"node_modules/prop-types": {
|
||||||
"version": "15.8.1",
|
"version": "15.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
@@ -6274,6 +6394,27 @@
|
|||||||
"pify": "^2.3.0"
|
"pify": "^2.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/readable-stream": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"core-util-is": "~1.0.0",
|
||||||
|
"inherits": "~2.0.3",
|
||||||
|
"isarray": "~1.0.0",
|
||||||
|
"process-nextick-args": "~2.0.0",
|
||||||
|
"safe-buffer": "~5.1.1",
|
||||||
|
"string_decoder": "~1.1.1",
|
||||||
|
"util-deprecate": "~1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/readable-stream/node_modules/isarray": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
@@ -6552,6 +6693,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/safe-buffer": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/safe-push-apply": {
|
"node_modules/safe-push-apply": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
|
||||||
@@ -6646,6 +6793,12 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/setimmediate": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/sharp": {
|
"node_modules/sharp": {
|
||||||
"version": "0.34.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
|
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
|
||||||
@@ -6816,6 +6969,18 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ssf": {
|
||||||
|
"version": "0.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
|
||||||
|
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"frac": "~1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/stable-hash": {
|
"node_modules/stable-hash": {
|
||||||
"version": "0.0.5",
|
"version": "0.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
|
||||||
@@ -6835,6 +7000,15 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string_decoder": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "~5.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/string.prototype.includes": {
|
"node_modules/string.prototype.includes": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
|
||||||
@@ -7754,6 +7928,24 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wmf": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/word": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/word-wrap": {
|
"node_modules/word-wrap": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||||
@@ -7763,6 +7955,27 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/xlsx": {
|
||||||
|
"version": "0.18.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
|
||||||
|
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"adler-32": "~1.3.0",
|
||||||
|
"cfb": "~1.2.1",
|
||||||
|
"codepage": "~1.15.0",
|
||||||
|
"crc-32": "~1.2.1",
|
||||||
|
"ssf": "~0.11.2",
|
||||||
|
"wmf": "~1.0.1",
|
||||||
|
"word": "~0.3.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"xlsx": "bin/xlsx.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yocto-queue": {
|
"node_modules/yocto-queue": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||||
|
|||||||
10
package.json
10
package.json
@@ -18,6 +18,8 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"eslint": "^9.16.0",
|
"eslint": "^9.16.0",
|
||||||
"eslint-config-next": "^15.0.3",
|
"eslint-config-next": "^15.0.3",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
|
"jszip": "^3.10.1",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"next": "^16.1.1",
|
"next": "^16.1.1",
|
||||||
"postcss": "^8.4.49",
|
"postcss": "^8.4.49",
|
||||||
@@ -31,10 +33,12 @@
|
|||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.2",
|
||||||
|
"xlsx": "^0.18.5",
|
||||||
"zod": "^4.2.1",
|
"zod": "^4.2.1",
|
||||||
"zustand": "^5.0.9"
|
"zustand": "^5.0.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/node": "^22.10.1",
|
"@types/node": "^22.10.1",
|
||||||
"@types/react": "^19.0.1",
|
"@types/react": "^19.0.1",
|
||||||
"@types/react-dom": "^19.0.2"
|
"@types/react-dom": "^19.0.2"
|
||||||
@@ -52,10 +56,10 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer.git"
|
"url": "https://github.rommark.dev/admin/PromptArch.git"
|
||||||
},
|
},
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer/issues"
|
"url": "https://github.rommark.dev/admin/PromptArch/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer#readme"
|
"homepage": "https://github.rommark.dev/admin/PromptArch"
|
||||||
}
|
}
|
||||||
135
types/index.ts
135
types/index.ts
@@ -157,8 +157,16 @@ export interface GoogleAdsCampaign {
|
|||||||
};
|
};
|
||||||
targeting: {
|
targeting: {
|
||||||
locations?: string[];
|
locations?: string[];
|
||||||
demographics?: string[];
|
demographics?: {
|
||||||
devices?: string[];
|
age?: string[];
|
||||||
|
gender?: string[];
|
||||||
|
interests?: string[];
|
||||||
|
};
|
||||||
|
devices?: {
|
||||||
|
mobile?: string;
|
||||||
|
desktop?: string;
|
||||||
|
tablet?: string;
|
||||||
|
};
|
||||||
schedule?: string[];
|
schedule?: string[];
|
||||||
};
|
};
|
||||||
adGroups: GoogleAdGroup[];
|
adGroups: GoogleAdGroup[];
|
||||||
@@ -197,8 +205,131 @@ export interface GoogleAdsResult {
|
|||||||
estimatedImpressions?: string;
|
estimatedImpressions?: string;
|
||||||
estimatedCtr?: string;
|
estimatedCtr?: string;
|
||||||
estimatedConversions?: string;
|
estimatedConversions?: string;
|
||||||
|
conversionRate?: string;
|
||||||
|
avgCpc?: string;
|
||||||
|
};
|
||||||
|
historicalBenchmarks?: {
|
||||||
|
industryAverageCtr?: string;
|
||||||
|
industryAverageCpc?: string;
|
||||||
|
seasonalTrends?: string;
|
||||||
|
geographicInsights?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
rawContent: string;
|
rawContent: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MagicWandStrategy {
|
||||||
|
id: string;
|
||||||
|
direction: string;
|
||||||
|
rationale: string;
|
||||||
|
targetAudience: string;
|
||||||
|
targetingDetails?: {
|
||||||
|
geography?: string;
|
||||||
|
demographics?: string;
|
||||||
|
behavior?: string;
|
||||||
|
};
|
||||||
|
competitiveAdvantage: string;
|
||||||
|
messagingPillars?: string[];
|
||||||
|
keyMessages: string[];
|
||||||
|
adCopyGuide: {
|
||||||
|
headlines: string[];
|
||||||
|
descriptions: string[];
|
||||||
|
keywords: string[];
|
||||||
|
setupGuide: string;
|
||||||
|
};
|
||||||
|
recommendedChannels: string[];
|
||||||
|
estimatedBudgetAllocation: {
|
||||||
|
search?: number;
|
||||||
|
display?: number;
|
||||||
|
video?: number;
|
||||||
|
social?: number;
|
||||||
|
};
|
||||||
|
expectedROI: string;
|
||||||
|
riskLevel: "low" | "medium" | "high";
|
||||||
|
timeToResults: string;
|
||||||
|
successMetrics?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MagicWandResult {
|
||||||
|
id: string;
|
||||||
|
websiteUrl: string;
|
||||||
|
product: string;
|
||||||
|
budget: number;
|
||||||
|
generatedAt: Date;
|
||||||
|
|
||||||
|
// Market Intelligence
|
||||||
|
marketAnalysis: {
|
||||||
|
industrySize: string;
|
||||||
|
growthRate: string;
|
||||||
|
topCompetitors: string[];
|
||||||
|
marketTrends: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Competitive Intelligence
|
||||||
|
competitorInsights: {
|
||||||
|
competitor: string;
|
||||||
|
website?: string;
|
||||||
|
estimatedSpend?: string;
|
||||||
|
targetAudience?: string;
|
||||||
|
strengths: string[];
|
||||||
|
weaknesses: string[];
|
||||||
|
adStrategy: string;
|
||||||
|
topKeywords?: string[];
|
||||||
|
adCopyExamples?: { headline: string; description: string }[];
|
||||||
|
}[];
|
||||||
|
|
||||||
|
// Strategic Directions
|
||||||
|
strategies: MagicWandStrategy[];
|
||||||
|
|
||||||
|
rawContent: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MarketResearchResult {
|
||||||
|
id: string;
|
||||||
|
websiteUrl: string;
|
||||||
|
additionalUrls: string[];
|
||||||
|
competitors: string[];
|
||||||
|
productMapping: {
|
||||||
|
productName: string;
|
||||||
|
features: string[];
|
||||||
|
pricePoint?: string;
|
||||||
|
}[];
|
||||||
|
generatedAt: Date;
|
||||||
|
|
||||||
|
executiveSummary: string;
|
||||||
|
priceComparisonMatrix: {
|
||||||
|
product: string;
|
||||||
|
userPrice: string;
|
||||||
|
competitorPrices: { competitor: string; price: string; url?: string }[];
|
||||||
|
}[];
|
||||||
|
featureComparisonTable: {
|
||||||
|
feature: string;
|
||||||
|
userStatus: boolean | string;
|
||||||
|
competitorStatus: { competitor: string; status: boolean | string }[];
|
||||||
|
}[];
|
||||||
|
marketPositioning: {
|
||||||
|
landscape: string;
|
||||||
|
segmentation: string;
|
||||||
|
};
|
||||||
|
competitiveAnalysis: {
|
||||||
|
advantages: string[];
|
||||||
|
disadvantages: string[];
|
||||||
|
};
|
||||||
|
recommendations: string[];
|
||||||
|
methodology: string;
|
||||||
|
rawContent: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AIAssistMessage {
|
||||||
|
role: "user" | "assistant" | "system";
|
||||||
|
content: string;
|
||||||
|
agent?: string;
|
||||||
|
preview?: {
|
||||||
|
type: string;
|
||||||
|
data: string;
|
||||||
|
language?: string;
|
||||||
|
};
|
||||||
|
timestamp: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AppView = "prompt-enhancer" | "prd-generator" | "action-plan" | "slides-gen" | "google-ads" | "ux-designer" | "market-research" | "ai-assist" | "settings" | "history";
|
||||||
|
|||||||
Reference in New Issue
Block a user