Compare commits

..

No commits in common. "c18bb27794dd769b3981134daab95d72f4ecb83a" and "6e37e6142020a1e8b4c5de0cca86a0dc6b2e7a99" have entirely different histories.

16 changed files with 5 additions and 2134 deletions

View File

@ -1,97 +0,0 @@
import { NextResponse } from 'next/server';
import { db } from '@/drizzle/db';
import { promptOptimizations } from '@/drizzle/schema';
import { eq, desc } from 'drizzle-orm';
import { getCurrentUser } from '@/lib/auth';
// GET /api/prompt/history - 获取用户的优化历史
export async function GET() {
try {
// 获取当前用户
const user = await getCurrentUser();
if (!user) {
return NextResponse.json(
{ error: '请先登录' },
{ status: 401 }
);
}
// 查询该用户的优化历史(最近 50 条)
const history = await db
.select()
.from(promptOptimizations)
.where(eq(promptOptimizations.userId, user.userId))
.orderBy(desc(promptOptimizations.createdAt))
.limit(50);
return NextResponse.json({
success: true,
data: history,
});
} catch (error) {
console.error('获取优化历史失败:', error);
return NextResponse.json(
{ error: '获取历史记录失败' },
{ status: 500 }
);
}
}
// DELETE /api/prompt/history - 删除指定的优化历史
export async function DELETE(request: Request) {
try {
// 获取当前用户
const user = await getCurrentUser();
if (!user) {
return NextResponse.json(
{ error: '请先登录' },
{ status: 401 }
);
}
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
if (!id) {
return NextResponse.json(
{ error: '缺少记录 ID' },
{ status: 400 }
);
}
// 验证记录属于当前用户后删除
const record = await db
.select()
.from(promptOptimizations)
.where(eq(promptOptimizations.id, parseInt(id)))
.limit(1);
if (record.length === 0) {
return NextResponse.json(
{ error: '记录不存在' },
{ status: 404 }
);
}
if (record[0].userId !== user.userId) {
return NextResponse.json(
{ error: '无权删除此记录' },
{ status: 403 }
);
}
await db
.delete(promptOptimizations)
.where(eq(promptOptimizations.id, parseInt(id)));
return NextResponse.json({
success: true,
});
} catch (error) {
console.error('删除优化历史失败:', error);
return NextResponse.json(
{ error: '删除失败' },
{ status: 500 }
);
}
}

View File

@ -1,166 +0,0 @@
import { NextResponse } from 'next/server';
import { db } from '@/drizzle/db';
import { promptOptimizations, userSettings } from '@/drizzle/schema';
import { eq } from 'drizzle-orm';
import { getCurrentUser } from '@/lib/auth';
import { decryptApiKey } from '@/lib/crypto';
// 简洁版系统提示词
const CONCISE_SYSTEM_PROMPT = `你是一个提示词优化专家。用户会给你一个原始的想法或问题,请帮助优化成简洁明了的提示词。
1.
2.
3. 使
4. 150
5. `;
// 详细版系统提示词
const DETAILED_SYSTEM_PROMPT = `你是一个提示词优化专家。用户会给你一个原始的想法或问题,请帮助优化成结构化的详细提示词。
1. ****
2. ****
3. ****
4. ****
5. ****
- 使
-
-
- `;
// 规范化 URL
function normalizeBaseUrl(url: string): string {
return url.replace(/\/+$/, '');
}
// POST /api/prompt/optimize - 优化提示词
export async function POST(request: Request) {
try {
// 获取当前用户
const user = await getCurrentUser();
if (!user) {
return NextResponse.json(
{ error: '请先登录' },
{ status: 401 }
);
}
const body = await request.json();
const { originalPrompt, mode } = body;
if (!originalPrompt || !mode) {
return NextResponse.json(
{ error: '缺少必要参数' },
{ status: 400 }
);
}
if (!['concise', 'detailed'].includes(mode)) {
return NextResponse.json(
{ error: '无效的优化模式' },
{ status: 400 }
);
}
// 获取用户设置
const settings = await db.query.userSettings.findFirst({
where: eq(userSettings.userId, user.userId),
});
if (!settings?.cchApiKey) {
return NextResponse.json(
{ error: '请先在设置中配置 API Key' },
{ status: 400 }
);
}
// 解密 API Key
const apiKey = decryptApiKey(settings.cchApiKey);
const cchUrl = settings.cchUrl || process.env.CCH_DEFAULT_URL || 'https://claude.leocoder.cn/';
const apiFormat = (settings.apiFormat as 'claude' | 'openai') || 'claude';
// 选择系统提示词
const systemPrompt = mode === 'concise' ? CONCISE_SYSTEM_PROMPT : DETAILED_SYSTEM_PROMPT;
// 调用 AI 进行优化
let optimizedPrompt: string;
if (apiFormat === 'openai') {
// OpenAI 兼容格式
const response = await fetch(`${normalizeBaseUrl(cchUrl)}/v1/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: 'claude-sonnet-4-5-20250929',
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: `请优化以下提示词:\n\n${originalPrompt}` },
],
temperature: 0.7,
}),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`API 调用失败: ${errorText}`);
}
const data = await response.json();
optimizedPrompt = data.choices?.[0]?.message?.content || '';
} else {
// Claude 原生格式
const response = await fetch(`${normalizeBaseUrl(cchUrl)}/v1/messages`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': apiKey,
'anthropic-version': '2023-06-01',
},
body: JSON.stringify({
model: 'claude-sonnet-4-5-20250929',
max_tokens: 2048,
system: systemPrompt,
messages: [
{ role: 'user', content: `请优化以下提示词:\n\n${originalPrompt}` },
],
}),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`API 调用失败: ${errorText}`);
}
const data = await response.json();
optimizedPrompt = data.content?.[0]?.text || '';
}
// 清理 <think> 标签(如果有)
optimizedPrompt = optimizedPrompt.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
// 保存到数据库
await db.insert(promptOptimizations).values({
userId: user.userId,
originalPrompt,
optimizedPrompt,
mode,
});
return NextResponse.json({
success: true,
optimizedPrompt,
});
} catch (error) {
console.error('提示词优化失败:', error);
return NextResponse.json(
{ error: error instanceof Error ? error.message : '优化失败,请稍后重试' },
{ status: 500 }
);
}
}

View File

@ -9,13 +9,11 @@ import { ChatInput } from '@/components/features/ChatInput';
import { MessageBubble } from '@/components/features/MessageBubble';
import { ChatHeaderInfo } from '@/components/features/ChatHeader';
import { SaveToNoteModal } from '@/components/features/SaveToNoteModal';
import { PromptOptimizer } from '@/components/features/PromptOptimizer';
import { cn } from '@/lib/utils';
import { useConversation, useConversations } from '@/hooks/useConversations';
import { useStreamChat, type ChatMessage } from '@/hooks/useStreamChat';
import { useModels, useTools, useSettings } from '@/hooks/useSettings';
import { useAuth } from '@/providers/AuthProvider';
import { usePromptOptimizer } from '@/providers/PromptOptimizerProvider';
import type { UploadFile } from '@/types/file-upload';
interface PageProps {
@ -28,7 +26,6 @@ export default function ChatPage({ params }: PageProps) {
const searchParams = useSearchParams();
const initialMessage = searchParams.get('message');
const { user } = useAuth();
const { setOptimizedPrompt } = usePromptOptimizer();
const [sidebarOpen, setSidebarOpen] = useState(true);
const messagesEndRef = useRef<HTMLDivElement>(null);
@ -617,9 +614,6 @@ export default function ChatPage({ params }: PageProps) {
initialContent={noteContent}
conversationId={chatId}
/>
{/* 提示词优化工具浮动按钮 */}
<PromptOptimizer onUsePrompt={setOptimizedPrompt} />
</div>
);
}

View File

@ -194,19 +194,6 @@ body {
background-color: var(--color-text-tertiary);
}
/* 更细的滚动条 - 用于小区域 */
.scrollbar-thin::-webkit-scrollbar {
width: 4px;
}
.scrollbar-thin::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.15);
}
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
background-color: rgba(0, 0, 0, 0.25);
}
/* ========================================
动画
======================================== */

View File

@ -2,7 +2,6 @@ import type { Metadata } from "next";
import "./globals.css";
import { SettingsProvider } from "@/components/providers/SettingsProvider";
import { AuthProvider } from "@/providers/AuthProvider";
import { PromptOptimizerProvider } from "@/providers/PromptOptimizerProvider";
import { Toaster } from "@/components/ui/Toast";
export const metadata: Metadata = {
@ -23,10 +22,8 @@ export default function RootLayout({
<body className="antialiased">
<AuthProvider>
<SettingsProvider>
<PromptOptimizerProvider>
{children}
<Toaster />
</PromptOptimizerProvider>
{children}
<Toaster />
</SettingsProvider>
</AuthProvider>
</body>

View File

@ -6,15 +6,13 @@ import { AppLayout } from '@/components/layout/AppLayout';
import { Welcome } from '@/components/features/Welcome';
import { ChatInput } from '@/components/features/ChatInput';
import { QuickActions } from '@/components/features/QuickActions';
import { getGreeting } from '@/data/mock';
import { useAuth } from '@/providers/AuthProvider';
import { currentUser, getGreeting } from '@/data/mock';
import { useConversations } from '@/hooks/useConversations';
import { useModels, useTools, useSettings } from '@/hooks/useSettings';
import type { QuickAction } from '@/types';
export default function HomePage() {
const router = useRouter();
const { user } = useAuth();
const { createConversation } = useConversations();
const { models, loading: modelsLoading } = useModels();
const { tools: availableTools, loading: toolsLoading } = useTools();
@ -24,8 +22,7 @@ export default function HomePage() {
const [enabledTools, setEnabledTools] = useState<string[]>([]);
const [isSending, setIsSending] = useState(false);
// 使用真实登录用户的昵称,如果未登录则显示"用户"
const greeting = getGreeting(user?.nickname || '用户');
const greeting = getGreeting(currentUser.name);
// 初始化默认设置
useEffect(() => {

View File

@ -1,13 +1,12 @@
'use client';
import { useState, useRef, useEffect } from 'react';
import { useState, useRef } from 'react';
import { Paperclip, ArrowUp, Upload } from 'lucide-react';
import { ModelSelector } from './ModelSelector';
import { ToolsDropdown } from './ToolsDropdown';
import { FilePreviewList } from './FilePreviewList';
import { Tooltip } from '@/components/ui/Tooltip';
import { useFileUpload } from '@/hooks/useFileUpload';
import { usePromptOptimizer } from '@/providers/PromptOptimizerProvider';
import { cn } from '@/lib/utils';
import type { Model, Tool } from '@/types';
import type { UploadFile } from '@/types/file-upload';
@ -38,17 +37,6 @@ export function ChatInput({
const [message, setMessage] = useState('');
const fileInputRef = useRef<HTMLInputElement>(null);
// 使用提示词优化 Hook
const { consumeOptimizedPrompt } = usePromptOptimizer();
// 监听优化后的提示词并填入输入框
useEffect(() => {
const prompt = consumeOptimizedPrompt();
if (prompt) {
setMessage(prompt);
}
}, [consumeOptimizedPrompt]);
// 使用文件上传 Hook
const {
files,

View File

@ -1,485 +0,0 @@
'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>
);
}

View File

@ -1,77 +0,0 @@
'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}
/>
</>
);
}

View File

@ -2,8 +2,6 @@
import { useState, type ReactNode } from 'react';
import { Sidebar, SidebarToggle } from '@/components/layout/Sidebar';
import { PromptOptimizer } from '@/components/features/PromptOptimizer';
import { usePromptOptimizer } from '@/providers/PromptOptimizerProvider';
import { cn } from '@/lib/utils';
interface AppLayoutProps {
@ -14,7 +12,6 @@ interface AppLayoutProps {
export function AppLayout({ children, showHeader = true, headerContent }: AppLayoutProps) {
const [sidebarOpen, setSidebarOpen] = useState(true);
const { setOptimizedPrompt } = usePromptOptimizer();
return (
<div className="flex min-h-screen">
@ -44,9 +41,6 @@ export function AppLayout({ children, showHeader = true, headerContent }: AppLay
{children}
</div>
</main>
{/* 提示词优化工具浮动按钮 */}
<PromptOptimizer onUsePrompt={setOptimizedPrompt} />
</div>
);
}

View File

@ -1,10 +0,0 @@
CREATE TABLE "prompt_optimizations" (
"id" serial PRIMARY KEY NOT NULL,
"user_id" varchar(64) NOT NULL,
"original_prompt" text NOT NULL,
"optimized_prompt" text NOT NULL,
"mode" varchar(20) NOT NULL,
"created_at" timestamp with time zone DEFAULT now()
);
--> statement-breakpoint
ALTER TABLE "user_settings" ALTER COLUMN "default_model" SET DEFAULT 'claude-sonnet-4-5-20250929';

File diff suppressed because it is too large Load Diff

View File

@ -64,13 +64,6 @@
"when": 1766314954003,
"tag": "0008_flat_star_brand",
"breakpoints": true
},
{
"idx": 9,
"version": "7",
"when": 1766323563111,
"tag": "0009_omniscient_vision",
"breakpoints": true
}
]
}

View File

@ -289,23 +289,6 @@ export const notes = pgTable('notes', {
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(),
});
// ============================================
// 提示词优化历史表
// ============================================
export const promptOptimizations = pgTable('prompt_optimizations', {
id: serial('id').primaryKey(),
// 关联用户
userId: varchar('user_id', { length: 64 }).notNull(),
// 原始提示词
originalPrompt: text('original_prompt').notNull(),
// 优化后的提示词
optimizedPrompt: text('optimized_prompt').notNull(),
// 优化模式: concise(简洁版) | detailed(详细版)
mode: varchar('mode', { length: 20 }).notNull(),
// 时间戳
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),
});
// ============================================
// 关系定义
// ============================================
@ -448,6 +431,3 @@ export type NewUserFavoriteAssistant = typeof userFavoriteAssistants.$inferInser
export type Note = typeof notes.$inferSelect;
export type NewNote = typeof notes.$inferInsert;
export type PromptOptimization = typeof promptOptimizations.$inferSelect;
export type NewPromptOptimization = typeof promptOptimizations.$inferInsert;

View File

@ -1,45 +0,0 @@
'use client';
import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
interface PromptOptimizerContextType {
// 优化后的提示词(用于填入输入框)
optimizedPrompt: string | null;
// 设置优化后的提示词
setOptimizedPrompt: (prompt: string | null) => void;
// 消费优化后的提示词(获取后清空)
consumeOptimizedPrompt: () => string | null;
}
const PromptOptimizerContext = createContext<PromptOptimizerContextType | null>(null);
export function PromptOptimizerProvider({ children }: { children: ReactNode }) {
const [optimizedPrompt, setOptimizedPrompt] = useState<string | null>(null);
// 消费优化后的提示词(获取后清空,避免重复使用)
const consumeOptimizedPrompt = useCallback(() => {
const prompt = optimizedPrompt;
setOptimizedPrompt(null);
return prompt;
}, [optimizedPrompt]);
return (
<PromptOptimizerContext.Provider
value={{
optimizedPrompt,
setOptimizedPrompt,
consumeOptimizedPrompt,
}}
>
{children}
</PromptOptimizerContext.Provider>
);
}
export function usePromptOptimizer() {
const context = useContext(PromptOptimizerContext);
if (!context) {
throw new Error('usePromptOptimizer must be used within a PromptOptimizerProvider');
}
return context;
}

View File

@ -1,2 +1 @@
export { AuthProvider, useAuth, type AuthUser } from './AuthProvider';
export { PromptOptimizerProvider, usePromptOptimizer } from './PromptOptimizerProvider';