feat:支持用户自定义prompt

This commit is contained in:
SengokuCola
2026-05-07 00:45:58 +08:00
parent 8edf13df14
commit 57100797a5
5 changed files with 306 additions and 36 deletions

View File

@@ -11,6 +11,7 @@ export interface PromptFileInfo {
display_name: string
advanced: boolean
description: string
customized: boolean
}
export interface PromptCatalog {
@@ -24,6 +25,7 @@ export interface PromptFileContent {
language: string
filename: string
content: string
customized: boolean
}
export async function getPromptCatalog(): Promise<ApiResponse<PromptCatalog>> {
@@ -39,6 +41,16 @@ export async function getPromptFile(
return parseResponse<PromptFileContent>(response)
}
export async function getDefaultPromptFile(
language: string,
filename: string
): Promise<ApiResponse<PromptFileContent>> {
const response = await fetchWithAuth(
`${API_BASE}/${encodeURIComponent(language)}/${encodeURIComponent(filename)}/default`
)
return parseResponse<PromptFileContent>(response)
}
export async function updatePromptFile(
language: string,
filename: string,
@@ -50,3 +62,13 @@ export async function updatePromptFile(
})
return parseResponse<PromptFileContent>(response)
}
export async function resetPromptFile(
language: string,
filename: string
): Promise<ApiResponse<PromptFileContent>> {
const response = await fetchWithAuth(`${API_BASE}/${encodeURIComponent(language)}/${encodeURIComponent(filename)}`, {
method: 'DELETE',
})
return parseResponse<PromptFileContent>(response)
}

View File

@@ -1,18 +1,27 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import { FileText, Loader2, RefreshCw, Save, Search, SlidersHorizontal } from 'lucide-react'
import { Eye, FileText, Loader2, RefreshCw, RotateCcw, Save, Search, SlidersHorizontal } from 'lucide-react'
import { CodeEditor } from '@/components/CodeEditor'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Separator } from '@/components/ui/separator'
import { useToast } from '@/hooks/use-toast'
import {
getDefaultPromptFile,
getPromptCatalog,
getPromptFile,
resetPromptFile,
updatePromptFile,
type PromptCatalog,
type PromptFileInfo,
@@ -35,6 +44,10 @@ export function PromptManagementPage() {
const [loadingCatalog, setLoadingCatalog] = useState(true)
const [loadingFile, setLoadingFile] = useState(false)
const [saving, setSaving] = useState(false)
const [resetting, setResetting] = useState(false)
const [loadingDefaultPrompt, setLoadingDefaultPrompt] = useState(false)
const [defaultPromptOpen, setDefaultPromptOpen] = useState(false)
const [defaultPromptContent, setDefaultPromptContent] = useState('')
const [query, setQuery] = useState('')
const [showAdvancedPrompts, setShowAdvancedPrompts] = useState(false)
@@ -63,6 +76,7 @@ export function PromptManagementPage() {
}, [visiblePromptFiles, query])
const selectedFile = promptFiles.find((file) => file.name === filename)
const isCustomized = selectedFile?.customized ?? false
useEffect(() => {
if (!filename || showAdvancedPrompts) return
const currentFile = promptFiles.find((file) => file.name === filename)
@@ -181,6 +195,58 @@ export function PromptManagementPage() {
}
}
const handleShowDefault = async () => {
if (!language || !filename) return
try {
setLoadingDefaultPrompt(true)
setDefaultPromptOpen(true)
const result = await getDefaultPromptFile(language, filename)
if (!result.success) {
toast({ title: '读取默认 Prompt 失败', description: result.error, variant: 'destructive' })
setDefaultPromptOpen(false)
return
}
setDefaultPromptContent(result.data.content)
} catch (error) {
toast({
title: '读取默认 Prompt 失败',
description: (error as Error).message,
variant: 'destructive',
})
setDefaultPromptOpen(false)
} finally {
setLoadingDefaultPrompt(false)
}
}
const handleReset = async () => {
if (!language || !filename || !isCustomized) return
try {
setResetting(true)
const result = await resetPromptFile(language, filename)
if (!result.success) {
toast({ title: '恢复默认 Prompt 失败', description: result.error, variant: 'destructive' })
return
}
setContent(result.data.content)
setSavedContent(result.data.content)
toast({ title: '已恢复默认 Prompt', description: `${language}/${filename}` })
void loadCatalog()
} catch (error) {
toast({
title: '恢复默认 Prompt 失败',
description: (error as Error).message,
variant: 'destructive',
})
} finally {
setResetting(false)
}
}
return (
<div className="flex h-[calc(100vh-140px)] flex-col gap-4 p-4 sm:p-6">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
@@ -211,6 +277,24 @@ export function PromptManagementPage() {
<SlidersHorizontal className="mr-2 h-4 w-4" />
{showAdvancedPrompts ? '隐藏高级' : '显示高级'}
</Button>
<Button
variant="outline"
size="sm"
onClick={handleReset}
disabled={!isCustomized || resetting || loadingFile || !filename}
>
<RotateCcw className={cn('mr-2 h-4 w-4', resetting && 'animate-spin')} />
</Button>
<Button
variant="outline"
size="sm"
onClick={handleShowDefault}
disabled={loadingDefaultPrompt || loadingFile || !filename}
>
<Eye className={cn('mr-2 h-4 w-4', loadingDefaultPrompt && 'animate-pulse')} />
</Button>
<Button size="sm" onClick={handleSave} disabled={!hasUnsavedChanges || saving || loadingFile || !filename}>
<Save className="mr-2 h-4 w-4" />
{saving ? '保存中' : hasUnsavedChanges ? '保存' : '已保存'}
@@ -261,6 +345,7 @@ export function PromptManagementPage() {
{file.display_name || file.name}
</div>
{file.advanced && <Badge variant="outline" className="shrink-0 text-[10px]"></Badge>}
{file.customized && <Badge variant="secondary" className="shrink-0 text-[10px]"></Badge>}
</div>
<div className="mt-0.5 truncate text-xs text-muted-foreground">{file.name} · {formatFileSize(file.size)}</div>
{file.description && (
@@ -281,6 +366,7 @@ export function PromptManagementPage() {
<CardTitle className="flex items-center gap-2 truncate text-sm">
<span className="truncate">{selectedFile?.display_name || filename || '未选择文件'}</span>
{selectedFile?.advanced && <Badge variant="outline" className="shrink-0"></Badge>}
{isCustomized && <Badge variant="secondary" className="shrink-0"></Badge>}
</CardTitle>
<p className="mt-1 text-xs text-muted-foreground">
{language}
@@ -311,6 +397,31 @@ export function PromptManagementPage() {
</CardContent>
</Card>
</div>
<Dialog open={defaultPromptOpen} onOpenChange={setDefaultPromptOpen}>
<DialogContent className="max-w-[min(96vw,1100px)]">
<DialogHeader>
<DialogTitle> Prompt</DialogTitle>
<DialogDescription>
{language}/{filename}
</DialogDescription>
</DialogHeader>
{loadingDefaultPrompt ? (
<div className="flex h-[520px] items-center justify-center gap-2 text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" />
</div>
) : (
<CodeEditor
value={defaultPromptContent}
readOnly
language="text"
height="min(62vh, 620px)"
minHeight="420px"
/>
)}
</DialogContent>
</Dialog>
</div>
)
}