feat: add knowledge management pages
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import { DesignSystemPreview } from '@/features/design-system/DesignSystemPreview'
|
||||
import { KnowledgeBaseListPage } from '@/features/knowledge/KnowledgeBaseListPage'
|
||||
import { KnowledgeItemEditorPage } from '@/features/knowledge/KnowledgeItemEditorPage'
|
||||
import { KnowledgeSearchPage } from '@/features/knowledge/KnowledgeSearchPage'
|
||||
import { SolutionCreatePage } from '@/features/solutions/SolutionCreatePage'
|
||||
import { SolutionListPage } from '@/features/solutions/SolutionListPage'
|
||||
import { AuditLogPage } from '@/features/system/AuditLogPage'
|
||||
@@ -54,6 +57,18 @@ export function Router() {
|
||||
return <WorkbenchPage />
|
||||
}
|
||||
|
||||
if (route === '/knowledge') {
|
||||
return <KnowledgeBaseListPage />
|
||||
}
|
||||
|
||||
if (route === '/knowledge/search') {
|
||||
return <KnowledgeSearchPage />
|
||||
}
|
||||
|
||||
if (route === '/knowledge/items/new') {
|
||||
return <KnowledgeItemEditorPage />
|
||||
}
|
||||
|
||||
if (route === '/solutions') {
|
||||
return <SolutionListPage />
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as React from 'react'
|
||||
import {
|
||||
Bot,
|
||||
BookOpen,
|
||||
BriefcaseBusiness,
|
||||
ClipboardList,
|
||||
Database,
|
||||
@@ -8,6 +9,7 @@ import {
|
||||
Library,
|
||||
Menu,
|
||||
ScrollText,
|
||||
Search,
|
||||
Settings,
|
||||
ShieldCheck,
|
||||
Ticket,
|
||||
@@ -23,6 +25,7 @@ import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sh
|
||||
|
||||
const iconMap = {
|
||||
audit: ScrollText,
|
||||
book_open: BookOpen,
|
||||
bot: Bot,
|
||||
briefcase: BriefcaseBusiness,
|
||||
database: Database,
|
||||
@@ -30,6 +33,7 @@ const iconMap = {
|
||||
library: Library,
|
||||
menu: Menu,
|
||||
roles: UserCog,
|
||||
search: Search,
|
||||
settings: Settings,
|
||||
shield: ShieldCheck,
|
||||
ticket: Ticket,
|
||||
@@ -57,6 +61,41 @@ function visibleChildren(menu: AuthMenu) {
|
||||
return menu.children.filter((item) => item.is_visible && item.is_enabled)
|
||||
}
|
||||
|
||||
const knowledgeNavGroup: AuthMenu = {
|
||||
id: -100,
|
||||
code: 'knowledge_builtin',
|
||||
title: '知识库',
|
||||
path: '#/knowledge',
|
||||
icon: 'book_open',
|
||||
permission_code: 'knowledge.view',
|
||||
is_visible: true,
|
||||
is_enabled: true,
|
||||
children: [
|
||||
{
|
||||
id: -101,
|
||||
code: 'knowledge_bases',
|
||||
title: '知识库',
|
||||
path: '#/knowledge',
|
||||
icon: 'book_open',
|
||||
permission_code: 'knowledge.view',
|
||||
is_visible: true,
|
||||
is_enabled: true,
|
||||
children: []
|
||||
},
|
||||
{
|
||||
id: -102,
|
||||
code: 'knowledge_search',
|
||||
title: '知识检索',
|
||||
path: '#/knowledge/search',
|
||||
icon: 'search',
|
||||
permission_code: 'knowledge.view',
|
||||
is_visible: true,
|
||||
is_enabled: true,
|
||||
children: []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
type SidebarNavContentProps = {
|
||||
groups: AuthMenu[]
|
||||
currentHash: string
|
||||
@@ -120,7 +159,11 @@ type AppSidebarProps = {
|
||||
export function AppSidebar({ mobileOpen = false, onMobileOpenChange }: AppSidebarProps) {
|
||||
const { user } = useAuth()
|
||||
const currentHash = useCurrentHash()
|
||||
const groups = (user?.menus ?? []).filter((menu) => menu.is_visible && menu.is_enabled)
|
||||
const menuGroups = (user?.menus ?? []).filter((menu) => menu.is_visible && menu.is_enabled)
|
||||
const hasKnowledgeMenu = menuGroups.some(
|
||||
(group) => group.code.startsWith('knowledge') || visibleChildren(group).some((item) => item.path.startsWith('#/knowledge'))
|
||||
)
|
||||
const groups = hasKnowledgeMenu ? menuGroups : [...menuGroups, knowledgeNavGroup]
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
151
frontend/src/features/knowledge/KnowledgeBaseListPage.tsx
Normal file
151
frontend/src/features/knowledge/KnowledgeBaseListPage.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
import * as React from 'react'
|
||||
import { Plus, Search } from 'lucide-react'
|
||||
|
||||
import { AppDataTable, type AppDataTableColumn } from '@/components/app/AppDataTable'
|
||||
import { AppEmptyState } from '@/components/app/AppEmptyState'
|
||||
import { AppField } from '@/components/app/AppField'
|
||||
import { AppPage } from '@/components/app/AppPage'
|
||||
import { AppPageHeader } from '@/components/app/AppPageHeader'
|
||||
import { AppSection } from '@/components/app/AppSection'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { formatDateTime } from '@/lib/format'
|
||||
|
||||
import { createKnowledgeBase, listKnowledgeBases } from './api'
|
||||
import type { KnowledgeBase } from './types'
|
||||
|
||||
type KnowledgeBaseForm = {
|
||||
name: string
|
||||
description: string
|
||||
scope: string
|
||||
}
|
||||
|
||||
const emptyForm: KnowledgeBaseForm = {
|
||||
name: '',
|
||||
description: '',
|
||||
scope: ''
|
||||
}
|
||||
|
||||
const columns: AppDataTableColumn<KnowledgeBase>[] = [
|
||||
{
|
||||
key: 'name',
|
||||
header: '名称',
|
||||
cell: (base) => (
|
||||
<div>
|
||||
<p className="font-medium text-foreground">{base.name}</p>
|
||||
<p className="mt-1 line-clamp-1 text-sm text-muted-foreground">
|
||||
{base.description || '暂无描述'}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
header: '范围',
|
||||
cell: (base) => <span className="text-muted-foreground">{base.scope || '-'}</span>
|
||||
},
|
||||
{
|
||||
key: 'enabled',
|
||||
header: '启用状态',
|
||||
cell: (base) => (
|
||||
<Badge variant={base.enabled ? 'success' : 'secondary'}>
|
||||
{base.enabled ? '已启用' : '已停用'}
|
||||
</Badge>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'updated_at',
|
||||
header: '更新时间',
|
||||
cell: (base) => <span className="text-muted-foreground">{formatDateTime(base.updated_at)}</span>
|
||||
}
|
||||
]
|
||||
|
||||
export function KnowledgeBaseListPage() {
|
||||
const [bases, setBases] = React.useState<KnowledgeBase[]>([])
|
||||
const [form, setForm] = React.useState<KnowledgeBaseForm>(emptyForm)
|
||||
const [error, setError] = React.useState('')
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
setBases(await listKnowledgeBases())
|
||||
setError('')
|
||||
} catch (reason) {
|
||||
setError(reason instanceof Error ? reason.message : '加载知识库失败')
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
void load()
|
||||
}, [])
|
||||
|
||||
async function handleCreate(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault()
|
||||
await createKnowledgeBase(form)
|
||||
setForm(emptyForm)
|
||||
await load()
|
||||
}
|
||||
|
||||
return (
|
||||
<AppPage contentClassName="gap-5">
|
||||
<AppPageHeader
|
||||
title="知识库"
|
||||
actions={
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={() => (window.location.hash = '/knowledge/search')}>
|
||||
<Search data-icon="inline-start" />
|
||||
知识检索
|
||||
</Button>
|
||||
<Button onClick={() => (window.location.hash = '/knowledge/items/new')}>
|
||||
<Plus data-icon="inline-start" />
|
||||
新建知识条目
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<AppSection title="新建知识库">
|
||||
<form className="grid gap-4 md:grid-cols-[1fr_1fr_1fr_auto] md:items-end" onSubmit={handleCreate}>
|
||||
<AppField label="名称" htmlFor="knowledge-base-name" required>
|
||||
<Input
|
||||
id="knowledge-base-name"
|
||||
required
|
||||
value={form.name}
|
||||
onChange={(event) => setForm((current) => ({ ...current, name: event.target.value }))}
|
||||
placeholder="例如:税务采集知识库"
|
||||
/>
|
||||
</AppField>
|
||||
<AppField label="范围" htmlFor="knowledge-base-scope">
|
||||
<Input
|
||||
id="knowledge-base-scope"
|
||||
value={form.scope}
|
||||
onChange={(event) => setForm((current) => ({ ...current, scope: event.target.value }))}
|
||||
placeholder="例如:数据采集"
|
||||
/>
|
||||
</AppField>
|
||||
<AppField label="描述" htmlFor="knowledge-base-description">
|
||||
<Input
|
||||
id="knowledge-base-description"
|
||||
value={form.description}
|
||||
onChange={(event) => setForm((current) => ({ ...current, description: event.target.value }))}
|
||||
placeholder="知识库用途说明"
|
||||
/>
|
||||
</AppField>
|
||||
<Button type="submit">创建知识库</Button>
|
||||
</form>
|
||||
</AppSection>
|
||||
|
||||
{error ? <AppEmptyState title="无法加载知识库" description={error} /> : null}
|
||||
{!error ? (
|
||||
<AppDataTable
|
||||
columns={columns}
|
||||
rows={bases}
|
||||
getRowKey={(base) => base.id}
|
||||
emptyTitle="暂无知识库"
|
||||
emptyDescription="创建知识库后,可继续维护文档、知识条目和检索内容。"
|
||||
footer={<p className="text-sm text-muted-foreground">共 {bases.length} 个知识库</p>}
|
||||
/>
|
||||
) : null}
|
||||
</AppPage>
|
||||
)
|
||||
}
|
||||
202
frontend/src/features/knowledge/KnowledgeItemEditorPage.tsx
Normal file
202
frontend/src/features/knowledge/KnowledgeItemEditorPage.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
import * as React from 'react'
|
||||
import { Save } from 'lucide-react'
|
||||
|
||||
import { AppEmptyState } from '@/components/app/AppEmptyState'
|
||||
import { AppField } from '@/components/app/AppField'
|
||||
import { AppPage } from '@/components/app/AppPage'
|
||||
import { AppPageHeader } from '@/components/app/AppPageHeader'
|
||||
import { AppSection } from '@/components/app/AppSection'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
|
||||
import { createKnowledgeItem, listKnowledgeBases } from './api'
|
||||
import type { KnowledgeBase, KnowledgeItemInput } from './types'
|
||||
|
||||
const initialForm: KnowledgeItemInput = {
|
||||
knowledge_base_id: 0,
|
||||
title: '',
|
||||
problem: '',
|
||||
applicable_scope: '',
|
||||
error_code: '',
|
||||
root_cause: '',
|
||||
troubleshooting_steps: '',
|
||||
solution: '',
|
||||
risk_note: '',
|
||||
status: 'draft'
|
||||
}
|
||||
|
||||
export function KnowledgeItemEditorPage() {
|
||||
const [bases, setBases] = React.useState<KnowledgeBase[]>([])
|
||||
const [form, setForm] = React.useState<KnowledgeItemInput>(initialForm)
|
||||
const [error, setError] = React.useState('')
|
||||
const [submitting, setSubmitting] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
listKnowledgeBases()
|
||||
.then((items) => {
|
||||
setBases(items)
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
knowledge_base_id: current.knowledge_base_id || items[0]?.id || 0
|
||||
}))
|
||||
})
|
||||
.catch((reason: unknown) => setError(reason instanceof Error ? reason.message : '加载知识库失败'))
|
||||
}, [])
|
||||
|
||||
function updateField<Key extends keyof KnowledgeItemInput>(field: Key, value: KnowledgeItemInput[Key]) {
|
||||
setForm((current) => ({ ...current, [field]: value }))
|
||||
}
|
||||
|
||||
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault()
|
||||
if (!form.knowledge_base_id) {
|
||||
setError('请先创建并选择知识库。')
|
||||
return
|
||||
}
|
||||
setSubmitting(true)
|
||||
try {
|
||||
await createKnowledgeItem(form)
|
||||
window.location.hash = '/knowledge/search'
|
||||
} catch (reason) {
|
||||
setError(reason instanceof Error ? reason.message : '保存知识条目失败')
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AppPage maxWidth="6xl" contentClassName="gap-5">
|
||||
<AppPageHeader title="新建知识条目" />
|
||||
{error ? <AppEmptyState title="知识条目操作失败" description={error} /> : null}
|
||||
|
||||
<form className="grid gap-4" onSubmit={handleSubmit}>
|
||||
<AppSection title="基础信息">
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<AppField label="所属知识库" required>
|
||||
<Select
|
||||
value={form.knowledge_base_id ? String(form.knowledge_base_id) : ''}
|
||||
onValueChange={(value) => updateField('knowledge_base_id', Number(value))}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="请选择知识库" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{bases.map((base) => (
|
||||
<SelectItem key={base.id} value={String(base.id)}>
|
||||
{base.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</AppField>
|
||||
<AppField label="标题" htmlFor="knowledge-item-title" required>
|
||||
<Input
|
||||
id="knowledge-item-title"
|
||||
required
|
||||
value={form.title}
|
||||
onChange={(event) => updateField('title', event.target.value)}
|
||||
placeholder="例如:税局采集进度查询"
|
||||
/>
|
||||
</AppField>
|
||||
<AppField label="发布状态" required>
|
||||
<Select
|
||||
value={form.status}
|
||||
onValueChange={(value) => updateField('status', value as KnowledgeItemInput['status'])}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="请选择状态" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="draft">草稿</SelectItem>
|
||||
<SelectItem value="published">已发布</SelectItem>
|
||||
<SelectItem value="disabled">已停用</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</AppField>
|
||||
</div>
|
||||
</AppSection>
|
||||
|
||||
<AppSection title="问题与范围">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<AppField label="问题描述" htmlFor="knowledge-item-problem" required>
|
||||
<Textarea
|
||||
id="knowledge-item-problem"
|
||||
required
|
||||
value={form.problem}
|
||||
onChange={(event) => updateField('problem', event.target.value)}
|
||||
placeholder="描述问题表现、触发条件或常见问法。"
|
||||
/>
|
||||
</AppField>
|
||||
<AppField label="适用范围" htmlFor="knowledge-item-scope">
|
||||
<Textarea
|
||||
id="knowledge-item-scope"
|
||||
value={form.applicable_scope}
|
||||
onChange={(event) => updateField('applicable_scope', event.target.value)}
|
||||
placeholder="例如:生产环境、税务采集、指定系统版本。"
|
||||
/>
|
||||
</AppField>
|
||||
</div>
|
||||
<AppField label="错误码" htmlFor="knowledge-item-error-code">
|
||||
<Input
|
||||
id="knowledge-item-error-code"
|
||||
value={form.error_code}
|
||||
onChange={(event) => updateField('error_code', event.target.value)}
|
||||
placeholder="可选,例如:HTTP 500、TASK_TIMEOUT"
|
||||
/>
|
||||
</AppField>
|
||||
</AppSection>
|
||||
|
||||
<AppSection title="原因与处理">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<AppField label="可能原因" htmlFor="knowledge-item-root-cause">
|
||||
<Textarea
|
||||
id="knowledge-item-root-cause"
|
||||
value={form.root_cause}
|
||||
onChange={(event) => updateField('root_cause', event.target.value)}
|
||||
placeholder="说明常见原因或定位依据。"
|
||||
/>
|
||||
</AppField>
|
||||
<AppField label="排查步骤" htmlFor="knowledge-item-steps">
|
||||
<Textarea
|
||||
id="knowledge-item-steps"
|
||||
value={form.troubleshooting_steps}
|
||||
onChange={(event) => updateField('troubleshooting_steps', event.target.value)}
|
||||
placeholder="列出处理人员应检查的日志、配置或状态。"
|
||||
/>
|
||||
</AppField>
|
||||
</div>
|
||||
<AppField label="解决方案" htmlFor="knowledge-item-solution" required>
|
||||
<Textarea
|
||||
id="knowledge-item-solution"
|
||||
required
|
||||
value={form.solution}
|
||||
onChange={(event) => updateField('solution', event.target.value)}
|
||||
placeholder="写入可复用的标准处理方式或客户回复。"
|
||||
/>
|
||||
</AppField>
|
||||
<AppField label="风险提示" htmlFor="knowledge-item-risk-note">
|
||||
<Textarea
|
||||
id="knowledge-item-risk-note"
|
||||
value={form.risk_note}
|
||||
onChange={(event) => updateField('risk_note', event.target.value)}
|
||||
placeholder="说明不建议执行的动作、需要人工确认的风险。"
|
||||
/>
|
||||
</AppField>
|
||||
</AppSection>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit" disabled={submitting}>
|
||||
<Save data-icon="inline-start" />
|
||||
{submitting ? '保存中...' : '保存知识条目'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</AppPage>
|
||||
)
|
||||
}
|
||||
127
frontend/src/features/knowledge/KnowledgeSearchPage.tsx
Normal file
127
frontend/src/features/knowledge/KnowledgeSearchPage.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import * as React from 'react'
|
||||
import { Search } from 'lucide-react'
|
||||
|
||||
import { AppEmptyState } from '@/components/app/AppEmptyState'
|
||||
import { AppField } from '@/components/app/AppField'
|
||||
import { AppPage } from '@/components/app/AppPage'
|
||||
import { AppPageHeader } from '@/components/app/AppPageHeader'
|
||||
import { AppSection } from '@/components/app/AppSection'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
|
||||
import { listKnowledgeBases, searchKnowledge } from './api'
|
||||
import type { KnowledgeBase, KnowledgeSearchResult } from './types'
|
||||
|
||||
const sourceLabels: Record<string, string> = {
|
||||
document: '文档',
|
||||
knowledge_item: '知识条目',
|
||||
solution: '解决方案'
|
||||
}
|
||||
|
||||
export function KnowledgeSearchPage() {
|
||||
const [bases, setBases] = React.useState<KnowledgeBase[]>([])
|
||||
const [knowledgeBaseId, setKnowledgeBaseId] = React.useState('all')
|
||||
const [query, setQuery] = React.useState('')
|
||||
const [results, setResults] = React.useState<KnowledgeSearchResult[]>([])
|
||||
const [searched, setSearched] = React.useState(false)
|
||||
const [loading, setLoading] = React.useState(false)
|
||||
const [error, setError] = React.useState('')
|
||||
|
||||
React.useEffect(() => {
|
||||
listKnowledgeBases()
|
||||
.then(setBases)
|
||||
.catch((reason: unknown) => setError(reason instanceof Error ? reason.message : '加载知识库失败'))
|
||||
}, [])
|
||||
|
||||
async function handleSearch(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault()
|
||||
if (!query.trim()) {
|
||||
setError('请输入需要检索的问题、错误码或处理场景。')
|
||||
return
|
||||
}
|
||||
setError('')
|
||||
setLoading(true)
|
||||
try {
|
||||
const items = await searchKnowledge(query.trim(), knowledgeBaseId === 'all' ? undefined : Number(knowledgeBaseId))
|
||||
setResults(items)
|
||||
setSearched(true)
|
||||
} catch (reason) {
|
||||
setError(reason instanceof Error ? reason.message : '知识检索失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AppPage maxWidth="6xl" contentClassName="gap-5">
|
||||
<AppPageHeader
|
||||
title="知识检索"
|
||||
actions={
|
||||
<Button variant="outline" onClick={() => (window.location.hash = '/knowledge/items/new')}>
|
||||
新建知识条目
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<AppSection title="检索条件">
|
||||
<form className="grid gap-4 md:grid-cols-[220px_1fr_auto]" onSubmit={handleSearch}>
|
||||
<AppField label="知识库">
|
||||
<Select value={knowledgeBaseId} onValueChange={setKnowledgeBaseId}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="选择知识库" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="all">全部知识库</SelectItem>
|
||||
{bases.map((base) => (
|
||||
<SelectItem key={base.id} value={String(base.id)}>
|
||||
{base.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</AppField>
|
||||
<AppField label="检索内容" htmlFor="knowledge-query">
|
||||
<Input
|
||||
id="knowledge-query"
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder="输入问题、错误码、客户问法或处理场景"
|
||||
/>
|
||||
</AppField>
|
||||
<div className="flex items-end">
|
||||
<Button className="w-full md:w-auto" disabled={loading} type="submit">
|
||||
<Search data-icon="inline-start" />
|
||||
{loading ? '检索中...' : '检索知识'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</AppSection>
|
||||
|
||||
{error ? <AppEmptyState title="知识检索暂不可用" description={error} /> : null}
|
||||
|
||||
<div className="grid gap-3">
|
||||
{results.map((item) => (
|
||||
<Card key={item.id} className="shadow-sm">
|
||||
<CardContent className="grid gap-3 p-4">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h2 className="text-base font-semibold text-foreground">{item.title}</h2>
|
||||
<Badge variant="secondary">{sourceLabels[item.source_type] ?? item.source_type}</Badge>
|
||||
<Badge variant="outline">匹配度 {item.score.toFixed(2)}</Badge>
|
||||
</div>
|
||||
<p className="line-clamp-4 whitespace-pre-line text-sm leading-6 text-muted-foreground">{item.content}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{searched && results.length === 0 && !error ? (
|
||||
<AppEmptyState title="未找到匹配知识" description="可以调整关键词,或沉淀新的知识条目后再次检索。" />
|
||||
) : null}
|
||||
</AppPage>
|
||||
)
|
||||
}
|
||||
37
frontend/src/features/knowledge/api.ts
Normal file
37
frontend/src/features/knowledge/api.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { apiRequest } from '@/lib/api'
|
||||
|
||||
import type { KnowledgeBase, KnowledgeItemInput, KnowledgeSearchResult } from './types'
|
||||
|
||||
export function listKnowledgeBases() {
|
||||
return apiRequest<KnowledgeBase[]>('/knowledge/bases')
|
||||
}
|
||||
|
||||
export function createKnowledgeBase(payload: {
|
||||
name: string
|
||||
description: string
|
||||
scope: string
|
||||
enabled?: boolean
|
||||
}) {
|
||||
return apiRequest<KnowledgeBase>('/knowledge/bases', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ ...payload, enabled: payload.enabled ?? true })
|
||||
})
|
||||
}
|
||||
|
||||
export function createKnowledgeItem(payload: KnowledgeItemInput) {
|
||||
return apiRequest('/knowledge/items', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
}
|
||||
|
||||
export function searchKnowledge(query: string, knowledgeBaseId?: number) {
|
||||
return apiRequest<KnowledgeSearchResult[]>('/knowledge/search', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
query,
|
||||
knowledge_base_id: knowledgeBaseId,
|
||||
limit: 8
|
||||
})
|
||||
})
|
||||
}
|
||||
32
frontend/src/features/knowledge/types.ts
Normal file
32
frontend/src/features/knowledge/types.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
export type KnowledgeBase = {
|
||||
id: number
|
||||
name: string
|
||||
description: string
|
||||
scope: string
|
||||
enabled: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export type KnowledgeItemInput = {
|
||||
knowledge_base_id: number
|
||||
title: string
|
||||
problem: string
|
||||
applicable_scope: string
|
||||
error_code: string
|
||||
root_cause: string
|
||||
troubleshooting_steps: string
|
||||
solution: string
|
||||
risk_note: string
|
||||
status: 'draft' | 'published' | 'disabled'
|
||||
}
|
||||
|
||||
export type KnowledgeSearchResult = {
|
||||
id: number
|
||||
knowledge_base_id: number
|
||||
title: string
|
||||
source_type: string
|
||||
content: string
|
||||
score: number
|
||||
metadata: Record<string, unknown>
|
||||
}
|
||||
Reference in New Issue
Block a user