mirror of
https://github.com/ZhuLinsen/daily_stock_analysis
synced 2026-09-20 10:53:33 +08:00
* feat(issue-1646): [feature]-建议调整下移动问股页面的布局 * fix(review-feedback-1697): 补齐移动端问股页面的截图/前后对比或等价可视证据,并同步 PR 描述中的前端验证结论 * fix(review-feedback-1697): PR 描述缺少移动端问股页变更截图或等价可视证据 * fix(review-feedback-1697): apps/dsa-web/src/pages/ChatPage.tsx 引入移动端策略选择默认收起/展开,这是用户可见 Web UI 变化 * fix(review-feedback-1697): apps/dsa-web/src/pages/ChatPage.tsx 改动了问股页移动端可见交互,PR * fix(review-feedback-1697): apps/dsa-web/src/pages/ChatPage.tsx 改动了问股页移动端可见交互,但 PR * fix(review-feedback-1697): docs/CHANGELOG.md 新增了“如截图不可获取,替代可视证据见测试用例”这类 PR 验收/审查过程说明
This commit is contained in:
@@ -2,6 +2,7 @@ import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import Markdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { ChevronDown, SlidersHorizontal } from 'lucide-react';
|
||||
import { cn } from '../utils/cn';
|
||||
import { agentApi } from '../api/agent';
|
||||
import { systemConfigApi } from '../api/systemConfig';
|
||||
@@ -154,6 +155,7 @@ const ChatPage: React.FC = () => {
|
||||
const [skills, setSkills] = useState<SkillInfo[]>([]);
|
||||
const [selectedSkillIds, setSelectedSkillIds] = useState<string[]>([]);
|
||||
const [showSkillDesc, setShowSkillDesc] = useState<string | null>(null);
|
||||
const [mobileSkillPickerOpen, setMobileSkillPickerOpen] = useState(false);
|
||||
const [expandedThinking, setExpandedThinking] = useState<Set<string>>(new Set());
|
||||
const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null);
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
@@ -593,6 +595,7 @@ const ChatPage: React.FC = () => {
|
||||
setIsFollowUpContextLoading(false);
|
||||
|
||||
setInput('');
|
||||
setMobileSkillPickerOpen(false);
|
||||
requestScrollToBottom('smooth');
|
||||
await startStream(payload, {
|
||||
skillNames: usedSkillNames,
|
||||
@@ -859,6 +862,10 @@ const ChatPage: React.FC = () => {
|
||||
</>
|
||||
);
|
||||
|
||||
const selectedSkillSummary = selectedSkillIds.length > 0
|
||||
? getSkillNames(selectedSkillIds).join('、')
|
||||
: '通用分析';
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="chat-workspace"
|
||||
@@ -1278,61 +1285,91 @@ const ChatPage: React.FC = () => {
|
||||
className="rounded-xl px-3 py-2 text-xs shadow-none"
|
||||
/>
|
||||
) : null}
|
||||
{skills.length > 0 && (
|
||||
<div className="flex flex-wrap items-start gap-x-5 gap-y-2">
|
||||
<span className="text-xs text-muted-text font-medium uppercase tracking-wider flex-shrink-0 mt-1">
|
||||
策略
|
||||
</span>
|
||||
<label className="flex items-center gap-1.5 text-sm cursor-pointer group mt-0.5">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="general-analysis"
|
||||
value=""
|
||||
checked={selectedSkillIds.length === 0}
|
||||
onChange={() => setSelectedSkillIds([])}
|
||||
className="chat-skill-checkbox"
|
||||
/>
|
||||
<span
|
||||
className={`transition-colors text-sm ${selectedSkillIds.length === 0 ? 'text-foreground font-medium' : 'text-secondary-text group-hover:text-foreground'}`}
|
||||
{skills.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
type="button"
|
||||
className="home-surface-button flex h-10 w-full items-center justify-between gap-3 rounded-xl px-3 text-left text-sm text-foreground md:hidden"
|
||||
aria-label={mobileSkillPickerOpen ? '收起策略选择' : '展开策略选择'}
|
||||
aria-expanded={mobileSkillPickerOpen}
|
||||
aria-controls="chat-skill-picker-panel"
|
||||
onClick={() => setMobileSkillPickerOpen((open) => !open)}
|
||||
>
|
||||
通用分析
|
||||
</span>
|
||||
</label>
|
||||
{skills.map((s) => {
|
||||
const checked = selectedSkillIdSet.has(s.id);
|
||||
const disabled = !checked && skillLimitReached;
|
||||
return (
|
||||
<label
|
||||
key={s.id}
|
||||
className={`flex items-center gap-1.5 cursor-pointer group relative mt-0.5 ${disabled ? 'opacity-60 cursor-not-allowed' : ''}`}
|
||||
onMouseEnter={() => setShowSkillDesc(s.id)}
|
||||
onMouseLeave={() => setShowSkillDesc(null)}
|
||||
>
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
<SlidersHorizontal className="h-4 w-4 flex-shrink-0" aria-hidden="true" />
|
||||
<span className="flex-shrink-0 font-medium">策略</span>
|
||||
<span className="truncate text-xs text-muted-text">{selectedSkillSummary}</span>
|
||||
</span>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
'h-4 w-4 flex-shrink-0 text-muted-text transition-transform',
|
||||
mobileSkillPickerOpen ? 'rotate-180' : '',
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
id="chat-skill-picker-panel"
|
||||
data-testid="chat-skill-picker-panel"
|
||||
className={cn(
|
||||
mobileSkillPickerOpen ? 'flex' : 'hidden',
|
||||
'max-h-40 flex-wrap items-start gap-x-5 gap-y-2 overflow-y-auto rounded-xl border border-white/6 bg-surface/25 px-3 py-2 md:flex md:max-h-none md:overflow-visible md:border-0 md:bg-transparent md:p-0',
|
||||
)}
|
||||
>
|
||||
<span className="text-xs text-muted-text font-medium uppercase tracking-wider flex-shrink-0 mt-1">
|
||||
策略
|
||||
</span>
|
||||
<label className="flex items-center gap-1.5 text-sm cursor-pointer group mt-0.5">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="skills"
|
||||
value={s.id}
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
onChange={() => toggleSkillSelection(s.id)}
|
||||
name="general-analysis"
|
||||
value=""
|
||||
checked={selectedSkillIds.length === 0}
|
||||
onChange={() => setSelectedSkillIds([])}
|
||||
className="chat-skill-checkbox"
|
||||
/>
|
||||
<span
|
||||
className={`transition-colors text-sm ${checked ? 'text-foreground font-medium' : 'text-secondary-text group-hover:text-foreground'}`}
|
||||
className={`transition-colors text-sm ${selectedSkillIds.length === 0 ? 'text-foreground font-medium' : 'text-secondary-text group-hover:text-foreground'}`}
|
||||
>
|
||||
{s.name}
|
||||
通用分析
|
||||
</span>
|
||||
{showSkillDesc === s.id && s.description && (
|
||||
<div className="skill-desc-tooltip">
|
||||
<p className="skill-title">{s.name}</p>
|
||||
<p>{s.description}</p>
|
||||
</div>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{skills.map((s) => {
|
||||
const checked = selectedSkillIdSet.has(s.id);
|
||||
const disabled = !checked && skillLimitReached;
|
||||
return (
|
||||
<label
|
||||
key={s.id}
|
||||
className={`flex items-center gap-1.5 cursor-pointer group relative mt-0.5 ${disabled ? 'opacity-60 cursor-not-allowed' : ''}`}
|
||||
onMouseEnter={() => setShowSkillDesc(s.id)}
|
||||
onMouseLeave={() => setShowSkillDesc(null)}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="skills"
|
||||
value={s.id}
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
onChange={() => toggleSkillSelection(s.id)}
|
||||
className="chat-skill-checkbox"
|
||||
/>
|
||||
<span
|
||||
className={`transition-colors text-sm ${checked ? 'text-foreground font-medium' : 'text-secondary-text group-hover:text-foreground'}`}
|
||||
>
|
||||
{s.name}
|
||||
</span>
|
||||
{showSkillDesc === s.id && s.description && (
|
||||
<div className="skill-desc-tooltip">
|
||||
<p className="skill-title">{s.name}</p>
|
||||
<p>{s.description}</p>
|
||||
</div>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeStockCode && (
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -442,6 +442,54 @@ describe('ChatPage', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('collapses the mobile skill picker by default and keeps selected skills when sending', async () => {
|
||||
mockGetSkills.mockResolvedValue({
|
||||
skills: [
|
||||
{ id: 'bull_trend', name: '趋势分析', description: '默认趋势' },
|
||||
{ id: 'ma_golden_cross', name: '均线金叉', description: '均线交叉' },
|
||||
],
|
||||
default_skill_id: 'bull_trend',
|
||||
});
|
||||
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/chat']}>
|
||||
<ChatPage />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
const mobileToggle = await screen.findByRole('button', { name: '展开策略选择' });
|
||||
const skillPanel = screen.getByTestId('chat-skill-picker-panel');
|
||||
expect(mobileToggle).toHaveAttribute('aria-expanded', 'false');
|
||||
expect(skillPanel).toHaveClass('hidden');
|
||||
|
||||
fireEvent.click(mobileToggle);
|
||||
|
||||
expect(screen.getByRole('button', { name: '收起策略选择' })).toHaveAttribute('aria-expanded', 'true');
|
||||
expect(skillPanel).not.toHaveClass('hidden');
|
||||
expect(skillPanel).toHaveClass('flex');
|
||||
|
||||
fireEvent.click(screen.getByRole('checkbox', { name: '均线金叉' }));
|
||||
fireEvent.change(screen.getByPlaceholderText(/分析 600519/), {
|
||||
target: { value: '分析 600519' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: '发送' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockStartStream).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: '分析 600519',
|
||||
skills: ['bull_trend', 'ma_golden_cross'],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
skillName: '趋势分析、均线金叉',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
expect(screen.getByRole('button', { name: '展开策略选择' })).toHaveAttribute('aria-expanded', 'false');
|
||||
expect(skillPanel).toHaveClass('hidden');
|
||||
});
|
||||
|
||||
it('omits skills when all concrete skills are cleared', async () => {
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/chat']}>
|
||||
|
||||
@@ -40,6 +40,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
||||
- [文档] 补充 AlphaSift 迁移与回退边界:明确 `ALPHASIFT_INSTALL_SPEC` 显式覆盖语义、`requirements.txt + DEFAULT_ALPHASIFT_INSTALL_SPEC` 与运行时兼容边界、以及回滚路径(关闭功能/完整 revert)说明,覆盖旧 pin 用户升级行为。
|
||||
|
||||
- [新功能] 个股分析历史成功保存后会从最终报告 best-effort 提取 `DecisionSignal` 决策信号,复用现有信号去重、计划质量计算和脱敏契约。
|
||||
- [改进] 问股页移动端策略选择改为默认收起的按钮入口,展开后仍可多选策略并在发送后自动收起,减少对对话内容的遮挡。
|
||||
|
||||
|
||||
## [3.22.0] - 2026-06-13
|
||||
|
||||
|
||||
Reference in New Issue
Block a user