feat: release contextual omnichannel customer platform
Add Telegram, WhatsApp and email account management, realtime messaging, contextual multilingual AI, customer intelligence, knowledge workflows, enterprise controls, a unified design system and complete repository documentation.
This commit is contained in:
@@ -29,20 +29,28 @@ export function KnowledgePage() {
|
||||
const [createOpen, setCreateOpen] = useState(false)
|
||||
const [editItem, setEditItem] = useState<KnowledgeItem | null>(null)
|
||||
const [searchQ, setSearchQ] = useState('')
|
||||
const [createDraft, setCreateDraft] = useState({ title: '', content: '' })
|
||||
|
||||
const { data, isLoading, isError, refetch } = useQuery({
|
||||
queryKey: ['knowledge', category, keyword],
|
||||
queryFn: () => getKnowledge({ category, keyword }),
|
||||
})
|
||||
const { data: stats } = useQuery({ queryKey: ['knowledge-stats'], queryFn: getKnowledgeStats, enabled: tab === 'stats' })
|
||||
const { data: stats } = useQuery({ queryKey: ['knowledge-stats'], queryFn: getKnowledgeStats })
|
||||
const [searchResult, setSearchResult] = useState<any>(null)
|
||||
const [searchLoading, setSearchLoading] = useState(false)
|
||||
const [uploadFiles, setUploadFiles] = useState<File[]>([])
|
||||
const [uploadDialog, setUploadDialog] = useState(false)
|
||||
const [uploadLoading, setUploadLoading] = useState(false)
|
||||
|
||||
const createMut = useMutation({
|
||||
mutationFn: (p: any) => createKnowledge(p),
|
||||
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['knowledge'] }); toast.success('知识已创建'); setCreateOpen(false) },
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['knowledge'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['knowledge-stats'] })
|
||||
toast.success('知识已创建并完成索引')
|
||||
setCreateDraft({ title: '', content: '' })
|
||||
setCreateOpen(false)
|
||||
},
|
||||
onError: (e) => toast.error(errorMessage(e)),
|
||||
})
|
||||
|
||||
@@ -65,7 +73,7 @@ export function KnowledgePage() {
|
||||
<div className="p-4 md:p-6">
|
||||
<PageHeader
|
||||
title="企业知识库"
|
||||
description={`${list.length} 条知识 · 用于回复建议与翻译参考`}
|
||||
description="把产品、价格、物流和售后资料变成可检索、可引用的企业答案"
|
||||
actions={
|
||||
<>
|
||||
<Button size="sm" variant="outline" onClick={() => setUploadDialog(true)}><Upload className="h-4 w-4" />上传文件</Button>
|
||||
@@ -74,6 +82,20 @@ export function KnowledgePage() {
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="mb-4 grid grid-cols-2 gap-3 lg:grid-cols-4">
|
||||
{[
|
||||
['当前条目', data?.total || 0],
|
||||
['已分段索引', list.reduce((sum, item) => sum + Number(item.chunkCount || 0), 0)],
|
||||
['真实引用', stats?.totalCitations || 0],
|
||||
['待优化内容', (stats?.lowQuality?.length || 0) + (stats?.outdated?.length || 0)],
|
||||
].map(([label, value]) => (
|
||||
<Card key={String(label)} className="px-4 py-3">
|
||||
<p className="text-[11px] font-medium text-slate-400">{label}</p>
|
||||
<p className="mt-1 text-xl font-semibold tracking-tight text-slate-900">{value}</p>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Tabs value={tab} onChange={setTab} className="mb-4" items={[
|
||||
{ value: 'list', label: '知识列表' },
|
||||
{ value: 'search', label: '检索测试' },
|
||||
@@ -109,10 +131,14 @@ export function KnowledgePage() {
|
||||
<div className="mt-2 flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<span>引用 {item.citedCount} 次</span>
|
||||
<span>·</span>
|
||||
<span>{formatRelative(item.updatedAt)}</span>
|
||||
<span>{item.chunkCount || 0} 个检索片段</span>
|
||||
<span>·</span>
|
||||
<span>{item.status === 'parsing' ? '解析中…' : item.status === 'failed' ? '解析失败' : ''}</span>
|
||||
<span>{Number(item.characterCount || item.content?.length || 0).toLocaleString('zh-CN')} 字符</span>
|
||||
<span>·</span>
|
||||
<span>{formatRelative(item.updatedAt)}</span>
|
||||
{item.status !== 'ready' && <><span>·</span><span>{item.status === 'parsing' ? '解析中…' : '解析失败'}</span></>}
|
||||
</div>
|
||||
{item.parseMessage && <p className="mt-1 text-[11px] text-emerald-600">{item.parseMessage}</p>}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<Switch checked={item.enabled} onCheckedChange={(v) => toggleMut.mutate({ id: item.id, enabled: v })} />
|
||||
@@ -148,8 +174,9 @@ export function KnowledgePage() {
|
||||
<p className="text-xs font-semibold text-muted-foreground">命中知识 ({searchResult.hits.length})</p>
|
||||
{searchResult.hits.map((h: any) => (
|
||||
<div key={h.item.id} className="rounded-md border border-border p-3">
|
||||
<div className="flex items-center justify-between"><span className="text-sm font-medium">{h.item.title}</span><Badge variant="primary">{(h.score * 100).toFixed(0)}%</Badge></div>
|
||||
<div className="flex items-center justify-between"><span className="text-sm font-medium">{h.item.title}</span><Badge variant="primary">相关度 {(h.score * 100).toFixed(0)}%</Badge></div>
|
||||
<p className="mt-1 text-xs text-muted-foreground">{h.snippet}</p>
|
||||
<p className="mt-1 text-[10px] text-slate-400">命中片段 #{Number(h.chunkIndex || 0) + 1}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -175,34 +202,41 @@ export function KnowledgePage() {
|
||||
|
||||
<Dialog open={createOpen} onOpenChange={setCreateOpen} title="新建知识条目" footer={
|
||||
<Button onClick={() => {
|
||||
const title = (document.querySelector('#k-title') as HTMLInputElement)?.value
|
||||
const content = (document.querySelector('#k-content') as HTMLTextAreaElement)?.value
|
||||
if (!title) { toast.warning('请输入标题'); return }
|
||||
createMut.mutate({ title, content, category: category || 'faq' })
|
||||
}}>创建</Button>
|
||||
if (!createDraft.title.trim()) { toast.warning('请输入标题'); return }
|
||||
if (createDraft.content.trim().length < 20) { toast.warning('知识内容至少需要 20 个字符'); return }
|
||||
createMut.mutate({ ...createDraft, category: category || 'faq' })
|
||||
}} loading={createMut.isPending}>创建并索引</Button>
|
||||
}>
|
||||
<Field label="标题" required><Input id="k-title" placeholder="知识条目标题" /></Field>
|
||||
<Field label="内容" className="mt-3"><Textarea id="k-content" rows={5} placeholder="知识内容…" /></Field>
|
||||
<Field label="标题" required><Input value={createDraft.title} onChange={(event) => setCreateDraft((value) => ({ ...value, title: event.target.value }))} placeholder="例如:欧洲地区退换货政策" /></Field>
|
||||
<Field label="内容" className="mt-3"><Textarea value={createDraft.content} onChange={(event) => setCreateDraft((value) => ({ ...value, content: event.target.value }))} rows={8} placeholder="写明适用范围、条件、时效、例外和处理方式。系统会自动分段并用于回复建议。" /></Field>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={uploadDialog} onOpenChange={setUploadDialog} title="上传文件" footer={
|
||||
<Button disabled={!uploadFiles.length} onClick={async () => {
|
||||
const payload = await Promise.all(uploadFiles.map(async (file) => ({
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
base64: await new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onerror = () => reject(reader.error)
|
||||
reader.onload = () => resolve(String(reader.result).split(',')[1] || '')
|
||||
reader.readAsDataURL(file)
|
||||
}),
|
||||
})))
|
||||
await uploadKnowledgeFiles(payload)
|
||||
toast.success(`${payload.length} 个知识文件解析完成`)
|
||||
queryClient.invalidateQueries({ queryKey: ['knowledge'] })
|
||||
setUploadFiles([])
|
||||
setUploadDialog(false)
|
||||
}}>上传并解析</Button>
|
||||
<Button disabled={!uploadFiles.length} loading={uploadLoading} onClick={async () => {
|
||||
setUploadLoading(true)
|
||||
try {
|
||||
const payload = await Promise.all(uploadFiles.map(async (file) => ({
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
base64: await new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onerror = () => reject(reader.error)
|
||||
reader.onload = () => resolve(String(reader.result).split(',')[1] || '')
|
||||
reader.readAsDataURL(file)
|
||||
}),
|
||||
})))
|
||||
const result = await uploadKnowledgeFiles(payload)
|
||||
toast.success(`${result.items.length} 个文件解析并建立检索索引`)
|
||||
queryClient.invalidateQueries({ queryKey: ['knowledge'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['knowledge-stats'] })
|
||||
setUploadFiles([])
|
||||
setUploadDialog(false)
|
||||
} catch (error) {
|
||||
toast.error(errorMessage(error))
|
||||
} finally {
|
||||
setUploadLoading(false)
|
||||
}
|
||||
}}>上传、解析并索引</Button>
|
||||
}>
|
||||
<input
|
||||
type="file"
|
||||
|
||||
Reference in New Issue
Block a user