Files
PromptArch/components/UXDesignerPrompt.tsx

253 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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 { Palette, Copy, Loader2, CheckCircle2, Settings } from "lucide-react";
import { cn } from "@/lib/utils";
export default function UXDesignerPrompt() {
const {
currentPrompt,
selectedProvider,
selectedModels,
availableModels,
apiKeys,
isProcessing,
error,
setSelectedProvider,
setCurrentPrompt,
setEnhancedPrompt,
setProcessing,
setError,
setAvailableModels,
setSelectedModel,
} = useStore();
const [copied, setCopied] = useState(false);
const [generatedPrompt, setGeneratedPrompt] = useState<string | null>(null);
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.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 app description");
return;
}
const apiKey = apiKeys[selectedProvider];
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
return;
}
setProcessing(true);
setError(null);
setGeneratedPrompt(null);
console.log("[UXDesignerPrompt] Starting generation...", { selectedProvider, selectedModel, hasQwenAuth: modelAdapter.hasQwenAuth() });
try {
const result = await modelAdapter.generateUXDesignerPrompt(currentPrompt, selectedProvider, selectedModel);
console.log("[UXDesignerPrompt] Generation result:", result);
if (result.success && result.data) {
setGeneratedPrompt(result.data);
setEnhancedPrompt(result.data);
} else {
console.error("[UXDesignerPrompt] Generation failed:", result.error);
setError(result.error || "Failed to generate UX designer prompt");
}
} catch (err) {
console.error("[UXDesignerPrompt] Generation error:", err);
setError(err instanceof Error ? err.message : "An error occurred");
} finally {
setProcessing(false);
}
};
const handleCopy = async () => {
if (generatedPrompt) {
await navigator.clipboard.writeText(generatedPrompt);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
const handleClear = () => {
setCurrentPrompt("");
setGeneratedPrompt(null);
setEnhancedPrompt(null);
setError(null);
};
return (
<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 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 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-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
<div className="space-y-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}
variant={selectedProvider === provider ? "default" : "outline"}
size="sm"
onClick={() => setSelectedProvider(provider)}
className={cn(
"capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3",
selectedProvider === provider && "bg-primary text-primary-foreground"
)}
>
{provider === "ollama" ? "Ollama" : "Z.AI"}
</Button>
))}
</div>
</div>
<div className="space-y-2">
<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-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}>
{model}
</option>
))}
</select>
</div>
<div className="space-y-2">
<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-[150px] lg:min-h-[200px] resize-y text-sm"
/>
<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-2.5 lg:p-3 text-xs lg:text-sm text-destructive">
{error}
{!apiKeys[selectedProvider] && (
<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 h-9 lg:h-10 text-xs lg:text-sm">
{isProcessing ? (
<>
<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-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} 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 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-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} className="h-8 w-8 lg:h-9 lg:w-9">
{copied ? (
<CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
) : (
<Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
)}
</Button>
)}
</CardTitle>
<CardDescription className="text-xs lg:text-sm">
Comprehensive UX design prompt ready for designers
</CardDescription>
</CardHeader>
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
{generatedPrompt ? (
<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-[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>
)}
</CardContent>
</Card>
</div>
);
}