feat(组件): 添加提示词优化工具组件
- 新增 PromptOptimizer 浮动按钮组件 - 新增 PromptOptimizerModal 优化弹窗组件 - 支持简洁版和详细版两种优化模式 - 支持快捷键 Cmd/Ctrl + Shift + P 快速打开 - 支持优化历史记录查看和管理 - 支持一键使用优化后的提示词
This commit is contained in:
parent
4b4732a583
commit
31d227dca9
485
src/components/features/PromptOptimizer/PromptOptimizerModal.tsx
Normal file
485
src/components/features/PromptOptimizer/PromptOptimizerModal.tsx
Normal file
@ -0,0 +1,485 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
|
import {
|
||||||
|
X,
|
||||||
|
Sparkles,
|
||||||
|
Zap,
|
||||||
|
FileText,
|
||||||
|
Copy,
|
||||||
|
Check,
|
||||||
|
Loader2,
|
||||||
|
History,
|
||||||
|
Trash2,
|
||||||
|
ArrowRight,
|
||||||
|
Wand2,
|
||||||
|
Command,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface PromptOptimization {
|
||||||
|
id: number;
|
||||||
|
originalPrompt: string;
|
||||||
|
optimizedPrompt: string;
|
||||||
|
mode: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PromptOptimizerModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onUsePrompt: (prompt: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type TabType = 'optimize' | 'history';
|
||||||
|
type ModeType = 'concise' | 'detailed';
|
||||||
|
|
||||||
|
export function PromptOptimizerModal({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onUsePrompt,
|
||||||
|
}: PromptOptimizerModalProps) {
|
||||||
|
const [activeTab, setActiveTab] = useState<TabType>('optimize');
|
||||||
|
const [mode, setMode] = useState<ModeType>('detailed');
|
||||||
|
const [inputText, setInputText] = useState('');
|
||||||
|
const [optimizedText, setOptimizedText] = useState('');
|
||||||
|
const [isOptimizing, setIsOptimizing] = useState(false);
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [history, setHistory] = useState<PromptOptimization[]>([]);
|
||||||
|
const [isLoadingHistory, setIsLoadingHistory] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
|
// 加载历史记录
|
||||||
|
const loadHistory = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setIsLoadingHistory(true);
|
||||||
|
const response = await fetch('/api/prompt/history');
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setHistory(data.data || []);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('加载历史记录失败:', err);
|
||||||
|
} finally {
|
||||||
|
setIsLoadingHistory(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 打开弹窗时聚焦输入框
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
setTimeout(() => {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
// 切换到历史标签时加载历史
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeTab === 'history' && history.length === 0) {
|
||||||
|
loadHistory();
|
||||||
|
}
|
||||||
|
}, [activeTab, history.length, loadHistory]);
|
||||||
|
|
||||||
|
// 优化提示词
|
||||||
|
const handleOptimize = async () => {
|
||||||
|
if (!inputText.trim()) return;
|
||||||
|
|
||||||
|
setError(null);
|
||||||
|
setIsOptimizing(true);
|
||||||
|
setOptimizedText('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/prompt/optimize', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
originalPrompt: inputText,
|
||||||
|
mode,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.error || '优化失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
setOptimizedText(data.optimizedPrompt);
|
||||||
|
// 刷新历史记录
|
||||||
|
loadHistory();
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : '优化失败,请重试');
|
||||||
|
} finally {
|
||||||
|
setIsOptimizing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 复制优化结果
|
||||||
|
const handleCopy = async () => {
|
||||||
|
await navigator.clipboard.writeText(optimizedText);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 使用优化后的提示词
|
||||||
|
const handleUse = () => {
|
||||||
|
onUsePrompt(optimizedText);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除历史记录
|
||||||
|
const handleDeleteHistory = async (id: number) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/prompt/history?id=${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
setHistory((prev) => prev.filter((item) => item.id !== id));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('删除失败:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 使用历史记录中的提示词
|
||||||
|
const handleUseHistoryPrompt = (prompt: string) => {
|
||||||
|
onUsePrompt(prompt);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 键盘快捷键
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleOptimize();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
|
{/* 背景遮罩 */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-black/40 backdrop-blur-sm"
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 弹窗主体 */}
|
||||||
|
<div
|
||||||
|
className="relative w-full max-w-2xl mx-4 bg-white rounded-md shadow-2xl overflow-hidden animate-in fade-in zoom-in-95 duration-200"
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(180deg, #FFFAF7 0%, #FFFFFF 100%)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 头部 */}
|
||||||
|
<div className="flex items-center justify-between px-6 py-4 border-b border-[#E06B3E]/10">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
className="p-2 rounded-xl shadow-lg"
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(135deg, #E06B3E 0%, #D05A2E 100%)',
|
||||||
|
boxShadow: '0 4px 14px 0 rgba(224, 107, 62, 0.3)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Wand2 size={20} className="text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold text-gray-800">提示词优化工具</h2>
|
||||||
|
<p className="text-xs text-gray-500">让 AI 更好地理解你的需求</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<X size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 标签切换 */}
|
||||||
|
<div className="flex px-6 pt-4 gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('optimize')}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all',
|
||||||
|
activeTab === 'optimize'
|
||||||
|
? 'text-white shadow-md'
|
||||||
|
: 'text-gray-600 hover:bg-gray-100'
|
||||||
|
)}
|
||||||
|
style={activeTab === 'optimize' ? {
|
||||||
|
background: '#E06B3E',
|
||||||
|
boxShadow: '0 4px 6px -1px rgba(224, 107, 62, 0.3)'
|
||||||
|
} : {}}
|
||||||
|
>
|
||||||
|
<Sparkles size={16} />
|
||||||
|
优化提示词
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('history')}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all',
|
||||||
|
activeTab === 'history'
|
||||||
|
? 'text-white shadow-md'
|
||||||
|
: 'text-gray-600 hover:bg-gray-100'
|
||||||
|
)}
|
||||||
|
style={activeTab === 'history' ? {
|
||||||
|
background: '#E06B3E',
|
||||||
|
boxShadow: '0 4px 6px -1px rgba(224, 107, 62, 0.3)'
|
||||||
|
} : {}}
|
||||||
|
>
|
||||||
|
<History size={16} />
|
||||||
|
历史记录
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 内容区域 */}
|
||||||
|
<div className="p-6">
|
||||||
|
{activeTab === 'optimize' ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 输入区域 */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
输入你的想法
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
ref={inputRef}
|
||||||
|
value={inputText}
|
||||||
|
onChange={(e) => setInputText(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder="例如:帮我写一个登录页面..."
|
||||||
|
className="w-full h-28 px-4 py-3 bg-white border border-gray-200 rounded resize-none focus:outline-none focus:ring-2 focus:ring-[#E06B3E]/20 focus:border-[#E06B3E] transition-all placeholder:text-gray-400"
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end mt-1">
|
||||||
|
<span className="text-xs text-gray-400 flex items-center gap-1">
|
||||||
|
<Command size={12} />
|
||||||
|
+ Enter 快速优化
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 模式选择 */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
选择优化模式
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => setMode('concise')}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-3 p-4 rounded border-2 transition-all',
|
||||||
|
mode === 'concise'
|
||||||
|
? 'border-[#E06B3E] bg-[#E06B3E]/5 shadow-md'
|
||||||
|
: 'border-gray-200 hover:border-gray-300 bg-white'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="p-2 rounded-sm"
|
||||||
|
style={mode === 'concise' ? { background: '#E06B3E' } : { background: '#f3f4f6' }}
|
||||||
|
>
|
||||||
|
<Zap
|
||||||
|
size={18}
|
||||||
|
className={mode === 'concise' ? 'text-white' : 'text-gray-500'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-left">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'font-medium',
|
||||||
|
mode === 'concise' ? 'text-[#E06B3E]' : 'text-gray-700'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
简洁版
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500">精炼核心,简短明了</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setMode('detailed')}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-3 p-4 rounded border-2 transition-all',
|
||||||
|
mode === 'detailed'
|
||||||
|
? 'border-[#E06B3E] bg-[#E06B3E]/5 shadow-md'
|
||||||
|
: 'border-gray-200 hover:border-gray-300 bg-white'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="p-2 rounded-sm"
|
||||||
|
style={mode === 'detailed' ? { background: '#E06B3E' } : { background: '#f3f4f6' }}
|
||||||
|
>
|
||||||
|
<FileText
|
||||||
|
size={18}
|
||||||
|
className={mode === 'detailed' ? 'text-white' : 'text-gray-500'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-left">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'font-medium',
|
||||||
|
mode === 'detailed' ? 'text-[#E06B3E]' : 'text-gray-700'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
详细版
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500">结构完整,内容详尽</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 优化按钮 */}
|
||||||
|
<button
|
||||||
|
onClick={handleOptimize}
|
||||||
|
disabled={!inputText.trim() || isOptimizing}
|
||||||
|
className={cn(
|
||||||
|
'w-full py-3 rounded-md font-medium flex items-center justify-center gap-2 transition-all',
|
||||||
|
inputText.trim() && !isOptimizing
|
||||||
|
? 'text-white shadow-lg hover:shadow-xl active:scale-[0.98]'
|
||||||
|
: 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||||
|
)}
|
||||||
|
style={inputText.trim() && !isOptimizing ? {
|
||||||
|
background: 'linear-gradient(to right, #E06B3E, #D05A2E)',
|
||||||
|
boxShadow: '0 10px 15px -3px rgba(224, 107, 62, 0.3)'
|
||||||
|
} : {}}
|
||||||
|
>
|
||||||
|
{isOptimizing ? (
|
||||||
|
<>
|
||||||
|
<Loader2 size={18} className="animate-spin" />
|
||||||
|
正在优化...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Sparkles size={18} />
|
||||||
|
开始优化
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* 错误提示 */}
|
||||||
|
{error && (
|
||||||
|
<div className="p-3 bg-red-50 border border-red-200 rounded-md text-sm text-red-600">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 优化结果 */}
|
||||||
|
{optimizedText && (
|
||||||
|
<div className="space-y-3 animate-in slide-in-from-bottom-2 duration-300">
|
||||||
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
|
🎯 优化结果
|
||||||
|
</label>
|
||||||
|
{/* 结果内容区域 - 固定最大高度,内部滚动 */}
|
||||||
|
<div className="relative">
|
||||||
|
<div className="max-h-[200px] overflow-y-auto p-4 bg-gradient-to-br from-green-50 to-emerald-50 border border-green-200 rounded-md scrollbar-thin">
|
||||||
|
<p className="text-gray-700 whitespace-pre-wrap text-sm leading-relaxed pr-2">
|
||||||
|
{optimizedText}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/* 底部渐变遮罩 - 提示可滚动 */}
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 h-8 bg-gradient-to-t from-emerald-50/90 to-transparent pointer-events-none rounded-b-md" />
|
||||||
|
</div>
|
||||||
|
{/* 操作按钮 - 始终可见 */}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={handleCopy}
|
||||||
|
className="flex-1 py-2.5 px-4 border border-gray-200 rounded-md text-sm font-medium text-gray-600 hover:bg-gray-50 flex items-center justify-center gap-2 transition-colors"
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<>
|
||||||
|
<Check size={16} className="text-green-500" />
|
||||||
|
已复制
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Copy size={16} />
|
||||||
|
复制
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleUse}
|
||||||
|
className="flex-1 py-2.5 px-4 text-white rounded-md text-sm font-medium hover:shadow-lg flex items-center justify-center gap-2 transition-all active:scale-[0.98]"
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(to right, #E06B3E, #D05A2E)',
|
||||||
|
boxShadow: '0 4px 6px -1px rgba(224, 107, 62, 0.3)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ArrowRight size={16} />
|
||||||
|
使用此提示词
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
/* 历史记录 */
|
||||||
|
<div className="space-y-3 max-h-[400px] overflow-y-auto">
|
||||||
|
{isLoadingHistory ? (
|
||||||
|
<div className="flex items-center justify-center py-12">
|
||||||
|
<Loader2 size={24} className="animate-spin text-[#E06B3E]" />
|
||||||
|
</div>
|
||||||
|
) : history.length === 0 ? (
|
||||||
|
<div className="text-center py-12 text-gray-500">
|
||||||
|
<History size={40} className="mx-auto mb-3 text-gray-300" />
|
||||||
|
<p>暂无优化历史</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
history.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
className="p-4 bg-white border border-gray-200 rounded-md hover:border-[#E06B3E]/30 hover:shadow-sm transition-all group"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'px-2 py-0.5 text-xs rounded-full',
|
||||||
|
item.mode === 'concise'
|
||||||
|
? 'bg-blue-100 text-blue-700'
|
||||||
|
: 'bg-purple-100 text-purple-700'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.mode === 'concise' ? '简洁版' : '详细版'}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-gray-400">
|
||||||
|
{new Date(item.createdAt).toLocaleString('zh-CN')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-500 line-clamp-1 mb-1">
|
||||||
|
原始:{item.originalPrompt}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-700 line-clamp-2">
|
||||||
|
优化:{item.optimizedPrompt}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
<button
|
||||||
|
onClick={() => handleUseHistoryPrompt(item.optimizedPrompt)}
|
||||||
|
className="p-2 text-[#E06B3E] hover:bg-[#E06B3E]/10 rounded-lg transition-colors"
|
||||||
|
title="使用此提示词"
|
||||||
|
>
|
||||||
|
<ArrowRight size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDeleteHistory(item.id)}
|
||||||
|
className="p-2 text-red-400 hover:bg-red-50 rounded-lg transition-colors"
|
||||||
|
title="删除"
|
||||||
|
>
|
||||||
|
<Trash2 size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
77
src/components/features/PromptOptimizer/index.tsx
Normal file
77
src/components/features/PromptOptimizer/index.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { Wand2 } from 'lucide-react';
|
||||||
|
import { PromptOptimizerModal } from './PromptOptimizerModal';
|
||||||
|
|
||||||
|
interface PromptOptimizerProps {
|
||||||
|
onUsePrompt?: (prompt: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PromptOptimizer({ onUsePrompt }: PromptOptimizerProps) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
// 全局快捷键 Cmd/Ctrl + Shift + P
|
||||||
|
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||||||
|
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === 'p') {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsOpen((prev) => !prev);
|
||||||
|
}
|
||||||
|
// ESC 关闭
|
||||||
|
if (e.key === 'Escape' && isOpen) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, [handleKeyDown]);
|
||||||
|
|
||||||
|
const handleUsePrompt = (prompt: string) => {
|
||||||
|
onUsePrompt?.(prompt);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* 浮动按钮 - 右下角,避开输入框 */}
|
||||||
|
<button
|
||||||
|
onClick={() => setIsOpen(true)}
|
||||||
|
className="fixed bottom-28 right-6 z-50 group"
|
||||||
|
title="提示词优化工具 (⌘+Shift+P)"
|
||||||
|
>
|
||||||
|
<div className="relative">
|
||||||
|
{/* 光晕效果 */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 rounded-full blur-lg opacity-40 group-hover:opacity-60 transition-opacity"
|
||||||
|
style={{ background: 'linear-gradient(to right, #E06B3E, #D05A2E)' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 按钮主体 */}
|
||||||
|
<div
|
||||||
|
className="relative flex items-center gap-2 px-4 py-3 text-white rounded-full shadow-lg hover:shadow-xl transition-all group-hover:scale-105 active:scale-95"
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(to right, #E06B3E, #D05A2E)',
|
||||||
|
boxShadow: '0 10px 15px -3px rgba(224, 107, 62, 0.3), 0 4px 6px -4px rgba(224, 107, 62, 0.3)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Wand2 size={18} className="group-hover:rotate-12 transition-transform" />
|
||||||
|
<span className="text-sm font-medium">优化提示词</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 快捷键提示 */}
|
||||||
|
<div className="absolute -top-8 left-1/2 -translate-x-1/2 px-2 py-1 bg-gray-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap">
|
||||||
|
⌘ + Shift + P
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* 优化弹窗 */}
|
||||||
|
<PromptOptimizerModal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={() => setIsOpen(false)}
|
||||||
|
onUsePrompt={handleUsePrompt}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user