《60天AI学习计划启动 | Day 50: 知识中心页面(搜索 + 文档 + QA 一体化)》

Day 50:知识中心页面(搜索 + 文档 + QA 一体化)

学习目标

  • 设计 一个“搜索 + 文档列表 + 文档详情 + QA 区”的整体布局
  • 抽象 文档与搜索结果的数据结构
  • 实现 一个最小可用的知识中心页面骨架(前端视角)

核心知识点

  • 页面布局(典型三栏):

    • 左:搜索框 + 过滤条件 + 搜索结果列表
    • 中:选中文档详情(标题 + 内容预览 + 版本信息)
    • 右/下:基于当前文档的 RAG 问答区
  • 数据模型示例:

    export interface KnowledgeDoc {
      id: string
      title: string
      summary: string
      content?: string
      tags?: string[]
      updatedAt: number
    }
    
    export interface SearchResult {
      docId: string
      highlight: string
      score: number
    }
    

实战代码示例(页面骨架)

import React, { useState } from 'react'
import type { KnowledgeDoc, SearchResult } from './types'

interface Props {
  docs: KnowledgeDoc[]
}

export const KnowledgeCenter: React.FC<Props> = ({ docs }) => {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState<SearchResult[]>([])
  const [activeDoc, setActiveDoc] = useState<KnowledgeDoc | null>(null)
  const [question, setQuestion] = useState('')
  const [answer, setAnswer] = useState('')

  const runSearch = () => {
    // demo:前端简单过滤,本质应调后端搜索
    const list = docs
      .filter(d => d.title.includes(query) || d.summary.includes(query))
      .map(d => ({
        docId: d.id,
        highlight: d.summary.slice(0, 60),
        score: 1
      }))
    setResults(list)
    if (list[0]) {
      const doc = docs.find(d => d.id === list[0].docId) || null
      setActiveDoc(doc)
    }
  }

  const ask = async () => {
    if (!activeDoc || !question.trim()) return
    const res = await fetch('/api/knowledge-qa', {
      method: 'POST',
      headers: { 'Content-Type':'application/json' },
      body: JSON.stringify({ docId: activeDoc.id, question: question.trim() })
    })
    const data = await res.json()
    setAnswer(data.answer ?? '')
  }

  return (
    <div style={{ display:'grid', gridTemplateColumns:'260px 1fr 320px', height:'100vh' }}>
      {/* 左:搜索 + 结果 */}
      <div style={{ borderRight:'1px solid #eee', padding:8 }}>
        <h3>知识搜索</h3>
        <input
          value={query}
          onChange={e => setQuery(e.target.value)}
          placeholder="输入关键词"
          style={{ width:'100%' }}
        />
        <button onClick={runSearch}>搜索</button>
        <ul>
          {results.map(r => {
            const doc = docs.find(d => d.id === r.docId)!
            return (
              <li key={r.docId} onClick={() => setActiveDoc(doc)} style={{ cursor:'pointer', marginTop:4 }}>
                <div>{doc.title}</div>
                <div style={{ fontSize:12, color:'#666' }}>{r.highlight}...</div>
              </li>
            )
          })}
        </ul>
      </div>

      {/* 中:文档详情 */}
      <div style={{ borderRight:'1px solid #eee', padding:8, overflowY:'auto' }}>
        {activeDoc ? (
          <>
            <h2>{activeDoc.title}</h2>
            <p style={{ color:'#666' }}>{activeDoc.summary}</p>
            <pre style={{ whiteSpace:'pre-wrap' }}>{activeDoc.content}</pre>
          </>
        ) : (
          <div>请选择左侧文档</div>
        )}
      </div>

      {/* 右:QA 区 */}
      <div style={{ padding:8, display:'flex', flexDirection:'column' }}>
        <h3>基于当前文档的问答</h3>
        <textarea
          rows={4}
          value={question}
          onChange={e => setQuestion(e.target.value)}
          placeholder="针对右侧文档提问"
        />
        <button onClick={ask}>提问</button>
        <div style={{ marginTop:8, flex:1, overflowY:'auto' }}>
          <pre style={{ whiteSpace:'pre-wrap' }}>{answer}</pre>
        </div>
      </div>
    </div>
  )
}

明日学习计划预告(Day 51)

  • 主题:第二个综合实战项目选型与需求拆解
  • 方向
    • 选一个「前端 + AI」具体场景(代码助手 / 评审助手 / 设计检查等)
    • 写出用户故事、主要功能列表、数据流草图,为 Day 52–55 实战做准备
posted @ 2025-12-17 11:46  XiaoZhengTou  阅读(2)  评论(0)    收藏  举报