ManimCat / frontend /src /components /PromptsManager.tsx
Bin29's picture
Sync from main: 68df783 feat: support multimodal studio reference images
d47b053
/**
* 提示词管理器 - 简洁风格
*/
import { PromptSidebar } from './PromptSidebar';
import { usePrompts } from '../hooks/usePrompts';
import type { RoleType, SharedModuleType } from '../types/api';
import { useI18n } from '../i18n';
// ============================================================================
// 配置
// ============================================================================
interface Props {
isOpen: boolean;
onClose: () => void;
}
export function PromptsManager({ isOpen, onClose }: Props) {
const { t } = useI18n();
const {
isLoading,
selection,
setSelection,
getCurrentContent,
setCurrentContent,
restoreCurrent,
hasOverride
} = usePrompts();
// 获取当前标题
const getTitle = () => {
const roleLabels: Record<RoleType, string> = {
problemFraming: t('prompts.role.problemFraming'),
conceptDesigner: t('prompts.role.conceptDesigner'),
codeGeneration: t('prompts.role.codeGeneration'),
codeRetry: t('prompts.role.codeRetry'),
codeEdit: t('prompts.role.codeEdit')
};
const sharedLabels: Record<SharedModuleType, string> = {
apiIndex: t('prompts.shared.apiIndex'),
specification: t('prompts.shared.specification')
};
if (selection.kind === 'role') {
const roleLabel = roleLabels[selection.role];
return selection.promptType === 'system'
? t('prompts.role.systemTitle', { role: roleLabel })
: t('prompts.role.userTitle', { role: roleLabel });
}
return sharedLabels[selection.module];
};
// 获取当前描述
const getDescription = () => {
if (selection.kind === 'role') {
if (selection.promptType === 'system') {
return t('prompts.role.systemDescription');
}
return t('prompts.role.userDescription');
}
return selection.module === 'apiIndex'
? t('prompts.shared.apiIndexDescription')
: t('prompts.shared.specificationDescription');
};
if (!isOpen) return null;
const content = getCurrentContent();
const isModified = hasOverride();
return (
<div
className={`fixed inset-0 z-50 flex flex-col bg-bg-primary transition-all duration-300 ${
'opacity-100'
}`}
>
{/* 顶栏 */}
<div className="h-14 bg-bg-secondary/50 border-b border-bg-tertiary/30 flex items-center justify-between px-4">
<div className="flex items-center gap-3">
<button
onClick={onClose}
className="p-2 text-text-secondary/70 hover:text-text-primary hover:bg-bg-tertiary/50 rounded-lg transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</button>
<span className="text-sm text-text-primary font-medium">{t('prompts.title')}</span>
</div>
{/* 修改状态 + 恢复按钮 */}
<div className="flex items-center gap-2">
{isModified && (
<>
<span className="text-xs text-accent/70">{t('prompts.modified')}</span>
<button
onClick={restoreCurrent}
className="px-3 py-1.5 text-xs text-text-secondary/70 hover:text-text-primary hover:bg-bg-tertiary/50 rounded-lg transition-colors"
>
{t('prompts.restore')}
</button>
</>
)}
</div>
</div>
{/* 主内容 */}
<div className="flex-1 flex overflow-hidden">
{/* 侧边栏 */}
<PromptSidebar selection={selection} onSelect={setSelection} />
{/* 编辑区 */}
<div className="flex-1 flex flex-col overflow-hidden">
{/* 标题区 */}
<div className="px-6 py-4 border-b border-bg-tertiary/30">
<h2 className="text-base font-medium text-text-primary">{getTitle()}</h2>
<p className="text-xs text-text-secondary/60 mt-1">{getDescription()}</p>
</div>
{/* 编辑器 */}
<div className="flex-1 p-4 overflow-hidden">
{isLoading ? (
<div className="h-full flex items-center justify-center text-text-secondary/50 text-sm">
{t('common.loading')}
</div>
) : (
<textarea
value={content}
onChange={e => setCurrentContent(e.target.value)}
className="w-full h-full px-4 py-3 bg-bg-secondary/30 border border-bg-tertiary/30 rounded-lg text-sm text-text-primary font-mono leading-relaxed resize-none focus:outline-none focus:border-accent/30 focus:ring-1 focus:ring-accent/20 transition-colors"
placeholder={t('prompts.placeholder')}
/>
)}
</div>
{/* 底栏 */}
<div className="px-6 py-3 border-t border-bg-tertiary/30 flex items-center justify-between text-xs text-text-secondary/50">
<span>{t('prompts.characters', { count: content.length })}</span>
<span>{t('prompts.autosave')}</span>
</div>
</div>
</div>
</div>
);
}