ai 요청 탭으로 이동

This commit is contained in:
Macbook
2026-06-17 15:51:58 +09:00
parent afd87b10c1
commit 9be08c1a50
3 changed files with 292 additions and 223 deletions
@@ -49,7 +49,7 @@ import BacktestAnalysisReportModal from './backtest/BacktestAnalysisReportModal'
import type { BacktestAnalysisReportModel } from './backtest/BacktestAnalysisReportModal'; import type { BacktestAnalysisReportModel } from './backtest/BacktestAnalysisReportModal';
import { buildChartBacktestReportModel } from '../utils/backtestReportModel'; import { buildChartBacktestReportModel } from '../utils/backtestReportModel';
import { BACKTEST_DISPLAY_BAR_COUNT } from '../utils/backtestWarmup'; import { BACKTEST_DISPLAY_BAR_COUNT } from '../utils/backtestWarmup';
import StrategyEvaluationAiVerifyModal from './strategyEvaluation/StrategyEvaluationAiVerifyModal'; import StrategyEvaluationAiVerifyPanel from './strategyEvaluation/StrategyEvaluationAiVerifyPanel';
import { import {
buildStrategyEvaluationAiVerifyContext, buildStrategyEvaluationAiVerifyContext,
requestStrategyEvaluationAiVerify, requestStrategyEvaluationAiVerify,
@@ -65,6 +65,7 @@ const LEFT_DEFAULT = 380;
const RIGHT_DEFAULT = 440; const RIGHT_DEFAULT = 440;
type LeftTab = 'strategy' | 'market' | 'settings'; type LeftTab = 'strategy' | 'market' | 'settings';
type RightTab = 'signal' | 'ai';
function readMinWidth(key: string, fallback: number): number { function readMinWidth(key: string, fallback: number): number {
return Math.max(readStoredSize(key, fallback), fallback); return Math.max(readStoredSize(key, fallback), fallback);
@@ -99,16 +100,15 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
const [reportOpen, setReportOpen] = useState(false); const [reportOpen, setReportOpen] = useState(false);
const [reportModel, setReportModel] = useState<BacktestAnalysisReportModel | null>(null); const [reportModel, setReportModel] = useState<BacktestAnalysisReportModel | null>(null);
const [reportLoading, setReportLoading] = useState(false); const [reportLoading, setReportLoading] = useState(false);
const [aiVerifyOpen, setAiVerifyOpen] = useState(false); const [rightTab, setRightTab] = useState<RightTab>('signal');
const [aiVerifyUnread, setAiVerifyUnread] = useState(false);
const [aiVerifyRunning, setAiVerifyRunning] = useState(false); const [aiVerifyRunning, setAiVerifyRunning] = useState(false);
const [aiVerifyError, setAiVerifyError] = useState<string | null>(null); const [aiVerifyError, setAiVerifyError] = useState<string | null>(null);
const [aiVerifyResult, setAiVerifyResult] = useState<StrategyEvaluationAiVerifyResponse | null>(null); const [aiVerifyResult, setAiVerifyResult] = useState<StrategyEvaluationAiVerifyResponse | null>(null);
const [aiVerifyContext, setAiVerifyContext] = useState<import('../utils/strategyEvaluationAiVerify').StrategyEvaluationAiVerifyContext | null>(null); const [aiVerifyContext, setAiVerifyContext] = useState<import('../utils/strategyEvaluationAiVerify').StrategyEvaluationAiVerifyContext | null>(null);
const [aiVerifySummary, setAiVerifySummary] = useState<string | null>(null); const [aiVerifySummary, setAiVerifySummary] = useState<string | null>(null);
const [aiVerifyCompleteNotice, setAiVerifyCompleteNotice] = useState(false);
const [aiVerifyErrorNotice, setAiVerifyErrorNotice] = useState<string | null>(null);
const aiVerifyGenRef = useRef(0); const aiVerifyGenRef = useRef(0);
const aiVerifyOpenRef = useRef(aiVerifyOpen); const rightTabRef = useRef<RightTab>('signal');
const aiVerifyPollAbortRef = useRef<AbortController | null>(null); const aiVerifyPollAbortRef = useRef<AbortController | null>(null);
const reportCacheKeyRef = useRef<string | null>(null); const reportCacheKeyRef = useRef<string | null>(null);
const reportGenRef = useRef(0); const reportGenRef = useRef(0);
@@ -122,12 +122,11 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
}, []); }, []);
useEffect(() => { useEffect(() => {
aiVerifyOpenRef.current = aiVerifyOpen; rightTabRef.current = rightTab;
if (aiVerifyOpen) { if (rightTab === 'ai') {
setAiVerifyCompleteNotice(false); setAiVerifyUnread(false);
setAiVerifyErrorNotice(null);
} }
}, [aiVerifyOpen]); }, [rightTab]);
const getEvalParams = useMemo( const getEvalParams = useMemo(
() => mergeEvalGetParams(baseGetParams, appliedIndicatorParams), () => mergeEvalGetParams(baseGetParams, appliedIndicatorParams),
@@ -415,13 +414,13 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
aiVerifyPollAbortRef.current = pollAbort; aiVerifyPollAbortRef.current = pollAbort;
const runId = ++aiVerifyGenRef.current; const runId = ++aiVerifyGenRef.current;
setAiVerifyOpen(true); setRightTab('ai');
setRightOpen(true);
setAiVerifyRunning(true); setAiVerifyRunning(true);
setAiVerifyError(null); setAiVerifyError(null);
setAiVerifyResult(null); setAiVerifyResult(null);
setAiVerifyContext(null); setAiVerifyContext(null);
setAiVerifyCompleteNotice(false); setAiVerifyUnread(false);
setAiVerifyErrorNotice(null);
try { try {
const indicatorParams = buildEvalParamsFromStrategy(selectedStrategy, getEvalParams); const indicatorParams = buildEvalParamsFromStrategy(selectedStrategy, getEvalParams);
@@ -448,17 +447,15 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
}); });
if (runId !== aiVerifyGenRef.current) return; if (runId !== aiVerifyGenRef.current) return;
setAiVerifyResult(result); setAiVerifyResult(result);
if (!aiVerifyOpenRef.current) { if (rightTabRef.current !== 'ai') {
setAiVerifyCompleteNotice(true); setAiVerifyUnread(true);
} }
} catch (e) { } catch (e) {
if (runId !== aiVerifyGenRef.current) return; if (runId !== aiVerifyGenRef.current) return;
if (e instanceof DOMException && e.name === 'AbortError') return; if (e instanceof DOMException && e.name === 'AbortError') return;
const msg = e instanceof Error ? e.message : 'AI 검증 요청 실패'; const msg = e instanceof Error ? e.message : 'AI 검증 요청 실패';
setAiVerifyError(msg); setAiVerifyError(msg);
if (!aiVerifyOpenRef.current) { setAiVerifyUnread(true);
setAiVerifyErrorNotice(msg);
}
} finally { } finally {
if (runId === aiVerifyGenRef.current) { if (runId === aiVerifyGenRef.current) {
setAiVerifyRunning(false); setAiVerifyRunning(false);
@@ -482,17 +479,12 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
]); ]);
const handleOpenAiVerify = useCallback(() => { const handleOpenAiVerify = useCallback(() => {
if (aiVerifyRunning) { setRightOpen(true);
setAiVerifyOpen(true); setRightTab('ai');
return; if (aiVerifyRunning) return;
}
void runAiVerify(); void runAiVerify();
}, [aiVerifyRunning, runAiVerify]); }, [aiVerifyRunning, runAiVerify]);
const handleCloseAiVerify = useCallback(() => {
setAiVerifyOpen(false);
}, []);
const handleOpenReport = useCallback(async () => { const handleOpenReport = useCallback(async () => {
if (reportDisabled || !selectedStrategyId || !selectedStrategy) return; if (reportDisabled || !selectedStrategyId || !selectedStrategy) return;
@@ -779,6 +771,44 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
/> />
)} )}
<aside className={`btd-right seval-right-panel seval-right-sidebar${rightOpen ? ' seval-right-sidebar--open' : ''}`}> <aside className={`btd-right seval-right-panel seval-right-sidebar${rightOpen ? ' seval-right-sidebar--open' : ''}`}>
<div className="seval-right-tabs" role="tablist" aria-label="우측 패널">
<button
type="button"
role="tab"
id="seval-right-tab-signal"
aria-selected={rightTab === 'signal'}
aria-controls="seval-right-panel-signal"
className={`btd-exec-tab seval-right-tab${rightTab === 'signal' ? ' btd-exec-tab--on' : ''}`}
onClick={() => setRightTab('signal')}
>
</button>
<button
type="button"
role="tab"
id="seval-right-tab-ai"
aria-selected={rightTab === 'ai'}
aria-controls="seval-right-panel-ai"
className={`btd-exec-tab seval-right-tab${rightTab === 'ai' ? ' btd-exec-tab--on' : ''}`}
onClick={() => setRightTab('ai')}
>
AI
{aiVerifyRunning && (
<span className="seval-right-tab-spinner btd-run-spinner" aria-label="분석 중" />
)}
{!aiVerifyRunning && aiVerifyUnread && (
<span className="seval-right-tab-badge" aria-label="새 결과" />
)}
</button>
</div>
{rightTab === 'signal' && (
<div
id="seval-right-panel-signal"
role="tabpanel"
aria-labelledby="seval-right-tab-signal"
className="seval-right-tab-panel"
>
<div className="seval-signal-head"> <div className="seval-signal-head">
<span className="seval-signal-market">{getKoreanName(market)}</span> <span className="seval-signal-market">{getKoreanName(market)}</span>
<span <span
@@ -820,6 +850,27 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
</span> </span>
<span className="seval-strategy-chip">{strategyLabel}</span> <span className="seval-strategy-chip">{strategyLabel}</span>
</footer> </footer>
</div>
)}
{rightTab === 'ai' && (
<div
id="seval-right-panel-ai"
role="tabpanel"
aria-labelledby="seval-right-tab-ai"
className="seval-right-tab-panel seval-right-tab-panel--ai"
>
<StrategyEvaluationAiVerifyPanel
loading={aiVerifyRunning}
error={aiVerifyError}
contextSummary={aiVerifySummary}
verifyContext={aiVerifyContext}
result={aiVerifyResult}
runDisabled={aiVerifyDisabled}
onRun={() => { void runAiVerify(); }}
/>
</div>
)}
</aside> </aside>
</div> </div>
</div> </div>
@@ -838,79 +889,6 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
onClose={() => setReportOpen(false)} onClose={() => setReportOpen(false)}
model={reportModel} model={reportModel}
/> />
<StrategyEvaluationAiVerifyModal
open={aiVerifyOpen}
onClose={handleCloseAiVerify}
loading={aiVerifyRunning}
error={aiVerifyError}
contextSummary={aiVerifySummary}
verifyContext={aiVerifyContext}
result={aiVerifyResult}
theme={theme}
onRetry={() => { void runAiVerify(); }}
/>
{aiVerifyRunning && !aiVerifyOpen && (
<div className="seval-ai-verify-bg-pill" role="status" aria-live="polite">
<span className="btd-run-spinner seval-ai-verify-bg-pill-spinner" aria-hidden />
<span>AI </span>
<button
type="button"
className="seval-ai-verify-bg-pill-btn"
onClick={() => setAiVerifyOpen(true)}
>
</button>
</div>
)}
{(aiVerifyCompleteNotice || aiVerifyErrorNotice) && !aiVerifyOpen && (
<div
className={`seval-ai-verify-notice${aiVerifyErrorNotice ? ' seval-ai-verify-notice--error' : ''}`}
role="status"
>
<span className="seval-ai-verify-notice-text">
{aiVerifyErrorNotice ?? 'AI 분석이 완료되었습니다.'}
</span>
<div className="seval-ai-verify-notice-actions">
{!aiVerifyErrorNotice && (
<button
type="button"
className="seval-ai-verify-notice-btn seval-ai-verify-notice-btn--primary"
onClick={() => {
setAiVerifyCompleteNotice(false);
setAiVerifyOpen(true);
}}
>
</button>
)}
<button
type="button"
className="seval-ai-verify-notice-btn"
onClick={() => {
setAiVerifyCompleteNotice(false);
setAiVerifyErrorNotice(null);
}}
>
</button>
{aiVerifyErrorNotice && (
<button
type="button"
className="seval-ai-verify-notice-btn seval-ai-verify-notice-btn--primary"
onClick={() => {
setAiVerifyErrorNotice(null);
setAiVerifyOpen(true);
}}
>
</button>
)}
</div>
</div>
)}
</div> </div>
); );
} }
@@ -1,8 +1,7 @@
/** /**
* AI * AI
*/ */
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import type { import type {
AiVerifyFixPointsBundle, AiVerifyFixPointsBundle,
StrategyEvaluationAiVerifyContext, StrategyEvaluationAiVerifyContext,
@@ -10,20 +9,6 @@ import type {
import { buildAiVerifyFixPoints } from '../../utils/strategyEvaluationAiVerify'; import { buildAiVerifyFixPoints } from '../../utils/strategyEvaluationAiVerify';
import type { StrategyEvaluationAiVerifyResponse } from '../../utils/strategyEvaluationAiVerify'; import type { StrategyEvaluationAiVerifyResponse } from '../../utils/strategyEvaluationAiVerify';
import type { Theme } from '../../types';
interface Props {
open: boolean;
onClose: () => void;
loading: boolean;
error: string | null;
contextSummary: string | null;
verifyContext: StrategyEvaluationAiVerifyContext | null;
result: StrategyEvaluationAiVerifyResponse | null;
onRetry?: () => void;
theme?: Theme;
}
function renderAnalysisMarkdown(text: string): React.ReactNode { function renderAnalysisMarkdown(text: string): React.ReactNode {
const lines = text.split('\n'); const lines = text.split('\n');
const nodes: React.ReactNode[] = []; const nodes: React.ReactNode[] = [];
@@ -141,9 +126,7 @@ const Ta4jDiagnosticsSummary: React.FC<{ diagnostics: Record<string, unknown> }>
); );
}; };
const FixPointsPanel: React.FC<{ const FixPointsPanel: React.FC<{ bundle: AiVerifyFixPointsBundle }> = ({ bundle }) => {
bundle: AiVerifyFixPointsBundle;
}> = ({ bundle }) => {
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const [expandedId, setExpandedId] = useState<string | null>( const [expandedId, setExpandedId] = useState<string | null>(
bundle.points[0]?.id ?? null, bundle.points[0]?.id ?? null,
@@ -167,7 +150,7 @@ const FixPointsPanel: React.FC<{
(Cursor AI) (Cursor AI)
</h3> </h3>
<p className="seval-ai-verify-fixpoints-desc"> <p className="seval-ai-verify-fixpoints-desc">
. ·· Cursor에 . . Cursor에 · .
</p> </p>
{bundle.verdictHint && ( {bundle.verdictHint && (
<p className="seval-ai-verify-fixpoints-verdict">AI : {bundle.verdictHint}</p> <p className="seval-ai-verify-fixpoints-verdict">AI : {bundle.verdictHint}</p>
@@ -221,16 +204,24 @@ const FixPointsPanel: React.FC<{
); );
}; };
const StrategyEvaluationAiVerifyModal: React.FC<Props> = ({ export interface StrategyEvaluationAiVerifyPanelProps {
open, loading: boolean;
onClose, error: string | null;
contextSummary: string | null;
verifyContext: StrategyEvaluationAiVerifyContext | null;
result: StrategyEvaluationAiVerifyResponse | null;
runDisabled?: boolean;
onRun: () => void;
}
const StrategyEvaluationAiVerifyPanel: React.FC<StrategyEvaluationAiVerifyPanelProps> = ({
loading, loading,
error, error,
contextSummary, contextSummary,
verifyContext, verifyContext,
result, result,
onRetry, runDisabled = false,
theme = 'dark', onRun,
}) => { }) => {
const bodyRef = useRef<HTMLDivElement>(null); const bodyRef = useRef<HTMLDivElement>(null);
@@ -242,115 +233,92 @@ const StrategyEvaluationAiVerifyModal: React.FC<Props> = ({
); );
useEffect(() => { useEffect(() => {
if (!open) return; if (result && bodyRef.current) {
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [open, onClose]);
useEffect(() => {
if (open && result && bodyRef.current) {
bodyRef.current.scrollTop = 0; bodyRef.current.scrollTop = 0;
} }
}, [open, result]); }, [result?.analysis]);
const handleBackdrop = useCallback((e: React.MouseEvent) => { const hasContent = loading || error || result;
if (e.target === e.currentTarget) onClose();
}, [onClose]);
if (!open) return null; return (
<div className="seval-ai-verify-panel">
const content = ( <header className="seval-ai-verify-panel-head">
<div <div className="seval-ai-verify-panel-head-main">
className="seval-ai-verify-overlay app-popup-overlay--dim" {contextSummary && (
role="presentation" <p className="seval-ai-verify-summary">{contextSummary}</p>
onClick={handleBackdrop}
>
<div
className={`seval-ai-verify-modal app-popup-shell se-page se-page--${theme}`}
role="dialog"
aria-modal="true"
aria-labelledby="seval-ai-verify-title"
onClick={e => e.stopPropagation()}
>
<header className="seval-ai-verify-head app-popup-header">
<div className="seval-ai-verify-head-main app-popup-header-left">
<h2 id="seval-ai-verify-title" className="seval-ai-verify-title app-popup-header-title">
AI
</h2>
{contextSummary && (
<p className="seval-ai-verify-summary">{contextSummary}</p>
)}
</div>
<button
type="button"
className="seval-ai-verify-close app-popup-close"
aria-label="닫기"
onClick={onClose}
>
×
</button>
</header>
<div ref={bodyRef} className="seval-ai-verify-body app-popup-body">
{loading && (
<div className="seval-ai-verify-loading" aria-live="polite">
<span className="btd-run-spinner seval-ai-verify-spinner" aria-hidden />
<p>LLM이 </p>
<p className="seval-ai-verify-loading-hint">
Ta4j LLM 2~3 .
.
</p>
</div>
)} )}
{!contextSummary && !hasContent && (
{!loading && error && ( <p className="seval-ai-verify-panel-empty-hint">
<div className="seval-ai-verify-error" role="alert"> AI LLM .
<p>{error}</p> </p>
{onRetry && (
<button type="button" className="seval-ai-verify-retry" onClick={onRetry}>
</button>
)}
</div>
)}
{!loading && !error && result && (
<>
<div className="seval-ai-verify-meta">
<span>{result.model}</span>
<span>{(result.latencyMs / 1000).toFixed(1)}</span>
</div>
<div className="seval-ai-verify-analysis">
{renderAnalysisMarkdown(result.analysis)}
</div>
{result.ta4jDiagnostics && (
<Ta4jDiagnosticsSummary diagnostics={result.ta4jDiagnostics} />
)}
{fixPoints && fixPoints.hasIssue && (
<FixPointsPanel bundle={fixPoints} />
)}
</>
)} )}
</div> </div>
<button
type="button"
className="seval-ai-verify-panel-run-btn"
disabled={runDisabled || loading}
onClick={onRun}
>
{loading ? (
<>
<span className="btd-run-spinner seval-ai-verify-panel-run-spinner" aria-hidden />
</>
) : (
result ? '다시 분석' : '분석 실행'
)}
</button>
</header>
<footer className="seval-ai-verify-foot"> <div ref={bodyRef} className="seval-ai-verify-panel-body">
<p className="seval-ai-verify-footnote"> {loading && (
{loading <div className="seval-ai-verify-loading" aria-live="polite">
? '※ 분석 중 닫기를 눌러도 작업은 백그라운드에서 계속됩니다.' <span className="btd-run-spinner seval-ai-verify-spinner" aria-hidden />
: '※ AI 분석은 참고용입니다. 수정 포인트는 Cursor 등에 붙여넣어 코드 수정을 요청할 수 있습니다.'} <p>LLM이 </p>
</p> <p className="seval-ai-verify-loading-hint">
<button type="button" className="seval-ai-verify-close-btn" onClick={onClose}> Ta4j LLM 2~3 .
· .
</button> </p>
</footer> </div>
)}
{!loading && error && (
<div className="seval-ai-verify-error" role="alert">
<p>{error}</p>
<button type="button" className="seval-ai-verify-retry" onClick={onRun}>
</button>
</div>
)}
{!loading && !error && result && (
<>
<div className="seval-ai-verify-meta">
<span>{result.model}</span>
<span>{(result.latencyMs / 1000).toFixed(1)}</span>
</div>
<div className="seval-ai-verify-analysis">
{renderAnalysisMarkdown(result.analysis)}
</div>
{result.ta4jDiagnostics && (
<Ta4jDiagnosticsSummary diagnostics={result.ta4jDiagnostics} />
)}
{fixPoints && fixPoints.hasIssue && (
<FixPointsPanel bundle={fixPoints} />
)}
</>
)}
</div> </div>
<footer className="seval-ai-verify-panel-foot">
<p className="seval-ai-verify-footnote">
{loading
? '※ 분석은 백그라운드에서 진행됩니다. 시그널 탭으로 이동해도 결과는 이 탭에 표시됩니다.'
: '※ AI 분석은 참고용입니다. 수정 포인트는 Cursor 등에 붙여넣어 코드 수정을 요청할 수 있습니다.'}
</p>
</footer>
</div> </div>
); );
return createPortal(content, document.body);
}; };
export default StrategyEvaluationAiVerifyModal; export default StrategyEvaluationAiVerifyPanel;
+123
View File
@@ -118,6 +118,8 @@
border-left: 1px solid var(--btd-divider, var(--se-border)); border-left: 1px solid var(--btd-divider, var(--se-border));
} }
.seval-right-wrap:not(.seval-right-wrap--open) .seval-right-sidebar.btd-right .seval-right-tabs,
.seval-right-wrap:not(.seval-right-wrap--open) .seval-right-sidebar.btd-right .seval-right-tab-panel,
.seval-right-wrap:not(.seval-right-wrap--open) .seval-right-sidebar.btd-right .seval-signal-head, .seval-right-wrap:not(.seval-right-wrap--open) .seval-right-sidebar.btd-right .seval-signal-head,
.seval-right-wrap:not(.seval-right-wrap--open) .seval-right-sidebar.btd-right .seval-signal-body, .seval-right-wrap:not(.seval-right-wrap--open) .seval-right-sidebar.btd-right .seval-signal-body,
.seval-right-wrap:not(.seval-right-wrap--open) .seval-right-sidebar.btd-right .seval-signal-footer, .seval-right-wrap:not(.seval-right-wrap--open) .seval-right-sidebar.btd-right .seval-signal-footer,
@@ -1126,6 +1128,127 @@
min-height: 0; min-height: 0;
} }
.seval-right-tabs {
flex-shrink: 0;
display: flex;
gap: 4px;
padding: 0 2px 8px;
border-bottom: 1px solid var(--se-border, var(--btd-divider));
margin-bottom: 4px;
}
.seval-right-tab {
flex: 1;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
min-height: 32px;
font-size: 0.72rem;
}
.seval-right-tab-spinner {
width: 12px;
height: 12px;
flex-shrink: 0;
}
.seval-right-tab-badge {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--se-gold, #e6c200);
box-shadow: 0 0 6px color-mix(in srgb, var(--se-gold, #e6c200) 60%, transparent);
flex-shrink: 0;
}
.seval-right-tab-panel {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.seval-right-tab-panel--ai {
overflow: hidden;
}
/* ── AI 검증 우측 패널 탭 ── */
.seval-ai-verify-panel {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.seval-ai-verify-panel-head {
flex-shrink: 0;
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 8px;
padding: 0 2px 8px;
border-bottom: 1px solid var(--se-border, rgba(255, 255, 255, 0.08));
}
.seval-ai-verify-panel-head-main {
flex: 1;
min-width: 0;
}
.seval-ai-verify-panel-empty-hint {
margin: 0;
font-size: 0.72rem;
line-height: 1.45;
color: var(--se-text-muted, #8b949e);
}
.seval-ai-verify-panel-run-btn {
flex-shrink: 0;
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
border-radius: 6px;
border: 1px solid color-mix(in srgb, var(--se-gold, #e6c200) 40%, var(--se-border));
background: color-mix(in srgb, var(--se-gold, #e6c200) 10%, var(--bg3, #1f2335));
color: var(--text, var(--se-text));
font-size: 0.72rem;
font-weight: 600;
cursor: pointer;
}
.seval-ai-verify-panel-run-btn:hover:not(:disabled) {
background: color-mix(in srgb, var(--se-gold, #e6c200) 18%, var(--bg3));
}
.seval-ai-verify-panel-run-btn:disabled {
opacity: 0.65;
cursor: not-allowed;
}
.seval-ai-verify-panel-run-spinner {
width: 14px;
height: 14px;
}
.seval-ai-verify-panel-body {
flex: 1;
min-height: 0;
overflow: auto;
padding: 10px 2px 8px;
}
.seval-ai-verify-panel-foot {
flex-shrink: 0;
padding: 8px 2px 0;
border-top: 1px solid var(--se-border, rgba(255, 255, 255, 0.08));
}
.seval-ai-verify-panel-body .seval-ai-verify-loading {
min-height: 120px;
}
.seval-signal-head { .seval-signal-head {
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;