QueryBoardPage.jsx 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. import { useEffect, useMemo, useRef, useState } from 'react'
  2. import ErrorState from '../../components/ErrorState.jsx'
  3. import LoadingState from '../../components/LoadingState.jsx'
  4. import DecodeKnowledgeModal from '../decode/DecodeKnowledgeModal.jsx'
  5. import AxisColumn from './AxisColumn.jsx'
  6. import ManualQueryModal from './ManualQueryModal.jsx'
  7. import QueryColumn from './QueryColumn.jsx'
  8. import SearchResultColumn from './SearchResultColumn.jsx'
  9. import TopicTableQueryPanel from './TopicTableQueryPanel.jsx'
  10. import { useLatestQueryDetail, useQueryBoard } from './useQueryBoard.js'
  11. import { isQuerySearched } from './model.js'
  12. function selectedQueryPayload(queryText, latest) {
  13. if (!latest?.query_id) return null
  14. return {
  15. query: queryText,
  16. query_id: latest.query_id,
  17. detail_version: [
  18. latest.candidate_count || 0,
  19. latest.creation_hit_count || 0,
  20. latest.decoded_count || 0,
  21. latest.payload_count || 0,
  22. ].join(':'),
  23. }
  24. }
  25. function manualFamilyFromBoard(board) {
  26. const rows = (board?.queries || []).filter((row) => row.family_key === 'manual')
  27. return {
  28. key: 'manual',
  29. label: '手动 Query',
  30. axes: [],
  31. items: rows.map((row, index) => ({
  32. query: row.query_text,
  33. parts: {},
  34. keep: true,
  35. reason: row.filter_reason || '',
  36. sort_order: row.sort_order ?? index,
  37. })),
  38. }
  39. }
  40. function topicTableFamily() {
  41. return {
  42. key: 'topic_table',
  43. label: '选题表生成',
  44. axes: [],
  45. items: [],
  46. }
  47. }
  48. export default function QueryBoardPage() {
  49. const {
  50. preview,
  51. board,
  52. latestByQuery,
  53. loading,
  54. error,
  55. lastUpdatedAt,
  56. refreshBoard,
  57. } = useQueryBoard()
  58. const [activeKey, setActiveKey] = useState('f1')
  59. const [selectedQuery, setSelectedQuery] = useState(null)
  60. const [decodeItemId, setDecodeItemId] = useState('')
  61. const [manualMode, setManualMode] = useState('')
  62. const didSetDefaultTab = useRef(false)
  63. const families = useMemo(() => {
  64. const base = preview?.families || []
  65. const manual = manualFamilyFromBoard(board)
  66. return [...base, manual, topicTableFamily()]
  67. }, [board, preview])
  68. const family = families.find((row) => row.key === activeKey) || families[0] || { axes: [], items: [] }
  69. const familyItems = family.items || []
  70. const familyRunCount = familyItems.filter((item) => isQuerySearched(latestByQuery.get(item.query))).length
  71. const totalRunCount = families.reduce((count, row) => {
  72. return count + (row.items || []).filter((item) => isQuerySearched(latestByQuery.get(item.query))).length
  73. }, 0)
  74. const totalItemCount = families.reduce((sum, row) => sum + (row.items || []).length, 0)
  75. const detailState = useLatestQueryDetail(selectedQuery)
  76. const lastUpdatedText = useMemo(() => {
  77. return lastUpdatedAt ? lastUpdatedAt.toLocaleTimeString('zh-CN', { hour12: false }) : ''
  78. }, [lastUpdatedAt])
  79. useEffect(() => {
  80. if (!didSetDefaultTab.current && (window.location.hash || '#/') === '#/' && families.some((row) => row.key === 'f1')) {
  81. didSetDefaultTab.current = true
  82. setActiveKey('f1')
  83. }
  84. }, [families])
  85. useEffect(() => {
  86. const firstKey = families[0]?.key
  87. if (firstKey && !families.some((row) => row.key === activeKey) && preview) {
  88. setActiveKey(firstKey)
  89. }
  90. }, [families, activeKey, preview])
  91. useEffect(() => {
  92. if (!familyItems.length) {
  93. if (selectedQuery) setSelectedQuery(null)
  94. return
  95. }
  96. if (selectedQuery && familyItems.some((item) => item.query === selectedQuery.query)) {
  97. const latest = latestByQuery.get(selectedQuery.query)
  98. const next = selectedQueryPayload(selectedQuery.query, latest)
  99. if (next && (next.query_id !== selectedQuery.query_id || next.detail_version !== selectedQuery.detail_version)) {
  100. setSelectedQuery(next)
  101. }
  102. return
  103. }
  104. const first = familyItems.find((item) => isQuerySearched(latestByQuery.get(item.query)))
  105. if (first) {
  106. const latest = latestByQuery.get(first.query)
  107. setSelectedQuery(selectedQueryPayload(first.query, latest))
  108. } else {
  109. setSelectedQuery(null)
  110. }
  111. }, [activeKey, board, familyItems, latestByQuery, selectedQuery])
  112. if (error) return <ErrorState>{error}</ErrorState>
  113. if (loading || !preview) return <LoadingState />
  114. return (
  115. <div className="dashboard-page">
  116. <div className="dashboard-toolbar">
  117. <div className="family-switch">
  118. {families.map((row) => (
  119. <button
  120. className={`fbtn ${row.key === family.key ? 'on' : ''}`}
  121. key={row.key}
  122. type="button"
  123. onClick={() => setActiveKey(row.key)}
  124. >
  125. <span className="fbtn-pre">{row.label || row.axes?.[0] || row.key}</span>
  126. <span className="fbtn-suf">{(row.axes || []).slice(1).join(' × ')}</span>
  127. </button>
  128. ))}
  129. </div>
  130. {family.key === 'topic_table' ? (
  131. <span className="refresh-state">选题字段 + LLM · 只生成 Query</span>
  132. ) : (
  133. <>
  134. <span className="refresh-state">
  135. {lastUpdatedText ? `上次刷新 ${lastUpdatedText}` : '等待后台写入数据'}
  136. </span>
  137. <div className="manual-entry-actions">
  138. <button type="button" onClick={() => setManualMode('text')}>手动输入</button>
  139. <button type="button" onClick={() => setManualMode('json')}>上传 JSON</button>
  140. </div>
  141. </>
  142. )}
  143. </div>
  144. {family.key === 'topic_table' ? <TopicTableQueryPanel /> : <div className={`cdemo ${family.key === 'manual' ? 'manual-cdemo' : ''}`}>
  145. {(family.axes || []).map((axis) => (
  146. <AxisColumn
  147. key={axis}
  148. axis={axis}
  149. data={preview}
  150. family={family}
  151. />
  152. ))}
  153. <QueryColumn
  154. familyItems={familyItems}
  155. latestByQuery={latestByQuery}
  156. selectedQuery={selectedQuery}
  157. onSelect={(queryItem, queryLatest) => {
  158. setSelectedQuery(selectedQueryPayload(queryItem.query, queryLatest))
  159. }}
  160. familyRunCount={familyRunCount}
  161. totalRunCount={totalRunCount}
  162. totalItemCount={totalItemCount}
  163. emptyMessage={
  164. family.key === 'manual'
  165. ? '暂无手动 Query,点击右上角手动输入或上传 JSON 创建。'
  166. : undefined
  167. }
  168. />
  169. <SearchResultColumn
  170. selected={selectedQuery}
  171. detail={detailState.detail}
  172. loading={detailState.loading}
  173. error={detailState.error}
  174. onKnowledgeClick={setDecodeItemId}
  175. />
  176. </div>}
  177. {decodeItemId && (
  178. <div className="decode-modal-mask" onClick={() => setDecodeItemId('')}>
  179. <div className="decode-modal-panel" onClick={(event) => event.stopPropagation()}>
  180. <DecodeKnowledgeModal itemId={decodeItemId} embedded onClose={() => setDecodeItemId('')} />
  181. </div>
  182. </div>
  183. )}
  184. {manualMode && (
  185. <ManualQueryModal
  186. mode={manualMode}
  187. onClose={() => setManualMode('')}
  188. onSubmitted={async () => {
  189. setManualMode('')
  190. await refreshBoard()
  191. setActiveKey('manual')
  192. }}
  193. />
  194. )}
  195. </div>
  196. )
  197. }