Skip to content

Commit 73285c0

Browse files
committed
feat: show home setup status hint
1 parent d6e4699 commit 73285c0

8 files changed

Lines changed: 175 additions & 1 deletion

File tree

apps/dsa-web/src/api/__tests__/systemConfig.test.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
import { beforeEach, describe, expect, it, vi } from 'vitest';
22
import { systemConfigApi } from '../systemConfig';
33

4+
const get = vi.hoisted(() => vi.fn());
45
const post = vi.hoisted(() => vi.fn());
56

67
vi.mock('../index', () => ({
78
default: {
8-
get: vi.fn(),
9+
get,
910
post,
1011
put: vi.fn(),
1112
},
1213
}));
1314

1415
describe('systemConfigApi', () => {
1516
beforeEach(() => {
17+
get.mockReset();
1618
post.mockReset();
1719
post.mockResolvedValue({
1820
data: {
@@ -111,4 +113,33 @@ describe('systemConfigApi', () => {
111113
expect(result.attempts[0].errorCode).toBeNull();
112114
expect(result.attempts[0].httpStatus).toBe(200);
113115
});
116+
117+
it('loads first-run setup status with camelCase fields', async () => {
118+
get.mockResolvedValueOnce({
119+
data: {
120+
is_complete: false,
121+
ready_for_smoke: false,
122+
required_missing_keys: ['llm_primary'],
123+
next_step_key: 'llm_primary',
124+
checks: [
125+
{
126+
key: 'llm_primary',
127+
title: 'LLM 主渠道',
128+
category: 'ai_model',
129+
required: true,
130+
status: 'needs_action',
131+
message: '缺少主模型配置',
132+
next_step: '打开系统设置',
133+
},
134+
],
135+
},
136+
});
137+
138+
const result = await systemConfigApi.getSetupStatus();
139+
140+
expect(get).toHaveBeenCalledWith('/api/v1/system/config/setup/status');
141+
expect(result.isComplete).toBe(false);
142+
expect(result.nextStepKey).toBe('llm_primary');
143+
expect(result.checks[0].nextStep).toBe('打开系统设置');
144+
});
114145
});

apps/dsa-web/src/api/systemConfig.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
DiscoverLLMChannelModelsResponse,
77
ExportSystemConfigResponse,
88
ImportSystemConfigRequest,
9+
SetupStatusResponse,
910
SystemConfigConflictResponse,
1011
SystemConfigResponse,
1112
SystemConfigSchemaResponse,
@@ -144,6 +145,11 @@ export const systemConfigApi = {
144145
return toCamelCase<SystemConfigSchemaResponse>(response.data);
145146
},
146147

148+
async getSetupStatus(): Promise<SetupStatusResponse> {
149+
const response = await apiClient.get<Record<string, unknown>>('/api/v1/system/config/setup/status');
150+
return toCamelCase<SetupStatusResponse>(response.data);
151+
},
152+
147153
async validate(payload: ValidateSystemConfigRequest): Promise<ValidateSystemConfigResponse> {
148154
const response = await apiClient.post<Record<string, unknown>>(
149155
'/api/v1/system/config/validate',

apps/dsa-web/src/pages/HomePage.tsx

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
import type React from 'react';
22
import { useCallback, useEffect, useMemo, useState } from 'react';
33
import { useNavigate } from 'react-router-dom';
4+
import { systemConfigApi } from '../api/systemConfig';
45
import { ApiErrorAlert, ConfirmDialog, Button, EmptyState, InlineAlert } from '../components/common';
56
import { DashboardStateBlock } from '../components/dashboard';
67
import { StockAutocomplete } from '../components/StockAutocomplete';
78
import { HistoryList } from '../components/history';
89
import { ReportMarkdown, ReportSummary } from '../components/report';
910
import { TaskPanel } from '../components/tasks';
1011
import { useDashboardLifecycle, useHomeDashboardState } from '../hooks';
12+
import type { SetupStatusResponse } from '../types/systemConfig';
1113
import { getReportText, normalizeReportLanguage } from '../utils/reportLanguage';
1214

1315
const HomePage: React.FC = () => {
1416
const navigate = useNavigate();
1517
const [sidebarOpen, setSidebarOpen] = useState(false);
1618
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
19+
const [setupStatus, setSetupStatus] = useState<SetupStatusResponse | null>(null);
1720

1821
const {
1922
query,
@@ -55,8 +58,38 @@ const HomePage: React.FC = () => {
5558
useEffect(() => {
5659
document.title = '每日选股分析 - DSA';
5760
}, []);
61+
62+
useEffect(() => {
63+
let active = true;
64+
systemConfigApi.getSetupStatus()
65+
.then((status) => {
66+
if (active) {
67+
setSetupStatus(status);
68+
}
69+
})
70+
.catch(() => {
71+
if (active) {
72+
setSetupStatus(null);
73+
}
74+
});
75+
76+
return () => {
77+
active = false;
78+
};
79+
}, []);
80+
5881
const reportLanguage = normalizeReportLanguage(selectedReport?.meta.reportLanguage);
5982
const reportText = getReportText(reportLanguage);
83+
const setupNeedsAction = setupStatus ? !setupStatus.isComplete : false;
84+
const setupMissingLabels = useMemo(() => {
85+
if (!setupStatus) {
86+
return '';
87+
}
88+
const requiredNeedsAction = setupStatus.checks
89+
.filter((check) => check.required && check.status === 'needs_action')
90+
.map((check) => check.title);
91+
return requiredNeedsAction.slice(0, 3).join('、');
92+
}, [setupStatus]);
6093

6194
useDashboardLifecycle({
6295
loadInitialHistory,
@@ -235,6 +268,31 @@ const HomePage: React.FC = () => {
235268
</div>
236269
) : null}
237270

271+
{setupNeedsAction ? (
272+
<div className="px-3 pb-2 md:px-4">
273+
<InlineAlert
274+
variant="warning"
275+
title="基础配置未完成"
276+
message={
277+
setupMissingLabels
278+
? `还缺少 ${setupMissingLabels},完成后即可开始最小可用分析。`
279+
: '还缺少基础配置,完成后即可开始最小可用分析。'
280+
}
281+
action={(
282+
<Button
283+
type="button"
284+
variant="secondary"
285+
size="sm"
286+
onClick={() => navigate('/settings')}
287+
>
288+
去配置
289+
</Button>
290+
)}
291+
className="rounded-xl px-3 py-2 text-xs shadow-none"
292+
/>
293+
</div>
294+
) : null}
295+
238296
<div className="flex-1 flex min-h-0 overflow-hidden">
239297
<div className="hidden min-h-0 w-64 shrink-0 flex-col overflow-hidden pl-4 pb-4 md:flex lg:w-72">
240298
{sidebarContent}

apps/dsa-web/src/pages/__tests__/HomePage.test.tsx

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { MemoryRouter } from 'react-router-dom';
33
import { beforeEach, describe, expect, it, vi } from 'vitest';
44
import { analysisApi, DuplicateTaskError } from '../../api/analysis';
55
import { historyApi } from '../../api/history';
6+
import { systemConfigApi } from '../../api/systemConfig';
67
import { useStockPoolStore } from '../../stores';
78
import { getReportText, normalizeReportLanguage } from '../../utils/reportLanguage';
89
import HomePage from '../HomePage';
@@ -37,6 +38,12 @@ vi.mock('../../api/analysis', async () => {
3738
};
3839
});
3940

41+
vi.mock('../../api/systemConfig', () => ({
42+
systemConfigApi: {
43+
getSetupStatus: vi.fn(),
44+
},
45+
}));
46+
4047
vi.mock('../../hooks/useTaskStream', () => ({
4148
useTaskStream: vi.fn(),
4249
}));
@@ -74,6 +81,13 @@ describe('HomePage', () => {
7481
vi.clearAllMocks();
7582
navigateMock.mockReset();
7683
useStockPoolStore.getState().resetDashboardState();
84+
vi.mocked(systemConfigApi.getSetupStatus).mockResolvedValue({
85+
isComplete: true,
86+
readyForSmoke: true,
87+
requiredMissingKeys: [],
88+
nextStepKey: null,
89+
checks: [],
90+
});
7791
});
7892

7993
it('renders the dashboard workspace and auto-loads the first report', async () => {
@@ -157,6 +171,50 @@ describe('HomePage', () => {
157171
expect(screen.getByText(/ 600519 /).closest('[role="alert"]')).toBeInTheDocument();
158172
});
159173

174+
it('shows first-run setup gaps and links to settings', async () => {
175+
vi.mocked(historyApi.getList).mockResolvedValue({
176+
total: 0,
177+
page: 1,
178+
limit: 20,
179+
items: [],
180+
});
181+
vi.mocked(systemConfigApi.getSetupStatus).mockResolvedValue({
182+
isComplete: false,
183+
readyForSmoke: false,
184+
requiredMissingKeys: ['llm_primary', 'stock_list'],
185+
nextStepKey: 'llm_primary',
186+
checks: [
187+
{
188+
key: 'llm_primary',
189+
title: 'LLM 主渠道',
190+
category: 'ai_model',
191+
required: true,
192+
status: 'needs_action',
193+
message: '缺少主模型配置',
194+
},
195+
{
196+
key: 'stock_list',
197+
title: '自选股',
198+
category: 'base',
199+
required: true,
200+
status: 'needs_action',
201+
message: '缺少自选股',
202+
},
203+
],
204+
});
205+
206+
render(
207+
<MemoryRouter>
208+
<HomePage />
209+
</MemoryRouter>,
210+
);
211+
212+
expect(await screen.findByText('基础配置未完成')).toBeInTheDocument();
213+
expect(screen.getByText(/LLM /)).toBeInTheDocument();
214+
fireEvent.click(screen.getByRole('button', { name: '去配置' }));
215+
expect(navigateMock).toHaveBeenCalledWith('/settings');
216+
});
217+
160218
it('navigates to chat with report context when asking a follow-up question', async () => {
161219
vi.mocked(historyApi.getList).mockResolvedValue({
162220
total: 1,

apps/dsa-web/src/types/systemConfig.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,24 @@ export interface SystemConfigResponse {
8484
updatedAt?: string;
8585
}
8686

87+
export interface SetupStatusCheck {
88+
key: string;
89+
title: string;
90+
category: 'base' | 'ai_model' | 'agent' | 'notification' | 'system';
91+
required: boolean;
92+
status: 'configured' | 'inherited' | 'optional' | 'needs_action';
93+
message: string;
94+
nextStep?: string | null;
95+
}
96+
97+
export interface SetupStatusResponse {
98+
isComplete: boolean;
99+
readyForSmoke: boolean;
100+
requiredMissingKeys: string[];
101+
nextStepKey?: string | null;
102+
checks: SetupStatusCheck[];
103+
}
104+
87105
export interface ExportSystemConfigResponse {
88106
content: string;
89107
configVersion: string;

docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
2525
- [测试] 补充设置项帮助元数据、API schema、前端弹窗交互测试,并修复 Bot 名称路由与调度时间 provider 测试的离线 CI 稳定性问题。
2626
- [修复] 港股日线跳过不支持港股的内置历史数据源,避免港股代码错配到非港股市场数据。
2727
- [修复] 修正分析 API 对北交所 `BJ` 前缀与 `.BJ` 后缀股票代码的校验,保持前端自动补全与 Tushare `ts_code` 调用格式一致。
28+
- [改进] Web 首页接入首次启动配置状态,基础配置未完成时提示缺口并引导进入系统设置。
2829

2930
## [3.15.0] - 2026-05-05
3031

docs/full-guide.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,6 +1077,7 @@ FastAPI 提供 RESTful API 服务,支持配置管理和触发分析。
10771077

10781078
- 📝 **配置管理** - 查看/修改自选股列表
10791079
- 🚀 **快速分析** - 通过 API 接口触发分析
1080+
- 🧭 **首次配置提示** - 首页会读取只读配置状态,缺少 LLM 主渠道、自选股等基础项时提示缺口并引导进入系统设置
10801081
- 📊 **实时进度** - 分析任务状态实时更新,支持多任务并行;普通分析链路在进入 LLM 阶段后会优先尝试 LiteLLM 流式生成,并通过任务 SSE 回灌更细粒度的 `message/progress`
10811082
- 📈 **回测验证** - 评估历史分析准确率,查询方向胜率与模拟收益
10821083
- 🔗 **API 文档** - 访问 `/docs` 查看 Swagger UI

docs/full-guide_EN.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -923,6 +923,7 @@ FastAPI provides RESTful API service for configuration management and triggering
923923

924924
- **Configuration Management** - View/modify watchlist
925925
- **Quick Analysis** - Trigger analysis via API
926+
- **First-run Setup Hint** - The Home page reads the read-only setup status and points users to Settings when required items such as the primary LLM channel or watchlist are missing
926927
- **Real-time Progress** - Analysis task status updates in real-time, supports parallel tasks; the regular stock-analysis path now prefers LiteLLM streaming during the LLM stage and pushes finer-grained `message/progress` updates through task SSE
927928
- **Backtest Validation** - Evaluate historical analysis accuracy, query direction win rate and simulated returns
928929
- **API Documentation** - Visit `/docs` for Swagger UI

0 commit comments

Comments
 (0)