Initialize PromptArch: The Prompt Enhancer (Fork of ClavixDev/Clavix)

This commit is contained in:
Gemini AI
2025-12-25 16:58:36 +04:00
Unverified
commit c0a01b5840
35 changed files with 11054 additions and 0 deletions

View File

@@ -0,0 +1,257 @@
"use client";
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Textarea } from "@/components/ui/textarea";
import useStore from "@/lib/store";
import modelAdapter from "@/lib/services/adapter-instance";
import { ListTodo, Copy, Loader2, CheckCircle2, Clock, AlertTriangle, Settings } from "lucide-react";
import { cn } from "@/lib/utils";
export default function ActionPlanGenerator() {
const {
currentPrompt,
actionPlan,
selectedProvider,
selectedModels,
availableModels,
apiKeys,
isProcessing,
error,
setCurrentPrompt,
setSelectedProvider,
setActionPlan,
setProcessing,
setError,
setAvailableModels,
setSelectedModel,
} = useStore();
const [copied, setCopied] = useState(false);
const selectedModel = selectedModels[selectedProvider];
const models = availableModels[selectedProvider] || modelAdapter.getAvailableModels(selectedProvider);
useEffect(() => {
if (typeof window !== "undefined") {
loadAvailableModels();
const saved = localStorage.getItem("promptarch-api-keys");
if (saved) {
try {
const keys = JSON.parse(saved);
if (keys.qwen) modelAdapter.updateQwenApiKey(keys.qwen);
if (keys.ollama) modelAdapter.updateOllamaApiKey(keys.ollama);
if (keys.zai) modelAdapter.updateZaiApiKey(keys.zai);
} catch (e) {
console.error("Failed to load API keys:", e);
}
}
}
}, [selectedProvider]);
const loadAvailableModels = async () => {
const fallbackModels = modelAdapter.getAvailableModels(selectedProvider);
setAvailableModels(selectedProvider, fallbackModels);
try {
const result = await modelAdapter.listModels(selectedProvider);
if (result.success && result.data) {
setAvailableModels(selectedProvider, result.data[selectedProvider] || fallbackModels);
}
} catch (error) {
console.error("Failed to load models:", error);
}
};
const handleGenerate = async () => {
if (!currentPrompt.trim()) {
setError("Please enter PRD or project requirements");
return;
}
const apiKey = apiKeys[selectedProvider];
if (!apiKey || !apiKey.trim()) {
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
return;
}
setProcessing(true);
setError(null);
try {
const result = await modelAdapter.generateActionPlan(currentPrompt, selectedProvider, selectedModel);
if (result.success && result.data) {
const newPlan = {
id: Math.random().toString(36).substr(2, 9),
prdId: "",
tasks: [],
frameworks: [],
architecture: {
pattern: "",
structure: "",
technologies: [],
bestPractices: [],
},
estimatedDuration: "",
createdAt: new Date(),
rawContent: result.data,
};
setActionPlan(newPlan);
} else {
setError(result.error || "Failed to generate action plan");
}
} catch (err) {
setError(err instanceof Error ? err.message : "An error occurred");
} finally {
setProcessing(false);
}
};
const handleCopy = async () => {
if (actionPlan?.rawContent) {
await navigator.clipboard.writeText(actionPlan.rawContent);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
return (
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-2">
<Card className="h-fit">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<ListTodo className="h-5 w-5" />
Action Plan Generator
</CardTitle>
<CardDescription>
Convert PRD into actionable implementation plan
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium">AI Provider</label>
<div className="flex gap-2">
{(["qwen", "ollama", "zai"] as const).map((provider) => (
<Button
key={provider}
variant={selectedProvider === provider ? "default" : "outline"}
size="sm"
onClick={() => setSelectedProvider(provider)}
className="capitalize"
>
{provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"}
</Button>
))}
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Model</label>
<select
value={selectedModel}
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
{models.map((model) => (
<option key={model} value={model}>
{model}
</option>
))}
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">PRD / Requirements</label>
<Textarea
placeholder="Paste your PRD or project requirements here..."
value={currentPrompt}
onChange={(e) => setCurrentPrompt(e.target.value)}
className="min-h-[200px] resize-y"
/>
</div>
{error && (
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
{error}
{!apiKeys[selectedProvider] && (
<div className="mt-2 flex items-center gap-2">
<Settings className="h-4 w-4" />
<span className="text-xs">Configure API key in Settings</span>
</div>
)}
</div>
)}
<Button onClick={handleGenerate} disabled={isProcessing || !currentPrompt.trim()} className="w-full">
{isProcessing ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Generating Action Plan...
</>
) : (
<>
<ListTodo className="mr-2 h-4 w-4" />
Generate Action Plan
</>
)}
</Button>
</CardContent>
</Card>
<Card className={cn(!actionPlan && "opacity-50")}>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span className="flex items-center gap-2">
<CheckCircle2 className="h-5 w-5 text-green-500" />
Action Plan
</span>
{actionPlan && (
<Button variant="ghost" size="icon" onClick={handleCopy}>
{copied ? (
<CheckCircle2 className="h-4 w-4 text-green-500" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
)}
</CardTitle>
<CardDescription>
Task breakdown, frameworks, and architecture recommendations
</CardDescription>
</CardHeader>
<CardContent>
{actionPlan ? (
<div className="space-y-4">
<div className="rounded-md border bg-primary/5 p-4">
<h4 className="mb-2 flex items-center gap-2 font-semibold">
<Clock className="h-4 w-4" />
Implementation Roadmap
</h4>
<pre className="whitespace-pre-wrap text-sm">{actionPlan.rawContent}</pre>
</div>
<div className="rounded-md border bg-muted/30 p-4">
<h4 className="mb-2 flex items-center gap-2 font-semibold">
<AlertTriangle className="h-4 w-4" />
Quick Notes
</h4>
<ul className="list-inside list-disc space-y-1 text-sm text-muted-foreground">
<li>Review all task dependencies before starting</li>
<li>Set up recommended framework architecture</li>
<li>Follow best practices for security and performance</li>
<li>Use specified deployment strategy</li>
</ul>
</div>
</div>
) : (
<div className="flex h-[300px] items-center justify-center text-center text-sm text-muted-foreground">
Action plan will appear here
</div>
)}
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,74 @@
"use client";
import useStore from "@/lib/store";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Clock, Trash2, RotateCcw } from "lucide-react";
import { cn } from "@/lib/utils";
export default function HistoryPanel() {
const { history, setCurrentPrompt, clearHistory } = useStore();
const handleRestore = (prompt: string) => {
setCurrentPrompt(prompt);
};
const handleClear = () => {
if (confirm("Are you sure you want to clear all history?")) {
clearHistory();
}
};
if (history.length === 0) {
return (
<Card>
<CardContent className="flex h-[400px] items-center justify-center">
<div className="text-center">
<Clock className="mx-auto h-12 w-12 text-muted-foreground/50" />
<p className="mt-4 text-muted-foreground">No history yet</p>
<p className="mt-2 text-sm text-muted-foreground">
Start enhancing prompts to see them here
</p>
</div>
</CardContent>
</Card>
);
}
return (
<Card>
<CardHeader className="flex-row items-center justify-between">
<div>
<CardTitle>History</CardTitle>
<CardDescription>{history.length} items</CardDescription>
</div>
<Button variant="outline" size="icon" onClick={handleClear}>
<Trash2 className="h-4 w-4" />
</Button>
</CardHeader>
<CardContent className="space-y-3">
{history.map((item) => (
<div
key={item.id}
className="rounded-md border bg-muted/30 p-4 transition-colors hover:bg-muted/50"
>
<div className="mb-2 flex items-center justify-between">
<span className="text-xs text-muted-foreground">
{new Date(item.timestamp).toLocaleString()}
</span>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={() => handleRestore(item.prompt)}
>
<RotateCcw className="h-3 w-3" />
</Button>
</div>
<p className="line-clamp-3 text-sm">{item.prompt}</p>
</div>
))}
</CardContent>
</Card>
);
}

271
components/PRDGenerator.tsx Normal file
View File

@@ -0,0 +1,271 @@
"use client";
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Textarea } from "@/components/ui/textarea";
import useStore from "@/lib/store";
import modelAdapter from "@/lib/services/adapter-instance";
import { FileText, Copy, Loader2, CheckCircle2, ChevronDown, ChevronUp, Settings } from "lucide-react";
import { cn } from "@/lib/utils";
export default function PRDGenerator() {
const {
currentPrompt,
prd,
selectedProvider,
selectedModels,
availableModels,
apiKeys,
isProcessing,
error,
setCurrentPrompt,
setSelectedProvider,
setPRD,
setProcessing,
setError,
setAvailableModels,
setSelectedModel,
} = useStore();
const [copied, setCopied] = useState(false);
const [expandedSections, setExpandedSections] = useState<string[]>([]);
const selectedModel = selectedModels[selectedProvider];
const models = availableModels[selectedProvider] || modelAdapter.getAvailableModels(selectedProvider);
const toggleSection = (section: string) => {
setExpandedSections((prev) =>
prev.includes(section) ? prev.filter((s) => s !== section) : [...prev, section]
);
};
useEffect(() => {
if (typeof window !== "undefined") {
loadAvailableModels();
const saved = localStorage.getItem("promptarch-api-keys");
if (saved) {
try {
const keys = JSON.parse(saved);
if (keys.qwen) modelAdapter.updateQwenApiKey(keys.qwen);
if (keys.ollama) modelAdapter.updateOllamaApiKey(keys.ollama);
if (keys.zai) modelAdapter.updateZaiApiKey(keys.zai);
} catch (e) {
console.error("Failed to load API keys:", e);
}
}
}
}, [selectedProvider]);
const loadAvailableModels = async () => {
const fallbackModels = modelAdapter.getAvailableModels(selectedProvider);
setAvailableModels(selectedProvider, fallbackModels);
try {
const result = await modelAdapter.listModels(selectedProvider);
if (result.success && result.data) {
setAvailableModels(selectedProvider, result.data[selectedProvider] || fallbackModels);
}
} catch (error) {
console.error("Failed to load models:", error);
}
};
const handleGenerate = async () => {
if (!currentPrompt.trim()) {
setError("Please enter an idea to generate PRD");
return;
}
const apiKey = apiKeys[selectedProvider];
if (!apiKey || !apiKey.trim()) {
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
return;
}
setProcessing(true);
setError(null);
try {
const result = await modelAdapter.generatePRD(currentPrompt, selectedProvider, selectedModel);
if (result.success && result.data) {
const newPRD = {
id: Math.random().toString(36).substr(2, 9),
title: currentPrompt.slice(0, 50) + "...",
overview: result.data,
objectives: [],
userPersonas: [],
functionalRequirements: [],
nonFunctionalRequirements: [],
technicalArchitecture: "",
successMetrics: [],
createdAt: new Date(),
updatedAt: new Date(),
};
setPRD(newPRD);
} else {
setError(result.error || "Failed to generate PRD");
}
} catch (err) {
setError(err instanceof Error ? err.message : "An error occurred");
} finally {
setProcessing(false);
}
};
const handleCopy = async () => {
if (prd?.overview) {
await navigator.clipboard.writeText(prd.overview);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
const sections = [
{ id: "overview", title: "Overview & Objectives" },
{ id: "personas", title: "User Personas & Use Cases" },
{ id: "functional", title: "Functional Requirements" },
{ id: "nonfunctional", title: "Non-functional Requirements" },
{ id: "architecture", title: "Technical Architecture" },
{ id: "metrics", title: "Success Metrics" },
];
return (
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-2">
<Card className="h-fit">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FileText className="h-5 w-5" />
PRD Generator
</CardTitle>
<CardDescription>
Generate comprehensive Product Requirements Document from your idea
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium">AI Provider</label>
<div className="flex gap-2">
{(["qwen", "ollama", "zai"] as const).map((provider) => (
<Button
key={provider}
variant={selectedProvider === provider ? "default" : "outline"}
size="sm"
onClick={() => setSelectedProvider(provider)}
className="capitalize"
>
{provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"}
</Button>
))}
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Model</label>
<select
value={selectedModel}
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
{models.map((model) => (
<option key={model} value={model}>
{model}
</option>
))}
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Your Idea</label>
<Textarea
placeholder="e.g., A task management app with real-time collaboration features"
value={currentPrompt}
onChange={(e) => setCurrentPrompt(e.target.value)}
className="min-h-[200px] resize-y"
/>
</div>
{error && (
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
{error}
{!apiKeys[selectedProvider] && (
<div className="mt-2 flex items-center gap-2">
<Settings className="h-4 w-4" />
<span className="text-xs">Configure API key in Settings</span>
</div>
)}
</div>
)}
<Button onClick={handleGenerate} disabled={isProcessing || !currentPrompt.trim()} className="w-full">
{isProcessing ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Generating PRD...
</>
) : (
<>
<FileText className="mr-2 h-4 w-4" />
Generate PRD
</>
)}
</Button>
</CardContent>
</Card>
<Card className={cn(!prd && "opacity-50")}>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span className="flex items-center gap-2">
<CheckCircle2 className="h-5 w-5 text-green-500" />
Generated PRD
</span>
{prd && (
<Button variant="ghost" size="icon" onClick={handleCopy}>
{copied ? (
<CheckCircle2 className="h-4 w-4 text-green-500" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
)}
</CardTitle>
<CardDescription>
Structured requirements document ready for development
</CardDescription>
</CardHeader>
<CardContent>
{prd ? (
<div className="space-y-3">
{sections.map((section) => (
<div key={section.id} className="rounded-md border bg-muted/30">
<button
onClick={() => toggleSection(section.id)}
className="flex w-full items-center justify-between px-4 py-3 text-left font-medium transition-colors hover:bg-muted/50"
>
<span>{section.title}</span>
{expandedSections.includes(section.id) ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</button>
{expandedSections.includes(section.id) && (
<div className="border-t bg-background px-4 py-3">
<pre className="whitespace-pre-wrap text-sm">{prd.overview}</pre>
</div>
)}
</div>
))}
</div>
) : (
<div className="flex h-[300px] items-center justify-center text-center text-sm text-muted-foreground">
PRD will appear here
</div>
)}
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,238 @@
"use client";
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Textarea } from "@/components/ui/textarea";
import useStore from "@/lib/store";
import modelAdapter from "@/lib/services/adapter-instance";
import { Sparkles, Copy, RefreshCw, Loader2, CheckCircle2, Settings } from "lucide-react";
import { cn } from "@/lib/utils";
export default function PromptEnhancer() {
const {
currentPrompt,
enhancedPrompt,
selectedProvider,
selectedModels,
availableModels,
apiKeys,
isProcessing,
error,
setSelectedProvider,
setCurrentPrompt,
setEnhancedPrompt,
setProcessing,
setError,
setAvailableModels,
setSelectedModel,
} = useStore();
const [copied, setCopied] = useState(false);
const selectedModel = selectedModels[selectedProvider];
const models = availableModels[selectedProvider] || modelAdapter.getAvailableModels(selectedProvider);
useEffect(() => {
if (typeof window !== "undefined") {
loadAvailableModels();
const saved = localStorage.getItem("promptarch-api-keys");
if (saved) {
try {
const keys = JSON.parse(saved);
if (keys.qwen) modelAdapter.updateQwenApiKey(keys.qwen);
if (keys.ollama) modelAdapter.updateOllamaApiKey(keys.ollama);
if (keys.zai) modelAdapter.updateZaiApiKey(keys.zai);
} catch (e) {
console.error("Failed to load API keys:", e);
}
}
}
}, [selectedProvider]);
const loadAvailableModels = async () => {
const fallbackModels = modelAdapter.getAvailableModels(selectedProvider);
setAvailableModels(selectedProvider, fallbackModels);
try {
const result = await modelAdapter.listModels(selectedProvider);
if (result.success && result.data) {
setAvailableModels(selectedProvider, result.data[selectedProvider] || fallbackModels);
}
} catch (error) {
console.error("Failed to load models:", error);
}
};
const handleEnhance = async () => {
if (!currentPrompt.trim()) {
setError("Please enter a prompt to enhance");
return;
}
const apiKey = apiKeys[selectedProvider];
if (!apiKey || !apiKey.trim()) {
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
return;
}
setProcessing(true);
setError(null);
try {
const result = await modelAdapter.enhancePrompt(currentPrompt, selectedProvider, selectedModel);
if (result.success && result.data) {
setEnhancedPrompt(result.data);
} else {
setError(result.error || "Failed to enhance prompt");
}
} catch (err) {
setError(err instanceof Error ? err.message : "An error occurred");
} finally {
setProcessing(false);
}
};
const handleCopy = async () => {
if (enhancedPrompt) {
await navigator.clipboard.writeText(enhancedPrompt);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
const handleClear = () => {
setCurrentPrompt("");
setEnhancedPrompt(null);
setError(null);
};
return (
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-2">
<Card className="h-fit">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Sparkles className="h-5 w-5" />
Input Prompt
</CardTitle>
<CardDescription>
Enter your prompt and we'll enhance it for AI coding agents
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium">AI Provider</label>
<div className="flex flex-wrap gap-2">
{(["qwen", "ollama", "zai"] as const).map((provider) => (
<Button
key={provider}
variant={selectedProvider === provider ? "default" : "outline"}
size="sm"
onClick={() => setSelectedProvider(provider)}
className={cn(
"capitalize",
selectedProvider === provider && "bg-primary text-primary-foreground"
)}
>
{provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"}
</Button>
))}
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Model</label>
<select
value={selectedModel}
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
{models.map((model) => (
<option key={model} value={model}>
{model}
</option>
))}
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Your Prompt</label>
<Textarea
placeholder="e.g., Create a user authentication system with JWT tokens"
value={currentPrompt}
onChange={(e) => setCurrentPrompt(e.target.value)}
className="min-h-[200px] resize-y"
/>
</div>
{error && (
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
{error}
{!apiKeys[selectedProvider] && (
<div className="mt-2 flex items-center gap-2">
<Settings className="h-4 w-4" />
<span className="text-xs">Configure API key in Settings</span>
</div>
)}
</div>
)}
<div className="flex gap-2">
<Button onClick={handleEnhance} disabled={isProcessing || !currentPrompt.trim()} className="flex-1">
{isProcessing ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Enhancing...
</>
) : (
<>
<Sparkles className="mr-2 h-4 w-4" />
Enhance Prompt
</>
)}
</Button>
<Button variant="outline" onClick={handleClear} disabled={isProcessing}>
<RefreshCw className="mr-2 h-4 w-4" />
Clear
</Button>
</div>
</CardContent>
</Card>
<Card className={cn(!enhancedPrompt && "opacity-50")}>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span className="flex items-center gap-2">
<CheckCircle2 className="h-5 w-5 text-green-500" />
Enhanced Prompt
</span>
{enhancedPrompt && (
<Button variant="ghost" size="icon" onClick={handleCopy}>
{copied ? (
<CheckCircle2 className="h-4 w-4 text-green-500" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
)}
</CardTitle>
<CardDescription>
Professional prompt ready for coding agents
</CardDescription>
</CardHeader>
<CardContent>
{enhancedPrompt ? (
<div className="rounded-md border bg-muted/50 p-4">
<pre className="whitespace-pre-wrap text-sm">{enhancedPrompt}</pre>
</div>
) : (
<div className="flex h-[200px] items-center justify-center text-center text-sm text-muted-foreground">
Enhanced prompt will appear here
</div>
)}
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,288 @@
"use client";
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Textarea } from "@/components/ui/textarea";
import { Input } from "@/components/ui/input";
import useStore from "@/lib/store";
import modelAdapter from "@/lib/services/adapter-instance";
import { Save, Key, Server, Eye, EyeOff } from "lucide-react";
import { cn } from "@/lib/utils";
export default function SettingsPanel() {
const { apiKeys, setApiKey, selectedProvider, setSelectedProvider, qwenTokens, setQwenTokens } = useStore();
const [showApiKey, setShowApiKey] = useState<Record<string, boolean>>({});
const handleSave = () => {
if (typeof window !== "undefined") {
localStorage.setItem("promptarch-api-keys", JSON.stringify(apiKeys));
alert("API keys saved successfully!");
}
};
const handleLoad = () => {
if (typeof window !== "undefined") {
const saved = localStorage.getItem("promptarch-api-keys");
if (saved) {
try {
const keys = JSON.parse(saved);
if (keys.qwen) {
setApiKey("qwen", keys.qwen);
modelAdapter.updateQwenApiKey(keys.qwen);
}
if (keys.ollama) {
setApiKey("ollama", keys.ollama);
modelAdapter.updateOllamaApiKey(keys.ollama);
}
if (keys.zai) {
setApiKey("zai", keys.zai);
modelAdapter.updateZaiApiKey(keys.zai);
}
} catch (e) {
console.error("Failed to load API keys:", e);
}
}
}
};
const handleApiKeyChange = (provider: string, value: string) => {
setApiKey(provider as "qwen" | "ollama" | "zai", value);
switch (provider) {
case "qwen":
modelAdapter.updateQwenApiKey(value);
break;
case "ollama":
modelAdapter.updateOllamaApiKey(value);
break;
case "zai":
modelAdapter.updateZaiApiKey(value);
break;
}
};
useEffect(() => {
handleLoad();
}, []);
return (
<div className="mx-auto max-w-3xl space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Key className="h-5 w-5" />
API Configuration
</CardTitle>
<CardDescription>
Configure API keys for different AI providers
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-medium">
<Server className="h-4 w-4" />
Qwen Code API Key
</label>
<div className="relative">
<Input
type={showApiKey.qwen ? "text" : "password"}
placeholder="Enter your Qwen API key"
value={apiKeys.qwen || ""}
onChange={(e) => handleApiKeyChange("qwen", e.target.value)}
className="font-mono text-sm"
/>
<Button
type="button"
variant="ghost"
size="icon"
className="absolute right-0 top-0 h-full"
onClick={() => setShowApiKey((prev) => ({ ...prev, qwen: !prev.qwen }))}
>
{showApiKey.qwen ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</Button>
</div>
<div className="flex items-center gap-4">
<p className="text-xs text-muted-foreground flex-1">
Get API key from{" "}
<a
href="https://help.aliyun.com/zh/dashscope/"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
Alibaba DashScope
</a>
</p>
<Button
variant={qwenTokens ? "secondary" : "outline"}
size="sm"
className="h-8"
onClick={() => {
if (qwenTokens) {
setQwenTokens(undefined as any);
localStorage.removeItem("promptarch-qwen-tokens");
modelAdapter.updateQwenApiKey(apiKeys.qwen || "");
} else {
window.location.href = modelAdapter.getQwenAuthUrl();
}
}}
>
{qwenTokens ? "Logout from Qwen" : "Login with Qwen (OAuth)"}
</Button>
</div>
{qwenTokens && (
<p className="text-[10px] text-green-600 dark:text-green-400 font-medium">
Authenticated via OAuth (Expires: {new Date(qwenTokens.expiresAt || 0).toLocaleString()})
</p>
)}
</div>
<div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-medium">
<Server className="h-4 w-4" />
Ollama Cloud API Key
</label>
<div className="relative">
<Input
type={showApiKey.ollama ? "text" : "password"}
placeholder="Enter your Ollama API key"
value={apiKeys.ollama || ""}
onChange={(e) => handleApiKeyChange("ollama", e.target.value)}
className="font-mono text-sm"
/>
<Button
type="button"
variant="ghost"
size="icon"
className="absolute right-0 top-0 h-full"
onClick={() => setShowApiKey((prev) => ({ ...prev, ollama: !prev.ollama }))}
>
{showApiKey.ollama ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</Button>
</div>
<p className="text-xs text-muted-foreground">
Get API key from{" "}
<a
href="https://ollama.com/cloud"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
ollama.com/cloud
</a>
</p>
</div>
<div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-medium">
<Server className="h-4 w-4" />
Z.AI Plan API Key
</label>
<div className="relative">
<Input
type={showApiKey.zai ? "text" : "password"}
placeholder="Enter your Z.AI API key"
value={apiKeys.zai || ""}
onChange={(e) => handleApiKeyChange("zai", e.target.value)}
className="font-mono text-sm"
/>
<Button
type="button"
variant="ghost"
size="icon"
className="absolute right-0 top-0 h-full"
onClick={() => setShowApiKey((prev) => ({ ...prev, zai: !prev.zai }))}
>
{showApiKey.zai ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</Button>
</div>
<p className="text-xs text-muted-foreground">
Get API key from{" "}
<a
href="https://docs.z.ai"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
docs.z.ai
</a>
</p>
</div>
<Button onClick={handleSave} className="w-full">
<Save className="mr-2 h-4 w-4" />
Save API Keys
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Default Provider</CardTitle>
<CardDescription>
Select your preferred AI provider
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-3">
{(["qwen", "ollama", "zai"] as const).map((provider) => (
<button
key={provider}
onClick={() => setSelectedProvider(provider)}
className={`flex items-center gap-3 rounded-lg border p-4 text-left transition-colors hover:bg-muted/50 ${
selectedProvider === provider
? "border-primary bg-primary/5"
: "border-border"
}`}
>
<div className="flex h-10 w-10 items-center justify-center rounded-md bg-primary/10">
<Server className="h-5 w-5 text-primary" />
</div>
<div className="flex-1">
<h3 className="font-medium capitalize">{provider}</h3>
<p className="text-sm text-muted-foreground">
{provider === "qwen" && "Alibaba DashScope API"}
{provider === "ollama" && "Ollama Cloud API"}
{provider === "zai" && "Z.AI Plan API"}
</p>
</div>
{selectedProvider === provider && (
<div className="h-2 w-2 rounded-full bg-primary" />
)}
</button>
))}
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Data Privacy</CardTitle>
<CardDescription>
Your data handling preferences
</CardDescription>
</CardHeader>
<CardContent>
<div className="rounded-md border bg-muted/30 p-4">
<p className="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.
</p>
</div>
</CardContent>
</Card>
</div>
);
}

89
components/Sidebar.tsx Normal file
View File

@@ -0,0 +1,89 @@
"use client";
import { Button } from "@/components/ui/button";
import useStore from "@/lib/store";
import { Sparkles, FileText, ListTodo, Settings, History } from "lucide-react";
import { cn } from "@/lib/utils";
export type View = "enhance" | "prd" | "action" | "history" | "settings";
interface SidebarProps {
currentView: View;
onViewChange: (view: View) => void;
}
export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
const history = useStore((state) => state.history);
const menuItems = [
{ id: "enhance" as View, label: "Prompt Enhancer", icon: Sparkles },
{ id: "prd" as View, label: "PRD Generator", icon: FileText },
{ id: "action" as View, label: "Action Plan", icon: ListTodo },
{ id: "history" as View, label: "History", icon: History, count: history.length },
{ id: "settings" as View, label: "Settings", icon: Settings },
];
return (
<aside className="flex h-screen w-64 flex-col border-r bg-card">
<div className="border-b p-6">
<h1 className="flex items-center gap-2 text-xl font-bold">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
PA
</div>
PromptArch
</h1>
</div>
<nav className="flex-1 space-y-1 p-4">
{menuItems.map((item) => (
<Button
key={item.id}
variant={currentView === item.id ? "default" : "ghost"}
className={cn(
"w-full justify-start gap-2",
currentView === item.id && "bg-primary text-primary-foreground"
)}
onClick={() => onViewChange(item.id)}
>
<item.icon className="h-4 w-4" />
<span className="flex-1 text-left">{item.label}</span>
{item.count !== undefined && item.count > 0 && (
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-primary-foreground text-xs font-medium">
{item.count}
</span>
)}
</Button>
))}
<div className="mt-8 p-3 text-[10px] leading-relaxed text-muted-foreground border-t border-border/50 pt-4">
<p className="font-semibold text-foreground mb-1">Developed by Roman | RyzenAdvanced</p>
<div className="space-y-1">
<p>
GitHub: <a href="https://github.com/roman-ryzenadvanced/Custom-Engineered-Agents-and-Tools-for-Vibe-Coders" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">roman-ryzenadvanced</a>
</p>
<p>
Telegram: <a href="https://t.me/VibeCodePrompterSystem" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">@VibeCodePrompterSystem</a>
</p>
<p className="mt-2 text-[9px] opacity-80">
100% Developed using GLM 4.7 model on TRAE.AI IDE.
</p>
<p className="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>
</p>
</div>
</div>
</nav>
<div className="border-t p-4">
<div className="rounded-md bg-muted/50 p-3 text-xs text-muted-foreground">
<p className="font-medium text-foreground">Quick Tips</p>
<ul className="mt-2 space-y-1">
<li> Use different providers for best results</li>
<li> Copy enhanced prompts to your AI agent</li>
<li> PRDs generate better action plans</li>
</ul>
</div>
</div>
</aside>
);
}

38
components/ui/button.tsx Normal file
View File

@@ -0,0 +1,38 @@
import * as React from "react";
import { cn } from "@/lib/utils";
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link";
size?: "default" | "sm" | "lg" | "icon";
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant = "default", size = "default", ...props }, ref) => {
const baseStyles =
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50";
const variants = {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
};
const sizes = {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
};
return (
<button className={cn(baseStyles, variants[variant], sizes[size], className)} ref={ref} {...props} />
);
}
);
Button.displayName = "Button";
export { Button };

52
components/ui/card.tsx Normal file
View File

@@ -0,0 +1,52 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)} {...props} />
)
);
Card.displayName = "Card";
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
)
);
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h3 ref={ref} className={cn("text-2xl font-semibold leading-none tracking-tight", className)} {...props} />
)
);
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => (
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
)
);
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
)
);
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
)
);
CardFooter.displayName = "CardFooter";
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };

25
components/ui/input.tsx Normal file
View File

@@ -0,0 +1,25 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

25
components/ui/select.tsx Normal file
View File

@@ -0,0 +1,25 @@
import * as React from "react";
import { cn } from "@/lib/utils";
export interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {}
const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
({ className, children, ...props }, ref) => {
return (
<select
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
>
{children}
</select>
);
}
);
Select.displayName = "Select";
export { Select };

View File

@@ -0,0 +1,23 @@
import * as React from "react";
import { cn } from "@/lib/utils";
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
);
}
);
Textarea.displayName = "Textarea";
export { Textarea };