feat: Refactor service management scripts to use a unified dev script
- Added package.json to manage development scripts. - Updated restart-services.ps1 to call the new dev script for starting services. - Refactored start-admin.ps1, start-backend.ps1, start-frontend.ps1, and start-mcp.ps1 to utilize the dev script for starting respective services. - Enhanced stop-services.ps1 to improve process termination logic by matching command patterns.
This commit is contained in:
302
admin/src/pages/post-polish-page.tsx
Normal file
302
admin/src/pages/post-polish-page.tsx
Normal file
@@ -0,0 +1,302 @@
|
||||
import { DiffEditor } from '@monaco-editor/react'
|
||||
import { Bot, CheckCheck, RefreshCcw, WandSparkles } from 'lucide-react'
|
||||
import { startTransition, useEffect, useMemo, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import {
|
||||
configureMonaco,
|
||||
editorTheme,
|
||||
sharedOptions,
|
||||
} from '@/components/markdown-workbench'
|
||||
import { MarkdownPreview } from '@/components/markdown-preview'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { adminApi, ApiError } from '@/lib/api'
|
||||
import { computeDiffHunks, applySelectedDiffHunks } from '@/lib/markdown-merge'
|
||||
import {
|
||||
loadDraftWindowSnapshot,
|
||||
savePolishWindowResult,
|
||||
type DraftWindowSnapshot,
|
||||
} from '@/lib/post-draft-window'
|
||||
|
||||
type PolishTarget = 'editor' | 'create'
|
||||
|
||||
function getDraftKey() {
|
||||
if (typeof window === 'undefined') {
|
||||
return null
|
||||
}
|
||||
|
||||
return new URLSearchParams(window.location.search).get('draftKey')
|
||||
}
|
||||
|
||||
function getTarget(): PolishTarget {
|
||||
if (typeof window === 'undefined') {
|
||||
return 'editor'
|
||||
}
|
||||
|
||||
const value = new URLSearchParams(window.location.search).get('target')
|
||||
return value === 'create' ? 'create' : 'editor'
|
||||
}
|
||||
|
||||
function buildApplyMessage(draftKey: string, markdown: string, target: PolishTarget) {
|
||||
return {
|
||||
type: 'termi-admin-post-polish-apply',
|
||||
draftKey,
|
||||
markdown,
|
||||
target,
|
||||
}
|
||||
}
|
||||
|
||||
export function PostPolishPage() {
|
||||
const draftKey = getDraftKey()
|
||||
const target = getTarget()
|
||||
const [snapshot, setSnapshot] = useState<DraftWindowSnapshot | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [polishing, setPolishing] = useState(false)
|
||||
const [polishedMarkdown, setPolishedMarkdown] = useState('')
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
|
||||
|
||||
useEffect(() => {
|
||||
const draft = loadDraftWindowSnapshot(draftKey)
|
||||
if (!draft) {
|
||||
setError('没有找到要润色的草稿快照,请从文章编辑页重新打开 AI 润色窗口。')
|
||||
} else {
|
||||
startTransition(() => {
|
||||
setSnapshot(draft)
|
||||
})
|
||||
}
|
||||
setLoading(false)
|
||||
}, [draftKey])
|
||||
|
||||
const originalMarkdown = snapshot?.markdown ?? ''
|
||||
const hunks = useMemo(
|
||||
() => (polishedMarkdown ? computeDiffHunks(originalMarkdown, polishedMarkdown) : []),
|
||||
[originalMarkdown, polishedMarkdown],
|
||||
)
|
||||
const mergedMarkdown = useMemo(
|
||||
() => applySelectedDiffHunks(originalMarkdown, hunks, selectedIds),
|
||||
[hunks, originalMarkdown, selectedIds],
|
||||
)
|
||||
|
||||
const applyAll = () => {
|
||||
setSelectedIds(new Set(hunks.map((hunk) => hunk.id)))
|
||||
}
|
||||
|
||||
const keepOriginal = () => {
|
||||
setSelectedIds(new Set())
|
||||
}
|
||||
|
||||
const applyToParent = () => {
|
||||
if (!draftKey) {
|
||||
toast.error('当前窗口缺少草稿标识,无法回填。')
|
||||
return
|
||||
}
|
||||
|
||||
const result = savePolishWindowResult(draftKey, mergedMarkdown, target)
|
||||
window.opener?.postMessage(buildApplyMessage(draftKey, mergedMarkdown, target), window.location.origin)
|
||||
toast.success('已把 AI 润色结果回填到原编辑器。')
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background px-4 py-6 text-foreground lg:px-6">
|
||||
<div className="mx-auto max-w-[1560px] space-y-6">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div className="space-y-3">
|
||||
<Badge variant="secondary">AI 润色工作台</Badge>
|
||||
<div>
|
||||
<h1 className="text-3xl font-semibold tracking-tight">
|
||||
{snapshot?.title || 'AI 润色与选择性合并'}
|
||||
</h1>
|
||||
<p className="mt-2 text-sm leading-6 text-muted-foreground">
|
||||
左边保留润色前的原稿,右边是当前选中的合并结果。你可以先生成 AI 润色稿,再按改动块决定要保留哪些内容。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={!snapshot || polishing}
|
||||
onClick={async () => {
|
||||
if (!snapshot) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setPolishing(true)
|
||||
const result = await adminApi.polishPostMarkdown(snapshot.markdown)
|
||||
const nextHunks = computeDiffHunks(snapshot.markdown, result.polished_markdown)
|
||||
startTransition(() => {
|
||||
setPolishedMarkdown(result.polished_markdown)
|
||||
setSelectedIds(new Set(nextHunks.map((hunk) => hunk.id)))
|
||||
})
|
||||
toast.success(`AI 已生成润色稿,共识别 ${nextHunks.length} 个改动块。`)
|
||||
} catch (requestError) {
|
||||
toast.error(requestError instanceof ApiError ? requestError.message : 'AI 润色失败。')
|
||||
} finally {
|
||||
setPolishing(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Bot className="h-4 w-4" />
|
||||
{polishing ? '润色中...' : polishedMarkdown ? '重新生成润色稿' : '生成 AI 润色稿'}
|
||||
</Button>
|
||||
<Button variant="outline" disabled={!hunks.length} onClick={applyAll}>
|
||||
<CheckCheck className="h-4 w-4" />
|
||||
全部采用
|
||||
</Button>
|
||||
<Button variant="outline" disabled={!hunks.length} onClick={keepOriginal}>
|
||||
<RefreshCcw className="h-4 w-4" />
|
||||
全部还原
|
||||
</Button>
|
||||
<Button disabled={!hunks.length} onClick={applyToParent}>
|
||||
<WandSparkles className="h-4 w-4" />
|
||||
应用到原编辑器
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<Card>
|
||||
<CardContent className="py-12 text-sm text-muted-foreground">正在加载草稿快照...</CardContent>
|
||||
</Card>
|
||||
) : error ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>AI 润色窗口加载失败</CardTitle>
|
||||
<CardDescription>{error}</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
) : snapshot ? (
|
||||
<div className="grid gap-6 xl:grid-cols-[1.14fr_0.86fr]">
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>润色前 vs 当前合并结果</CardTitle>
|
||||
<CardDescription>{snapshot.path}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Badge variant="secondary">改动块 {hunks.length}</Badge>
|
||||
<Badge variant="success">已采用 {selectedIds.size}</Badge>
|
||||
<Badge variant="outline">目标 {target === 'create' ? '新建草稿' : '现有文章'}</Badge>
|
||||
</div>
|
||||
<div className="overflow-hidden rounded-[28px] border border-slate-800 bg-[#1e1e1e]">
|
||||
<div className="flex items-center justify-between border-b border-slate-800 bg-[#141414] px-4 py-2 text-[11px] uppercase tracking-[0.18em] text-slate-400">
|
||||
<span>润色前原稿</span>
|
||||
<span>当前合并结果</span>
|
||||
</div>
|
||||
<div className="h-[560px]">
|
||||
<DiffEditor
|
||||
height="100%"
|
||||
language="markdown"
|
||||
original={originalMarkdown}
|
||||
modified={mergedMarkdown}
|
||||
originalModelPath={`${snapshot.path}#ai-original`}
|
||||
modifiedModelPath={`${snapshot.path}#ai-merged`}
|
||||
keepCurrentOriginalModel
|
||||
keepCurrentModifiedModel
|
||||
theme={editorTheme}
|
||||
beforeMount={configureMonaco}
|
||||
options={{
|
||||
...sharedOptions,
|
||||
originalEditable: false,
|
||||
readOnly: true,
|
||||
renderSideBySide: true,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>当前合并结果预览</CardTitle>
|
||||
<CardDescription>边挑选改动,边查看最终会保存成什么样。</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-[420px] overflow-hidden rounded-[28px] border border-slate-200 bg-white">
|
||||
<MarkdownPreview markdown={mergedMarkdown || originalMarkdown} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>改动块选择</CardTitle>
|
||||
<CardDescription>
|
||||
每一块都可以单独采用或保留原文,合并结果会立即同步到右侧 diff 和预览。
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{!polishedMarkdown ? (
|
||||
<div className="rounded-3xl border border-dashed border-border/70 px-5 py-10 text-sm text-muted-foreground">
|
||||
先点“生成 AI 润色稿”,这里才会出现可选的改动块。
|
||||
</div>
|
||||
) : hunks.length ? (
|
||||
hunks.map((hunk, index) => {
|
||||
const accepted = selectedIds.has(hunk.id)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={hunk.id}
|
||||
className={`rounded-3xl border p-4 transition ${
|
||||
accepted
|
||||
? 'border-emerald-500/30 bg-emerald-500/10'
|
||||
: 'border-border/70 bg-background/60'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium">改动块 {index + 1}</p>
|
||||
<p className="mt-1 text-xs leading-5 text-muted-foreground">
|
||||
原文 {hunk.originalStart}-{Math.max(hunk.originalEnd, hunk.originalStart - 1)} 行
|
||||
,润色稿 {hunk.modifiedStart}-{Math.max(hunk.modifiedEnd, hunk.modifiedStart - 1)} 行
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={accepted ? 'default' : 'outline'}
|
||||
onClick={() => {
|
||||
setSelectedIds((current) => {
|
||||
const next = new Set(current)
|
||||
if (next.has(hunk.id)) {
|
||||
next.delete(hunk.id)
|
||||
} else {
|
||||
next.add(hunk.id)
|
||||
}
|
||||
return next
|
||||
})
|
||||
}}
|
||||
>
|
||||
{accepted ? '已采用' : '采用这块'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p className="mt-3 rounded-2xl border border-border/60 bg-background/70 px-3 py-2 text-xs leading-6 text-muted-foreground">
|
||||
{hunk.preview}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div className="rounded-3xl border border-border/70 px-5 py-10 text-sm text-muted-foreground">
|
||||
AI 已返回结果,但没有检测到行级差异。可以直接应用,或者重新生成一次。
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user