Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions frontend/src/api/kb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ export function createKb(body: InitRequest): Promise<InitResponse> {
return apiFetch<InitResponse>("/api/v1/init", { method: "POST", body })
}

/** Permanently delete a knowledge base (physical removal + unregister).
* `confirmName` must equal `kb` (re-checked server-side); the caller collects
* the type-the-name confirmation. */
export function deleteKb(kb: string, confirmName: string): Promise<{ deleted: boolean; kb: string; path: string }> {
return apiFetch("/api/v1/kb/delete", { body: { kb, confirm_name: confirmName } })
}

export type ConfigSource = "kb" | "global" | "default"

export interface KbConfig {
Expand Down
42 changes: 42 additions & 0 deletions frontend/src/api/wiki.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,45 @@ export function getPage(kb: string, path: string): Promise<{ path: string; conte
export function getKbInventory(kb: string): Promise<KbInventory> {
return apiFetch<KbInventory>("/api/v1/list", { body: { kb } })
}

/** Result of `/api/v1/page/delete`. `backlinks` are 'section/stem' refs whose
* inbound [[links]] will be / were demoted to plain text. */
export interface PageDeleteResult {
status: string
target: string
backlinks: string[]
files_changed?: number | null
ghosts_stripped?: number | null
}

/** Delete a concept/entity page. With `dryRun`, only reports the impact
* (backlinks) without changing anything — used for the confirm preview. */
export function deletePage(kb: string, path: string, dryRun = false): Promise<PageDeleteResult> {
return apiFetch<PageDeleteResult>("/api/v1/page/delete", { body: { kb, path, dry_run: dryRun } })
}

/** Outbound + inbound links for a page (`/api/v1/page/links`) — the edit panel. */
export interface PageLinks {
status: string
target: string
outlinks: string[]
backlinks: string[]
}

export function getPageLinks(kb: string, path: string): Promise<PageLinks> {
return apiFetch<PageLinks>("/api/v1/page/links", { body: { kb, path } })
}

/** Result of editing a page (`PUT /api/v1/page`). `ghosts_stripped` are the
* dead [[links]] demoted to plain text on save. */
export interface PageEditResult {
status: string
target: string
ghosts_stripped: string[]
content: string | null
}

/** Save new BODY for a concept/entity page (frontmatter preserved server-side). */
export function editPage(kb: string, path: string, content: string): Promise<PageEditResult> {
return apiFetch<PageEditResult>("/api/v1/page", { method: "PUT", body: { kb, path, content } })
}
86 changes: 85 additions & 1 deletion frontend/src/components/KbSettingsSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
ShieldCheck, Clock, Radio,
} from 'lucide-react'
import { toast } from 'sonner'
import { getKbConfig, patchKbConfig, type KbConfig, type ConfigSource } from '@/api/kb'
import { deleteKb, getKbConfig, patchKbConfig, type KbConfig, type ConfigSource } from '@/api/kb'
import {
watchStart, watchStop, watchStatus, runRecompile, runLint, type WatchStatus,
} from '@/api/maintenance'
Expand All @@ -28,12 +28,14 @@ export default function KbSettingsSheet({
onClose,
docCount,
onChanged,
onDeleted,
}: {
kb: string
open: boolean
onClose: () => void
docCount: number
onChanged?: () => void
onDeleted?: () => void
}) {
const { t } = useTranslation(['kbSettings', 'common'])
const reduce = useReducedMotion()
Expand Down Expand Up @@ -114,6 +116,7 @@ export default function KbSettingsSheet({
<div className="flex-1 min-h-0 overflow-y-auto scroll-edge-top px-4 py-4 space-y-6">
<KbConfigSection kb={kb} />
<KbMaintenanceSection kb={kb} open={open} docCount={docCount} onChanged={onChanged} />
<KbDangerSection kb={kb} onDeleted={onDeleted} />
</div>
</motion.aside>
</Dialog.Content>
Expand Down Expand Up @@ -732,3 +735,84 @@ function KbMaintenanceSection({
</div>
)
}

/** Danger zone: permanently delete this KB. A type-the-name confirmation gates
* POST /api/v1/kb/delete; on success the parent (KbDetail) navigates away. */
function KbDangerSection({ kb, onDeleted }: { kb: string; onDeleted?: () => void }) {
const { t } = useTranslation(['kbSettings', 'common'])
const [confirming, setConfirming] = useState(false)
const [typed, setTyped] = useState('')
const [busy, setBusy] = useState(false)
const matches = typed.trim() === kb

const doDelete = useCallback(async () => {
if (!matches || busy) return
setBusy(true)
try {
await deleteKb(kb, kb)
toast.success(t('kbSettings:dangerDeletedToast', { kb }))
onDeleted?.()
} catch (e) {
toast.error(t('kbSettings:dangerDeleteError', { error: errMsg(e) }))
setBusy(false)
}
}, [kb, matches, busy, onDeleted, t])

return (
<div className="space-y-3 pt-2">
<h3 className="text-[12px] font-semibold text-red-600 dark:text-red-400 tracking-wide">
{t('kbSettings:dangerHeading')}
</h3>
<div className="rounded-2xl border border-red-200/70 dark:border-red-500/25 bg-red-50/50 dark:bg-red-500/5 px-4 py-3.5 space-y-3">
<p className="text-[12.5px] text-muted-foreground">{t('kbSettings:dangerDesc')}</p>
{!confirming ? (
<button
onClick={() => setConfirming(true)}
className="inline-flex items-center gap-1.5 h-8 px-3 rounded-lg border border-red-300 dark:border-red-500/40 text-[12.5px] font-medium text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-500/10 transition-colors"
>
<Trash2 className="w-3.5 h-3.5" />
{t('kbSettings:dangerDeleteButton')}
</button>
) : (
<div className="space-y-2">
<label className="block text-[12px] text-muted-foreground">
{t('kbSettings:dangerConfirmPrompt', { kb })}
</label>
<input
autoFocus
value={typed}
disabled={busy}
onChange={(e) => setTyped(e.target.value)}
placeholder={kb}
className="w-full h-9 rounded-md border border-input bg-transparent px-3 text-[13px] font-mono2 outline-none focus-visible:ring-2 focus-visible:ring-ring focus:border-red-400"
/>
<div className="flex items-center gap-2">
<button
onClick={doDelete}
disabled={busy || !matches}
className="inline-flex items-center gap-1.5 h-8 px-3 rounded-lg bg-red-600 text-white text-[12.5px] font-medium hover:bg-red-700 transition-colors disabled:opacity-50"
>
{busy ? (
<Loader2 className="w-3.5 h-3.5 animate-spin" />
) : (
<Trash2 className="w-3.5 h-3.5" />
)}
{t('kbSettings:dangerConfirmDelete')}
</button>
<button
onClick={() => {
setConfirming(false)
setTyped('')
}}
disabled={busy}
className="inline-flex items-center h-8 px-3 rounded-lg border border-[hsl(var(--glass-border))] text-[12.5px] font-medium text-muted-foreground hover:bg-accent transition-colors disabled:opacity-60"
>
{t('common:actions.cancel')}
</button>
</div>
</div>
)}
</div>
</div>
)
}
3 changes: 2 additions & 1 deletion frontend/src/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"actions": {
"newKb": "New knowledge base",
"close": "Close",
"refresh": "Refresh"
"refresh": "Refresh",
"cancel": "Cancel"
},
"fields": {
"model": "Model",
Expand Down
24 changes: 24 additions & 0 deletions frontend/src/locales/en/kb.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,30 @@
"multiple": "Delete failed: {{message}} (candidates: {{names}})"
}
},
"pageOps": {
"edit": "Edit",
"delete": "Delete",
"editorAria": "Page source (Markdown)",
"deletePrompt": "Delete {{title}}?",
"deleteImpact_one": "{{count}} page links here — its link will become plain text",
"deleteImpact_other": "{{count}} pages link here — their links will become plain text",
"deleteNoBacklinks": "No other pages link here",
"deleteConfirm": "Delete page",
"deleteSuccess": "Deleted {{title}}",
"deleteError": "Delete failed: {{error}}",
"save": "Save",
"saveSuccess": "Page saved",
"saveError": "Save failed: {{error}}",
"ghostsDemoted": "These links didn't resolve and became plain text: {{links}}",
"editRecompileNote": "Manual edits may be overwritten the next time this KB is recompiled.",
"links": {
"heading": "Page links",
"outlinks": "This page links to",
"backlinks": "Pages linking here",
"none": "None",
"error": "Failed to load links: {{error}}"
}
},
"remote": {
"heading": "Remote data sources",
"note": "Cloud connectors are in development and not yet available · want one? Click a card to vote on GitHub"
Expand Down
9 changes: 8 additions & 1 deletion frontend/src/locales/en/kbSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,12 @@
"lint": "Structure check",
"recompileHeading": "Recompile",
"recompileSummary": "· {{total}} total · {{recompiled}} compiled · {{skipped}} skipped",
"preparing": "Preparing…"
"preparing": "Preparing…",
"dangerHeading": "Danger zone",
"dangerDesc": "Permanently delete this knowledge base — its raw documents and compiled wiki. This cannot be undone.",
"dangerDeleteButton": "Delete this knowledge base",
"dangerConfirmPrompt": "Type the KB name ({{kb}}) to confirm",
"dangerConfirmDelete": "Delete permanently",
"dangerDeletedToast": "Deleted knowledge base “{{kb}}”",
"dangerDeleteError": "Delete failed: {{error}}"
}
3 changes: 2 additions & 1 deletion frontend/src/locales/zh/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"actions": {
"newKb": "新建知识库",
"close": "关闭",
"refresh": "刷新"
"refresh": "刷新",
"cancel": "取消"
},
"fields": {
"model": "模型",
Expand Down
24 changes: 24 additions & 0 deletions frontend/src/locales/zh/kb.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,30 @@
"multiple": "删除失败:{{message}}(候选:{{names}})"
}
},
"pageOps": {
"edit": "编辑",
"delete": "删除",
"editorAria": "页面源码(Markdown)",
"deletePrompt": "确认删除 {{title}}?",
"deleteImpact_one": "{{count}} 个页面链接到此页 — 删除后这些链接将变为纯文本",
"deleteImpact_other": "{{count}} 个页面链接到此页 — 删除后这些链接将变为纯文本",
"deleteNoBacklinks": "没有其他页面链接到此页",
"deleteConfirm": "删除页面",
"deleteSuccess": "已删除 {{title}}",
"deleteError": "删除失败:{{error}}",
"save": "保存",
"saveSuccess": "页面已保存",
"saveError": "保存失败:{{error}}",
"ghostsDemoted": "以下链接无法解析,已变为纯文本:{{links}}",
"editRecompileNote": "手动修改在下次重新编译该库时可能被覆盖。",
"links": {
"heading": "页面链接",
"outlinks": "此页链接到",
"backlinks": "链接到此页的页面",
"none": "无",
"error": "链接加载失败:{{error}}"
}
},
"remote": {
"heading": "远程数据源",
"note": "云端连接器开发中,尚不可用 · 想要它?点卡片去 GitHub 投票"
Expand Down
9 changes: 8 additions & 1 deletion frontend/src/locales/zh/kbSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,12 @@
"lint": "结构检查",
"recompileHeading": "重新编译",
"recompileSummary": "· 共 {{total}} · 编译 {{recompiled}} · 跳过 {{skipped}}",
"preparing": "正在准备…"
"preparing": "正在准备…",
"dangerHeading": "危险操作",
"dangerDesc": "永久删除这个知识库(原始文档 + 已编译的 wiki),不可撤销。",
"dangerDeleteButton": "删除此知识库",
"dangerConfirmPrompt": "输入知识库名称({{kb}})以确认",
"dangerConfirmDelete": "永久删除",
"dangerDeletedToast": "已删除知识库「{{kb}}」",
"dangerDeleteError": "删除失败:{{error}}"
}
Loading
Loading