177 lines
9.9 KiB
TypeScript
177 lines
9.9 KiB
TypeScript
|
|
import React, { useState, useEffect } from 'react';
|
|
import { Prompt, PromptVersion, Category } from '../types';
|
|
import { SparklesIcon, HistoryIcon, TrashIcon } from './Icons';
|
|
import { optimizePrompt } from '../services/geminiService';
|
|
|
|
interface PromptEditorProps {
|
|
prompt: Prompt;
|
|
versions: PromptVersion[];
|
|
categoryName: string;
|
|
onSave: (content: string, summary: string) => void;
|
|
onDelete: (id: string) => void;
|
|
}
|
|
|
|
const PromptEditor: React.FC<PromptEditorProps> = ({ prompt, versions, categoryName, onSave, onDelete }) => {
|
|
const currentVersion = versions.find(v => v.id === prompt.currentVersionId);
|
|
const [content, setContent] = useState(currentVersion?.content || '');
|
|
const [summary, setSummary] = useState('');
|
|
const [isOptimizing, setIsOptimizing] = useState(false);
|
|
const [showHistory, setShowHistory] = useState(false);
|
|
|
|
useEffect(() => {
|
|
setContent(currentVersion?.content || '');
|
|
setSummary('');
|
|
}, [prompt.id, prompt.currentVersionId, currentVersion]);
|
|
|
|
const handleOptimize = async () => {
|
|
if (!content.trim()) return;
|
|
setIsOptimizing(true);
|
|
const optimized = await optimizePrompt(content, categoryName);
|
|
setContent(optimized);
|
|
setIsOptimizing(false);
|
|
};
|
|
|
|
const handleSave = () => {
|
|
if (!content.trim()) return;
|
|
onSave(content, summary || `${versions.length + 1}차 고도화 진행`);
|
|
};
|
|
|
|
return (
|
|
<div className="flex-1 flex flex-col h-screen bg-slate-50/30">
|
|
{/* Header */}
|
|
<header className="px-8 py-6 bg-white border-b border-slate-100 flex items-center justify-between sticky top-0 z-10">
|
|
<div className="flex items-center space-x-4">
|
|
<div className="w-12 h-12 rounded-2xl bg-slate-50 border border-slate-100 flex items-center justify-center">
|
|
<svg className="w-6 h-6 text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<div className="flex items-center space-x-2 text-[10px] font-black uppercase tracking-widest text-slate-400 mb-0.5">
|
|
<span className="text-indigo-500">{categoryName}</span>
|
|
<span className="opacity-30">/</span>
|
|
<span className="bg-indigo-50 text-indigo-600 px-1.5 py-0.5 rounded uppercase font-black">진화 단계 v{currentVersion?.versionNumber || 1}</span>
|
|
</div>
|
|
<h2 className="text-2xl font-black text-slate-900">{prompt.name}</h2>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center space-x-4">
|
|
<button
|
|
onClick={() => setShowHistory(!showHistory)}
|
|
className={`flex items-center space-x-2.5 px-4 py-2.5 rounded-xl text-sm font-bold transition-all ${showHistory ? 'bg-indigo-600 text-white shadow-lg shadow-indigo-200' : 'bg-white border border-slate-200 text-slate-600 hover:bg-slate-50 hover:border-slate-300 shadow-sm'}`}
|
|
>
|
|
<HistoryIcon className="w-4 h-4" />
|
|
<span>버전 히스토리</span>
|
|
</button>
|
|
<div className="h-6 w-px bg-slate-200 mx-1"></div>
|
|
<button
|
|
onClick={() => onDelete(prompt.id)}
|
|
className="p-2.5 text-slate-300 hover:text-red-500 hover:bg-red-50 rounded-xl transition-all"
|
|
title="프롬프트 연구 삭제"
|
|
>
|
|
<TrashIcon className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<main className="flex-1 flex overflow-hidden p-6 gap-6">
|
|
{/* Editor Area */}
|
|
<div className="flex-1 flex flex-col space-y-4 max-w-5xl mx-auto w-full">
|
|
<div className="bg-white rounded-3xl shadow-xl shadow-slate-200/50 border border-slate-100 flex-1 flex flex-col overflow-hidden">
|
|
<div className="px-6 py-4 border-b border-slate-50 flex items-center justify-between bg-slate-50/30">
|
|
<div className="flex items-center space-x-3">
|
|
<div className="w-2 h-2 rounded-full bg-indigo-500 animate-pulse"></div>
|
|
<span className="text-xs font-black text-slate-500 uppercase tracking-widest">활성 연구 워크스페이스</span>
|
|
</div>
|
|
<button
|
|
onClick={handleOptimize}
|
|
disabled={isOptimizing}
|
|
className="flex items-center space-x-2 px-4 py-2 bg-gradient-to-r from-indigo-600 to-violet-600 text-white rounded-xl text-xs font-black hover:from-indigo-700 hover:to-violet-700 disabled:opacity-50 transition-all shadow-lg shadow-indigo-200 active:scale-95"
|
|
>
|
|
<SparklesIcon className={`w-3.5 h-3.5 ${isOptimizing ? 'animate-spin' : ''}`} />
|
|
<span>{isOptimizing ? 'AI 연구 및 최적화 중...' : 'Gemini AI로 프롬프트 고도화'}</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex-1 p-0">
|
|
<textarea
|
|
value={content}
|
|
onChange={(e) => setContent(e.target.value)}
|
|
placeholder="여기에 프롬프트를 작성하여 연구를 시작하세요..."
|
|
className="w-full h-full p-8 text-slate-800 font-mono text-base bg-white focus:outline-none resize-none leading-relaxed"
|
|
/>
|
|
</div>
|
|
|
|
<div className="px-8 py-6 border-t border-slate-50 bg-slate-50/20">
|
|
<div className="flex items-center space-x-4">
|
|
<div className="flex-1 relative group">
|
|
<div className="absolute inset-y-0 left-4 flex items-center pointer-events-none">
|
|
<svg className="w-4 h-4 text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
|
</svg>
|
|
</div>
|
|
<input
|
|
type="text"
|
|
value={summary}
|
|
onChange={(e) => setSummary(e.target.value)}
|
|
placeholder="이번 연구 단계의 핵심 개선 내용을 기록하세요 (변경 요약)..."
|
|
className="w-full pl-12 pr-4 py-3.5 bg-white border border-slate-200 rounded-2xl text-sm font-bold transition-all focus:ring-4 focus:ring-indigo-50/50 focus:border-indigo-300 outline-none"
|
|
/>
|
|
</div>
|
|
<button
|
|
onClick={handleSave}
|
|
className="px-8 py-3.5 bg-slate-900 text-white rounded-2xl font-black text-sm hover:bg-black transition-all shadow-xl shadow-slate-200 active:scale-95 whitespace-nowrap"
|
|
>
|
|
연구 성과 저장
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Version History Sidebar */}
|
|
{showHistory && (
|
|
<div className="w-96 glass-panel rounded-3xl border border-slate-200 overflow-hidden flex flex-col animate-in slide-in-from-right duration-500 shadow-2xl">
|
|
<div className="p-6 border-b border-slate-100 flex items-center justify-between bg-white">
|
|
<h3 className="font-black text-slate-900 tracking-tight">프롬프트 진화 히스토리</h3>
|
|
<button onClick={() => setShowHistory(false)} className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-slate-100 text-slate-400 transition-all">✕</button>
|
|
</div>
|
|
<div className="flex-1 overflow-y-auto p-6 space-y-6 bg-slate-50/30">
|
|
{[...versions].sort((a, b) => b.versionNumber - a.versionNumber).map((v, idx) => (
|
|
<div key={v.id} className="relative pl-8 group">
|
|
{/* Connection line */}
|
|
{idx < versions.length - 1 && (
|
|
<div className="absolute left-[11px] top-6 bottom-[-24px] w-0.5 bg-slate-200"></div>
|
|
)}
|
|
{/* Circle */}
|
|
<div className={`absolute left-0 top-1 w-6 h-6 rounded-full border-4 border-white shadow-md z-10 ${v.id === prompt.currentVersionId ? 'bg-indigo-600' : 'bg-slate-300 group-hover:bg-slate-400'}`}></div>
|
|
|
|
<div className={`p-4 rounded-2xl border transition-all duration-200 ${v.id === prompt.currentVersionId ? 'bg-white border-indigo-100 shadow-lg' : 'bg-white/60 border-slate-100 hover:border-slate-200'}`}>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<span className={`text-[10px] font-black px-2 py-0.5 rounded-full ${v.id === prompt.currentVersionId ? 'bg-indigo-600 text-white' : 'bg-slate-100 text-slate-500'}`}>버전 {v.versionNumber}</span>
|
|
<span className="text-[10px] text-slate-400 font-bold">{new Date(v.createdAt).toLocaleDateString()}</span>
|
|
</div>
|
|
<p className="text-xs text-slate-700 font-bold mb-3 leading-relaxed">"{v.changeSummary}"</p>
|
|
<button
|
|
onClick={() => setContent(v.content)}
|
|
className="text-[10px] text-indigo-600 font-black hover:text-indigo-800 transition-colors uppercase tracking-widest flex items-center"
|
|
>
|
|
에디터로 복구하여 연구 재개
|
|
<svg className="w-2.5 h-2.5 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M14 5l7 7-7 7M5 5l7 7-7 7" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</main>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default PromptEditor;
|