Compare commits
No commits in common. "c18bb27794dd769b3981134daab95d72f4ecb83a" and "6e37e6142020a1e8b4c5de0cca86a0dc6b2e7a99" have entirely different histories.
c18bb27794
...
6e37e61420
@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
动画
|
||||
======================================== */
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -1,2 +1 @@
|
||||
export { AuthProvider, useAuth, type AuthUser } from './AuthProvider';
|
||||
export { PromptOptimizerProvider, usePromptOptimizer } from './PromptOptimizerProvider';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user