| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208 |
- import { useEffect, useMemo, useRef, useState } from 'react'
- import ErrorState from '../../components/ErrorState.jsx'
- import LoadingState from '../../components/LoadingState.jsx'
- import DecodeKnowledgeModal from '../decode/DecodeKnowledgeModal.jsx'
- import AxisColumn from './AxisColumn.jsx'
- import ManualQueryModal from './ManualQueryModal.jsx'
- import QueryColumn from './QueryColumn.jsx'
- import SearchResultColumn from './SearchResultColumn.jsx'
- import TopicTableQueryPanel from './TopicTableQueryPanel.jsx'
- import { useLatestQueryDetail, useQueryBoard } from './useQueryBoard.js'
- import { isQuerySearched } from './model.js'
- function selectedQueryPayload(queryText, latest) {
- if (!latest?.query_id) return null
- return {
- query: queryText,
- query_id: latest.query_id,
- detail_version: [
- latest.candidate_count || 0,
- latest.creation_hit_count || 0,
- latest.decoded_count || 0,
- latest.payload_count || 0,
- ].join(':'),
- }
- }
- function manualFamilyFromBoard(board) {
- const rows = (board?.queries || []).filter((row) => row.family_key === 'manual')
- return {
- key: 'manual',
- label: '手动 Query',
- axes: [],
- items: rows.map((row, index) => ({
- query: row.query_text,
- parts: {},
- keep: true,
- reason: row.filter_reason || '',
- sort_order: row.sort_order ?? index,
- })),
- }
- }
- function topicTableFamily() {
- return {
- key: 'topic_table',
- label: '选题表生成',
- axes: [],
- items: [],
- }
- }
- export default function QueryBoardPage() {
- const {
- preview,
- board,
- latestByQuery,
- loading,
- error,
- lastUpdatedAt,
- refreshBoard,
- } = useQueryBoard()
- const [activeKey, setActiveKey] = useState('f1')
- const [selectedQuery, setSelectedQuery] = useState(null)
- const [decodeItemId, setDecodeItemId] = useState('')
- const [manualMode, setManualMode] = useState('')
- const didSetDefaultTab = useRef(false)
- const families = useMemo(() => {
- const base = preview?.families || []
- const manual = manualFamilyFromBoard(board)
- return [...base, manual, topicTableFamily()]
- }, [board, preview])
- const family = families.find((row) => row.key === activeKey) || families[0] || { axes: [], items: [] }
- const familyItems = family.items || []
- const familyRunCount = familyItems.filter((item) => isQuerySearched(latestByQuery.get(item.query))).length
- const totalRunCount = families.reduce((count, row) => {
- return count + (row.items || []).filter((item) => isQuerySearched(latestByQuery.get(item.query))).length
- }, 0)
- const totalItemCount = families.reduce((sum, row) => sum + (row.items || []).length, 0)
- const detailState = useLatestQueryDetail(selectedQuery)
- const lastUpdatedText = useMemo(() => {
- return lastUpdatedAt ? lastUpdatedAt.toLocaleTimeString('zh-CN', { hour12: false }) : ''
- }, [lastUpdatedAt])
- useEffect(() => {
- if (!didSetDefaultTab.current && (window.location.hash || '#/') === '#/' && families.some((row) => row.key === 'f1')) {
- didSetDefaultTab.current = true
- setActiveKey('f1')
- }
- }, [families])
- useEffect(() => {
- const firstKey = families[0]?.key
- if (firstKey && !families.some((row) => row.key === activeKey) && preview) {
- setActiveKey(firstKey)
- }
- }, [families, activeKey, preview])
- useEffect(() => {
- if (!familyItems.length) {
- if (selectedQuery) setSelectedQuery(null)
- return
- }
- if (selectedQuery && familyItems.some((item) => item.query === selectedQuery.query)) {
- const latest = latestByQuery.get(selectedQuery.query)
- const next = selectedQueryPayload(selectedQuery.query, latest)
- if (next && (next.query_id !== selectedQuery.query_id || next.detail_version !== selectedQuery.detail_version)) {
- setSelectedQuery(next)
- }
- return
- }
- const first = familyItems.find((item) => isQuerySearched(latestByQuery.get(item.query)))
- if (first) {
- const latest = latestByQuery.get(first.query)
- setSelectedQuery(selectedQueryPayload(first.query, latest))
- } else {
- setSelectedQuery(null)
- }
- }, [activeKey, board, familyItems, latestByQuery, selectedQuery])
- if (error) return <ErrorState>{error}</ErrorState>
- if (loading || !preview) return <LoadingState />
- return (
- <div className="dashboard-page">
- <div className="dashboard-toolbar">
- <div className="family-switch">
- {families.map((row) => (
- <button
- className={`fbtn ${row.key === family.key ? 'on' : ''}`}
- key={row.key}
- type="button"
- onClick={() => setActiveKey(row.key)}
- >
- <span className="fbtn-pre">{row.label || row.axes?.[0] || row.key}</span>
- <span className="fbtn-suf">{(row.axes || []).slice(1).join(' × ')}</span>
- </button>
- ))}
- </div>
- {family.key === 'topic_table' ? (
- <span className="refresh-state">选题字段 + LLM · 只生成 Query</span>
- ) : (
- <>
- <span className="refresh-state">
- {lastUpdatedText ? `上次刷新 ${lastUpdatedText}` : '等待后台写入数据'}
- </span>
- <div className="manual-entry-actions">
- <button type="button" onClick={() => setManualMode('text')}>手动输入</button>
- <button type="button" onClick={() => setManualMode('json')}>上传 JSON</button>
- </div>
- </>
- )}
- </div>
- {family.key === 'topic_table' ? <TopicTableQueryPanel /> : <div className={`cdemo ${family.key === 'manual' ? 'manual-cdemo' : ''}`}>
- {(family.axes || []).map((axis) => (
- <AxisColumn
- key={axis}
- axis={axis}
- data={preview}
- family={family}
- />
- ))}
- <QueryColumn
- familyItems={familyItems}
- latestByQuery={latestByQuery}
- selectedQuery={selectedQuery}
- onSelect={(queryItem, queryLatest) => {
- setSelectedQuery(selectedQueryPayload(queryItem.query, queryLatest))
- }}
- familyRunCount={familyRunCount}
- totalRunCount={totalRunCount}
- totalItemCount={totalItemCount}
- emptyMessage={
- family.key === 'manual'
- ? '暂无手动 Query,点击右上角手动输入或上传 JSON 创建。'
- : undefined
- }
- />
- <SearchResultColumn
- selected={selectedQuery}
- detail={detailState.detail}
- loading={detailState.loading}
- error={detailState.error}
- onKnowledgeClick={setDecodeItemId}
- />
- </div>}
- {decodeItemId && (
- <div className="decode-modal-mask" onClick={() => setDecodeItemId('')}>
- <div className="decode-modal-panel" onClick={(event) => event.stopPropagation()}>
- <DecodeKnowledgeModal itemId={decodeItemId} embedded onClose={() => setDecodeItemId('')} />
- </div>
- </div>
- )}
- {manualMode && (
- <ManualQueryModal
- mode={manualMode}
- onClose={() => setManualMode('')}
- onSubmitted={async () => {
- setManualMode('')
- await refreshBoard()
- setActiveKey('manual')
- }}
- />
- )}
- </div>
- )
- }
|