@@ -1,29 +1,59 @@
// app.js — 视图路由、运行列表渲染、健康检查。整个控制台的入口编排。
// 会话保持: URL hash 路由(#runs / #new / #profiles / #report/{runId})
// + sessionStorage 兜底, F5 刷新 / 浏览器前进后退均可恢复。
const App = {
currentRunId : null ,
activeView : null ,
views : [ "runs" , "new" , "report" , "profiles" ] ,
titles : { runs : "运行列表" , new : "新建评估" , report : "报告详情" , profiles : "LLM 配置" } ,
// 初始化:绑定导航、加载首屏 、启动健康检查。
// 初始化:绑定导航、从 URL/sessionStorage 恢复上次位置 、启动健康检查。
init ( ) {
document . querySelectorAll ( ".nav-item" ) . forEach ( ( btn ) => {
btn . addEventListener ( "click" , ( ) => App . switchView ( btn . dataset . view ) ) ;
btn . addEventListener ( "click" , ( ) => App . navigate ( btn . dataset . view ) ) ;
} ) ;
document . getElementById ( "refresh-btn" ) . addEventListener ( "click" , ( ) => App . refreshCurrent ( ) ) ;
Runner . init ( ) ;
Profiles . init ( ) ;
App . switchView ( "runs" ) ;
// 恢复上次会话(优先 URL hash, 其次 sessionStorage)
App . _restoreSession ( ) ;
App . checkHealth ( ) ;
setInterval ( App . checkHealth , 15000 ) ;
// 浏览器前进 / 后退按钮
window . addEventListener ( "popstate" , ( ) => App . _restoreSession ( ) ) ;
} ,
// 切换主视图,并同步导航高亮与标题。
switchView ( view ) {
if ( view === "report" && ! App . currentRunId ) {
// 没有选中的运行时,报告页显示占位。
// ----------------------------------------------------------------
// 路由 —— 有历史记录的主动导航(更新 URL hash)
// ----------------------------------------------------------------
navigate ( view , runId ) {
if ( runId !== undefined ) App . currentRunId = runId ;
const hash = App . _buildHash ( view , App . currentRunId ) ;
if ( location . hash !== ` # ${ hash } ` ) {
history . pushState ( { view , runId : App . currentRunId } , "" , ` # ${ hash } ` ) ;
}
App . _doSwitch ( view ) ;
} ,
// 供内部调用(不产生历史记录),例如刷新同一视图
switchView ( view ) {
App . _doSwitch ( view ) ;
} ,
// 刷新当前视图数据
refreshCurrent ( ) {
App . _doSwitch ( App . activeView || "runs" ) ;
} ,
// ----------------------------------------------------------------
// 内部:实际切换 DOM + 触发数据加载
// ----------------------------------------------------------------
_doSwitch ( view ) {
App . views . forEach ( ( name ) => {
const el = document . getElementById ( ` view- ${ name } ` ) ;
if ( el ) el . hidden = name !== view ;
@@ -34,18 +64,53 @@ const App = {
document . getElementById ( "view-title" ) . textContent = App . titles [ view ] || view ;
App . activeView = view ;
// 持久化到 sessionStorage( URL 共享场景的备份)
sessionStorage . setItem ( "rag_view" , view ) ;
if ( App . currentRunId ) sessionStorage . setItem ( "rag_run_id" , App . currentRunId ) ;
if ( view === "runs" ) App . loadRuns ( ) ;
if ( view === "new" ) Runner . loadScenarios ( ) ;
if ( view === "report" ) Report . render ( App . currentRunId ) ;
if ( view === "profiles" ) Profiles . load ( ) ;
} ,
// 刷新当前视图的数据。
refreshCurrent ( ) {
App . switchView ( App . activeView || "runs" ) ;
// ----------------------------------------------------------------
// Hash 工具
// ----------------------------------------------------------------
_buildHash ( view , runId ) {
if ( view === "report" && runId ) {
return ` report/ ${ encodeURIComponent ( runId ) } ` ;
}
return view || "runs" ;
} ,
// 加载并渲染运行列表。
_parseHash ( ) {
const raw = location . hash . replace ( /^#\/?/ , "" ) ;
if ( ! raw ) return { view : null , runId : null } ;
if ( raw . startsWith ( "report/" ) ) {
const runId = decodeURIComponent ( raw . slice ( "report/" . length ) ) ;
return { view : "report" , runId } ;
}
const view = App . views . includes ( raw ) ? raw : null ;
return { view , runId : null } ;
} ,
// 会话恢复: hash → sessionStorage → 默认 runs
_restoreSession ( ) {
const { view : hView , runId : hRunId } = App . _parseHash ( ) ;
const view = hView || sessionStorage . getItem ( "rag_view" ) || "runs" ;
const runId = hRunId || sessionStorage . getItem ( "rag_run_id" ) || null ;
if ( runId ) {
App . currentRunId = runId ;
App . enableReportNav ( ) ;
}
App . _doSwitch ( view ) ;
} ,
// ----------------------------------------------------------------
// 运行列表
// ----------------------------------------------------------------
async loadRuns ( ) {
const container = document . getElementById ( "runs-container" ) ;
const empty = document . getElementById ( "runs-empty" ) ;
@@ -66,14 +131,16 @@ const App = {
}
} ,
// 构造一张运行卡片。
renderRunCard ( run ) {
const card = document . createElement ( "div" ) ;
card . className = "run-card" ;
card . className = "run-card" + ( run . run _id === App . currentRunId ? " selected" : "" ) ;
card . addEventListener ( "click" , ( ) => {
App . currentRunId = run . run _id ;
// 更新选中高亮
document . querySelectorAll ( ".run-card" ) . forEach ( ( c ) => c . classList . remove ( "selected" ) ) ;
card . classList . add ( "selected" ) ;
App . enableReportNav ( ) ;
App . switchView ( "report" ) ;
App . navigate ( "report" , run . run _id );
} ) ;
const chips = ( run . metrics || [ ] )
@@ -81,7 +148,7 @@ const App = {
const val = run . metric _means ? run . metric _means [ m ] : null ;
const cls = App . scoreClass ( val ) ;
const text = val === null || val === undefined ? "n/a" : val . toFixed ( 2 ) ;
return ` <span class="metric-chip"> ${ App . escape ( App . shortMetric ( m ) ) } <b class=" ${ cls } "> ${ text } </b></span> ` ;
return ` <span class="metric-chip" title=" ${ App . escape ( m ) } " > ${ App . escape ( App . shortMetric ( m ) ) } <b class=" ${ cls } "> ${ text } </b></span> ` ;
} )
. join ( "" ) ;
@@ -98,13 +165,14 @@ const App = {
return card ;
} ,
// 启用报告导航项(选中运行后)。
// ----------------------------------------------------------------
// 工具方法
// ----------------------------------------------------------------
enableReportNav ( ) {
const btn = document . querySelector ( '.nav-item[data-view="report"]' ) ;
if ( btn ) btn . disabled = false ;
} ,
// 根据分值返回 good/warn/bad/na 配色类。
scoreClass ( value ) {
if ( value === null || value === undefined ) return "na" ;
if ( value >= 0.8 ) return "good" ;
@@ -112,7 +180,6 @@ const App = {
return "bad" ;
} ,
// 指标名缩写,节省卡片横向空间。
shortMetric ( name ) {
const map = {
faithfulness : "faith." ,
@@ -126,20 +193,17 @@ const App = {
return map [ name ] || name ;
} ,
// 截取时间戳到分钟,便于阅读。
shortTime ( iso ) {
if ( ! iso ) return "—" ;
return String ( iso ) . replace ( "T" , " " ) . slice ( 0 , 16 ) ;
} ,
// 简单 HTML 转义,防止注入。
escape ( text ) {
const div = document . createElement ( "div" ) ;
div . textContent = text == null ? "" : String ( text ) ;
return div . innerHTML ;
} ,
// 健康检查,更新左下角状态点。
async checkHealth ( ) {
const dot = document . getElementById ( "health-dot" ) ;
const label = document . getElementById ( "health-text" ) ;