Add full mobile responsive design with hamburger menu, responsive components, and mobile optimizations
This commit is contained in:
@@ -65,5 +65,51 @@
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* Mobile optimizations */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/* Better touch targets */
|
||||
button, a, [role="button"] {
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
/* Prevent text selection on buttons */
|
||||
button {
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Safe area padding for notched devices */
|
||||
.safe-area-inset {
|
||||
padding-left: env(safe-area-inset-left);
|
||||
padding-right: env(safe-area-inset-right);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
/* Scrollbar styling for mobile-like experience */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: hsl(var(--border));
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: hsl(var(--muted-foreground));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ const inter = Inter({ subsets: ["latin"] });
|
||||
export const metadata: Metadata = {
|
||||
title: "PromptArch - AI Prompt Engineering Platform",
|
||||
description: "Transform vague ideas into production-ready prompts and PRDs",
|
||||
viewport: "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
|
||||
@@ -41,7 +41,7 @@ export default function Home() {
|
||||
return (
|
||||
<div className="flex min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800">
|
||||
<Sidebar currentView={currentView} onViewChange={setCurrentView} />
|
||||
<main className="flex-1 overflow-auto p-8">
|
||||
<main className="flex-1 overflow-auto pt-16 lg:pt-0 px-4 py-4 lg:p-8">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
{renderContent()}
|
||||
</div>
|
||||
|
||||
@@ -53,7 +53,7 @@ export default function ActionPlanGenerator() {
|
||||
const loadAvailableModels = async () => {
|
||||
const fallbackModels = modelAdapter.getAvailableModels(selectedProvider);
|
||||
setAvailableModels(selectedProvider, fallbackModels);
|
||||
|
||||
|
||||
try {
|
||||
const result = await modelAdapter.listModels(selectedProvider);
|
||||
if (result.success && result.data) {
|
||||
@@ -126,28 +126,28 @@ export default function ActionPlanGenerator() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-2">
|
||||
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2">
|
||||
<Card className="h-fit">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<ListTodo className="h-5 w-5" />
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||
<ListTodo className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||
Action Plan Generator
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Convert PRD into actionable implementation plan
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">AI Provider</label>
|
||||
<div className="flex gap-2">
|
||||
<label className="text-xs lg:text-sm font-medium">AI Provider</label>
|
||||
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
||||
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
||||
<Button
|
||||
key={provider}
|
||||
variant={selectedProvider === provider ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => setSelectedProvider(provider)}
|
||||
className="capitalize"
|
||||
className="capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3"
|
||||
>
|
||||
{provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"}
|
||||
</Button>
|
||||
@@ -156,11 +156,11 @@ export default function ActionPlanGenerator() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Model</label>
|
||||
<label className="text-xs lg: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"
|
||||
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"
|
||||
>
|
||||
{models.map((model) => (
|
||||
<option key={model} value={model}>
|
||||
@@ -171,36 +171,36 @@ export default function ActionPlanGenerator() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">PRD / Requirements</label>
|
||||
<label className="text-xs lg: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"
|
||||
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
|
||||
<div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg: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 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" />
|
||||
<span className="text-[10px] lg:text-xs">Configure API key in Settings</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button onClick={handleGenerate} disabled={isProcessing || !currentPrompt.trim()} className="w-full">
|
||||
<Button onClick={handleGenerate} disabled={isProcessing || !currentPrompt.trim()} className="w-full h-9 lg:h-10 text-xs lg:text-sm">
|
||||
{isProcessing ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Generating Action Plan...
|
||||
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
|
||||
Generating...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ListTodo className="mr-2 h-4 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
|
||||
</>
|
||||
)}
|
||||
@@ -209,43 +209,43 @@ export default function ActionPlanGenerator() {
|
||||
</Card>
|
||||
|
||||
<Card className={cn(!actionPlan && "opacity-50")}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
||||
<span className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
||||
Action Plan
|
||||
</span>
|
||||
{actionPlan && (
|
||||
<Button variant="ghost" size="icon" onClick={handleCopy}>
|
||||
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
||||
{copied ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
<CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
<Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Task breakdown, frameworks, and architecture recommendations
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
{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" />
|
||||
<div className="space-y-3 lg:space-y-4">
|
||||
<div className="rounded-md border bg-primary/5 p-3 lg:p-4">
|
||||
<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" />
|
||||
Implementation Roadmap
|
||||
</h4>
|
||||
<pre className="whitespace-pre-wrap text-sm">{actionPlan.rawContent}</pre>
|
||||
<pre className="whitespace-pre-wrap text-xs lg: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" />
|
||||
<div className="rounded-md border bg-muted/30 p-3 lg:p-4">
|
||||
<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" />
|
||||
Quick Notes
|
||||
</h4>
|
||||
<ul className="list-inside list-disc space-y-1 text-sm 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>
|
||||
<li>Set up recommended framework architecture</li>
|
||||
<li>Follow best practices for security and performance</li>
|
||||
@@ -254,7 +254,7 @@ export default function ActionPlanGenerator() {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-[300px] items-center justify-center text-center 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">
|
||||
Action plan will appear here
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -4,7 +4,6 @@ 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();
|
||||
@@ -22,11 +21,11 @@ export default function HistoryPanel() {
|
||||
if (history.length === 0) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex h-[400px] items-center justify-center">
|
||||
<CardContent className="flex h-[300px] lg:h-[400px] items-center justify-center p-4 lg:p-6">
|
||||
<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">
|
||||
<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-1.5 lg:mt-2 text-xs lg:text-sm text-muted-foreground">
|
||||
Start enhancing prompts to see them here
|
||||
</p>
|
||||
</div>
|
||||
@@ -37,35 +36,35 @@ export default function HistoryPanel() {
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex-row items-center justify-between">
|
||||
<CardHeader className="flex-row items-center justify-between p-4 lg:p-6">
|
||||
<div>
|
||||
<CardTitle>History</CardTitle>
|
||||
<CardDescription>{history.length} items</CardDescription>
|
||||
<CardTitle className="text-base lg:text-lg">History</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">{history.length} items</CardDescription>
|
||||
</div>
|
||||
<Button variant="outline" size="icon" onClick={handleClear}>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
<Button variant="outline" size="icon" onClick={handleClear} className="h-8 w-8 lg:h-9 lg:w-9">
|
||||
<Trash2 className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<CardContent className="space-y-2 lg:space-y-3 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
{history.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="rounded-md border bg-muted/30 p-4 transition-colors hover:bg-muted/50"
|
||||
className="rounded-md border bg-muted/30 p-3 lg:p-4 transition-colors hover:bg-muted/50"
|
||||
>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<div className="mb-1.5 lg:mb-2 flex items-center justify-between gap-2">
|
||||
<span className="text-[10px] lg:text-xs text-muted-foreground truncate">
|
||||
{new Date(item.timestamp).toLocaleString()}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
className="h-6 w-6 flex-shrink-0"
|
||||
onClick={() => handleRestore(item.prompt)}
|
||||
>
|
||||
<RotateCcw className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="line-clamp-3 text-sm">{item.prompt}</p>
|
||||
<p className="line-clamp-3 text-xs lg:text-sm">{item.prompt}</p>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
|
||||
@@ -60,7 +60,7 @@ export default function PRDGenerator() {
|
||||
const loadAvailableModels = async () => {
|
||||
const fallbackModels = modelAdapter.getAvailableModels(selectedProvider);
|
||||
setAvailableModels(selectedProvider, fallbackModels);
|
||||
|
||||
|
||||
try {
|
||||
const result = await modelAdapter.listModels(selectedProvider);
|
||||
if (result.success && result.data) {
|
||||
@@ -140,28 +140,28 @@ export default function PRDGenerator() {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-2">
|
||||
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2">
|
||||
<Card className="h-fit">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FileText className="h-5 w-5" />
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||
<FileText className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||
PRD Generator
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Generate comprehensive Product Requirements Document from your idea
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">AI Provider</label>
|
||||
<div className="flex gap-2">
|
||||
<label className="text-xs lg:text-sm font-medium">AI Provider</label>
|
||||
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
||||
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
||||
<Button
|
||||
key={provider}
|
||||
variant={selectedProvider === provider ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => setSelectedProvider(provider)}
|
||||
className="capitalize"
|
||||
className="capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3"
|
||||
>
|
||||
{provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"}
|
||||
</Button>
|
||||
@@ -170,11 +170,11 @@ export default function PRDGenerator() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Model</label>
|
||||
<label className="text-xs lg: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"
|
||||
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"
|
||||
>
|
||||
{models.map((model) => (
|
||||
<option key={model} value={model}>
|
||||
@@ -185,36 +185,36 @@ export default function PRDGenerator() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Your Idea</label>
|
||||
<label className="text-xs lg: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"
|
||||
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
|
||||
<div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg: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 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" />
|
||||
<span className="text-[10px] lg:text-xs">Configure API key in Settings</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button onClick={handleGenerate} disabled={isProcessing || !currentPrompt.trim()} className="w-full">
|
||||
<Button onClick={handleGenerate} disabled={isProcessing || !currentPrompt.trim()} className="w-full h-9 lg:h-10 text-xs lg:text-sm">
|
||||
{isProcessing ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 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...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FileText className="mr-2 h-4 w-4" />
|
||||
<FileText className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
Generate PRD
|
||||
</>
|
||||
)}
|
||||
@@ -223,52 +223,52 @@ export default function PRDGenerator() {
|
||||
</Card>
|
||||
|
||||
<Card className={cn(!prd && "opacity-50")}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
||||
<span className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
||||
Generated PRD
|
||||
</span>
|
||||
{prd && (
|
||||
<Button variant="ghost" size="icon" onClick={handleCopy}>
|
||||
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
||||
{copied ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
<CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
<Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Structured requirements document ready for development
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
{prd ? (
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2 lg: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"
|
||||
className="flex w-full items-center justify-between px-3 lg:px-4 py-2.5 lg:py-3 text-left font-medium transition-colors hover:bg-muted/50 text-xs lg:text-sm"
|
||||
>
|
||||
<span>{section.title}</span>
|
||||
{expandedSections.includes(section.id) ? (
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
<ChevronUp className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
<ChevronDown className="h-3.5 w-3.5 lg:h-4 lg: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 className="border-t bg-background px-3 lg:px-4 py-2.5 lg:py-3">
|
||||
<pre className="whitespace-pre-wrap text-xs lg:text-sm">{prd.overview}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-[300px] items-center justify-center text-center 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
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -53,7 +53,7 @@ export default function PromptEnhancer() {
|
||||
const loadAvailableModels = async () => {
|
||||
const fallbackModels = modelAdapter.getAvailableModels(selectedProvider);
|
||||
setAvailableModels(selectedProvider, fallbackModels);
|
||||
|
||||
|
||||
try {
|
||||
const result = await modelAdapter.listModels(selectedProvider);
|
||||
if (result.success && result.data) {
|
||||
@@ -117,21 +117,21 @@ export default function PromptEnhancer() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-2">
|
||||
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2">
|
||||
<Card className="h-fit">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Sparkles className="h-5 w-5" />
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||
<Sparkles className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||
Input Prompt
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Enter your prompt and we'll enhance it for AI coding agents
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">AI Provider</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<label className="text-xs lg:text-sm font-medium">AI Provider</label>
|
||||
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
||||
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
||||
<Button
|
||||
key={provider}
|
||||
@@ -139,7 +139,7 @@ export default function PromptEnhancer() {
|
||||
size="sm"
|
||||
onClick={() => setSelectedProvider(provider)}
|
||||
className={cn(
|
||||
"capitalize",
|
||||
"capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3",
|
||||
selectedProvider === provider && "bg-primary text-primary-foreground"
|
||||
)}
|
||||
>
|
||||
@@ -150,11 +150,11 @@ export default function PromptEnhancer() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Model</label>
|
||||
<label className="text-xs lg: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"
|
||||
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"
|
||||
>
|
||||
{models.map((model) => (
|
||||
<option key={model} value={model}>
|
||||
@@ -165,77 +165,77 @@ export default function PromptEnhancer() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Your Prompt</label>
|
||||
<label className="text-xs lg: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"
|
||||
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
|
||||
<div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg: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 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" />
|
||||
<span className="text-[10px] lg: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">
|
||||
<Button onClick={handleEnhance} disabled={isProcessing || !currentPrompt.trim()} className="flex-1 h-9 lg:h-10 text-xs lg:text-sm">
|
||||
{isProcessing ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 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...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Sparkles className="mr-2 h-4 w-4" />
|
||||
<Sparkles className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
Enhance Prompt
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleClear} disabled={isProcessing}>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
Clear
|
||||
<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" />
|
||||
<span className="hidden sm:inline">Clear</span>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className={cn(!enhancedPrompt && "opacity-50")}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
||||
<span className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
||||
Enhanced Prompt
|
||||
</span>
|
||||
{enhancedPrompt && (
|
||||
<Button variant="ghost" size="icon" onClick={handleCopy}>
|
||||
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
||||
{copied ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
<CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
<Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Professional prompt ready for coding agents
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
{enhancedPrompt ? (
|
||||
<div className="rounded-md border bg-muted/50 p-4">
|
||||
<pre className="whitespace-pre-wrap text-sm">{enhancedPrompt}</pre>
|
||||
<div className="rounded-md border bg-muted/50 p-3 lg:p-4">
|
||||
<pre className="whitespace-pre-wrap text-xs lg:text-sm">{enhancedPrompt}</pre>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-[200px] items-center justify-center text-center 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">
|
||||
Enhanced prompt will appear here
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -51,7 +51,7 @@ export default function SettingsPanel() {
|
||||
|
||||
const handleApiKeyChange = (provider: string, value: string) => {
|
||||
setApiKey(provider as "qwen" | "ollama" | "zai", value);
|
||||
|
||||
|
||||
switch (provider) {
|
||||
case "qwen":
|
||||
modelAdapter.updateQwenApiKey(value);
|
||||
@@ -93,21 +93,21 @@ export default function SettingsPanel() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl space-y-6">
|
||||
<div className="mx-auto max-w-3xl space-y-4 lg:space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Key className="h-5 w-5" />
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||
<Key className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||
API Configuration
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Configure API keys for different AI providers
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<CardContent className="space-y-4 lg:space-y-6 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center gap-2 text-sm font-medium">
|
||||
<Server className="h-4 w-4" />
|
||||
<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" />
|
||||
Qwen Code API Key
|
||||
</label>
|
||||
<div className="relative">
|
||||
@@ -116,24 +116,24 @@ export default function SettingsPanel() {
|
||||
placeholder="Enter your Qwen API key"
|
||||
value={apiKeys.qwen || ""}
|
||||
onChange={(e) => handleApiKeyChange("qwen", e.target.value)}
|
||||
className="font-mono text-sm"
|
||||
className="font-mono text-xs lg:text-sm pr-10"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-0 top-0 h-full"
|
||||
className="absolute right-0 top-0 h-full w-9 lg:w-10"
|
||||
onClick={() => setShowApiKey((prev) => ({ ...prev, qwen: !prev.qwen }))}
|
||||
>
|
||||
{showApiKey.qwen ? (
|
||||
<EyeOff className="h-4 w-4" />
|
||||
<EyeOff className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4" />
|
||||
<Eye className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<p className="text-xs text-muted-foreground flex-1">
|
||||
<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">
|
||||
Get API key from{" "}
|
||||
<a
|
||||
href="https://help.aliyun.com/zh/dashscope/"
|
||||
@@ -147,27 +147,27 @@ export default function SettingsPanel() {
|
||||
<Button
|
||||
variant={qwenTokens ? "secondary" : "outline"}
|
||||
size="sm"
|
||||
className="h-8"
|
||||
className="h-7 lg:h-8 text-[10px] lg:text-xs w-full sm:w-auto"
|
||||
onClick={handleQwenAuth}
|
||||
disabled={isAuthLoading}
|
||||
>
|
||||
{isAuthLoading
|
||||
? "Signing in..."
|
||||
: qwenTokens
|
||||
? "Logout from Qwen"
|
||||
: "Login with Qwen (OAuth)"}
|
||||
? "Logout from Qwen"
|
||||
: "Login with Qwen (OAuth)"}
|
||||
</Button>
|
||||
</div>
|
||||
{qwenTokens && (
|
||||
<p className="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()})
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center gap-2 text-sm font-medium">
|
||||
<Server className="h-4 w-4" />
|
||||
<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" />
|
||||
Ollama Cloud API Key
|
||||
</label>
|
||||
<div className="relative">
|
||||
@@ -176,23 +176,23 @@ export default function SettingsPanel() {
|
||||
placeholder="Enter your Ollama API key"
|
||||
value={apiKeys.ollama || ""}
|
||||
onChange={(e) => handleApiKeyChange("ollama", e.target.value)}
|
||||
className="font-mono text-sm"
|
||||
className="font-mono text-xs lg:text-sm pr-10"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-0 top-0 h-full"
|
||||
className="absolute right-0 top-0 h-full w-9 lg:w-10"
|
||||
onClick={() => setShowApiKey((prev) => ({ ...prev, ollama: !prev.ollama }))}
|
||||
>
|
||||
{showApiKey.ollama ? (
|
||||
<EyeOff className="h-4 w-4" />
|
||||
<EyeOff className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4" />
|
||||
<Eye className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<p className="text-[10px] lg:text-xs text-muted-foreground">
|
||||
Get API key from{" "}
|
||||
<a
|
||||
href="https://ollama.com/cloud"
|
||||
@@ -206,8 +206,8 @@ export default function SettingsPanel() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center gap-2 text-sm font-medium">
|
||||
<Server className="h-4 w-4" />
|
||||
<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" />
|
||||
Z.AI Plan API Key
|
||||
</label>
|
||||
<div className="relative">
|
||||
@@ -216,23 +216,23 @@ export default function SettingsPanel() {
|
||||
placeholder="Enter your Z.AI API key"
|
||||
value={apiKeys.zai || ""}
|
||||
onChange={(e) => handleApiKeyChange("zai", e.target.value)}
|
||||
className="font-mono text-sm"
|
||||
className="font-mono text-xs lg:text-sm pr-10"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-0 top-0 h-full"
|
||||
className="absolute right-0 top-0 h-full w-9 lg:w-10"
|
||||
onClick={() => setShowApiKey((prev) => ({ ...prev, zai: !prev.zai }))}
|
||||
>
|
||||
{showApiKey.zai ? (
|
||||
<EyeOff className="h-4 w-4" />
|
||||
<EyeOff className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4" />
|
||||
<Eye className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<p className="text-[10px] lg:text-xs text-muted-foreground">
|
||||
Get API key from{" "}
|
||||
<a
|
||||
href="https://docs.z.ai"
|
||||
@@ -245,45 +245,44 @@ export default function SettingsPanel() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button onClick={handleSave} className="w-full">
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
<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 API Keys
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Default Provider</CardTitle>
|
||||
<CardDescription>
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="text-base lg:text-lg">Default Provider</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Select your preferred AI provider
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid gap-3">
|
||||
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
<div className="grid gap-2 lg: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
|
||||
className={`flex items-center gap-2 lg:gap-3 rounded-lg border p-3 lg: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 className="flex h-8 w-8 lg:h-10 lg:w-10 items-center justify-center rounded-md bg-primary/10">
|
||||
<Server className="h-4 w-4 lg:h-5 lg:w-5 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-medium capitalize">{provider}</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-medium capitalize text-sm lg:text-base">{provider}</h3>
|
||||
<p className="text-[10px] lg:text-sm text-muted-foreground truncate">
|
||||
{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" />
|
||||
<div className="h-2 w-2 rounded-full bg-primary flex-shrink-0" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
@@ -292,15 +291,15 @@ export default function SettingsPanel() {
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Data Privacy</CardTitle>
|
||||
<CardDescription>
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="text-base lg:text-lg">Data Privacy</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Your data handling preferences
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="rounded-md border bg-muted/30 p-4">
|
||||
<p className="text-sm">
|
||||
<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">
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import useStore from "@/lib/store";
|
||||
import { Sparkles, FileText, ListTodo, Palette, History, Settings, Github } from "lucide-react";
|
||||
import { Sparkles, FileText, ListTodo, Palette, History, Settings, Github, Menu, X } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type View = "enhance" | "prd" | "action" | "uxdesigner" | "history" | "settings";
|
||||
@@ -14,87 +15,137 @@ interface SidebarProps {
|
||||
|
||||
export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
|
||||
const history = useStore((state) => state.history);
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
|
||||
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: "uxdesigner" as View, label: "UX Designer Prompt", icon: Palette },
|
||||
{ id: "uxdesigner" as View, label: "UX Designer", icon: Palette },
|
||||
{ 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">
|
||||
const handleViewChange = (view: View) => {
|
||||
onViewChange(view);
|
||||
setIsMobileMenuOpen(false);
|
||||
};
|
||||
|
||||
const SidebarContent = () => (
|
||||
<>
|
||||
<div className="border-b p-4 lg:p-6">
|
||||
<a href="https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer" target="_blank" rel="noopener noreferrer" className="block">
|
||||
<h1 className="flex items-center gap-2 text-xl font-bold hover:opacity-80 transition-opacity">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
|
||||
<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-primary text-primary-foreground text-sm lg:text-base">
|
||||
PA
|
||||
</div>
|
||||
PromptArch
|
||||
</h1>
|
||||
</a>
|
||||
<a href="https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer" target="_blank" rel="noopener noreferrer" className="mt-3 flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs text-primary hover:bg-primary/10 transition-colors">
|
||||
<Github className="h-3.5 w-3.5" />
|
||||
<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">
|
||||
<Github className="h-3 w-3 lg:h-3.5 lg:w-3.5" />
|
||||
<span>View on GitHub</span>
|
||||
</a>
|
||||
<p className="mt-2 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>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 space-y-1 p-4">
|
||||
<nav className="flex-1 space-y-1 p-3 lg:p-4 overflow-y-auto">
|
||||
{menuItems.map((item) => (
|
||||
<Button
|
||||
key={item.id}
|
||||
variant={currentView === item.id ? "default" : "ghost"}
|
||||
className={cn(
|
||||
"w-full justify-start gap-2",
|
||||
"w-full justify-start gap-2 h-9 lg:h-10 text-sm",
|
||||
currentView === item.id && "bg-primary text-primary-foreground"
|
||||
)}
|
||||
onClick={() => onViewChange(item.id)}
|
||||
onClick={() => handleViewChange(item.id)}
|
||||
>
|
||||
<item.icon className="h-4 w-4" />
|
||||
<span className="flex-1 text-left">{item.label}</span>
|
||||
<span className="flex-1 text-left truncate">{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">
|
||||
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-primary-foreground text-xs font-medium text-primary">
|
||||
{item.count}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
))}
|
||||
|
||||
<div className="mt-8 p-3 text-[10px] leading-relaxed text-muted-foreground border-t border-border/50 pt-4">
|
||||
<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">
|
||||
<p className="font-semibold text-foreground mb-1">Developed by Roman | RyzenAdvanced</p>
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-0.5 lg:space-y-1">
|
||||
<p>
|
||||
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>
|
||||
</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">
|
||||
<p className="mt-1 lg:mt-2 text-[8px] lg:text-[9px] opacity-80">
|
||||
100% Developed using GLM 4.7 model on TRAE.AI IDE.
|
||||
</p>
|
||||
<p className="text-[9px] opacity-80">
|
||||
<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>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className="border-t p-4">
|
||||
<div className="rounded-md bg-muted/50 p-3 text-xs text-muted-foreground">
|
||||
<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">
|
||||
<p className="font-medium text-foreground">Quick Tips</p>
|
||||
<ul className="mt-2 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>• Copy enhanced prompts to your AI agent</li>
|
||||
<li>• PRDs generate better action plans</li>
|
||||
<li>• UX Designer Prompt for design tasks</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 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">
|
||||
<a href="https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer" 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">
|
||||
PA
|
||||
</div>
|
||||
<span className="font-bold text-lg">PromptArch</span>
|
||||
</a>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||
className="h-9 w-9"
|
||||
>
|
||||
{isMobileMenuOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu Overlay */}
|
||||
{isMobileMenuOpen && (
|
||||
<div
|
||||
className="lg:hidden fixed inset-0 z-40 bg-black/50 backdrop-blur-sm"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Mobile Slide-out Menu */}
|
||||
<aside
|
||||
className={cn(
|
||||
"lg:hidden fixed top-0 left-0 z-50 flex h-full w-72 max-w-[80vw] flex-col border-r bg-card transition-transform duration-300 ease-in-out",
|
||||
isMobileMenuOpen ? "translate-x-0" : "-translate-x-full"
|
||||
)}
|
||||
>
|
||||
<SidebarContent />
|
||||
</aside>
|
||||
|
||||
{/* Desktop Sidebar */}
|
||||
<aside className="hidden lg:flex h-screen w-64 flex-col border-r bg-card flex-shrink-0">
|
||||
<SidebarContent />
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ export default function UXDesignerPrompt() {
|
||||
const loadAvailableModels = async () => {
|
||||
const fallbackModels = modelAdapter.getAvailableModels(selectedProvider);
|
||||
setAvailableModels(selectedProvider, fallbackModels);
|
||||
|
||||
|
||||
try {
|
||||
const result = await modelAdapter.listModels(selectedProvider);
|
||||
if (result.success && result.data) {
|
||||
@@ -119,21 +119,21 @@ export default function UXDesignerPrompt() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-2">
|
||||
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2">
|
||||
<Card className="h-fit">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Palette className="h-5 w-5" />
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||
<Palette className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||
UX Designer Prompt
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Describe your app idea and get the BEST EVER prompt for UX design
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">AI Provider</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<label className="text-xs lg:text-sm font-medium">AI Provider</label>
|
||||
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
||||
{(["ollama", "zai"] as const).map((provider) => (
|
||||
<Button
|
||||
key={provider}
|
||||
@@ -141,7 +141,7 @@ export default function UXDesignerPrompt() {
|
||||
size="sm"
|
||||
onClick={() => setSelectedProvider(provider)}
|
||||
className={cn(
|
||||
"capitalize",
|
||||
"capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3",
|
||||
selectedProvider === provider && "bg-primary text-primary-foreground"
|
||||
)}
|
||||
>
|
||||
@@ -152,11 +152,11 @@ export default function UXDesignerPrompt() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Model</label>
|
||||
<label className="text-xs lg: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"
|
||||
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"
|
||||
>
|
||||
{models.map((model) => (
|
||||
<option key={model} value={model}>
|
||||
@@ -167,79 +167,81 @@ export default function UXDesignerPrompt() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">App Description</label>
|
||||
<label className="text-xs lg:text-sm font-medium">App Description</label>
|
||||
<Textarea
|
||||
placeholder="e.g., A fitness tracking app with workout plans, nutrition tracking, and social features for sharing progress with friends"
|
||||
value={currentPrompt}
|
||||
onChange={(e) => setCurrentPrompt(e.target.value)}
|
||||
className="min-h-[200px] resize-y"
|
||||
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm"
|
||||
/>
|
||||
<p className="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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
|
||||
<div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg: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 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" />
|
||||
<span className="text-[10px] lg:text-xs">Configure API key in Settings</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleGenerate} disabled={isProcessing || !currentPrompt.trim()} className="flex-1">
|
||||
<Button onClick={handleGenerate} disabled={isProcessing || !currentPrompt.trim()} className="flex-1 h-9 lg:h-10 text-xs lg:text-sm">
|
||||
{isProcessing ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 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...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Palette className="mr-2 h-4 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
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleClear} disabled={isProcessing}>
|
||||
Clear
|
||||
<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="sm:hidden">×</span>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className={cn(!generatedPrompt && "opacity-50")}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
||||
<span className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||
Best Ever UX Prompt
|
||||
<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="sm:hidden">UX Prompt</span>
|
||||
</span>
|
||||
{generatedPrompt && (
|
||||
<Button variant="ghost" size="icon" onClick={handleCopy}>
|
||||
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
||||
{copied ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
<CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
<Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Comprehensive UX design prompt ready for designers
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
{generatedPrompt ? (
|
||||
<div className="rounded-md border bg-muted/50 p-4">
|
||||
<pre className="whitespace-pre-wrap text-sm">{generatedPrompt}</pre>
|
||||
<div className="rounded-md border bg-muted/50 p-3 lg:p-4 max-h-[350px] lg:max-h-[400px] overflow-y-auto">
|
||||
<pre className="whitespace-pre-wrap text-xs lg:text-sm">{generatedPrompt}</pre>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-[400px] items-center justify-center text-center text-sm text-muted-foreground">
|
||||
<div className="flex h-[250px] lg:h-[400px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground px-4">
|
||||
Your comprehensive UX designer prompt will appear here
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user