ai 요청 탭으로 이동
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-77
@@ -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,66 +233,51 @@ 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"
|
|
||||||
role="presentation"
|
|
||||||
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 && (
|
{contextSummary && (
|
||||||
<p className="seval-ai-verify-summary">{contextSummary}</p>
|
<p className="seval-ai-verify-summary">{contextSummary}</p>
|
||||||
)}
|
)}
|
||||||
|
{!contextSummary && !hasContent && (
|
||||||
|
<p className="seval-ai-verify-panel-empty-hint">
|
||||||
|
차트 툴바의 AI 검증 버튼 또는 아래 실행으로 선택 봉 기준 LLM 분석을 시작합니다.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="seval-ai-verify-close app-popup-close"
|
className="seval-ai-verify-panel-run-btn"
|
||||||
aria-label="닫기"
|
disabled={runDisabled || loading}
|
||||||
onClick={onClose}
|
onClick={onRun}
|
||||||
>
|
>
|
||||||
×
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<span className="btd-run-spinner seval-ai-verify-panel-run-spinner" aria-hidden />
|
||||||
|
분석 중…
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
result ? '다시 분석' : '분석 실행'
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div ref={bodyRef} className="seval-ai-verify-body app-popup-body">
|
<div ref={bodyRef} className="seval-ai-verify-panel-body">
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="seval-ai-verify-loading" aria-live="polite">
|
<div className="seval-ai-verify-loading" aria-live="polite">
|
||||||
<span className="btd-run-spinner seval-ai-verify-spinner" aria-hidden />
|
<span className="btd-run-spinner seval-ai-verify-spinner" aria-hidden />
|
||||||
<p>LLM이 평가 결과와 차트 시그널을 분석 중입니다…</p>
|
<p>LLM이 평가 결과와 차트 시그널을 분석 중입니다…</p>
|
||||||
<p className="seval-ai-verify-loading-hint">
|
<p className="seval-ai-verify-loading-hint">
|
||||||
Ta4j 재평가와 LLM 분석에 최대 2~3분 걸릴 수 있습니다.
|
Ta4j 재평가와 LLM 분석에 최대 2~3분 걸릴 수 있습니다.
|
||||||
모달을 닫아도 백그라운드에서 분석이 계속됩니다.
|
다른 탭·차트 조작은 계속 사용할 수 있습니다.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -309,11 +285,9 @@ const StrategyEvaluationAiVerifyModal: React.FC<Props> = ({
|
|||||||
{!loading && error && (
|
{!loading && error && (
|
||||||
<div className="seval-ai-verify-error" role="alert">
|
<div className="seval-ai-verify-error" role="alert">
|
||||||
<p>{error}</p>
|
<p>{error}</p>
|
||||||
{onRetry && (
|
<button type="button" className="seval-ai-verify-retry" onClick={onRun}>
|
||||||
<button type="button" className="seval-ai-verify-retry" onClick={onRetry}>
|
|
||||||
다시 시도
|
다시 시도
|
||||||
</button>
|
</button>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -336,21 +310,15 @@ const StrategyEvaluationAiVerifyModal: React.FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer className="seval-ai-verify-foot">
|
<footer className="seval-ai-verify-panel-foot">
|
||||||
<p className="seval-ai-verify-footnote">
|
<p className="seval-ai-verify-footnote">
|
||||||
{loading
|
{loading
|
||||||
? '※ 분석 중 닫기를 눌러도 작업은 백그라운드에서 계속됩니다.'
|
? '※ 분석은 백그라운드에서 진행됩니다. 시그널 탭으로 이동해도 결과는 이 탭에 표시됩니다.'
|
||||||
: '※ AI 분석은 참고용입니다. 수정 포인트는 Cursor 등에 붙여넣어 코드 수정을 요청할 수 있습니다.'}
|
: '※ AI 분석은 참고용입니다. 수정 포인트는 Cursor 등에 붙여넣어 코드 수정을 요청할 수 있습니다.'}
|
||||||
</p>
|
</p>
|
||||||
<button type="button" className="seval-ai-verify-close-btn" onClick={onClose}>
|
|
||||||
닫기
|
|
||||||
</button>
|
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return createPortal(content, document.body);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default StrategyEvaluationAiVerifyModal;
|
export default StrategyEvaluationAiVerifyPanel;
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user