Compare commits
7 Commits
6e37e61420
...
c18bb27794
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c18bb27794 | ||
|
|
d4de4381f2 | ||
|
|
249362a6eb | ||
|
|
57bb1ffab7 | ||
|
|
31d227dca9 | ||
|
|
4b4732a583 | ||
|
|
fae1dfb7c9 |
97
src/app/api/prompt/history/route.ts
Normal file
97
src/app/api/prompt/history/route.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
166
src/app/api/prompt/optimize/route.ts
Normal file
166
src/app/api/prompt/optimize/route.ts
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
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,11 +9,13 @@ import { ChatInput } from '@/components/features/ChatInput';
|
|||||||
import { MessageBubble } from '@/components/features/MessageBubble';
|
import { MessageBubble } from '@/components/features/MessageBubble';
|
||||||
import { ChatHeaderInfo } from '@/components/features/ChatHeader';
|
import { ChatHeaderInfo } from '@/components/features/ChatHeader';
|
||||||
import { SaveToNoteModal } from '@/components/features/SaveToNoteModal';
|
import { SaveToNoteModal } from '@/components/features/SaveToNoteModal';
|
||||||
|
import { PromptOptimizer } from '@/components/features/PromptOptimizer';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useConversation, useConversations } from '@/hooks/useConversations';
|
import { useConversation, useConversations } from '@/hooks/useConversations';
|
||||||
import { useStreamChat, type ChatMessage } from '@/hooks/useStreamChat';
|
import { useStreamChat, type ChatMessage } from '@/hooks/useStreamChat';
|
||||||
import { useModels, useTools, useSettings } from '@/hooks/useSettings';
|
import { useModels, useTools, useSettings } from '@/hooks/useSettings';
|
||||||
import { useAuth } from '@/providers/AuthProvider';
|
import { useAuth } from '@/providers/AuthProvider';
|
||||||
|
import { usePromptOptimizer } from '@/providers/PromptOptimizerProvider';
|
||||||
import type { UploadFile } from '@/types/file-upload';
|
import type { UploadFile } from '@/types/file-upload';
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
@ -26,6 +28,7 @@ export default function ChatPage({ params }: PageProps) {
|
|||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const initialMessage = searchParams.get('message');
|
const initialMessage = searchParams.get('message');
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
const { setOptimizedPrompt } = usePromptOptimizer();
|
||||||
|
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
@ -614,6 +617,9 @@ export default function ChatPage({ params }: PageProps) {
|
|||||||
initialContent={noteContent}
|
initialContent={noteContent}
|
||||||
conversationId={chatId}
|
conversationId={chatId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 提示词优化工具浮动按钮 */}
|
||||||
|
<PromptOptimizer onUsePrompt={setOptimizedPrompt} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -194,6 +194,19 @@ body {
|
|||||||
background-color: var(--color-text-tertiary);
|
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,6 +2,7 @@ import type { Metadata } from "next";
|
|||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { SettingsProvider } from "@/components/providers/SettingsProvider";
|
import { SettingsProvider } from "@/components/providers/SettingsProvider";
|
||||||
import { AuthProvider } from "@/providers/AuthProvider";
|
import { AuthProvider } from "@/providers/AuthProvider";
|
||||||
|
import { PromptOptimizerProvider } from "@/providers/PromptOptimizerProvider";
|
||||||
import { Toaster } from "@/components/ui/Toast";
|
import { Toaster } from "@/components/ui/Toast";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
@ -22,8 +23,10 @@ export default function RootLayout({
|
|||||||
<body className="antialiased">
|
<body className="antialiased">
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<SettingsProvider>
|
<SettingsProvider>
|
||||||
{children}
|
<PromptOptimizerProvider>
|
||||||
<Toaster />
|
{children}
|
||||||
|
<Toaster />
|
||||||
|
</PromptOptimizerProvider>
|
||||||
</SettingsProvider>
|
</SettingsProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -6,13 +6,15 @@ import { AppLayout } from '@/components/layout/AppLayout';
|
|||||||
import { Welcome } from '@/components/features/Welcome';
|
import { Welcome } from '@/components/features/Welcome';
|
||||||
import { ChatInput } from '@/components/features/ChatInput';
|
import { ChatInput } from '@/components/features/ChatInput';
|
||||||
import { QuickActions } from '@/components/features/QuickActions';
|
import { QuickActions } from '@/components/features/QuickActions';
|
||||||
import { currentUser, getGreeting } from '@/data/mock';
|
import { getGreeting } from '@/data/mock';
|
||||||
|
import { useAuth } from '@/providers/AuthProvider';
|
||||||
import { useConversations } from '@/hooks/useConversations';
|
import { useConversations } from '@/hooks/useConversations';
|
||||||
import { useModels, useTools, useSettings } from '@/hooks/useSettings';
|
import { useModels, useTools, useSettings } from '@/hooks/useSettings';
|
||||||
import type { QuickAction } from '@/types';
|
import type { QuickAction } from '@/types';
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { user } = useAuth();
|
||||||
const { createConversation } = useConversations();
|
const { createConversation } = useConversations();
|
||||||
const { models, loading: modelsLoading } = useModels();
|
const { models, loading: modelsLoading } = useModels();
|
||||||
const { tools: availableTools, loading: toolsLoading } = useTools();
|
const { tools: availableTools, loading: toolsLoading } = useTools();
|
||||||
@ -22,7 +24,8 @@ export default function HomePage() {
|
|||||||
const [enabledTools, setEnabledTools] = useState<string[]>([]);
|
const [enabledTools, setEnabledTools] = useState<string[]>([]);
|
||||||
const [isSending, setIsSending] = useState(false);
|
const [isSending, setIsSending] = useState(false);
|
||||||
|
|
||||||
const greeting = getGreeting(currentUser.name);
|
// 使用真实登录用户的昵称,如果未登录则显示"用户"
|
||||||
|
const greeting = getGreeting(user?.nickname || '用户');
|
||||||
|
|
||||||
// 初始化默认设置
|
// 初始化默认设置
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useRef } from 'react';
|
import { useState, useRef, useEffect } from 'react';
|
||||||
import { Paperclip, ArrowUp, Upload } from 'lucide-react';
|
import { Paperclip, ArrowUp, Upload } from 'lucide-react';
|
||||||
import { ModelSelector } from './ModelSelector';
|
import { ModelSelector } from './ModelSelector';
|
||||||
import { ToolsDropdown } from './ToolsDropdown';
|
import { ToolsDropdown } from './ToolsDropdown';
|
||||||
import { FilePreviewList } from './FilePreviewList';
|
import { FilePreviewList } from './FilePreviewList';
|
||||||
import { Tooltip } from '@/components/ui/Tooltip';
|
import { Tooltip } from '@/components/ui/Tooltip';
|
||||||
import { useFileUpload } from '@/hooks/useFileUpload';
|
import { useFileUpload } from '@/hooks/useFileUpload';
|
||||||
|
import { usePromptOptimizer } from '@/providers/PromptOptimizerProvider';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import type { Model, Tool } from '@/types';
|
import type { Model, Tool } from '@/types';
|
||||||
import type { UploadFile } from '@/types/file-upload';
|
import type { UploadFile } from '@/types/file-upload';
|
||||||
@ -37,6 +38,17 @@ export function ChatInput({
|
|||||||
const [message, setMessage] = useState('');
|
const [message, setMessage] = useState('');
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
// 使用提示词优化 Hook
|
||||||
|
const { consumeOptimizedPrompt } = usePromptOptimizer();
|
||||||
|
|
||||||
|
// 监听优化后的提示词并填入输入框
|
||||||
|
useEffect(() => {
|
||||||
|
const prompt = consumeOptimizedPrompt();
|
||||||
|
if (prompt) {
|
||||||
|
setMessage(prompt);
|
||||||
|
}
|
||||||
|
}, [consumeOptimizedPrompt]);
|
||||||
|
|
||||||
// 使用文件上传 Hook
|
// 使用文件上传 Hook
|
||||||
const {
|
const {
|
||||||
files,
|
files,
|
||||||
|
|||||||
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}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
import { useState, type ReactNode } from 'react';
|
import { useState, type ReactNode } from 'react';
|
||||||
import { Sidebar, SidebarToggle } from '@/components/layout/Sidebar';
|
import { Sidebar, SidebarToggle } from '@/components/layout/Sidebar';
|
||||||
|
import { PromptOptimizer } from '@/components/features/PromptOptimizer';
|
||||||
|
import { usePromptOptimizer } from '@/providers/PromptOptimizerProvider';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
interface AppLayoutProps {
|
interface AppLayoutProps {
|
||||||
@ -12,6 +14,7 @@ interface AppLayoutProps {
|
|||||||
|
|
||||||
export function AppLayout({ children, showHeader = true, headerContent }: AppLayoutProps) {
|
export function AppLayout({ children, showHeader = true, headerContent }: AppLayoutProps) {
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||||
|
const { setOptimizedPrompt } = usePromptOptimizer();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen">
|
<div className="flex min-h-screen">
|
||||||
@ -41,6 +44,9 @@ export function AppLayout({ children, showHeader = true, headerContent }: AppLay
|
|||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
{/* 提示词优化工具浮动按钮 */}
|
||||||
|
<PromptOptimizer onUsePrompt={setOptimizedPrompt} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
10
src/drizzle/migrations/0009_omniscient_vision.sql
Normal file
10
src/drizzle/migrations/0009_omniscient_vision.sql
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
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';
|
||||||
1178
src/drizzle/migrations/meta/0009_snapshot.json
Normal file
1178
src/drizzle/migrations/meta/0009_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -64,6 +64,13 @@
|
|||||||
"when": 1766314954003,
|
"when": 1766314954003,
|
||||||
"tag": "0008_flat_star_brand",
|
"tag": "0008_flat_star_brand",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 9,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1766323563111,
|
||||||
|
"tag": "0009_omniscient_vision",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -289,6 +289,23 @@ export const notes = pgTable('notes', {
|
|||||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(),
|
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(),
|
||||||
|
});
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// 关系定义
|
// 关系定义
|
||||||
// ============================================
|
// ============================================
|
||||||
@ -431,3 +448,6 @@ export type NewUserFavoriteAssistant = typeof userFavoriteAssistants.$inferInser
|
|||||||
|
|
||||||
export type Note = typeof notes.$inferSelect;
|
export type Note = typeof notes.$inferSelect;
|
||||||
export type NewNote = typeof notes.$inferInsert;
|
export type NewNote = typeof notes.$inferInsert;
|
||||||
|
|
||||||
|
export type PromptOptimization = typeof promptOptimizations.$inferSelect;
|
||||||
|
export type NewPromptOptimization = typeof promptOptimizations.$inferInsert;
|
||||||
|
|||||||
45
src/providers/PromptOptimizerProvider.tsx
Normal file
45
src/providers/PromptOptimizerProvider.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
'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 +1,2 @@
|
|||||||
export { AuthProvider, useAuth, type AuthUser } from './AuthProvider';
|
export { AuthProvider, useAuth, type AuthUser } from './AuthProvider';
|
||||||
|
export { PromptOptimizerProvider, usePromptOptimizer } from './PromptOptimizerProvider';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user