diff --git a/backend/features/mentor/agent.py b/backend/features/mentor/agent.py index f7c53fb..5109129 100644 --- a/backend/features/mentor/agent.py +++ b/backend/features/mentor/agent.py @@ -15,6 +15,11 @@ class MentorAgentError(RuntimeError): pass +FOLLOW_UP_START = "" +FOLLOW_UP_END = "" +MAX_FOLLOW_UP_LENGTH = 80 + + @dataclass(frozen=True) class MentorSkill: skill_id: str @@ -185,6 +190,8 @@ def stream_with_mentor( base_url: str, model: str, timeout: int = 90, + *, + follow_ups: list[str] | None = None, ) -> Iterator[str]: if not api_key or not model: raise MentorAgentError("LLM API Key 或模型尚未配置。") @@ -193,8 +200,10 @@ def stream_with_mentor( messages = [{"role": "system", "content": system_prompt}] messages.extend(history[-10:]) messages.append({"role": "user", "content": question}) + if follow_ups is not None: + follow_ups.clear() try: - yield from llm_transport.stream_chat_completion( + upstream = llm_transport.stream_chat_completion( api_key=api_key, base_url=base_url, model=model, @@ -202,6 +211,7 @@ def stream_with_mentor( timeout=timeout, user_agent="XiaobaiReviewWeb/0.6", ) + yield from _stream_answer_and_collect_follow_ups(upstream, follow_ups) except llm_transport.OpenAIEmptyResponseError as exc: raise MentorAgentError("问师模型未返回有效内容。") from exc except llm_transport.OpenAIHTTPError as exc: @@ -223,6 +233,10 @@ def _build_system_prompt(skill: MentorSkill, market_context: dict[str, Any]) -> 5. 优先回答用户真正的问题。市场分析通常按“判断、数据依据、思维模型下的应对、失效条件”组织;纯交易心理或方法问题可以自然回答,不强制套模板。 6. 保留该 Skill 的核心心智模型和表达节奏,但不要复述身份履历,不要宣称自己就是真人,不攻击或贬低用户。 7. 使用中文,信息密度高,避免空泛口号。引用数字时标明数据日期。 +8. 正文结束后必须输出2至3条与本轮问题和正文直接相关的追问。追问用于帮助用户继续核实条件、风险或失效边界,不得引入正文没有依据的新事实,不得给出无条件买卖指令。严格使用以下机器结构,不要放进Markdown代码块,结束标签后不要再输出文字: + +["追问一?","追问二?","追问三?"] + 网页市场数据: {context_json} @@ -233,6 +247,67 @@ def _build_system_prompt(skill: MentorSkill, market_context: dict[str, Any]) -> """.strip() +def _stream_answer_and_collect_follow_ups( + chunks: Iterator[str], follow_ups: list[str] | None +) -> Iterator[str]: + buffer = "" + collecting = False + for raw_chunk in chunks: + chunk = str(raw_chunk or "") + if not chunk: + continue + buffer += chunk + if collecting: + continue + marker_index = buffer.find(FOLLOW_UP_START) + if marker_index >= 0: + if marker_index: + yield buffer[:marker_index] + buffer = buffer[marker_index + len(FOLLOW_UP_START):] + collecting = True + continue + overlap = _marker_prefix_overlap(buffer, FOLLOW_UP_START) + emit_length = len(buffer) - overlap + if emit_length: + yield buffer[:emit_length] + buffer = buffer[emit_length:] + + if not collecting: + if buffer: + yield buffer + return + raw_follow_ups = buffer.split(FOLLOW_UP_END, 1)[0].strip() + parsed = _parse_follow_ups(raw_follow_ups) + if follow_ups is not None and len(parsed) >= 2: + follow_ups.extend(parsed) + + +def _marker_prefix_overlap(value: str, marker: str) -> int: + max_length = min(len(value), len(marker) - 1) + for length in range(max_length, 0, -1): + if value.endswith(marker[:length]): + return length + return 0 + + +def _parse_follow_ups(payload: str) -> list[str]: + try: + values = json.loads(payload) + except (TypeError, json.JSONDecodeError): + return [] + if not isinstance(values, list): + return [] + result: list[str] = [] + for value in values: + question = re.sub(r"\s+", " ", str(value or "")).strip() + if not question or len(question) > MAX_FOLLOW_UP_LENGTH or question in result: + continue + result.append(question) + if len(result) == 3: + break + return result + + def _parse_frontmatter(content: str) -> dict[str, str]: if not content.startswith("---"): return {} diff --git a/backend/features/mentor/service.py b/backend/features/mentor/service.py index ab1c0df..637d958 100644 --- a/backend/features/mentor/service.py +++ b/backend/features/mentor/service.py @@ -129,9 +129,10 @@ class MentorServiceMixin: def generate(): answer_parts: list[str] = [] + follow_ups: list[str] = [] events = self.llm_gateway.stream( "mentor", - f"mentor-skill-v1:{skill.skill_id}", + f"mentor-skill-v2:{skill.skill_id}", lambda profile: stream_with_mentor( skill, context, @@ -140,6 +141,7 @@ class MentorServiceMixin: profile.api_key, profile.base_url, profile.model, + follow_ups=follow_ups, ), (MentorAgentError,), ) @@ -160,6 +162,7 @@ class MentorServiceMixin: yield { "type": "meta", "data_trade_date": context["data_trade_date"], + "follow_ups": follow_ups or self._mentor_follow_up_fallback(question), "notice": "智能解读已自动切换可用服务。" if event.role == "fallback" else "", @@ -167,6 +170,27 @@ class MentorServiceMixin: return generate() + @staticmethod + def _mentor_follow_up_fallback(question: str) -> list[str]: + normalized = question.strip() + if any(keyword in normalized for keyword in ("风险", "亏损", "回撤", "止损")): + return [ + "这些风险最早会从哪些信号中暴露?", + "哪些变化会让当前风险判断失效?", + "如果风险继续扩大,仓位预案应如何调整?", + ] + if any(keyword in normalized for keyword in ("股票", "个股", "代码", "怎么看")): + return [ + "这个判断最关键的确认信号是什么?", + "哪些变化会让当前结论失效?", + "明日盘中应该优先观察哪些数据?", + ] + return [ + "这个判断最关键的确认依据是什么?", + "哪些变化会让当前结论失效?", + "下一步应该优先观察什么?", + ] + def mentor_messages(self, mentor_id: str, trade_date: str) -> list[dict[str, Any]]: mentor_id = validate_text(mentor_id, "问师角色", 100, required=True) trade_date = normalize_date(trade_date) diff --git a/config/architecture-inventory.json b/config/architecture-inventory.json index 1e68511..9cca123 100644 --- a/config/architecture-inventory.json +++ b/config/architecture-inventory.json @@ -370,29 +370,29 @@ } ], "css_layers": [ - "/shared/tokens.css?v=20260729-1", - "/shared/base.css?v=20260802-5", - "/shared/shell.css?v=20260802-5", - "/shared/auth.css?v=20260802-5", - "/shared/components/controls.css?v=20260802-5", - "/shared/components/navigation.css?v=20260802-5", - "/shared/components/cards.css?v=20260802-5", - "/shared/components/tables.css?v=20260802-5", - "/shared/components/dialogs.css?v=20260802-5", - "/shared/components/feedback.css?v=20260802-5", - "/pages/market/foundation.css?v=20260802-5", - "/pages/sentiment/foundation.css?v=20260802-5", - "/pages/pools/foundation.css?v=20260802-5", - "/pages/ladder/foundation.css?v=20260802-5", - "/pages/rotation/foundation.css?v=20260802-5", - "/pages/auction/foundation.css?v=20260802-5", - "/pages/themes/foundation.css?v=20260802-5", - "/pages/popularity/foundation.css?v=20260802-5", - "/pages/dragon-tiger/foundation.css?v=20260802-5", - "/pages/screener/foundation.css?v=20260804-1", - "/pages/mentor/foundation.css?v=20260802-5", - "/pages/heaven/foundation.css?v=20260804-3", - "/pages/review/foundation.css?v=20260802-5" + "/shared/tokens.css?v=20260806-2", + "/shared/base.css?v=20260806-1", + "/shared/shell.css?v=20260806-2", + "/shared/auth.css?v=20260806-1", + "/shared/components/controls.css?v=20260806-1", + "/shared/components/navigation.css?v=20260806-1", + "/shared/components/cards.css?v=20260806-1", + "/shared/components/tables.css?v=20260806-1", + "/shared/components/dialogs.css?v=20260806-1", + "/shared/components/feedback.css?v=20260806-1", + "/pages/market/foundation.css?v=20260806-1", + "/pages/sentiment/foundation.css?v=20260806-2", + "/pages/pools/foundation.css?v=20260806-2", + "/pages/ladder/foundation.css?v=20260806-1", + "/pages/rotation/foundation.css?v=20260806-1", + "/pages/auction/foundation.css?v=20260806-2", + "/pages/themes/foundation.css?v=20260806-1", + "/pages/popularity/foundation.css?v=20260806-1", + "/pages/dragon-tiger/foundation.css?v=20260806-1", + "/pages/screener/foundation.css?v=20260806-2", + "/pages/mentor/foundation.css?v=20260806-2", + "/pages/heaven/foundation.css?v=20260806-2", + "/pages/review/foundation.css?v=20260806-1" ], "frontend_composition": { "shell": "frontend/index.html", @@ -436,19 +436,24 @@ "code_hotspots": [ { "path": "frontend/pages/heaven/foundation.css", - "bytes": 185468, - "lines": 11721 + "bytes": 185936, + "lines": 11734 }, { "path": "frontend/pages/screener/foundation.css", - "bytes": 101609, - "lines": 6509 + "bytes": 102987, + "lines": 6565 }, { "path": "frontend/pages/heaven/page.js", "bytes": 97189, "lines": 2069 }, + { + "path": "frontend/shared/shell.css", + "bytes": 51978, + "lines": 3224 + }, { "path": "backend/features/heaven/engine.py", "bytes": 51764, @@ -456,13 +461,8 @@ }, { "path": "frontend/index.html", - "bytes": 44688, - "lines": 632 - }, - { - "path": "frontend/shared/shell.css", - "bytes": 40845, - "lines": 2825 + "bytes": 45846, + "lines": 643 }, { "path": "backend/features/screener/catalog.py", @@ -471,8 +471,8 @@ }, { "path": "frontend/pages/auction/foundation.css", - "bytes": 32995, - "lines": 2317 + "bytes": 34990, + "lines": 2409 }, { "path": "database.py", @@ -644,6 +644,11 @@ "bytes": 6202, "lines": 141 }, + { + "path": "frontend/pages/mentor/page.html", + "bytes": 6190, + "lines": 89 + }, { "path": "backend/features/screener/selection.py", "bytes": 6092, @@ -659,11 +664,6 @@ "bytes": 5690, "lines": 124 }, - { - "path": "frontend/pages/mentor/page.html", - "bytes": 5501, - "lines": 72 - }, { "path": "backend/data/providers/tushare_indices.py", "bytes": 5451, diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md new file mode 100644 index 0000000..d793cb1 --- /dev/null +++ b/docs/HANDOFF.md @@ -0,0 +1,320 @@ +# 小白复盘项目交接说明 + +> 核实日期:2026-08-06(Asia/Shanghai) +> 正式源码边界:`webapp/app/` +> 产品行为基准:`docs/product/小白复盘-完整产品规格说明书.md` + +本文件不是聊天摘要。内容以当前仓库、配置注册表、测试、Git 状态和产品规格交叉核实为准。后续维护者应先阅读根目录 `AGENTS.md`、`ARCHITECTURE.md`、本文件和产品规格,再修改代码。 + +## 0. 状态口径与证据 + +本文使用四种状态,不能混用: + +- **已实现**:当前正式源码中存在对应实现。 +- **自动验证通过**:有测试或注册表检查证明,不等同于人工视觉验收。 +- **人工已验收**:用户已经确认迁移后的正式 `app/` 在功能和视觉上与迁移前等价;该结论只覆盖当时基线。 +- **待验收/待实现**:代码尚未完成,或虽已写入工作区但尚未取得本轮人工确认和 Git 回档点。 + +### 0.1 Git 与运行快照 + +- 分支:`main`。 +- 当前提交:`bd97ba1 feat: unify trading workspace visual system`。 +- `HEAD` 与 `origin/main` 一致;远端为内部 Gitea 仓库。 +- 生成本文前工作区已有 37 个修改文件,约 `2490` 行新增、`2958` 行删除,主要是全站视觉调整和最新问师改造;这些改动不是本文创建的,禁止丢弃。 +- 生成本文时 `8797` 端口没有监听进程,因此实时数据源和 LLM 的运行可用性没有通过在线健康检查确认。 +- 当前正式数据库为 `data/review.db`,使用 SQLite WAL;数据库、`.env`、Token、私有 Skill、日志和运行产物不进入 Git。 +- 本轮文档生成后的自动验证结果见本文末尾“验证记录”。 + +## 1. 项目目标和当前状态 + +### 1.1 项目目标 + +小白复盘是面向 A 股盘后复盘和盘前观察的本地/局域网 Web 工作台。目标不是自动交易,而是把真实行情、市场情绪、涨跌停结构、集合竞价、板块题材、选股、思维模型问答、传统文化观察和个人复盘放在一套可追溯、可复现、账号隔离的系统中。 + +产品必须坚持以下底线: + +1. 不使用演示行情冒充真实数据,不静默混用日期、单位、复权或数据源。 +2. 计算型数据缺失时失败关闭;公开网页源只允许作为已登记的展示兜底。 +3. 阶段、策略筛选、情绪、观势取象和六爻排盘由确定性程序完成;LLM 只编译自然语言条件或解释确定性结果。 +4. 用户自选、复盘、交易日志、问师/问天历史等私有数据必须按账号隔离。 +5. PC 端优先达到稳定、精致、可长期维护;移动端必须独立设计,不能把 PC 页面简单压缩。 + +### 1.2 当前状态 + +正式版本已经从历史混乱目录保真迁入 `webapp/app/`,用户已人工确认迁移本身在功能和视觉上成功。项目已经完成模块化单体边界、页面碎片化、数据网关、LLM 网关、后台任务、数据库迁移、注册表和统一验收工具等结构治理。 + +当前不是“从零重写”状态,也不应再次从旧根目录或失败的 `next/` 复制实现。现阶段属于: + +- 核心 PC 产品可用,16 个主工作区均有正式实现。 +- 当前工作区正在进行全站 PC 视觉一致性调整,以及问师经典 QQ 式三栏界面和动态追问能力;自动化测试已覆盖,尚待本轮人工视觉验收和提交。 +- 移动端明确暂停,当前存在样式但不能据此宣称可用。 +- 完整 IC 动态加权、稳定宏观/政策/隔夜消息、分析师一致预期、Level-2 等依赖数据与算法的能力尚未完成。 +- 局域网单实例是当前部署边界;公网多实例能力不属于当前完成范围。 + +## 2. 技术架构与主要目录 + +### 2.1 总体架构 + +项目采用**模块化单体**:一个 Python 进程、一个 SQLite WAL 数据库、无构建工具的 HTML/CSS/JavaScript 前端。 + +```text +Browser + -> frontend/shared/api.js + -> backend/http + backend/features//routes.py + -> feature service + -> Repository / DataGateway / LLMGateway + -> SQLite / Tushare / iFinD / display-only providers / LLM provider + +Scheduler + -> backend/jobs + -> 同一套 feature service / repository / gateway +``` + +该结构适合当前局域网单实例产品:部署简单、数据本地、回档直接,同时通过领域边界避免再次退化成单文件应用。除非进入公网多实例阶段,不要提前引入微服务、消息队列或前端构建框架。 + +### 2.2 主要目录 + +| 路径 | 唯一职责 | +|---|---| +| `server.py` | 稳定启动/导入门面 | +| `backend/bootstrap/` | 配置、依赖组装、启动与组合根 | +| `backend/http/` | 鉴权、请求 ID、JSON/NDJSON、静态文件、流式连接和统一异常 | +| `backend/features/` | 按账户、市场、选股、问师、问天、复盘等领域组织业务、路由和 Repository | +| `backend/data/` | `DataGateway`、数据源策略、来源/日期/单位/新鲜度/覆盖率质量门 | +| `backend/data/providers/` | Tushare、iFinD 等供应商适配;不得由业务模块直接调用 | +| `backend/database/` | SQLite 连接、顺序迁移和 Repository 组合 | +| `backend/jobs/` | 行情刷新、盘后选股、事件补充的锁、状态、幂等和重试 | +| `backend/llm/` | 模型选择、会员/额度、主辅回退、流式协议、取消和审计 | +| `frontend/index.html` | 登录层、全站 Shell、摘要条、状态栏、全局弹窗和唯一页面挂载点 | +| `frontend/shared/` | 唯一 API 出口、状态、Shell、会话、主题和公共组件 | +| `frontend/pages/` | 页面局部 `page.html`、`page.js`、`foundation.css` | +| `config/` | 页面、功能、API、数据字段、质量和任务注册表 | +| `data/` | 正式数据库与私有数据,不入 Git | +| `runtime/` | 日志、PID、缓存、测试结果,不入 Git | +| `tests/` | Python 单元/边界/契约测试与 Playwright 浏览器回归 | +| `tools/` | 启动、注册表生成、架构清单和统一验收工具 | +| `docs/` | 产品规格、维护、治理、历史迁移和当前交接/Issue | + +### 2.3 注册表和运行事实 + +- `config/pages.config.json`:16 个主页面,默认页为情绪周期。 +- `config/features.config.json`:20 个功能及 `public/authenticated/member/admin` 权限。 +- `config/api.config.json`:当前 53 个精确 API 路径和 11 个正则路径,由工具生成并校验。 +- `config/jobs.config.json`:行情刷新、15:10 后盘后选股、iFinD 事件补充三类任务。 +- `config/data-fields.config.json`:数据源与字段用途;Tushare/iFinD 可进入已登记计算,东方财富/腾讯只允许展示,未解决数据集显式阻塞。 +- `config/data-quality.config.json`:单位、覆盖率、新鲜度和失败关闭规则。 +- `config/architecture-inventory.json`:生成的架构清单和代码热点,不应手工编造。 + +### 2.4 数据源边界 + +| 数据源 | 当前角色 | 约束 | +|---|---|---| +| Tushare | 交易日、股票主数据、日线、估值、财务、资金、申万行业、涨跌停、最终竞价、热榜、龙虎榜等主要计算数据 | 按接口权限和质量门使用 | +| iFinD | 动态竞价、展示型日 K/分时和盘后事件补充 | 凭据/授权到期时必须显式不可用,不得伪造 | +| 东方财富/腾讯 | 分时或实时指数的展示观察兜底 | 不得静默进入情绪、选股或问天计算 | +| Local | 情绪等确定性派生结果 | 保存算法/输入版本,保证可复现 | +| unresolved | 分析师一致预期、Level-2 | 当前阻塞,不能用名称或空字段冒充实现 | + +## 3. 已完成功能 + +以下表示当前正式源码存在实现;人工视觉结论仅继承用户对迁移基线的确认,不覆盖本轮未提交视觉改动。 + +### 3.1 全局与账户 + +- 注册、登录、退出、首账号管理员、普通/会员/管理员权限。 +- 个人资料、生辰资料、修改密码、会员状态、系统管理与公共凭据配置。 +- 顶栏日期、默认最近真实交易日、情绪摘要条、日间/夜间、全局搜索、提醒中心。 +- 股票、题材、板块、指数详情;日 K/分时与代码/题材悬浮预览。 +- 统一 Toast、弹窗、空态、加载、错误转换和页面生命周期基础设施。 + +### 3.2 市场复盘页面 + +- 情绪周期:温度、阶段、方向、置信度、构成、趋势和交易日明细。 +- 涨停池、炸板池、跌停池、昨日涨停、涨停表现。 +- 市场天梯、板块轮动与成分股联动。 +- 集合竞价:盘前状态、9:25 最终筛选、普通异动/一字板、成交额对比和自选。 +- 题材库、人气热榜、龙虎榜和游资名录/详情基础能力。 + +### 3.3 智能选股 + +- 六阶段盘后候选、29 套精选策略、策略适用说明和确定性候选结果。 +- 自定义公式 DSL、自然语言编译公式、因子与权重手动配置。 +- 候选按策略/日期隔离,盘后自动发布最近完整交易日结果。 +- 用户手动加入五交易日策略跟踪,T+1/T+3/T+5 反馈和幂等提醒。 +- 数据缺失、无符合条件、任务失败等状态区分。 +- 当前多因子为基础动态版;完整 IC 版不在“已完成”范围内。 + +### 3.4 问师与 LLM + +- 公共/管理员私有思维模型 Skill 注册、证据等级、关注维度和排序偏好。 +- 按账号、模型、交易日隔离对话;最多带入最近 10 条历史。 +- 按模型类型提供不同市场上下文,识别个股时追加有限标的数据。 +- 统一 LLM 会员/额度、主辅回退、流式去重、停止生成、审计和安全错误。 +- 当前工作区已经实现经典 QQ 式联系人/会话/资料三栏和同次调用动态追问;状态为“自动验证通过、待人工验收和提交”,详见 Issue 001。 + +### 3.5 问天 + +- 观势:真实行情安全门、三才六爻、势值、本卦/之卦、客观数据补录与恢复自动数据。 +- 观气:历法、节气、中运/司天在泉/主客气、个人合参、五行行业取象和每日解运持久化。 +- 观心:交易/心境/无题预设、呼吸流程、六次铜钱起卦、第一念、京房纳甲/八宫世应/六亲/六神/旬空等确定性排盘。 +- 本地知识检索、答案一致性校验和 LLM 解释;LLM 不起卦、不修改程序结果。 + +### 3.6 个人复盘 + +- 账号私有自选追踪、个股笔记、三个独立输入框的每日复盘及历史。 +- 结构化交易日志、编辑删除、胜率/盈亏/仓位统计。 +- 复盘助手流式对话,读取共享市场和当前用户记录,不执行交易。 +- 手工提醒、已读状态、策略跟踪 T+1/T+5 自动提醒和幂等去重。 + +### 3.7 工程治理 + +- 正式源码独立于父目录旧程序和失败 `next/`。 +- 页面结构、行为和样式已按领域拆分;浏览器请求统一经过 `frontend/shared/api.js`。 +- Tushare 大客户端、智能选股、问天、市场洞察和 HTTP 层已拆成职责明确的模块门面。 +- 有正式数据库 migration、数据/LLM/job 网关、API/功能/页面/数据注册表。 +- 统一验收工具覆盖 Python、注册表、JS 语法、Git 空白、SQLite 完整性和可选 Playwright。 + +## 4. 尚未完成的功能 + +每项均有独立 Issue,Issue 状态优先于历史聊天中的阶段编号。 + +| Issue | 状态 | 优先级 | 未完成内容 | +|---|---|---:|---| +| [ISSUE-001](issues/ISSUE-001-finalize-mentor-redesign.md) | 待人工验收/提交 | P0 | 问师三栏界面、停止生成和动态追问收口 | +| [ISSUE-002](issues/ISSUE-002-checkpoint-current-pc-visual-work.md) | 待审查/提交 | P0 | 当前全站 PC 视觉改动的逐页验收、拆分和回档点 | +| [ISSUE-003](issues/ISSUE-003-mobile-redesign.md) | 明确延期 | P2 | 独立移动 Shell、逐页信息架构和触控交互 | +| [ISSUE-004](issues/ISSUE-004-full-ic-multifactor.md) | 未实现 | P1 | 12 个月 Rank IC、季度重算、中性化和前 5% 输出 | +| [ISSUE-005](issues/ISSUE-005-policy-macro-overnight-data.md) | 数据源未定 | P1 | 稳定政策/宏观/隔夜消息序列与竞价量化 | +| [ISSUE-006](issues/ISSUE-006-analyst-consensus-data.md) | 数据阻塞 | P2 | 一致预期、预测修正、评级/目标价等字段 | +| [ISSUE-007](issues/ISSUE-007-level2-auction.md) | 授权阻塞 | P2 | Level-2 委托队列、逐笔和动态竞价深度 | +| [ISSUE-008](issues/ISSUE-008-hot-money-profile-history.md) | 低优先级 | P3 | 游资档案的更完整历史画像和归类质量 | +| [ISSUE-009](issues/ISSUE-009-documentation-status-drift.md) | 待整理 | P1 | 活跃文档/注册表中移动端、端口和验收状态漂移 | +| [ISSUE-010](issues/ISSUE-010-live-provider-llm-readiness.md) | 待运行核验 | P0 | 启动正式服务并验证数据源、iFinD、LLM 与任务健康 | +| [ISSUE-011](issues/ISSUE-011-public-deployment-hardening.md) | 未来范围 | P3 | 公网多实例、TLS、PostgreSQL、队列、缓存和集中监控 | + +明确不是待办:问师自主联网取数当前已因风险高于收益而延期;全能金融爬虫 Skill 已放弃;旧 `next/` 已冻结失败;不要把这些内容重新加入实现。 + +## 5. 已知问题与风险 + +### 5.1 用户可见问题 + +1. **移动端整体不可用或交互较差。** 当前存在大量媒体查询和 `mobile_layout: dedicated` 注册值,但这只证明代码存在,不证明通过人工可用性验收。 +2. **当前问师与全站视觉改动未完成交付闭环。** 自动化已通过,但工作区未提交,且用户尚未对本轮 QQ 式问师界面进行视觉确认。 +3. **实时数据和 LLM 当前在线状态未知。** 生成本文时 8797 未启动;外部服务还受本机网络、系统凭据、接口权限和 iFinD 授权有效期影响。 +4. **缺失数据不能被误显示为无信号。** 分析师一致预期、Level-2 和部分宏观/新闻数据目前无正式来源;相关策略或页面必须显示数据缺失/阻塞。 + +### 5.2 维护风险 + +- `frontend/pages/heaven/foundation.css` 约 11,734 行、`frontend/pages/screener/foundation.css` 约 6,565 行、`frontend/shared/shell.css` 约 3,224 行;它们是当前最大 CSS 热点。没有具体回归证据时不得为了“减行数”盲拆。 +- `frontend/pages/heaven/page.js` 约 2,069 行、`backend/features/heaven/engine.py` 约 1,183 行,问天仍是高复杂度领域。 +- 根 `database.py` 仍是历史 schema/Repository 组合锚点,不是新增业务查询的位置;继续向其中加功能会破坏治理结果。 +- 自动化测试不能替代产品规格第 25 至 27 节的全矩阵人工验收,尤其是外部真实数据、LLM、日夜主题、1080P/4K 和移动端。 +- 当前脏工作区横跨 37 个文件。提交前必须按功能拆分或至少留下清晰回档说明,不能把无关改动混成无法审计的大提交。 + +## 6. 已作出的重要技术决策及原因 + +| 决策 | 原因 | +|---|---| +| `webapp/app/` 是唯一正式源码 | 已完成保真迁移并人工确认;避免继续依赖父目录旧代码或失败 `next/` | +| 保持模块化单体 | 当前局域网单实例用一个进程和 SQLite 最简单;领域边界已经足以控制复杂度 | +| 不更换技术栈,前端保持无构建 HTML/CSS/JS | 迁移目标是整理和减法,不是重拍功能;减少部署与人工维护成本 | +| 页面、功能、API、数据和任务采用注册表 | 防止入口散落、权限漂移和“代码有但系统不知道” | +| 浏览器 API、外部数据和 LLM 各自只有一个网关出口 | 统一鉴权、错误、质量、额度、降级和审计 | +| 计算数据失败关闭,展示兜底隔离 | 防止公开网页源或旧快照静默污染情绪、选股、竞价和问天结果 | +| 智能选股由条件和数据确定执行,LLM 只编译公式 | 保证同日期同策略可复现,避免刷新结果漂移 | +| 问天确定性引擎负责历法/卦象,LLM 只解释 | 结果可复现、可测试,避免模型改卦或编造事实 | +| 问师外部工具自主取数暂缓 | 当前缺少成熟权限、来源和失败边界,风险高于收益 | +| 放弃通用金融爬虫 Skill | 网页规则不稳定、版权/安全/口径不可控,不适合进入正式计算链 | +| 移动端暂停并要求独立设计 | 密集 PC 表格不能靠压缩获得可用手机体验;先保证 PC 功能与视觉 | +| 不确定代码默认保留,删除需扫描、差异测试和人工验收 | 防止“减法”误删隐含功能;历史迁移日志用于回档证据 | +| 公网能力不提前实现 | 当前用户场景是本地/局域网;多实例、PostgreSQL 和队列应由真实部署需求驱动 | + +## 7. 当前正在处理的事项 + +### 7.1 问师改造 + +当前未提交代码已经完成: + +- 经典 QQ 式 PC 三栏结构:联系人、对话、当前模型资料/证据。 +- 动态追问:模型在同一次输出末尾返回 `` 机器块;服务端剥离机器块,并在最终 NDJSON `meta.follow_ups` 返回 2 至 3 条建议。 +- 动态追问不额外调用 LLM、不重复扣额度;点击只预填输入框。 +- 停止生成控制、Enter 发送、流式占位与回答状态。 +- 问师 CSS 从历史约 2,800 行收敛到约 988 行。 + +相关文件: + +- `backend/features/mentor/agent.py` +- `backend/features/mentor/service.py` +- `frontend/pages/mentor/page.html` +- `frontend/pages/mentor/page.js` +- `frontend/pages/mentor/foundation.css` +- `tests/test_mentor_stream.py` +- `tests/e2e/app-shell.spec.js` + +尚缺:启动正式服务、接入真实 LLM 做一次端到端验证、用户人工确认日间/夜间及 1080P/4K 视觉、建立提交并推送回档点。 + +### 7.2 当前全站视觉改动 + +工作区还包含 Shell、设计令牌、公共组件以及市场、情绪、股池、天梯、轮动、竞价、题材、热榜、龙虎榜、选股、问天、复盘等页面样式改动。它们已进入自动化回归,但尚未形成独立验收结论。移动端已被产品决策暂停,因此不能因为这些 CSS 中存在移动规则就标记移动端完成。 + +## 8. 推荐的后续执行顺序 + +1. **先恢复运行环境并核验外部能力。** 启动 8797,检查健康、登录、最近真实交易日、Tushare/iFinD、LLM 主辅模型和后台任务;不通过时先解决 Issue 010。 +2. **人工验收问师。** 完成 Issue 001 的真实 LLM、流式、停止、动态追问、日夜和分辨率检查。 +3. **审查当前全站视觉差异。** 按 16 页逐页检查 Issue 002,确认哪些是 PC 正式改动、哪些是已暂停移动尝试,保持功能等价。 +4. **建立回档点。** 将问师和全站视觉按可审计边界提交并推送,不夹带密钥、数据库或运行产物。 +5. **清理活跃文档状态漂移。** 完成 Issue 009,使 README、注册表和交接状态不再暗示移动端已验收。 +6. **先补可获得的高价值数据,再升级算法。** 先确定 Issue 005/006 的合法稳定来源,再实施 Issue 004;没有完整历史覆盖时不能伪造 IC。 +7. **有正式授权后再做 Level-2。** Issue 007 不能用普通快照模拟。 +8. **低优先级完善游资档案。** Issue 008 不应阻塞市场、选股、问师和问天稳定性。 +9. **PC 稳定后才重启移动端设计。** Issue 003 必须单独打样和逐页人工验收。 +10. **确定公网商业化再做部署升级。** Issue 011 需要单独架构决策和迁移方案。 + +## 9. 每项任务的验收标准 + +本节是交接总表;独立 Issue 内给出更具体的范围和命令。产品规格第 25 至 27 节固定案例仍是最终依据。 + +| 任务 | 必须满足的验收标准 | +|---|---| +| Issue 001 问师收口 | 真实 LLM 只输出一份正文;同次调用出现 2 至 3 条有效追问;点击只预填;停止后不上演回退重放;无额外额度;日夜、1080P/4K 人工通过 | +| Issue 002 PC 视觉回档 | 16 页日间/夜间、1920×1080、4K 无白块、遮挡、双滚动和功能回归;当前差异可解释;提交可独立回退 | +| Issue 003 移动端 | 320/375/390/430/768 及横屏无页面横溢;底部五入口、市场子导航、弹窗/抽屉、宽表和键盘交互可用;用户逐页验收 | +| Issue 004 完整 IC | 行业内去极值、z-score、行业/市值中性化、过去 12 月下期收益 Rank IC、季度重算、前 5% 均有版本化确定性测试;无未来函数;UI 明确基础/IC 模式 | +| Issue 005 政策宏观隔夜 | 合法稳定来源、字段/单位/时间/版权/新鲜度登记完整;历史归档可复现;缺失显式失败;消息只按确认规则进入竞价量化 | +| Issue 006 一致预期 | 五类字段有 point-in-time 历史、公告时点和覆盖率;策略缺数据与无命中可区分;回测无未来函数 | +| Issue 007 Level-2 | 有正式授权;委托队列/逐笔/快照时间可追溯;盘中断线不伪造;与 9:25 最终归档区分;回放测试通过 | +| Issue 008 游资画像 | 名录、别名、席位归类和历史操作可追溯;未知席位保留;同名误合并有回归测试;左名录右详情无超长弹窗 | +| Issue 009 文档漂移 | 活跃文档、端口、移动端状态、完成状态与注册表一致;历史迁移文档明确只作审计,不被当运行说明;文档链接有效 | +| Issue 010 在线就绪 | `/api/health` 可达;登录和最近真实快照正常;数据源与 LLM 分别可诊断;失效凭据不泄露;重启后任务与结果不重复 | +| Issue 011 公网部署 | 完成 ADR;TLS、可信 Host、限流、集中密钥、审计、备份恢复、多实例数据库和任务互斥全部通过;不破坏局域网数据边界 | + +### 9.1 通用自动验收 + +```powershell +cd C:\Users\MoBai\Documents\gupiaofupan\webapp\app +python tools/verify_baseline.py +python tools/verify_baseline.py --e2e +git diff --check +``` + +### 9.2 通用人工验收 + +- 普通、会员、管理员三种权限。 +- 正常、有数据为空、数据缺失、上游失败、请求超时、最近快照九类状态。 +- 日间、夜间、1920×1080、3840×2160;移动 Issue 开始后再加入完整移动视口矩阵。 +- 真实行情日期与图表一致;开盘前不制造当天空 K 线。 +- 用户甲乙的自选、复盘、日志、对话、问天历史互不可见。 +- 所有保存/删除/添加只出现可关闭的规范反馈,不出现超长空弹窗。 +- 密钥、数据库、日志和私有 Skill 不进入 Git diff。 + +## 10. 验证记录 + +2026-08-06 本轮结果;后续代码变化后不能沿用: + +- Python:326 个测试全部通过(约 11.8 秒)。 +- Playwright:49 个测试全部通过(约 2.2 分钟)。 +- API 注册表与架构清单:均为 current。 +- JavaScript:统一工具枚举的全部 `.js/.mjs` 均通过 `node --check`。 +- SQLite:`data/review.db` 的 `PRAGMA integrity_check` 为 `ok`,验证时大小为 455,434,240 字节。 +- Git:`git diff --check` 通过。 +- 说明:组合命令在本代理的 120 秒命令上限处被终止于 Playwright 阶段;Playwright 随后以同一配置单独运行并完整通过,因此上述各子项均有本轮实际结果。 diff --git a/docs/README.md b/docs/README.md index f0bf340..f333b8b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,9 +1,11 @@ # 文档索引 - `product/小白复盘-完整产品规格说明书.md`:从零恢复产品时的完整功能与行为资产。 +- `HANDOFF.md`:当前仓库、架构、完成度、风险和后续验收的交接基线。 +- `issues/README.md`:尚未完成事项的独立 Issue 索引;Issue 不是已创建的 Gitea 工单。 - `maintenance/人工维护指南.md`:当前正式源码的启动、修改、验收、数据和回退流程。 - `governance/`:架构决策、注册表治理和历次结构治理记录。 - `migration/`:从旧根目录保真迁入`app/`的历史账本、证据和失败版本记录。 -日常维护优先阅读根目录`AGENTS.md`、`ARCHITECTURE.md`和维护指南。`migration/`只用于审计与 +日常维护优先阅读根目录`AGENTS.md`、`ARCHITECTURE.md`、`HANDOFF.md`和维护指南。`migration/`只用于审计与 追溯,不参与应用启动、测试选择或运行时路径解析。 diff --git a/docs/issues/ISSUE-001-finalize-mentor-redesign.md b/docs/issues/ISSUE-001-finalize-mentor-redesign.md new file mode 100644 index 0000000..0926930 --- /dev/null +++ b/docs/issues/ISSUE-001-finalize-mentor-redesign.md @@ -0,0 +1,42 @@ +# ISSUE-001:问师界面与动态追问收口 + +- 状态:待人工验收/提交 +- 优先级:P0 +- 来源:当前工作区问师改造、产品规格 L06、14.3 + +## 目标 + +收口当前未提交的经典 QQ 式 PC 三栏问师界面和同次 LLM 调用动态追问,保持既有权限、流式、额度、对话隔离和错误回退行为。 + +## 已有实现 + +- 联系人、会话、资料/证据三栏。 +- `` 机器块剥离,最终 NDJSON `meta.follow_ups` 返回 2 至 3 条。 +- 点击追问只预填输入框;停止按钮不重新播放答案;没有第二次业务 LLM 调用。 +- 相关 Python 和 Playwright 测试已加入工作区。 + +## 范围外 + +不在本 Issue 内加入问师自动联网取数、金融爬虫、真人身份暗示或新的交易建议能力。 + +## 依赖 + +本地 LLM 主/辅助凭据、会员账号、可用的真实市场快照,以及 Issue 002 的公共 Shell 视觉状态。 + +## 验收标准 + +1. 会员可以切换模型、加载历史、发送问题、停止生成、清空当前会话。 +2. 流式正文只出现一次;主模型首字前失败最多辅助回退一次;已输出后失败不重放。 +3. 成功回答显示 2 至 3 条相关追问;点击不自动发送、不增加额度;下一轮、换模型、清空后旧追问消失。 +4. 追问结构无效、回答失败或用户停止时不显示追问。 +5. 日间/夜间及 1920×1080、3840×2160 无白边、遮挡、超长弹窗和输入区不可达。 +6. 普通用户不能发起 LLM 调用;账号甲乙对话互不可见。 + +## 验证 + +```powershell +python -m unittest tests.test_mentor_stream tests.test_llm_stream tests.test_llm_gateway +python tools/verify_baseline.py --e2e +``` + +通过人工验收后单独提交并推送,记录回档提交号。 diff --git a/docs/issues/ISSUE-002-checkpoint-current-pc-visual-work.md b/docs/issues/ISSUE-002-checkpoint-current-pc-visual-work.md new file mode 100644 index 0000000..6a26712 --- /dev/null +++ b/docs/issues/ISSUE-002-checkpoint-current-pc-visual-work.md @@ -0,0 +1,35 @@ +# ISSUE-002:当前 PC 全站视觉改动验收与回档 + +- 状态:待审查/提交 +- 优先级:P0 +- 来源:工作区现有 37 个修改文件和 `bd97ba1` 之后的视觉调整 + +## 目标 + +逐页确认当前公共 Shell、令牌、夜间模式和 16 个页面的视觉修改,保留用户认可的 PC 变化,拆出或回退无关变化,并建立可以人工回档的提交。 + +## 范围 + +检查 `frontend/shared/`、所有 `frontend/pages/*/foundation.css`、页面 HTML/JS、架构清单和对应 E2E 测试。重点是夜间白块、边距/滚动、表格对齐、弹窗层级、图表背景和问师三栏。 + +## 范围外 + +不以本 Issue 重写业务算法,不删除未确认的旧代码,不开始移动端独立设计;移动端另见 Issue 003。 + +## 验收标准 + +1. 16 个页面均可从默认情绪周期进入,功能和权限与提交前一致。 +2. 日间/夜间主题一次切换完成,不出现白闪、白色表头/搜索框/弹窗或 Hover 反色不可读。 +3. 1920×1080 和 3840×2160 下页面内容、固定状态栏、全页滚动和局部滚动符合规格,无双滚动冲突。 +4. 交易日明细、股池、天梯、轮动、竞价、题材、龙虎榜和选股的列宽/对齐/空态可读。 +5. 每个修改文件都有明确原因;提交不包含 `.env`、数据库、日志、缓存、截图或私有 Skill。 +6. 自动化、人工验收和 Git diff 检查均通过,提交可独立回退。 + +## 验证 + +```powershell +python tools/verify_baseline.py --e2e +git diff --check +``` + +人工截图至少覆盖日间/夜间、1920×1080、3840×2160;验收结论写回 `docs/HANDOFF.md`。 diff --git a/docs/issues/ISSUE-003-mobile-redesign.md b/docs/issues/ISSUE-003-mobile-redesign.md new file mode 100644 index 0000000..93b6c90 --- /dev/null +++ b/docs/issues/ISSUE-003-mobile-redesign.md @@ -0,0 +1,26 @@ +# ISSUE-003:移动端独立重设 + +- 状态:明确延期,PC 稳定前不启动 +- 优先级:P2 +- 来源:产品规格第 20 节;当前移动端人工验收未通过 + +## 目标 + +在不改变 PC DOM、API、权限、数据口径和业务结果的前提下,单独设计移动 Shell、底部五入口、行情子导航、对话输入、抽屉和宽表摘要视图。 + +## 范围外 + +不能把桌面页面缩小、不能用 Hover 作为唯一入口、不能通过隐藏页面解决不可达问题,也不修改 PC 视觉作为“移动适配”。 + +## 验收标准 + +1. 320、375、390、430、768px 及手机横屏无页面横向溢出和内容遮挡。 +2. 底部行情/选股/问师/问天/复盘五入口可触控,目标不小于 44×44px。 +3. 行情子页面通过选择器或抽屉切换;宽表在 320px 使用摘要+详情或局部横滚,页面本身不横滚。 +4. 弹窗改为底部抽屉或全屏页后仍有关闭/返回路径;键盘弹出不遮挡问师/复盘输入区。 +5. 问天动画、表格状态、日夜主题和权限锁定在窄屏可读。 +6. 用户人工逐页验收后,才能把 `pages.config` 的移动意图标为完成。 + +## 依赖与验证 + +依赖 Issue 002。需要 Playwright 视口回归、真实手机人工操作和产品规格第 25.8、27.3 验收案例。 diff --git a/docs/issues/ISSUE-004-full-ic-multifactor.md b/docs/issues/ISSUE-004-full-ic-multifactor.md new file mode 100644 index 0000000..ad34eef --- /dev/null +++ b/docs/issues/ISSUE-004-full-ic-multifactor.md @@ -0,0 +1,34 @@ +# ISSUE-004:完整 IC 动态多因子 + +- 状态:未实现,当前仅有基础动态多因子 +- 优先级:P1 +- 来源:产品规格 17.6、24.2;当前实现基线明确不得夸大 + +## 目标 + +把五类因子(估值、成长、质量、动量、情绪)从基础合成升级为可复现的 IC 动态加权模式,同时保留用户手动权重的专业模式。 + +## 必须实现 + +- 行业内去极值。 +- 因子 z-score 标准化。 +- 行业和市值中性化。 +- 使用过去 12 个月“因子值与下一期收益”的 Rank IC 均值定权。 +- 每季度重算、记录算法版本和样本覆盖。 +- 综合得分前 5% 输出;数据不足时逐因子说明缺失。 + +## 范围外 + +不使用 LLM 计算分数,不把缺失当 0,不将基础动态权重 UI 改名为 IC 完整版。 + +## 验收标准与验证 + +1. 固定样本可复现每个因子预处理、IC、权重和排名。 +2. 严格按公告时点和下一期收益计算,无未来函数。 +3. 季度边界、行业小样本、缺因子、负 IC 和极端值均有测试。 +4. UI 明确“基础动态权重/IC 自动权重”差异及数据日期。 +5. 回测结果保存输入快照、算法版本和输出版本。 + +```powershell +python -m unittest discover -s tests +``` diff --git a/docs/issues/ISSUE-005-policy-macro-overnight-data.md b/docs/issues/ISSUE-005-policy-macro-overnight-data.md new file mode 100644 index 0000000..144ef87 --- /dev/null +++ b/docs/issues/ISSUE-005-policy-macro-overnight-data.md @@ -0,0 +1,25 @@ +# ISSUE-005:政策、宏观与隔夜消息数据 + +- 状态:数据源未定 +- 优先级:P1 +- 来源:产品规格 14.4、17.6、23.3;当前字段注册表尚无稳定计算来源 + +## 目标 + +为宏观思维模型、集合竞价和问师补充可授权、可归档、带时间戳的政策、公告、指数、ETF、汇率、利率、商品和隔夜资讯数据。 + +## 验收标准 + +1. 每个字段登记来源、授权、发布时间、交易日、单位、新鲜度、覆盖率和显示/计算用途。 +2. 历史结果能按快照和版本复现,公告发布时间晚于目标时点的数据不能进入过去结果。 +3. 消息去重、来源冲突、撤回/修订和上游失败有明确规则。 +4. 竞价量化只使用已经登记且在目标时点可见的消息证据;无数据显示“数据缺失”,不显示“暂无信号”。 +5. 问师宏观模型只追加适用上下文,不把政策新闻强塞给其他流派。 + +## 范围外 + +不接入未经授权的网页爬虫,不把金融爬虫 Skill 作为正式 Provider。 + +## 依赖 + +供应商授权和 Issue 010 的数据质量/凭据核验。 diff --git a/docs/issues/ISSUE-006-analyst-consensus-data.md b/docs/issues/ISSUE-006-analyst-consensus-data.md new file mode 100644 index 0000000..e0fb0ed --- /dev/null +++ b/docs/issues/ISSUE-006-analyst-consensus-data.md @@ -0,0 +1,20 @@ +# ISSUE-006:分析师一致预期数据 + +- 状态:数据阻塞(`research.consensus` 为 `unresolved`) +- 优先级:P2 + +## 目标 + +提供按公告时间可追溯的盈利预测、一致预期、预测修正、评级变化、目标价和研报数量,供策略因子和问师宏观/基本面上下文按需使用。 + +## 验收标准 + +1. 字段包含来源、分析师/机构(如授权允许)、公告时间、报告期、单位、币种和版本。 +2. 目标日期只能读取当时已发布的数据;修正保留历史,不覆盖旧快照。 +3. 缺失、过期、覆盖不足和无命中状态可区分。 +4. 策略候选与回测固定输入下可复现;不能把缺失预测当 0 或中性。 +5. 权限和授权边界经过审计,私有/授权数据不进入浏览器或普通日志。 + +## 范围外 + +没有稳定授权来源前,不在 UI 中显示伪造的“分析师共识”指标。 diff --git a/docs/issues/ISSUE-007-level2-auction.md b/docs/issues/ISSUE-007-level2-auction.md new file mode 100644 index 0000000..5b7a425 --- /dev/null +++ b/docs/issues/ISSUE-007-level2-auction.md @@ -0,0 +1,20 @@ +# ISSUE-007:Level-2 与动态竞价深度 + +- 状态:授权阻塞(`market.level2` 为 `unresolved`) +- 优先级:P2 + +## 目标 + +在取得正式授权后,接入逐笔成交、逐笔委托、委托队列、未匹配量和开板深度,增强集合竞价与盘中观察。 + +## 验收标准 + +1. 供应商、授权、字段、时间精度、单位和保留期限登记在数据配置。 +2. 动态快照标出采集时间;断线、延迟和部分覆盖不会伪装成实时完整数据。 +3. 9:15–9:25 动态观察与 9:25 最终竞价归档分开保存;不能用最终快照冒充动态过程。 +4. Level-2 数据不进入未授权的历史回测;重连和重复消息幂等。 +5. UI 在数据缺失时保留已有成功快照并给出可执行提示。 + +## 范围外 + +未获得授权前不使用公开网页抓取或模拟队列填充。 diff --git a/docs/issues/ISSUE-008-hot-money-profile-history.md b/docs/issues/ISSUE-008-hot-money-profile-history.md new file mode 100644 index 0000000..85107c4 --- /dev/null +++ b/docs/issues/ISSUE-008-hot-money-profile-history.md @@ -0,0 +1,20 @@ +# ISSUE-008:游资档案历史画像 + +- 状态:低优先级,基础名录和详情已存在 +- 优先级:P3 + +## 目标 + +完善游资名录、席位别名、历史上榜、买卖倾向、常见题材和证据来源,支持龙虎榜页面左名录右详情,不再依赖超长弹窗。 + +## 验收标准 + +1. Tushare 已收录席位完整展示;未知或无法识别席位保留原名并标记待归类。 +2. 别名合并有来源、人工修订记录和可回退历史。 +3. 历史画像按日期、股票、方向和金额可追溯;不把单日行为直接概括为稳定风格。 +4. 点击名录进入详情,桌面/夜间/低分辨率可读,无异常空弹窗。 +5. 龙虎榜出现“有股票上榜但席位不可识别”时,展示该状态而不是“无数据”。 + +## 依赖 + +依赖稳定龙虎榜席位数据和人工别名维护;不阻塞其他市场页面。 diff --git a/docs/issues/ISSUE-009-documentation-status-drift.md b/docs/issues/ISSUE-009-documentation-status-drift.md new file mode 100644 index 0000000..e2aa9c1 --- /dev/null +++ b/docs/issues/ISSUE-009-documentation-status-drift.md @@ -0,0 +1,26 @@ +# ISSUE-009:活跃文档和状态漂移 + +- 状态:待整理 +- 优先级:P1 + +## 目标 + +让活跃文档、配置注册表、启动端口和完成状态与正式 `app/` 实际一致,历史迁移材料继续保留但明确仅用于审计。 + +## 检查范围 + +- `README.md`、`docs/README.md`、`docs/maintenance/人工维护指南.md`。 +- `config/pages.config.json` 的移动端意图与实际验收状态。 +- 默认端口 8765、局域网验收端口 8797 及 Docker 文档的区分。 +- 迁移文档中的“待人工验收”等历史表述是否被误读为当前状态。 + +## 验收标准 + +1. 活跃文档只描述当前正式源码和真实启动方式,过期内容链接到历史说明并标注日期。 +2. 文档声明移动端仍延期,不能把媒体查询或 `dedicated` 注册值当作已完成。 +3. 端口、健康检查、数据目录、备份和回退命令在干净环境可执行。 +4. `docs/HANDOFF.md` 与 Issue 索引同步更新,所有相对链接有效。 + +```powershell +python tools/verify_baseline.py +``` diff --git a/docs/issues/ISSUE-010-live-provider-llm-readiness.md b/docs/issues/ISSUE-010-live-provider-llm-readiness.md new file mode 100644 index 0000000..a9a58ac --- /dev/null +++ b/docs/issues/ISSUE-010-live-provider-llm-readiness.md @@ -0,0 +1,26 @@ +# ISSUE-010:数据源与 LLM 在线就绪核验 + +- 状态:待运行核验 +- 优先级:P0 + +## 目标 + +在不把任何密钥写入仓库的前提下,恢复本地/局域网服务并分别证明进程、数据库、Tushare、iFinD、LLM 主/辅助模型和后台任务的健康状态。 + +## 验收标准 + +1. `0.0.0.0:8797`(或部署指定端口)可访问 `/api/health`,健康响应区分进程、数据库、数据源、任务和模型。 +2. 登录后能读取最近真实交易日;无快照时显示等待同步,不显示演示数字。 +3. 管理员诊断页可看到脱敏的来源、错误类型、关联 ID 和最后成功时间;普通用户看不到 Token、URL、模型名或堆栈。 +4. Tushare/iFinD/LLM 单独测试成功或给出明确缺失/授权/网络原因;失败不清空已有成功数据。 +5. 重启后账号、行情、问师/问天历史、选股和复盘记录不丢,盘后任务不重复产出。 + +## 验证 + +```powershell +powershell -ExecutionPolicy Bypass -File tools/start_local.ps1 -Port 8797 +Invoke-RestMethod http://127.0.0.1:8797/api/health +python tools/verify_baseline.py --e2e +``` + +凭据只从管理员系统设置或受保护环境注入,不能写入 Issue、截图或日志。 diff --git a/docs/issues/ISSUE-011-public-deployment-hardening.md b/docs/issues/ISSUE-011-public-deployment-hardening.md new file mode 100644 index 0000000..f697b0d --- /dev/null +++ b/docs/issues/ISSUE-011-public-deployment-hardening.md @@ -0,0 +1,24 @@ +# ISSUE-011:公网部署加固 + +- 状态:未来范围,局域网版不阻塞 +- 优先级:P3 + +## 目标 + +在真正转向外网和商业授权前,将当前单实例局域网部署升级为可审计的公网部署方案。 + +## 必须完成 + +- 反向代理、TLS、可信 Host、Secure Cookie、CSRF、限流和审计。 +- PostgreSQL 或等价正式数据库迁移,连接池和并发写入策略。 +- 后台任务队列、分布式锁、缓存、健康检查、集中日志和告警。 +- 多实例数据一致性、密钥托管、备份恢复和升级回退演练。 +- 会员/激活码/授权模型的服务端鉴权和额度审计。 + +## 范围外 + +在没有部署决策和容量指标前,不为局域网版本预先拆微服务或引入云依赖。 + +## 验收标准 + +公网威胁模型、ADR、压测、故障注入、备份恢复、跨实例重复任务和安全扫描全部有记录;局域网数据边界和产品行为不变。 diff --git a/docs/issues/README.md b/docs/issues/README.md new file mode 100644 index 0000000..a148a80 --- /dev/null +++ b/docs/issues/README.md @@ -0,0 +1,19 @@ +# 未完成事项 Issue 索引 + +这些 Issue 是仓库内的可审计任务说明,供人工维护或后续智能体执行。它们不是通过 Gitea API 创建的远端工单;需要远端协作时,应在确认范围后逐项复制到 Gitea,并保留本地文件作为产品交接记录。 + +| Issue | 标题 | 状态 | 优先级 | +|---|---|---|---:| +| [001](ISSUE-001-finalize-mentor-redesign.md) | 问师界面与动态追问收口 | 待人工验收/提交 | P0 | +| [002](ISSUE-002-checkpoint-current-pc-visual-work.md) | 当前 PC 全站视觉改动验收与回档 | 待审查/提交 | P0 | +| [003](ISSUE-003-mobile-redesign.md) | 移动端独立重设 | 明确延期 | P2 | +| [004](ISSUE-004-full-ic-multifactor.md) | 完整 IC 动态多因子 | 未实现 | P1 | +| [005](ISSUE-005-policy-macro-overnight-data.md) | 政策、宏观与隔夜消息数据 | 数据源未定 | P1 | +| [006](ISSUE-006-analyst-consensus-data.md) | 分析师一致预期数据 | 数据阻塞 | P2 | +| [007](ISSUE-007-level2-auction.md) | Level-2 与动态竞价深度 | 授权阻塞 | P2 | +| [008](ISSUE-008-hot-money-profile-history.md) | 游资档案历史画像 | 低优先级 | P3 | +| [009](ISSUE-009-documentation-status-drift.md) | 活跃文档和状态漂移 | 待整理 | P1 | +| [010](ISSUE-010-live-provider-llm-readiness.md) | 数据源与 LLM 在线就绪核验 | 待运行核验 | P0 | +| [011](ISSUE-011-public-deployment-hardening.md) | 公网部署加固 | 未来范围 | P3 | + +关闭任一 Issue 前,必须更新 `docs/HANDOFF.md` 的状态、验证日期和回档提交;不能只改 Issue 标题。 diff --git a/docs/product/小白复盘-完整产品规格说明书.md b/docs/product/小白复盘-完整产品规格说明书.md index 0109613..92e9903 100644 --- a/docs/product/小白复盘-完整产品规格说明书.md +++ b/docs/product/小白复盘-完整产品规格说明书.md @@ -1065,6 +1065,9 @@ iFinD动态竞价不可用时,不能把Tushare最终竞价伪装成动态监 - 回答必须是真流式:每个文本片段只追加一次;流结束后不能再次追加完整答案,避免双份结果。 - 主模型在尚未输出任何内容前失败时可切换辅助模型。 - 一旦已经向用户输出文本,中途失败只能提示连接中断,不能切换模型后重复整个回答。 +- 每次成功回答可在同一次模型输出末尾生成2至3条动态追问;追问必须结合本轮问题、回答和当前思维模型,不得额外发起一次LLM业务调用或重复扣减额度。 +- 动态追问是当前回答的临时操作建议,不写入对话正文。点击追问只预填输入框,由用户确认或编辑后发送;切换模型、清空对话或开始下一次提问时,旧追问立即失效。 +- 回答失败、被用户停止或追问结构无效时不显示动态追问;追问不得包含无条件买卖指令、收益承诺或正文未支持的新事实。 - 回答必须声明这是基于公开资料蒸馏的思维模型,不是真人本人,不构成投资建议。 ### 14.4 按模型类型提供数据 @@ -1914,6 +1917,7 @@ iFinD动态竞价不可用时,不能把Tushare最终竞价伪装成动态监 | L03 | 主模型输出一半失败 | 不切辅助重放,提示连接中断并保留已输出 | | L04 | 选择宏观模型询问市场 | 上下文含宽基指数和ETF,不强塞短线席位数据 | | L05 | 普通用户打开问师/问天/助手 | 同结构锁定态,不能发起LLM调用 | +| L06 | 问师成功完成一轮回答 | 回答下方出现2至3条与本轮相关的动态追问;点击后只预填输入框,不自动发送,也不产生额外LLM额度调用 | | W01 | 观势未输入股票 | 只提示输入代码或名称,不显示暂不成卦 | | W02 | 中国平安所属保险行业仅5只成分且5只有行情 | 小样本但覆盖完整时行业安全门可通过 | | W03 | 行业缺涨跌但用户补录客观值 | 按原公式重算;用户不能直接选阴阳 | diff --git a/frontend/index.html b/frontend/index.html index f8520df..f811191 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -17,29 +17,29 @@ document.documentElement.style.colorScheme = theme; })(); - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + @@ -64,6 +64,10 @@ + + 行情 + 情绪周期 + 上涨 -- 下跌 -- @@ -83,6 +87,12 @@ + + 搜索 + 外观 + 提醒 + 助手 + 刷新 后台刷新 系统管理 @@ -105,6 +115,7 @@ + @@ -128,7 +139,7 @@ 智能工具 - 智能选股 + 智能选股选股 问师 问天 diff --git a/frontend/pages.config.js b/frontend/pages.config.js index 3ae7d0e..9a1b1f3 100644 --- a/frontend/pages.config.js +++ b/frontend/pages.config.js @@ -47,7 +47,7 @@ ["heaven", "/pages/heaven/page.html?v=20260803-1", ["heavenView"]], ["ladder", "/pages/ladder/page.html?v=20260803-1", ["ladderView"]], ["screener", "/pages/screener/page.html?v=20260804-1", ["screenerView", "screenerTrackingView"]], - ["mentor", "/pages/mentor/page.html?v=20260803-1", ["mentorView"]], + ["mentor", "/pages/mentor/page.html?v=20260806-2", ["mentorView"]], ["rotation", "/pages/rotation/page.html?v=20260803-1", ["rotationView"]], ["auction", "/pages/auction/page.html?v=20260803-1", ["auctionView"]], ["themes", "/pages/themes/page.html?v=20260803-1", ["themeLibraryView"]], @@ -71,7 +71,7 @@ "/pages/market/charts.js?v=20260803-1", "/pages/market/entity-detail.js?v=20260803-1", "/pages/market/stock-detail.js?v=20260803-1", - "/pages/market/preview.js?v=20260803-1", + "/pages/market/preview.js?v=20260806-1", "/pages/market/search.js?v=20260803-1", "/pages/market/bindings.js?v=20260803-1", "/pages/ladder/page.js?v=20260729-1", @@ -79,14 +79,14 @@ "/pages/auction/page.js?v=20260729-1", "/pages/themes/page.js?v=20260729-1", "/pages/popularity/page.js?v=20260729-1", - "/pages/dragon-tiger/page.js?v=20260729-1", - "/pages/screener/page.js?v=20260804-1", - "/pages/mentor/page.js?v=20260729-1", + "/pages/dragon-tiger/page.js?v=20260806-1", + "/pages/screener/page.js?v=20260806-1", + "/pages/mentor/page.js?v=20260806-2", "/pages/heaven/page.js?v=20260729-1", "/pages/review/page.js?v=20260729-1", "/shared/state.js?v=20260729-1", "/shared/api.js?v=20260729-1", - "/shared/shell.js?v=20260729-1", + "/shared/shell.js?v=20260806-1", "/shared/export.js?v=20260731-1", "/pages/heaven/loading-v2.js?v=20260728-2", "/shared/context.js?v=20260804-1", diff --git a/frontend/pages/auction/foundation.css b/frontend/pages/auction/foundation.css index 232cc12..521a05d 100644 --- a/frontend/pages/auction/foundation.css +++ b/frontend/pages/auction/foundation.css @@ -5,8 +5,8 @@ font-weight: 800; } -@media (max-width: 720px) { - #auctionView { +@media (max-width: 767px) { + body.mobile-shell #auctionView { border-radius: 0px; } } @@ -287,7 +287,7 @@ max-width: 155px; } -@media (max-width: 720px) { +@media (max-width: 767px) { .auction-phase-notice time { grid-column: 2; } @@ -506,7 +506,7 @@ } } -@media (max-width: 720px) { +@media (max-width: 767px) { .auction-phase-notice { grid-template-columns: 10px minmax(0px, 1fr); @@ -1962,7 +1962,7 @@ } } -@media (max-width: 720px) { +@media (max-width: 767px) { .redesigned-auction-view { padding: 0px; } @@ -2080,7 +2080,7 @@ } } -@media (min-width: 721px) { +@media (min-width: 768px) { body[data-active-view="auctionView"] .app-main { display: flex; @@ -2104,7 +2104,7 @@ } } -@media (min-width: 721px) and (max-height: 900px) { +@media (min-width: 768px) and (max-height: 900px) { .redesigned-auction-view { padding: 10px 0 12px; } @@ -2196,7 +2196,7 @@ padding-right: 0px; } -@media (min-width: 721px) { +@media (min-width: 768px) { #auctionView .auction-page-head-v2 { flex: 0 0 auto; } @@ -2242,7 +2242,7 @@ } } -@media (max-width: 720px), (max-width: 1023px) and (max-height: 600px) { +@media (max-width: 767px), (max-width: 1023px) and (max-height: 600px) { #auctionView .auction-workspace-v2 { display: block; } @@ -2315,3 +2315,95 @@ color: var(--text-secondary); } + +/* Mobile auction keeps one page scrollbar; dense rows scroll only sideways. */ +@media (max-width: 767px) { + #auctionView { + padding-inline: 0; + } + + #auctionView .auction-workspace-v2 { + min-height: 0; + display: flex; + flex-direction: column; + gap: var(--space-12); + overflow: visible; + } + + #auctionView .auction-primary-card { + width: 100%; + max-width: 100%; + min-height: 0; + min-width: 0; + overflow: visible; + } + + #auctionView .auction-tabs-v2 { + width: 100%; + max-width: 100%; + min-width: 0; + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + padding-inline: 0; + overflow: visible; + } + + #auctionView .auction-tabs-v2 button { + min-width: 0; + min-height: var(--mobile-touch-size); + justify-content: center; + padding-inline: var(--space-4); + font-size: var(--font-size-label); + } + + #auctionView .auction-tabs-v2 .auction-summary-v2 { + width: 100%; + min-width: 0; + grid-column: 1 / -1; + grid-template-columns: repeat(2, minmax(0, 1fr)); + margin-left: 0; + border-top: 1px solid var(--border-subtle); + border-left: 0; + } + + #auctionView .auction-tools-v2, + #auctionView .auction-expectation-v2, + #auctionView .auction-tool-actions { + width: 100%; + max-width: 100%; + min-width: 0; + } + + #auctionView .auction-filter-segments { + width: 100%; + min-width: 0; + } + + #auctionView .auction-filter-segments button { + min-width: 0; + flex: 1 1 0; + } + + #auctionView .auction-search-v2, + #auctionView .auction-search-v2 input { + min-width: 0; + } + + #auctionView .auction-table-frame-v2 { + width: 100%; + max-width: 100%; + min-height: var(--mobile-table-min-height); + max-height: none; + min-width: 0; + overflow-x: auto; + overflow-y: visible; + } + + #auctionView .auction-side-v2 { + max-height: none; + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: var(--space-12); + overflow: visible; + } +} diff --git a/frontend/pages/dragon-tiger/foundation.css b/frontend/pages/dragon-tiger/foundation.css index 0878320..6f2aeae 100644 --- a/frontend/pages/dragon-tiger/foundation.css +++ b/frontend/pages/dragon-tiger/foundation.css @@ -661,7 +661,7 @@ } } -@media (max-width: 720px) { +@media (max-width: 767px) { :where(#dragonView) .dragon-trader-list { padding: 0px 15px; } @@ -842,7 +842,7 @@ padding: 9px 14px; } -@media (max-width: 720px) { +@media (max-width: 767px) { .dragon-filterbar { align-items: stretch; @@ -1558,7 +1558,7 @@ height: 15px; } -@media (min-width: 721px) { +@media (min-width: 768px) { body[data-active-view="dragonView"] .app-main { height: var(--workspace-height); @@ -1576,7 +1576,7 @@ } } -@media (min-width: 721px) and (max-height: 900px) { +@media (min-width: 768px) and (max-height: 900px) { .redesigned-dragon-view { padding-top: 9px; @@ -1688,7 +1688,7 @@ } } -@media (max-width: 720px) { +@media (max-width: 767px) { .redesigned-dragon-view { padding: 10px; } @@ -1795,7 +1795,7 @@ flex-direction: column; } - #dragonView .dragon-trader-detail .trader-operations { + body.mobile-shell #dragonView .dragon-trader-detail .trader-operations { overflow-x: auto; } @@ -2306,13 +2306,13 @@ font-weight: var(--dragon-profile-weight-semibold); } -@media (min-width: 721px) { +@media (min-width: 768px) { body[data-active-view="dragonView"] .hot-money-profiles-v2 { flex: 1 1 auto; } } -@media (max-width: 720px) { +@media (max-width: 767px) { .hot-money-profiles-v2 { overflow: visible; } @@ -2354,7 +2354,7 @@ } } -@media (min-width: 721px) { +@media (min-width: 768px) { body[data-active-view="dragonView"] .dragon-daily-content-v2 { display: block; @@ -2473,3 +2473,30 @@ table.tbl { background: var(--surface-muted) !important; } + +/* Mobile dragon-tiger pages use document scrolling, including long operation lists. */ +@media (max-width: 767px) { + #dragonView { + padding-inline: 0; + } + + #dragonView :is(.dragon-daily-content-v2, .dragon-trader-detail-v2, .dragon-unclassified-v2, .hot-money-profiles-v2) { + max-height: none; + overflow: visible; + } + + #dragonView .dragon-trader-detail .trader-operations { + max-height: none; + overflow-x: auto; + overflow-y: visible; + } + + #dragonView .hot-money-profile-list-v2 { + max-height: none; + overflow: visible; + } + + #dragonView .dragon-operation-table { + min-width: var(--table-wide); + } +} diff --git a/frontend/pages/dragon-tiger/page.js b/frontend/pages/dragon-tiger/page.js index 15cb088..24ce530 100644 --- a/frontend/pages/dragon-tiger/page.js +++ b/frontend/pages/dragon-tiger/page.js @@ -261,7 +261,7 @@ function layoutDragonCards(container = document.querySelector("#dragonTraderList const cards = [...container.querySelectorAll(".dragon-trader-card")]; const hitZones = [...container.querySelectorAll(".dragon-card-hit-zone")]; if (!cards.length) return; - const compact = window.innerWidth <= 720; + const compact = window.innerWidth <= 767; const cardWidth = compact ? 148 : 176; const available = Math.max(cardWidth, container.clientWidth - (compact ? 30 : 72)); const spread = Math.min(available - cardWidth, compact ? 310 : 1050); diff --git a/frontend/pages/heaven/foundation.css b/frontend/pages/heaven/foundation.css index 8e0bffd..0bd40e6 100644 --- a/frontend/pages/heaven/foundation.css +++ b/frontend/pages/heaven/foundation.css @@ -81,7 +81,7 @@ body[data-active-view="heavenView"] .workspace-view { margin-top: 0px; } -@media (max-width: 720px) { +@media (max-width: 767px) { body[data-active-view="heavenView"] .workspace-view { margin-top: 0px; @@ -1440,7 +1440,7 @@ body[data-active-view="heavenView"] .workspace-view { } } -@media (max-width: 720px) { +@media (max-width: 767px) { body[data-active-view="heavenView"] .overview-strip { display: none; } @@ -5738,7 +5738,7 @@ body[data-active-view="heavenView"] .workspace-view { margin-top: 12px; } -@media (max-width: 720px) { +@media (max-width: 767px) { .heaven-calibration-heading { align-items: flex-start; @@ -6139,7 +6139,7 @@ body[data-active-view="heavenView"] .workspace-view { border-top: 1px solid var(--border); } -@media (max-width: 720px) { +@media (max-width: 767px) { .heart-toolbar-controls { top: 8px; @@ -6290,7 +6290,7 @@ body[data-active-view="heavenView"] .workspace-view { min-height: 340px; } -@media (max-width: 720px) { +@media (max-width: 767px) { #heavenView .heaven-toolbar { min-height: 62px; @@ -6653,7 +6653,7 @@ body[data-active-view="heavenView"] .workspace-view { --wt-history-bg: #fbfaf6; } -@media (min-width: 721px) { +@media (min-width: 768px) { :root[data-theme="light"] body[data-active-view="heavenView"] { --wt-bg: #f4f5f7; } @@ -9236,7 +9236,7 @@ body[data-active-view="heavenView"] .workspace-view { } } -@media (min-width: 721px) { +@media (min-width: 768px) { body[data-active-view="heavenView"] { --wt-bg: #0b1120; } @@ -11719,3 +11719,16 @@ body[data-active-view="heavenView"] .workspace-view { width: min(100% - 40px, 1280px); } } + +@media (max-width: 767px) { + :root[data-theme="dark"] body.mobile-shell[data-active-view="heavenView"] .app-main, + :root[data-theme="dark"] body.mobile-shell[data-active-view="heavenView"] #heavenView.heaven-shell { + background-color: var(--wt-bg); + background-image: var(--wt-stage-bg); + color: var(--wt-text); + } + + :root[data-theme="dark"] body.mobile-shell[data-active-view="heavenView"] #heavenView .heaven-panel { + background: transparent; + } +} diff --git a/frontend/pages/ladder/foundation.css b/frontend/pages/ladder/foundation.css index 1986fdd..4a5b884 100644 --- a/frontend/pages/ladder/foundation.css +++ b/frontend/pages/ladder/foundation.css @@ -66,7 +66,7 @@ } } -@media (max-width: 720px) { +@media (max-width: 767px) { .ladder-more { grid-column: auto; @@ -81,7 +81,7 @@ box-shadow: rgba(22, 34, 46, 0.04) 0px 1px 3px; } -@media (max-width: 720px) { +@media (max-width: 767px) { .ladder-board { align-items: stretch; @@ -1015,7 +1015,7 @@ } } -@media (max-width: 720px) { +@media (max-width: 767px) { .redesigned-ladder-view { padding: 0px; } @@ -1097,7 +1097,7 @@ } } -@media (min-width: 721px) { +@media (min-width: 768px) { :root #ladderView.active-view { height: auto; @@ -1170,3 +1170,98 @@ color: var(--text-secondary); } + +/* Mobile ladder becomes a readable vertical hierarchy instead of a narrow rail. */ +@media (max-width: 767px) { + #ladderView { + padding-inline: 0; + } + + #ladderView .ladder-page-head { + gap: var(--space-8); + } + + #ladderView .ladder-head-actions { + width: 100%; + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: var(--space-8); + } + + #ladderView .ladder-sort-segment { + min-width: 0; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + #ladderView .market-ladder-workspace, + #ladderView .lad-grid { + display: flex; + flex-direction: column; + gap: var(--space-12); + } + + #ladderView .market-ladder-board { + width: 100%; + display: flex; + flex-direction: column; + } + + #ladderView .market-ladder-tier { + width: 100%; + min-height: 0; + display: block; + } + + #ladderView .market-ladder-label { + width: 100%; + min-height: var(--mobile-touch-size); + display: flex; + align-items: center; + gap: var(--space-8); + padding: var(--space-8) var(--space-12); + border-right: 0; + border-bottom: 1px solid var(--border-subtle); + writing-mode: horizontal-tb; + } + + #ladderView .market-ladder-level { + display: inline-flex; + margin: 0; + writing-mode: horizontal-tb; + } + + #ladderView .market-ladder-rate { + display: inline-flex; + writing-mode: horizontal-tb; + margin-left: auto; + } + + #ladderView .market-ladder-stocks { + width: 100%; + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: var(--space-8); + padding: var(--space-8); + } + + #ladderView .market-ladder-stock { + width: 100%; + min-width: 0; + } + + #ladderView .market-ladder-gap-note, + #ladderView .market-ladder-more { + width: 100%; + min-height: var(--mobile-touch-size); + margin: 0; + justify-content: center; + } + + #ladderView .market-ladder-insights { + width: 100%; + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: var(--space-12); + } +} diff --git a/frontend/pages/market/foundation.css b/frontend/pages/market/foundation.css index e3fd017..a79dd85 100644 --- a/frontend/pages/market/foundation.css +++ b/frontend/pages/market/foundation.css @@ -1,5 +1,5 @@ /* Canonical CSS owner: market. Historical layers consolidated 2026-08-02. */ -@media (max-width: 720px) { +@media (max-width: 767px) { .stock-dialog { width: calc(-16px + 100vw); @@ -628,7 +628,7 @@ height: 14px; } -@media (max-width: 720px) { +@media (max-width: 767px) { body.stock-preview-open { overflow: hidden; } @@ -770,7 +770,7 @@ overflow-wrap: anywhere; } -@media (max-width: 720px) { +@media (max-width: 767px) { .entity-detail-dialog .detail-grid { grid-template-columns: repeat(2, minmax(0px, 1fr)); } diff --git a/frontend/pages/market/preview.js b/frontend/pages/market/preview.js index 64d619a..2bd83d6 100644 --- a/frontend/pages/market/preview.js +++ b/frontend/pages/market/preview.js @@ -96,7 +96,7 @@ function findStockFallback(code) { function supportsStockPreviewHover() { return window.matchMedia("(hover: hover) and (pointer: fine)").matches - && window.innerWidth > 720; + && window.innerWidth > 767; } function handleStockPreviewPointerOver(event) { @@ -138,7 +138,7 @@ function handleStockPreviewFocusOut(event) { } function handleMobileStockPreviewClick(event) { - if (window.innerWidth > 720) return; + if (window.innerWidth > 767) return; const trigger = event.target.closest?.(".stock-preview-trigger"); if (!trigger) return; const code = stockCodeFromTrigger(trigger); @@ -160,7 +160,7 @@ function handleStockPreviewKeydown(event) { const code = stockCodeFromTrigger(trigger); if (!code) return; event.preventDefault(); - if (window.innerWidth <= 720) showStockPreview(code, trigger); + if (window.innerWidth <= 767) showStockPreview(code, trigger); else openStock(code, findStockFallback(code)); } @@ -186,7 +186,7 @@ async function showStockPreview(code, trigger) { state.stockPreviewChart = "daily"; renderStockPreviewLoading(); elements.stockPreview.hidden = false; - const mobile = window.innerWidth <= 720; + const mobile = window.innerWidth <= 767; elements.stockPreviewBackdrop.hidden = !mobile; document.body.classList.toggle("stock-preview-open", mobile); requestAnimationFrame(repositionStockPreview); @@ -239,7 +239,7 @@ async function showEntityPreview(item, trigger) { state.stockPreviewChart = "daily"; renderStockPreviewLoading(); elements.stockPreview.hidden = false; - const mobile = window.innerWidth <= 720; + const mobile = window.innerWidth <= 767; elements.stockPreviewBackdrop.hidden = !mobile; document.body.classList.toggle("stock-preview-open", mobile); requestAnimationFrame(repositionStockPreview); @@ -433,7 +433,7 @@ function openStockDetailFromPreview() { } function repositionStockPreview() { - if (elements.stockPreview.hidden || window.innerWidth <= 720 || !stockPreviewAnchor?.isConnected) return; + if (elements.stockPreview.hidden || window.innerWidth <= 767 || !stockPreviewAnchor?.isConnected) return; const anchor = stockPreviewAnchor.getBoundingClientRect(); const preview = elements.stockPreview.getBoundingClientRect(); const gap = 12; diff --git a/frontend/pages/mentor/foundation.css b/frontend/pages/mentor/foundation.css index 424e409..b8bc780 100644 --- a/frontend/pages/mentor/foundation.css +++ b/frontend/pages/mentor/foundation.css @@ -1,2801 +1,988 @@ /* Canonical CSS owner: mentor. Historical layers consolidated 2026-08-02. */ -.mentor-option strong { - display: block; - font-size: 15px; -} - -.mentor-option span { - display: block; - - margin-top: 6px; - - overflow: hidden; - - color: var(--text-muted); - - font-size: 11px; - - line-height: 1.55; - - text-overflow: ellipsis; - - white-space: nowrap; -} - -.mentor-option small { - display: flex; - - flex-wrap: wrap; - - gap: 5px; - - margin-top: 8px; -} - -.mentor-option i { - padding: 2px 5px; - - border-radius: 3px; - - background: var(--surface-muted); - - color: var(--blue-dark); - - font-size: 10px; - - font-style: normal; -} - -.mentor-chat-header h3 { - margin: 5px 0px 0px; - - font-size: 18px; -} - -.mentor-message p { - margin: 0px; - - overflow-wrap: anywhere; - - line-height: 1.75; - - white-space: pre-wrap; -} - -.model-config-grid { - display: grid; - - grid-template-columns: 1fr 1fr; - - border: 1px solid var(--line); -} - -.model-role-selectors { - display: grid; - - grid-template-columns: repeat(2, minmax(0px, 1fr)); - - gap: 12px; - - margin: 4px 0px 16px; -} - -.model-role-selectors select { - width: 100%; - - min-height: 40px; - - padding: 0px 10px; - - border: 1px solid var(--line-strong); - - border-radius: 5px; - - background: var(--surface); -} - -.model-pool-list { - display: grid; - - gap: 10px; -} - -.model-pool-row { - display: grid; - - gap: 11px; - - padding: 14px; - - border: 1px solid var(--line); - - border-radius: 6px; - - background: var(--surface); -} - -.model-pool-heading { - display: flex; - - align-items: baseline; - - justify-content: space-between; - - gap: 12px; -} - -.model-pool-heading strong { - color: var(--text-primary); - - font-size: 14px; -} - -.model-pool-heading span { - color: var(--text-muted); - - font-size: 11px; -} - -.model-pool-fields { - display: grid; - - grid-template-columns: 0.75fr 1.35fr 1fr 1.15fr; - - gap: 10px; -} - -.model-delete-button { - margin-left: auto; - - color: var(--coral); -} - -@media (max-width: 900px) { - .model-pool-fields { - grid-template-columns: repeat(2, minmax(0px, 1fr)); - } -} - -@media (max-width: 600px) { - .model-pool-fields, - .model-role-selectors { - grid-template-columns: minmax(0px, 1fr); - } -} - -.model-config-panel { - min-width: 0px; - - display: grid; - - gap: 11px; - - padding: 14px; - - border-right: 1px solid var(--line); - - background: var(--surface); -} - -.model-config-panel:last-child { - border-right: 0px; -} - -.model-config-heading { - min-height: 30px; - - display: flex; - - align-items: center; - - justify-content: space-between; - - gap: 12px; -} - -.model-config-heading h4 { - margin: 0px; - - font-size: 14px; -} - -.model-role { - display: inline-flex; - - align-items: center; - - min-height: 22px; - - padding: 2px 7px; - - border-radius: 4px; - - font-size: 11px; - - font-weight: 700; -} - -.model-test-row { - display: flex; - - align-items: center; - - gap: 10px; - - margin-top: 2px; -} - -.model-test-status { - min-width: 0px; - - overflow: hidden; - - color: var(--text-muted); - - font-size: 12px; - - text-overflow: ellipsis; - - white-space: nowrap; -} - -.model-test-status.success { - color: var(--green); -} - -.model-test-status.failure { - color: var(--coral); -} - -@media (max-width: 860px) { - .mentor-layout { - grid-template-columns: 1fr; - } - - .mentor-sidebar { - max-height: 260px; - - overflow-y: auto; - - border-right: 0px; - - border-bottom: 1px solid var(--line); - } - - .mentor-list { - grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); - } -} - -@media (max-width: 520px) { - .settings-dialog .model-config-grid { - width: 100%; - - max-width: 100%; - - min-width: 0px; - } - - .model-test-row { - align-items: flex-start; - - flex-wrap: wrap; - } - - .model-test-status { - width: 100%; - - white-space: normal; - } - - .model-config-grid { - grid-template-columns: 1fr; - } - - .mentor-chat-panel, - .mentor-layout { - min-height: 580px; - } - - .mentor-list { - grid-template-columns: 1fr; - } - - .mentor-chat-header { - align-items: flex-start; - - flex-direction: column; - - gap: 6px; - - padding: 11px 12px; - } - - .mentor-messages { - padding: 12px; - } - - .mentor-message { - max-width: 92%; - } - - .mentor-quick-prompts { - padding-right: 12px; - - padding-left: 12px; - } - - .mentor-chat-form { - padding-right: 12px; - - padding-left: 12px; - - grid-template-columns: 1fr; - } - - .mentor-chat-form .button { - width: 100%; - - height: 38px; - } - - .mentor-disclaimer { - padding-right: 12px; - - padding-left: 12px; - - text-align: left; - } - - .model-config-panel { - border-right: 0px; - - border-bottom: 1px solid var(--line); - } - - .model-config-panel:last-child { - border-bottom: 0px; - } -} - -.mentor-directory-heading > div { - display: flex; - - align-items: baseline; - - gap: 8px; -} - -.mentor-directory-heading h3 { - margin: 0px; - - font-size: 14px; -} - -.mentor-directory-heading span { - color: var(--text-muted); - - font-size: 11px; -} - -:where(#mentorView) .mentor-evidence-filters button:hover { - background: var(--action-soft); -} - -:where(#mentorView) .mentor-option-main { - grid-template-columns: minmax(0px, 1fr) auto; - - align-items: center; - - gap: 8px; -} - -.mentor-option .mentor-option-tools { - display: flex; - - align-items: center; - - gap: 2px; - - margin: 0px; - - overflow: visible; - - color: inherit; - - font-size: inherit; - - line-height: normal; - - white-space: normal; -} - -:where(#mentorView) .mentor-order-button:disabled { - color: color-mix(in srgb, var(--text-muted) 35%, transparent); -} - -:where(#mentorView) .mentor-option.is-drag-over { - border-color: var(--action); -} - -.mentor-option:focus-visible { - outline: 2px solid var(--action); - - outline-offset: -2px; -} - -.mentor-option .mentor-option-copy { - display: grid; - - margin: 0px; - - overflow: visible; - - color: inherit; - - font-size: inherit; - - line-height: normal; - - white-space: normal; -} - -.mentor-option-copy strong { - overflow: hidden; - - font-size: 13px; - - text-overflow: ellipsis; - - white-space: nowrap; -} - -.mentor-option-copy em { - overflow: hidden; - - color: var(--text-muted); - - font-size: 10px; - - font-style: normal; - - line-height: 1.4; - - text-overflow: ellipsis; - - white-space: nowrap; -} - -.mentor-option .mentor-option-badges { - display: flex; - - margin: 0px; - - overflow: visible; - - color: inherit; - - font-size: inherit; - - line-height: normal; - - white-space: normal; -} - -:where(#mentorView) .mentor-option-badges { - max-width: 76px; -} - -.mentor-option .mentor-badge { - display: inline-flex; - - margin: 0px; - - overflow: visible; - - line-height: 1; -} - -:where(#mentorView) .mentor-active-title { - flex-wrap: wrap; -} - -:where(#mentorView) .mentor-active-focus { - flex-wrap: wrap; -} - -:where(#mentorView) .mentor-active-focus span { - border-radius: 3px; -} - -:where(#mentorView) .mentor-message { - margin-bottom: 12px; -} - -@media (min-width: 721px) and (max-width: 1023px) { - .mentor-layout { - grid-template-columns: 280px minmax(0px, 1fr); - } - - .mentor-sidebar { - max-height: none; - - overflow: hidden; - - border-right: 1px solid var(--line); - - border-bottom: 0px; - } - - .mentor-list { - grid-template-columns: minmax(0px, 1fr); - } -} - -@media (max-width: 720px) { - :where(#mentorView) .mentor-sidebar { - max-height: none; - - border-right: 0px; - - border-bottom: 1px solid var(--border); - - background: var(--surface); - } - - :where(#mentorView) .mentor-directory-content { - padding: 14px 12px 10px; - - background: var(--surface-muted); - } - - :where(#mentorView) .mentor-sort-toggle { - padding: 0px 10px; - - font-size: 11px; - } - - :where(#mentorView) .mentor-evidence-filters button { - font-size: 11px; - } -} - -:where(#mentorView) .mentor-quick-prompts { - flex-wrap: wrap; - - border-color: var(--border); -} - -:where(#mentorView) .mentor-option:hover { - border-color: var(--border-strong); -} - -:where(#mentorView) .mentor-option.active { - border-color: color-mix(in srgb, var(--action) 42%, var(--border)); -} - -@media (min-width: 721px) and (max-width: 1279px) { - .mentor-layout { - grid-template-columns: 280px minmax(0px, 1fr); - } -} - -@media (max-width: 720px) { - #mentorView > .section-toolbar { - min-height: 66px; - - align-items: center; - - flex-direction: row; - } - - #mentorView > .section-toolbar .section-title-group { - flex: 1 1 0%; - } - - :where(#mentorView) .mentor-empty-state { - min-height: 100%; - } - - .mentor-empty-state strong { - font-size: 16px; - } -} - -@media (max-width: 900px) { - body[data-active-view="mentorView"] .workspace-view { - padding: 10px 0 84px; - } -} - -body[data-active-view="mentorView"] .workspace-view { - width: auto; - - max-width: none; - - margin: 0px; - - border: 0px; - - border-radius: 0px; - - background: transparent; - - box-shadow: none; - - padding: var(--page-pad-y) 0 24px; -} - -.mentor-page-controls .mentor-evidence-filters { - margin: 0px; - - padding: 2px; - - border-radius: 7px; - - background: rgb(238, 240, 243); -} - -:where(#mentorView) .mentor-evidence-filters { - grid-template-columns: repeat(4, minmax(0px, 1fr)); -} - -:where(#mentorView) .mentor-evidence-filters button { - white-space: nowrap; -} - -@media (max-width: 900px) { - .mentor-page-controls { - width: 100%; - - justify-content: space-between; - } - - .mentor-page-controls .mentor-evidence-filters { - overflow-x: auto; - } -} - -@media (max-width: 720px) { - body[data-active-view="mentorView"] .workspace-view { - margin-top: 0px; - - border-radius: 7px; - - padding: 0px 0px 76px; - } -} - -body[data-active-view="mentorView"] .overview-strip { - display: grid; -} - -:where(#mentorView) .mentor-layout { - border-top: 1px solid var(--line); - - border-bottom: 1px solid var(--line); -} - -:where(#mentorView) .mentor-chat-panel { - border-color: var(--border); -} - -:where(#mentorView) .mentor-sidebar { - border-right-color: var(--border); -} - -:where(#mentorView) .mentor-directory-heading { - border-color: var(--border); -} - -:where(#mentorView) .mentor-list { - align-content: start; - - grid-template-columns: minmax(0px, 1fr); - - gap: 2px; - - padding-right: 3px; -} - -:where(#mentorView) .mentor-option { - min-width: 0px; - - cursor: pointer; - - text-align: left; - - border-bottom-color: rgb(231, 235, 239); -} - -:where(#mentorView) .mentor-chat-header { - justify-content: space-between; - - border-color: var(--border); -} - -:where(#mentorView) .mentor-chat-form { - border-color: var(--border); -} - -#mentorView { +#mentorView.workspace-view { --mentor-blue: var(--action); - --mentor-blue-dark: var(--action-hover); - --mentor-blue-soft: var(--action-soft); - - --mentor-blue-line: var(--color-action-line); - + --mentor-blue-line: var(--blue-line); --mentor-line: var(--border); - --mentor-line-soft: var(--border-subtle); - --mentor-ink: var(--text-primary); - --mentor-sub: var(--text-secondary); - --mentor-faint: var(--text-tertiary); - + height: 100%; + min-height: 0; + flex-direction: column; + padding-bottom: 0; color: var(--mentor-ink); } +#mentorView.active-view { + display: flex; +} + #mentorView .mentor-page-header { - min-height: 36px; - - display: flex; - - align-items: center; - - gap: var(--space-12); - - margin-bottom: 12px; - - padding: 0px; - - border: 0px; - - background: transparent; + flex: 0 0 auto; + min-height: var(--topbar-height); + margin: 0; } #mentorView .mentor-page-title { - min-width: 0px; - - display: flex; - - align-items: baseline; - - gap: 10px; + min-width: 0; } #mentorView .mentor-page-title h2 { - margin: 0px; - - color: var(--mentor-ink); - - font-size: var(--font-size-page-title); - - font-weight: var(--font-weight-bold); - - letter-spacing: 0px; + letter-spacing: 0; } -#mentorView .mentor-page-title .section-subtitle { - overflow: hidden; - - color: var(--mentor-faint); - - font-size: var(--font-size-caption); - - text-overflow: ellipsis; - - white-space: nowrap; -} - -#mentorView .mentor-page-controls { - min-width: 0px; - - display: flex; - - align-items: center; - - gap: 8px; - - margin-left: auto; -} - -#mentorView .mentor-evidence-filters { - display: inline-flex; - - align-items: center; - - gap: 2px; - - padding: 2px; - - border: 0px; - - border-radius: 8px; - - background: var(--surface-muted); -} - -#mentorView .mentor-evidence-filters button { - min-width: 52px; - - min-height: 28px; - - padding: 0px 12px; - - border: 0px; - - border-radius: 6px; - - background: transparent; - - color: var(--mentor-sub); - - font-style: inherit; - - font-variant: inherit; - - font-weight: inherit; - - font-stretch: inherit; - - line-height: inherit; - - font-family: inherit; - - font-optical-sizing: inherit; - - font-size-adjust: inherit; - - font-kerning: inherit; - - font-feature-settings: inherit; - - font-variation-settings: inherit; - - font-language-override: inherit; - - font-size: 12px; - - cursor: pointer; - - transition: color 150ms, background-color 150ms, box-shadow 150ms; -} - -#mentorView .mentor-evidence-filters button:hover { - color: var(--mentor-blue); -} - -#mentorView .mentor-evidence-filters button.active { - background: var(--control-surface); - - color: var(--mentor-ink); - - font-weight: var(--font-weight-semibold); - - box-shadow: var(--shadow-xs); -} - -#mentorView .mentor-evidence-filters button[data-mentor-grade="A"] { - color: rgb(22, 129, 74); -} - -#mentorView .mentor-evidence-filters button[data-mentor-grade="B"] { - color: rgb(46, 103, 199); -} - -#mentorView .mentor-evidence-filters button[data-mentor-grade="C"] { - color: rgb(167, 102, 8); -} - -#mentorView .mentor-evidence-filters button:focus-visible { - outline: var(--action) solid 2px; - - outline-offset: 1px; -} - -#mentorView #mentorNotice { - margin: 0px 0px 10px; +#mentorView #mentorNotice:not([hidden]) { + flex: 0 0 auto; + margin-bottom: var(--space-8); } #mentorView .mentor-layout { - height: auto; - - min-height: 0px; - + min-width: 0; + min-height: 0; + flex: 1 1 auto; display: grid; - - grid-template-columns: 340px minmax(0px, 1fr); - - align-items: stretch; - - gap: 12px; - - overflow: visible; - - border: 0px; - - border-radius: 0px; - - background: transparent; - - box-shadow: none; -} - -@media (min-width: 721px) { - body[data-active-view="mentorView"] .app-main { - min-height: 0px; - - display: flex; - - flex-direction: column; - - padding-bottom: 0px; - - overflow: hidden; - } - - body[data-active-view="mentorView"] .overview-strip { - flex: 0 0 auto; - } - - body[data-active-view="mentorView"] #mentorView.active-view { - min-height: 0px; - - flex: 1 1 auto; - - display: flex; - - flex-direction: column; - - overflow: hidden; - - padding-bottom: 6px; - } + grid-template-columns: var(--mentor-directory-width) minmax(0, 1fr) var(--mentor-profile-width); + overflow: hidden; + border: 1px solid var(--mentor-line); + border-radius: var(--radius-lg); + background: var(--surface); + box-shadow: var(--shadow-xs); } #mentorView .mentor-sidebar { - min-width: 0px; - - min-height: 0px; - + min-width: 0; + min-height: 0; + display: flex; + flex-direction: column; overflow: hidden; - - border: 1px solid var(--mentor-line); - - border-radius: var(--card-radius); - - background: var(--surface); - - box-shadow: var(--card-shadow); - - position: relative; - - padding: 0px; -} - -#mentorView .mentor-directory-backdrop { - display: none; -} - -#mentorView .mentor-directory-close { - display: none; -} - -#mentorView .mentor-directory-toggle { - display: none; -} - -#mentorView .mentor-directory-content { - height: 100%; - - min-height: 0px; - - display: grid; - - grid-template-rows: auto auto auto minmax(0px, 1fr) auto; - - gap: 0px; - - padding: 0px; - - background: var(--surface); + border-right: 1px solid var(--mentor-line); + background: var(--surface-muted); } #mentorView .mentor-directory-heading { - border-bottom: 1px solid var(--mentor-line-soft); - + height: var(--mentor-pane-header-height); + flex: 0 0 var(--mentor-pane-header-height); + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-8); + padding: 0 var(--space-12); + border-bottom: 1px solid var(--mentor-line); background: var(--surface); - - min-height: 44px; - - display: flex; - - align-items: center; - - gap: 10px; - - padding: 9px 12px 9px 14px; } -#mentorView .mentor-directory-heading > .mentor-directory-title { - min-width: 0px; - +#mentorView .mentor-directory-title { + min-width: 0; display: flex; - align-items: baseline; - - gap: 8px; + gap: var(--space-8); } -#mentorView .mentor-directory-title h3 { - flex: 0 0 auto; - - margin: 0px; - +#mentorView .mentor-directory-title strong { color: var(--mentor-ink); - - font-size: 14px; - - font-weight: 700; + font-size: var(--font-size-card-title); + font-weight: var(--font-weight-semibold); } -#mentorView .mentor-directory-title > span { - overflow: hidden; - +#mentorView .mentor-directory-title span { color: var(--mentor-faint); - - font-size: 10.5px; - - text-overflow: ellipsis; - - white-space: nowrap; -} - -#mentorView .mentor-directory-actions { - display: flex; - - align-items: center; - - gap: 5px; - - margin-left: auto; -} - -#mentorView .mentor-count { - color: var(--mentor-faint); - - font-size: 10.5px; - - font-weight: 500; - - white-space: nowrap; + font-size: var(--font-size-caption); + font-variant-numeric: tabular-nums; } #mentorView .mentor-sort-toggle { - min-height: 28px; - + height: var(--control-height); display: inline-flex; - align-items: center; - - gap: 4px; - - padding: 0px 7px; - - border: 1px solid transparent; - - border-radius: 6px; - + gap: var(--space-4); + padding: 0 var(--space-8); + border: 0; + border-radius: var(--radius-sm); background: transparent; - - color: var(--mentor-blue); - - font-style: inherit; - - font-variant: inherit; - - font-weight: inherit; - - font-stretch: inherit; - - line-height: inherit; - - font-family: inherit; - - font-optical-sizing: inherit; - - font-size-adjust: inherit; - - font-kerning: inherit; - - font-feature-settings: inherit; - - font-variation-settings: inherit; - - font-language-override: inherit; - - font-size: 11px; - + color: var(--mentor-sub); + font-size: var(--font-size-label); cursor: pointer; + transition: color var(--motion-fast), background var(--motion-fast); } -#mentorView .mentor-sort-toggle.active { - border-color: var(--mentor-blue-line); - - background: var(--mentor-blue-soft); - - color: var(--mentor-blue-dark); -} - -#mentorView .mentor-sort-toggle:hover { - border-color: var(--mentor-blue-line); - - background: var(--mentor-blue-soft); - - color: var(--mentor-blue-dark); +#mentorView .mentor-sort-toggle:is(:hover, .active) { + background: var(--action-soft); + color: var(--action); } #mentorView .mentor-sort-toggle .lucide { - width: 13px; + width: var(--font-size-body); + height: var(--font-size-body); +} - height: 13px; +#mentorView .mentor-directory-tools { + flex: 0 0 auto; + display: grid; + gap: var(--space-8); + padding: var(--space-12); + border-bottom: 1px solid var(--mentor-line-soft); } #mentorView .mentor-search-field { height: var(--control-height); - - display: grid; - - grid-template-columns: 16px minmax(0px, 1fr); - + display: flex; align-items: center; - - gap: 7px; - - margin: 9px 12px 7px; - - padding: 0px 9px; - - border: 1px solid var(--mentor-line); - + gap: var(--space-8); + padding: 0 var(--space-8); + border: 1px solid var(--control-border); border-radius: var(--radius-md); - background: var(--control-surface); - color: var(--mentor-faint); + transition: border-color var(--motion-fast), box-shadow var(--motion-fast); } #mentorView .mentor-search-field:focus-within { - border-color: var(--mentor-blue); - + border-color: var(--action); box-shadow: 0 0 0 3px var(--focus-ring); } #mentorView .mentor-search-field .lucide { - width: 14px; - - height: 14px; + width: var(--font-size-body); + height: var(--font-size-body); + flex: 0 0 auto; } #mentorView .mentor-search-field input { width: 100%; - - min-width: 0px; - - height: 32px; - - padding: 0px; - - border: 0px; - - outline: 0px; - + min-width: 0; + border: 0; + outline: 0; background: transparent; - color: var(--mentor-ink); - - font-style: inherit; - - font-variant: inherit; - - font-weight: inherit; - - font-stretch: inherit; - - line-height: inherit; - - font-family: inherit; - - font-optical-sizing: inherit; - - font-size-adjust: inherit; - - font-kerning: inherit; - - font-feature-settings: inherit; - - font-variation-settings: inherit; - - font-language-override: inherit; - - font-size: 12px; + font-size: var(--font-size-label); } #mentorView .mentor-search-field input::placeholder { color: var(--mentor-faint); } +#mentorView .mentor-evidence-filters { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: var(--space-4); +} + +#mentorView .mentor-evidence-filters button { + height: var(--control-height); + border: 0; + border-radius: var(--radius-sm); + background: transparent; + color: var(--mentor-sub); + font-size: var(--font-size-label); + cursor: pointer; + transition: color var(--motion-fast), background var(--motion-fast); +} + +#mentorView .mentor-evidence-filters button:is(:hover, .active) { + background: var(--surface-selected); + color: var(--action); + font-weight: var(--font-weight-semibold); +} + #mentorView .mentor-sort-hint { - margin: 0px; - - padding: 3px 14px 7px; - - color: var(--mentor-faint); - - font-size: 10px; - - line-height: 1.4; + margin: 0; + color: var(--warning-color); + font-size: var(--font-size-caption); + line-height: 1.5; } #mentorView .mentor-list { - min-height: 0px; - - display: block; - - padding: 0px; - + min-height: 0; + flex: 1 1 auto; overflow: hidden auto; - - scrollbar-color: rgb(215, 220, 228) transparent; - - scrollbar-width: thin; + scrollbar-gutter: stable; } #mentorView .mentor-option { - width: 100%; - - min-height: 70px; - - display: grid; - - grid-template-columns: minmax(0px, 1fr) auto; - + position: relative; + min-height: 68px; + display: flex; align-items: stretch; - - gap: 0px; - - padding: 0px 7px 0px 0px; - - overflow: hidden; - - border-top: 0px; - - border-right: 0px; - - border-left: 0px; - - border-image: none; - border-bottom: 1px solid var(--mentor-line-soft); - - border-radius: 0px; - - background: var(--surface); - - color: var(--mentor-ink); - - box-shadow: none; - - transition: background-color var(--motion-fast), box-shadow var(--motion-fast); + background: transparent; + transition: background var(--motion-fast), box-shadow var(--motion-fast); } -#mentorView .mentor-option:hover { +#mentorView .mentor-option:is(:hover, .active) { background: var(--surface-hover); } #mentorView .mentor-option.active { - background: var(--surface-selected); - - box-shadow: inset 2px 0 0 var(--mentor-blue); + box-shadow: inset 3px 0 0 var(--action); } #mentorView .mentor-option.is-dragging { - opacity: 0.45; + opacity: .48; } #mentorView .mentor-option.is-drag-over { - background: rgb(231, 239, 255); - - box-shadow: inset 3px 0 0 var(--mentor-blue); + box-shadow: inset 0 2px 0 var(--action); } #mentorView .mentor-option-main { - min-width: 0px; - - min-height: 69px; - - display: block; - - padding: 9px 7px 8px 14px; - - border: 0px; - + min-width: 0; + flex: 1 1 auto; + display: flex; + align-items: center; + gap: var(--space-8); + padding: var(--space-8) var(--space-4) var(--space-8) var(--space-12); + border: 0; background: transparent; - color: inherit; - - cursor: pointer; - - font: inherit; - text-align: left; + cursor: pointer; } -#mentorView .mentor-option-main:disabled { - cursor: default; +#mentorView .mentor-avatar { + width: var(--mentor-avatar-size); + height: var(--mentor-avatar-size); + flex: 0 0 var(--mentor-avatar-size); + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid var(--mentor-blue-line); + border-radius: 50%; + background: var(--mentor-blue-soft); + color: var(--mentor-blue-dark); + font-size: var(--font-size-card-title); + font-weight: var(--font-weight-semibold); } -#mentorView .mentor-option-main:focus-visible { - outline: var(--action) solid 2px; +#mentorView .mentor-avatar[data-grade="a"] { + border-color: var(--market-up); + background: var(--market-up-soft); + color: var(--market-up); +} - outline-offset: -3px; +#mentorView .mentor-avatar[data-grade="c"] { + border-color: var(--market-down); + background: var(--market-down-soft); + color: var(--market-down); } #mentorView .mentor-option-copy { - min-width: 0px; - + min-width: 0; display: grid; - - gap: 4px; - - margin: 0px; - - color: inherit; - - line-height: normal; + flex: 1 1 auto; + gap: var(--space-4); } #mentorView .mentor-option-heading { - min-width: 0px; - + min-width: 0; display: flex; - align-items: center; - - gap: 7px; + gap: var(--space-4); } #mentorView .mentor-option-heading > strong { - min-width: 0px; - + min-width: 0; overflow: hidden; - color: var(--mentor-ink); - - font-size: 13px; - - font-weight: 700; - + font-size: var(--font-size-body); + font-weight: var(--font-weight-semibold); text-overflow: ellipsis; + white-space: nowrap; +} +#mentorView .mentor-option-badges { + flex: 0 0 auto; + display: inline-flex; + gap: var(--space-4); +} + +#mentorView :is(.mentor-option-copy > em, .mentor-option-meta) { + min-width: 0; + overflow: hidden; + font-size: var(--font-size-caption); + font-style: normal; + line-height: 1.35; + text-overflow: ellipsis; white-space: nowrap; } #mentorView .mentor-option-copy > em { - display: block; - - overflow: hidden; - color: var(--mentor-sub); - - font-size: 11.5px; - - font-style: normal; - - line-height: 1.55; - - text-overflow: ellipsis; - - white-space: nowrap; } #mentorView .mentor-option-meta { - min-width: 0px; - - display: flex; - - align-items: center; - - gap: 9px; - - overflow: hidden; - - color: var(--mentor-faint); - - font-size: 10.5px; - - line-height: 1.25; - - white-space: nowrap; -} - -#mentorView .mentor-option-meta > span { - display: inline; - - flex: 0 0 auto; - - margin: 0px; - - color: inherit; - - font-size: inherit; -} - -#mentorView .mentor-option-meta .mentor-evidence-source { - max-width: 108px; - - overflow: hidden; - - border-bottom: 1px dashed rgb(199, 205, 214); - - text-overflow: ellipsis; - - cursor: help; -} - -#mentorView .mentor-active-badges { - min-width: 0px; - - display: flex; - - align-items: center; - - justify-content: flex-start; - - flex-wrap: nowrap; - - gap: 4px; - - margin: 0px; -} - -#mentorView .mentor-option-badges { - min-width: 0px; - - display: flex; - - align-items: center; - - justify-content: flex-start; - - flex-wrap: nowrap; - - gap: 4px; - - margin: 0px; -} - -#mentorView .mentor-option-heading .mentor-option-badges { - margin-left: auto; -} - -#mentorView .mentor-badge { - min-height: 20px; - - display: inline-flex; - - align-items: center; - - justify-content: center; - - gap: 3px; - - margin: 0px; - - padding: 0px 6px; - - border: 1px solid var(--mentor-line); - - border-radius: 5px; - - background: var(--surface-muted); - - color: var(--mentor-sub); - - font-size: 9.5px; - - font-style: normal; - - font-weight: 700; - - line-height: 1; - - white-space: nowrap; -} - -#mentorView .mentor-badge .lucide { - width: 10px; - - height: 10px; -} - -#mentorView .mentor-badge.grade-a { - border-color: rgb(184, 220, 199); - - background: rgb(237, 248, 241); - - color: rgb(22, 129, 74); -} - -#mentorView .mentor-badge.grade-b { - border-color: rgb(189, 208, 237); - - background: rgb(239, 245, 255); - - color: rgb(46, 103, 199); -} - -#mentorView .mentor-badge.grade-c { - border-color: rgb(234, 209, 167); - - background: rgb(253, 245, 232); - - color: rgb(167, 102, 8); -} - -#mentorView .mentor-badge.private { - border-color: rgb(232, 208, 156); - - background: rgb(255, 248, 231); - - color: rgb(146, 103, 19); -} - -#mentorView .mentor-badge.quality { - border-color: transparent; - - background: var(--surface-muted); - color: var(--mentor-faint); } #mentorView .mentor-option-tools { + flex: 0 0 auto; display: flex; - align-items: center; - - align-self: center; - - gap: 1px; + gap: var(--space-4); + padding-right: var(--space-8); } -#mentorView .mentor-order-button { - width: 26px; - - min-width: 26px; - - height: 28px; - - display: grid; - - place-items: center; - - padding: 0px; - - border: 0px; - - border-radius: 6px; - - background: transparent; - - color: rgb(161, 168, 179); - - cursor: pointer; -} - -#mentorView .mentor-pin-button { - width: 26px; - - min-width: 26px; - - height: 28px; - - display: grid; - - place-items: center; - - padding: 0px; - - border: 0px; - - border-radius: 6px; - - background: transparent; - - color: rgb(161, 168, 179); - - cursor: pointer; -} - -#mentorView .mentor-order-button:hover:not(:disabled) { - background: rgb(238, 241, 245); - - color: var(--mentor-ink); -} - -#mentorView .mentor-pin-button:hover { - background: rgb(238, 241, 245); - - color: var(--mentor-ink); -} - -#mentorView .mentor-pin-button.active { - background: rgb(255, 244, 216); - - color: rgb(163, 109, 13); -} - -#mentorView .mentor-pin-button.active .lucide { - fill: currentcolor; -} - -#mentorView .mentor-order-button .lucide { - width: 13px; - - height: 13px; -} - -#mentorView .mentor-pin-button .lucide { - width: 13px; - - height: 13px; -} - -#mentorView .mentor-order-button:disabled { - opacity: 0.3; - - cursor: default; -} - -#mentorView .mentor-list.is-sorting .mentor-option { - cursor: grab; -} - -#mentorView .mentor-list.is-sorting .mentor-option-badges { - display: none; -} - -#mentorView .mentor-list-empty { - padding: 32px 14px; - - color: var(--mentor-faint); - - font-size: 12px; - - text-align: center; -} - -#mentorView .mentor-evidence-legend { - margin: 0px; - - padding: 8px 12px 9px; - - border-top: 1px solid var(--mentor-line-soft); - - color: var(--mentor-faint); - - font-size: 9.5px; - - line-height: 1.5; -} - -#mentorView .mentor-chat-panel { - min-width: 0px; - - min-height: 0px; - - overflow: hidden; - - border: 1px solid var(--mentor-line); - - border-radius: var(--card-radius); - - background: var(--surface); - - box-shadow: var(--card-shadow); - - height: 100%; - - display: grid; - - grid-template-rows: auto minmax(0px, 1fr) auto auto auto; -} - -#mentorView .mentor-chat-header { - border-bottom: 1px solid var(--mentor-line-soft); - - background: var(--surface); - - min-height: 64px; - - display: flex; - - align-items: center; - - gap: 14px; - - padding: 10px 14px; -} - -#mentorView .mentor-active-profile { - min-width: 0px; - - display: grid; - - gap: 3px; -} - -#mentorView .mentor-active-title { - min-width: 0px; - - display: flex; - - align-items: center; - - gap: 7px; -} - -#mentorView .mentor-active-title h3 { - min-width: 0px; - - margin: 0px; - - overflow: hidden; - - color: var(--mentor-ink); - - font-size: 14px; - - font-weight: 700; - - text-overflow: ellipsis; - - white-space: nowrap; -} - -#mentorView .mentor-active-profile > p { - max-width: min(760px, 72vw); - - margin: 0px; - - overflow: hidden; - - color: var(--mentor-sub); - - font-size: 10.5px; - - line-height: 1.4; - - text-overflow: ellipsis; - - white-space: nowrap; -} - -#mentorView .mentor-active-focus { - min-width: 0px; - - display: flex; - - align-items: center; - - gap: 7px; - - overflow: hidden; -} - -#mentorView .mentor-active-focus span { - padding: 0px; - - background: transparent; - - color: var(--mentor-faint); - - font-size: 9.5px; - - white-space: nowrap; -} - -#mentorView .mentor-active-focus span::before { - content: "#"; -} - -#mentorView .mentor-clear-button { - min-width: 0px; - - min-height: 28px; - +#mentorView :is(.mentor-pin-button, .mentor-order-button) { + width: var(--control-height); + height: var(--control-height); display: inline-flex; - align-items: center; - - gap: 5px; - - margin-left: auto; - - padding: 0px 8px; - - border: 1px solid transparent; - - border-radius: 6px; - + justify-content: center; + border: 0; + border-radius: var(--radius-sm); background: transparent; - - color: var(--mentor-blue); - - font-size: 11px; - - white-space: nowrap; + color: var(--mentor-faint); + cursor: pointer; + opacity: 0; + transition: color var(--motion-fast), background var(--motion-fast), opacity var(--motion-fast); } -#mentorView .mentor-clear-button:hover:not(:disabled) { - border-color: var(--mentor-blue-line); - - background: var(--mentor-blue-soft); -} - -#mentorView .mentor-clear-button:disabled { - color: rgb(184, 190, 199); - +#mentorView .mentor-option:is(:hover, :focus-within) :is(.mentor-pin-button, .mentor-order-button), +#mentorView .mentor-pin-button.active, +#mentorView .mentor-list.is-sorting .mentor-order-button { opacity: 1; } -#mentorView .mentor-clear-button .lucide { - width: 13px; +#mentorView :is(.mentor-pin-button, .mentor-order-button):is(:hover, .active) { + background: var(--action-soft); + color: var(--action); +} - height: 13px; +#mentorView :is(.mentor-pin-button, .mentor-order-button) .lucide { + width: var(--font-size-body); + height: var(--font-size-body); +} + +#mentorView :is(.mentor-list-empty, .mentor-evidence-legend) { + margin: 0; + color: var(--mentor-faint); + font-size: var(--font-size-caption); + text-align: center; +} + +#mentorView .mentor-list-empty { + padding: var(--space-24) var(--space-12); +} + +#mentorView .mentor-evidence-legend { + flex: 0 0 auto; + padding: var(--space-8) var(--space-12); + border-top: 1px solid var(--mentor-line-soft); + background: var(--surface); +} + +#mentorView .mentor-badge { + height: 18px; + display: inline-flex; + align-items: center; + gap: var(--space-4); + padding: 0 var(--space-4); + border: 1px solid var(--mentor-line); + border-radius: var(--radius-sm); + background: var(--surface-muted); + color: var(--mentor-sub); + font-size: var(--font-size-aux); + font-weight: var(--font-weight-semibold); + line-height: 1; +} + +#mentorView .mentor-badge .lucide { + width: var(--font-size-aux); + height: var(--font-size-aux); +} + +#mentorView .mentor-badge.grade-a { + border-color: var(--market-up); + background: var(--market-up-soft); + color: var(--market-up); +} + +#mentorView .mentor-badge.grade-b { + border-color: var(--mentor-blue-line); + background: var(--mentor-blue-soft); + color: var(--mentor-blue-dark); +} + +#mentorView .mentor-badge.grade-c { + border-color: var(--market-down); + background: var(--market-down-soft); + color: var(--market-down); +} + +#mentorView .mentor-badge.private { + border-color: var(--warning-color); + background: var(--warning-soft); + color: var(--warning-color); +} + +#mentorView .mentor-chat-panel { + min-width: 0; + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; + background: var(--surface-subtle); +} + +#mentorView .mentor-chat-header { + height: var(--mentor-pane-header-height); + flex: 0 0 var(--mentor-pane-header-height); + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-12); + padding: 0 var(--space-16); + border-bottom: 1px solid var(--mentor-line); + background: var(--surface); +} + +#mentorView .mentor-chat-identity { + min-width: 0; + display: flex; + align-items: center; + gap: var(--space-8); +} + +#mentorView .mentor-chat-identity > div { + min-width: 0; +} + +#mentorView .mentor-active-title { + min-width: 0; + display: flex; + align-items: center; + gap: var(--space-8); +} + +#mentorView .mentor-active-title h3 { + min-width: 0; + margin: 0; + overflow: hidden; + color: var(--mentor-ink); + font-size: var(--font-size-card-title); + font-weight: var(--font-weight-semibold); + letter-spacing: 0; + text-overflow: ellipsis; + white-space: nowrap; +} + +#mentorView .mentor-active-badges { + display: inline-flex; + gap: var(--space-4); +} + +#mentorView #activeMentorStatus { + margin: var(--space-4) 0 0; + color: var(--mentor-faint); + font-size: var(--font-size-caption); +} + +#mentorView .mentor-clear-button { + flex: 0 0 auto; + color: var(--mentor-sub); } #mentorView .mentor-messages { - min-width: 0px; - - min-height: 0px; - - max-height: none; - - overflow-y: auto; - - padding: 20px 24px; - - background: var(--surface); - - scrollbar-color: rgb(215, 220, 228) transparent; - - scrollbar-width: thin; + min-height: 0; + flex: 1 1 auto; + overflow: hidden auto; + padding: var(--space-24); + scrollbar-gutter: stable; + scroll-behavior: smooth; } #mentorView .mentor-empty-state { min-height: 100%; - - display: grid; - - place-content: center; - - justify-items: center; - - padding: 28px; - - color: var(--mentor-faint); - + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: var(--space-8); + color: var(--mentor-sub); text-align: center; } #mentorView .mentor-empty-mark { - width: 44px; - - height: 40px; - - display: grid; - - place-items: center; - - margin-bottom: 12px; - - border: 0px; - - border-radius: 8px; - - background: var(--action-soft); - - color: var(--action); + width: var(--mentor-profile-avatar-size); + height: var(--mentor-profile-avatar-size); + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid var(--mentor-blue-line); + border-radius: 50%; + background: var(--mentor-blue-soft); + color: var(--mentor-blue-dark); } #mentorView .mentor-empty-mark .lucide { - width: 21px; - - height: 21px; - - stroke-width: 1.65; + width: var(--font-size-metric); + height: var(--font-size-metric); } #mentorView .mentor-empty-state strong { - color: var(--text-primary); - - font-size: 13px; - - font-weight: 600; + color: var(--mentor-ink); + font-size: var(--font-size-card-title); } #mentorView .mentor-empty-state p { - max-width: 580px; - - margin: 7px auto 0px; - - color: var(--mentor-faint); - - font-size: 11.5px; - + max-width: var(--table-compact); + margin: 0; + font-size: var(--font-size-label); line-height: 1.7; } #mentorView .mentor-message { - width: min(100%, 900px); - - max-width: 100%; - - margin: 0px 0px 18px; - - padding: 2px 8px 18px 16px; - - border: 0px; - - border-bottom: 1px solid var(--mentor-line-soft); - - border-left: 2px solid var(--mentor-blue); - - border-radius: 0px; - - background: transparent; - - box-shadow: none; + width: 100%; + display: flex; + align-items: flex-start; + gap: var(--space-8); + margin-bottom: var(--space-16); } #mentorView .mentor-message.user { - width: fit-content; - - max-width: min(72%, 680px); - - margin-left: auto; - - padding: 9px 12px; - - border: 1px solid var(--mentor-blue-line); - - border-radius: var(--radius-md); - - background: var(--mentor-blue-soft); + flex-direction: row-reverse; } -#mentorView .mentor-message.assistant { - margin-right: auto; +#mentorView .mentor-message-avatar { + width: var(--mentor-avatar-size); + height: var(--mentor-avatar-size); + flex: 0 0 var(--mentor-avatar-size); + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid var(--mentor-line); + border-radius: 50%; + background: var(--surface); + color: var(--mentor-sub); + font-size: var(--font-size-label); + font-weight: var(--font-weight-semibold); +} + +#mentorView .mentor-message.user .mentor-message-avatar { + border-color: var(--mentor-blue-line); + background: var(--mentor-blue-soft); + color: var(--mentor-blue-dark); +} + +#mentorView .mentor-message-body { + max-width: var(--mentor-message-max-width); + display: flex; + flex-direction: column; + align-items: flex-start; + gap: var(--space-4); +} + +#mentorView .mentor-message.user .mentor-message-body { + align-items: flex-end; } #mentorView .mentor-message-label { - margin-bottom: 5px; - color: var(--mentor-faint); - - font-size: 10.5px; - - font-weight: 650; + font-size: var(--font-size-caption); + line-height: 1.4; } #mentorView .mentor-message-content { - margin: 0px; - + min-width: 48px; + padding: var(--space-12) var(--space-16); + border: 1px solid var(--mentor-line); + border-radius: var(--radius-lg); + background: var(--surface); + color: var(--mentor-ink); + font-size: var(--font-size-body); + line-height: 1.75; overflow-wrap: anywhere; - - color: var(--text-primary); - - font-size: 13px; - - line-height: 1.72; - - white-space: normal; } -#mentorView .mentor-message .mentor-answer-paragraph { - margin: 0px 0px 6px; - - line-height: inherit; +#mentorView .mentor-message.assistant .mentor-message-content { + border-top-left-radius: var(--radius-sm); } -#mentorView .mentor-message .mentor-answer-paragraph:last-child { - margin-bottom: 0px; +#mentorView .mentor-message.user .mentor-message-content { + border-color: var(--mentor-blue-line); + border-top-right-radius: var(--radius-sm); + background: var(--mentor-blue-soft); +} + +#mentorView .mentor-message.is-error .mentor-message-content { + border-color: var(--market-up); +} + +#mentorView .mentor-message small { + color: var(--mentor-faint); + font-size: var(--font-size-caption); +} + +#mentorView .mentor-answer-paragraph { + margin: 0 0 var(--space-8); +} + +#mentorView .mentor-answer-paragraph:last-child { + margin-bottom: 0; } #mentorView .mentor-answer-heading { display: block; - - margin: 9px 0px 4px; - + margin: var(--space-12) 0 var(--space-4); color: var(--mentor-ink); - - font-size: 13px; - - line-height: 1.45; + font-size: var(--font-size-body); + font-weight: var(--font-weight-semibold); } -#mentorView .mentor-message-content > .mentor-answer-heading:first-child { - margin-top: 0px; +#mentorView .mentor-answer-heading:first-child { + margin-top: 0; } #mentorView .mentor-answer-list { - margin: 3px 0px 7px; - - padding-left: 20px; + margin: var(--space-4) 0 var(--space-8); + padding-left: var(--space-24); } #mentorView .mentor-answer-list li + li { - margin-top: 3px; -} - -#mentorView .mentor-answer-rule { - display: block; - - height: 1px; - - margin: 8px 0px; - - background: var(--mentor-line); + margin-top: var(--space-4); } #mentorView .mentor-answer-quote { display: block; - - padding-left: 9px; - - border-left: 2px solid var(--mentor-blue); - + margin: var(--space-8) 0; + padding-left: var(--space-12); + border-left: 2px solid var(--action); color: var(--mentor-sub); } -#mentorView .mentor-message small { +#mentorView .mentor-answer-rule { display: block; - - margin-top: 7px; - - color: var(--mentor-faint); - - font-size: 9.5px; + height: 1px; + margin: var(--space-12) 0; + background: var(--mentor-line-soft); } -#mentorView .mentor-message.is-error { - border-color: rgb(229, 165, 160); +#mentorView .mentor-loading-copy { + margin: 0; + color: var(--mentor-sub); +} + +#mentorView .assistant-stream-caret { + width: 2px; + height: var(--font-size-body); + display: inline-block; + margin-left: var(--space-4); + background: var(--action); + animation: mentor-caret 900ms steps(1, end) infinite; +} + +#mentorView .mentor-follow-ups { + width: 100%; + display: grid; + gap: var(--space-4); + margin-top: var(--space-4); + padding-top: var(--space-8); + border-top: 1px solid var(--mentor-line-soft); +} + +#mentorView .mentor-follow-ups > span { + color: var(--mentor-faint); + font-size: var(--font-size-caption); +} + +#mentorView .mentor-follow-ups button { + min-height: var(--control-height); + display: flex; + align-items: flex-start; + gap: var(--space-8); + padding: var(--space-4) 0; + border: 0; + background: transparent; + color: var(--action); + font-size: var(--font-size-label); + line-height: 1.5; + text-align: left; + cursor: pointer; + transition: color var(--motion-fast), transform var(--motion-fast); +} + +#mentorView .mentor-follow-ups button:hover { + color: var(--action-hover); + transform: translateX(var(--space-4)); +} + +#mentorView .mentor-follow-ups button .lucide { + width: var(--font-size-body); + height: var(--font-size-body); + flex: 0 0 auto; + margin-top: var(--space-4); } #mentorView .mentor-quick-prompts { - min-height: 42px; - + flex: 0 0 auto; display: flex; - align-items: center; - - gap: 7px; - - padding: 7px 14px; - - overflow-x: auto; - - border-top: 1px solid var(--mentor-line-soft); - - background: var(--surface-subtle); - - scrollbar-width: none; + gap: var(--space-8); + padding: var(--space-8) var(--space-16) 0; + border-top: 1px solid var(--mentor-line); + background: var(--surface); } -#mentorView .mentor-quick-prompts::-webkit-scrollbar { +#mentorView .mentor-quick-prompts[hidden] { display: none; } #mentorView .mentor-prompt-label { - flex: 0 0 auto; - color: var(--mentor-faint); - - font-size: 10.5px; - - white-space: nowrap; + font-size: var(--font-size-caption); } #mentorView .mentor-quick-prompts button { - min-height: 26px; - - flex: 0 0 auto; - - padding: 0px 10px; - + height: var(--control-height); + display: inline-flex; + align-items: center; + gap: var(--space-4); + padding: 0 var(--space-8); border: 1px solid var(--mentor-line); - border-radius: var(--radius-md); - - background: var(--control-surface); - + background: var(--surface-subtle); color: var(--mentor-sub); - - font-size: 11px; - - white-space: nowrap; - + font-size: var(--font-size-label); cursor: pointer; - - transition: border-color 150ms, color 150ms, background-color 150ms; + transition: border-color var(--motion-fast), color var(--motion-fast), background var(--motion-fast); } #mentorView .mentor-quick-prompts button:hover { border-color: var(--mentor-blue-line); - background: var(--mentor-blue-soft); + color: var(--action); +} - color: var(--mentor-blue); +#mentorView .mentor-quick-prompts button .lucide { + width: var(--font-size-body); + height: var(--font-size-body); } #mentorView .mentor-chat-form { - min-height: 55px; - - display: grid; - - grid-template-columns: minmax(0px, 1fr) auto; - - align-items: center; - - gap: 8px; - - padding: 9px 14px; - - border-top: 1px solid var(--mentor-line-soft); - + min-height: var(--mentor-composer-min-height); + flex: 0 0 auto; + display: flex; + flex-direction: column; + padding: var(--space-8) var(--space-16); + border-top: 1px solid var(--mentor-line); background: var(--surface); } -#mentorView .mentor-chat-form textarea { +#mentorView #mentorQuestion { width: 100%; - - min-width: 0px; - - height: 38px; - - min-height: 38px; - - max-height: 88px; - - padding: 8px 11px; - - resize: vertical; - - border: 1px solid var(--mentor-line); - - border-radius: var(--radius-md); - - outline: 0px; - - background: var(--control-surface); - + min-height: 50px; + flex: 1 1 auto; + resize: none; + border: 0; + outline: 0; + background: transparent; color: var(--mentor-ink); - - font-style: inherit; - - font-variant: inherit; - - font-weight: inherit; - - font-stretch: inherit; - font-family: inherit; - - font-optical-sizing: inherit; - - font-size-adjust: inherit; - - font-kerning: inherit; - - font-feature-settings: inherit; - - font-variation-settings: inherit; - - font-language-override: inherit; - - font-size: 12.5px; - - line-height: 1.55; + font-size: var(--font-size-body); + line-height: 1.6; } -#mentorView .mentor-chat-form textarea:focus { - border-color: var(--mentor-blue); - - box-shadow: 0 0 0 3px var(--focus-ring); +#mentorView #mentorQuestion::placeholder { + color: var(--mentor-faint); } -#mentorView .mentor-chat-form .button { - min-width: 76px; - - height: 38px; - - min-height: 38px; - - display: inline-flex; - +#mentorView .mentor-composer-actions { + display: flex; align-items: center; - - justify-content: center; - - gap: 5px; - - padding: 0px 13px; - - border-radius: 7px; - - background: var(--mentor-blue); - - color: var(--on-action); - - font-size: 12.5px; + justify-content: flex-end; + gap: var(--space-8); } -#mentorView .mentor-chat-form .button:hover:not(:disabled) { - background: var(--mentor-blue-dark); +#mentorView .mentor-composer-actions > span { + margin-right: auto; + color: var(--mentor-faint); + font-size: var(--font-size-caption); } -#mentorView .mentor-chat-form .button .lucide { - width: 14px; +#mentorView :is(.mentor-send-button, .mentor-stop-button) { + min-width: var(--col-action); +} - height: 14px; +#mentorView .mentor-stop-button { + color: var(--market-up); } #mentorView .mentor-disclaimer { - min-height: 23px; - - margin: 0px; - - padding: 0px 14px 8px; - + flex: 0 0 auto; + margin: 0; + padding: var(--space-4) var(--space-16) var(--space-8); background: var(--surface); - color: var(--mentor-faint); - - font-size: 9.5px; - - text-align: right; + font-size: var(--font-size-aux); + text-align: center; } -@media (min-width: 721px) and (max-width: 1100px) { - #mentorView .mentor-layout { - grid-template-columns: 272px minmax(0px, 1fr); - } - - #mentorView .mentor-directory-title > span { - display: none; - } - - #mentorView .mentor-active-profile > p { - max-width: 48vw; - } +#mentorView .mentor-profile-panel { + min-width: 0; + min-height: 0; + overflow: hidden auto; + border-left: 1px solid var(--mentor-line); + background: var(--surface); } -@media (max-width: 720px) { - body.mentor-directory-open { - overflow: hidden; - } - - #mentorView .mentor-page-header { - min-height: 84px; - - align-items: flex-start; - - flex-direction: column; - - gap: 8px; - - padding: 10px 12px 0px; - } - - #mentorView .mentor-page-title { - width: 100%; - } - - #mentorView .mentor-page-title h2 { - font-size: 17px; - } - - #mentorView .mentor-page-title .section-subtitle { - font-size: 10.5px; - } - - #mentorView .mentor-page-controls { - width: 100%; - - margin: 0px; - } - - #mentorView .mentor-evidence-filters { - width: 100%; - } - - #mentorView .mentor-evidence-filters button { - min-height: 36px; - - flex: 1 1 0%; - } - - #mentorView .mentor-layout { - height: calc(100dvh - 286px - env(safe-area-inset-bottom)); - - min-height: 520px; - - display: grid; - - grid-template-columns: minmax(0px, 1fr); - - grid-template-rows: 56px minmax(0px, 1fr); - - gap: 8px; - - padding: 0px 8px; - - overflow: hidden; - } - - #mentorView .mentor-sidebar { - height: 56px; - - min-height: 56px; - - overflow: visible; - - border-radius: 9px; - } - - #mentorView .mentor-directory-toggle { - width: 100%; - - min-height: 54px; - - display: flex; - - align-items: center; - - justify-content: space-between; - - gap: 10px; - - padding: 0px 12px; - - border: 0px; - - background: var(--surface); - - color: var(--mentor-ink); - - cursor: pointer; - - text-align: left; - } - - #mentorView .mentor-directory-toggle > span { - min-width: 0px; - - display: flex; - - align-items: center; - - gap: 10px; - } - - #mentorView .mentor-directory-toggle > span > span { - min-width: 0px; - - display: grid; - - gap: 2px; - } - - #mentorView .mentor-directory-toggle small { - color: var(--mentor-faint); - - font-size: 9px; - } - - #mentorView .mentor-directory-toggle strong { - overflow: hidden; - - font-size: 13px; - - text-overflow: ellipsis; - - white-space: nowrap; - } - - #mentorView .mentor-directory-toggle > .lucide { - width: 16px; - - transition: transform 180ms; - } - - #mentorView .mentor-sidebar.is-open .mentor-directory-toggle > .lucide { - transform: rotate(180deg); - } - - #mentorView .mentor-directory-backdrop { - position: fixed; - - inset: 0px; - - z-index: 79; - - display: block; - - background: rgba(20, 27, 33, 0.48); - } - - #mentorView .mentor-directory-content { - height: auto; - - position: fixed; - - inset: 62px 8px 72px; - - z-index: 80; - - overflow: hidden; - - border: 1px solid var(--mentor-line); - - border-radius: 10px; - - box-shadow: rgba(16, 24, 40, 0.18) 0px 16px 36px; - +#mentorView .mentor-profile-hero { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-8); + padding: var(--space-24) var(--space-16); + text-align: center; +} + +#mentorView .mentor-profile-avatar { + width: var(--mentor-profile-avatar-size); + height: var(--mentor-profile-avatar-size); + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid var(--mentor-blue-line); + border-radius: 50%; + background: var(--mentor-blue-soft); + color: var(--mentor-blue-dark); + font-size: var(--font-size-metric); + font-weight: var(--font-weight-semibold); +} + +#mentorView .mentor-profile-hero h3 { + margin: 0; + color: var(--mentor-ink); + font-size: var(--font-size-page-title); + font-weight: var(--font-weight-semibold); + letter-spacing: 0; +} + +#mentorView .mentor-profile-hero > p { + margin: 0; + color: var(--mentor-sub); + font-size: var(--font-size-label); + line-height: 1.65; +} + +#mentorView .mentor-profile-badges { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: var(--space-4); +} + +#mentorView .mentor-profile-section { + display: grid; + gap: var(--space-8); + padding: var(--space-16); + border-top: 1px solid var(--mentor-line-soft); +} + +#mentorView .mentor-profile-section h4 { + margin: 0; + color: var(--mentor-faint); + font-size: var(--font-size-caption); + font-weight: var(--font-weight-medium); +} + +#mentorView .mentor-profile-section > strong { + color: var(--mentor-ink); + font-size: var(--font-size-label); + font-weight: var(--font-weight-semibold); +} + +#mentorView .mentor-profile-section > p { + margin: 0; + color: var(--mentor-sub); + font-size: var(--font-size-label); + line-height: 1.7; +} + +#mentorView .mentor-active-focus { + display: flex; + flex-wrap: wrap; + gap: var(--space-4); +} + +#mentorView .mentor-active-focus span { + padding: var(--space-4) var(--space-8); + border-radius: var(--radius-sm); + background: var(--surface-muted); + color: var(--mentor-sub); + font-size: var(--font-size-caption); +} + +#mentorView .mentor-profile-boundary p { + display: flex; + align-items: flex-start; + gap: var(--space-8); +} + +#mentorView .mentor-profile-boundary .lucide { + width: var(--font-size-body); + height: var(--font-size-body); + flex: 0 0 auto; + margin-top: var(--space-4); + color: var(--mentor-faint); +} + +#mentorView :is(button, input, textarea):focus-visible { + outline: 2px solid var(--action); + outline-offset: 2px; +} + +@keyframes mentor-caret { + 50% { opacity: 0; - - pointer-events: none; - - transform: translateY(10px); - - transition: opacity 180ms, transform 180ms; - } - - #mentorView .mentor-sidebar.is-open .mentor-directory-content { - opacity: 1; - - pointer-events: auto; - - transform: translateY(0px); - } - - #mentorView .mentor-directory-close { - width: 38px; - - height: 38px; - - display: grid; - - place-items: center; - } - - #mentorView .mentor-sort-toggle { - min-height: 38px; - } - - #mentorView .mentor-search-field { - height: 42px; - } - - #mentorView .mentor-search-field input { - height: 40px; - - font-size: 16px; - } - - #mentorView .mentor-option { - min-height: 84px; - } - - #mentorView .mentor-order-button, - #mentorView .mentor-pin-button { - width: 40px; - - min-width: 40px; - - height: 42px; - } - - #mentorView .mentor-chat-panel { - height: 100%; - - min-height: 0px; - - border-radius: 9px; - } - - #mentorView .chat-box { - height: 100%; - - min-height: 0px; - - overflow: hidden; - } - - #mentorView .mentor-chat-header { - min-height: 68px; - - padding: 9px 10px; - } - - #mentorView .mentor-active-profile > p, - #mentorView .mentor-active-focus { - max-width: calc(-130px + 100vw); - } - - #mentorView .mentor-clear-button { - width: 36px; - - height: 36px; - - justify-content: center; - - padding: 0px; - } - - #mentorView .mentor-clear-button span { - display: none; - } - - #mentorView .mentor-messages { - flex: 1 1 0%; - - min-height: 0px; - - padding: 12px 10px; - } - - #mentorView .mentor-empty-state { - padding: 16px; - } - - #mentorView .mentor-empty-state p { - display: none; - } - - #mentorView .mentor-message { - max-width: 92%; - } - - #mentorView .mentor-quick-prompts { - padding-inline: 10px; - } - - #mentorView .mentor-prompt-label { - display: none; - } - - #mentorView .mentor-chat-form { - padding: 7px 9px; - } - - #mentorView .mentor-chat-form .button { - min-width: 42px; - - width: 42px; - - padding: 0px; - } - - #mentorView .mentor-chat-form .button span { - display: none; - } - - #mentorView .mentor-disclaimer { - padding-inline: 10px; - - font-size: 8.5px; } } @media (prefers-reduced-motion: reduce) { - #mentorView .mentor-directory-content, - #mentorView .mentor-directory-toggle > .lucide, - #mentorView .mentor-evidence-filters button, - #mentorView .mentor-option, - #mentorView .mentor-quick-prompts button { + #mentorView :is(.mentor-sort-toggle, .mentor-search-field, .mentor-evidence-filters button, .mentor-option, .mentor-pin-button, .mentor-order-button, .mentor-follow-ups button, .mentor-quick-prompts button) { transition: none; } -} -.mentor-grid { - display: grid; - - grid-template-columns: 340px minmax(0px, 1fr); - - gap: 12px; - - align-items: start; -} - -.chat-box { - display: flex; - - flex-direction: column; - - height: calc(-250px + 100vh); - - min-height: 420px; -} - -.chat-log { - flex: 1 1 0%; - - overflow: auto; - - padding: 18px; -} - -@media (min-width: 721px) { - #mentorView #mentorNotice, - #mentorView .member-gate, - #mentorView .mentor-page-header { - flex: 0 0 auto; + #mentorView .assistant-stream-caret { + animation: none; } - #mentorView .mentor-layout { - min-height: 0px; - - flex: 1 1 auto; - - height: auto; - - overflow: hidden; - } - - #mentorView .chat-box, - #mentorView .mentor-chat-panel, - #mentorView .mentor-directory-content, - #mentorView .mentor-sidebar { - min-height: 0px; - - height: 100%; - - overflow: hidden; - } - - #mentorView .mentor-list, #mentorView .mentor-messages { - min-height: 0px; - - overflow: auto; + scroll-behavior: auto; } } -:root[data-theme="dark"] :is(.global-search-result-icon, .membership-status-grid > div, .model-row) { - border-color: var(--border); - - background: var(--surface-subtle); -} - -:root[data-theme="dark"] :is(.mentor-option, .assistant-message-content) { - border-color: var(--border); - - background: var(--surface-subtle); - - color: var(--text-primary); -} - -:root[data-theme="dark"] .mentor-option.active, -:root[data-theme="dark"] .mentor-option:hover { - background: var(--action-soft); -} - -:root[data-theme="dark"] :is(.assistant-form, .trade-log-summary) { - border-color: var(--line-soft); - - background: var(--surface); -} - :root[data-theme="dark"] #mentorView { --mentor-blue: var(--action); - --mentor-blue-dark: var(--action-hover); - --mentor-blue-soft: var(--action-soft); - --mentor-blue-line: var(--blue-line); - --mentor-line: var(--border); - - --mentor-line-soft: var(--line-soft); - + --mentor-line-soft: var(--border-subtle); --mentor-ink: var(--text-primary); - --mentor-sub: var(--text-secondary); - --mentor-faint: var(--text-tertiary); } -:root[data-theme="dark"] #mentorView :is(.mentor-evidence-filters, .mentor-directory-content, .mentor-directory-heading, .mentor-search-field, .mentor-chat-panel, .mentor-option) { - border-color: var(--border); - - background: var(--surface); - - color: var(--text-primary); -} - -:root[data-theme="dark"] #mentorView :is(.mentor-evidence-filters button.active, .mentor-option.active) { - background: var(--action-soft); - - color: var(--action); -} - -:root[data-theme="dark"] #mentorView :is(.mentor-badge.quality, .mentor-badge.private, .mentor-pin-button) { - background: var(--surface-muted); - - color: var(--text-secondary); -} - -:root[data-theme="dark"] #mentorView .mentor-badge.grade-a { - background: var(--market-down-soft); - - color: var(--market-down); -} - -:root[data-theme="dark"] #mentorView .mentor-badge.grade-b { - background: var(--action-soft); - - color: var(--action); -} - -:root[data-theme="dark"] #mentorView .mentor-badge.grade-c { - background: var(--warning-soft); - - color: var(--warning-color); -} - -:root[data-theme="dark"] #mentorView :is(.mentor-chat-header, .mentor-messages, .mentor-quick-prompts, .mentor-chat-form, .mentor-disclaimer, .mentor-quick-prompts button, #mentorQuestion) { - border-color: var(--border); - - background: var(--surface); - - color: var(--text-primary); -} - -:root[data-theme="dark"] #mentorView :is(.mentor-messages, .mentor-empty-mark) { - background: var(--surface); -} - :root[data-theme="dark"] #mentorView .mentor-message { - border-color: var(--border-subtle); - - background: transparent; - + border-color: var(--line-soft); + background: var(--surface-subtle); box-shadow: none; } -:root[data-theme="dark"] #mentorView .mentor-message.user { +:root[data-theme="dark"] #mentorView .mentor-message.user .mentor-message-content { border-color: var(--blue-line); - background: var(--action-soft); -} - -:root[data-theme="dark"] #mentorView :is(.mentor-message-content, .mentor-answer-heading, .loading-message p) { color: var(--text-primary); } -:root[data-theme="dark"] #mentorView .mentor-answer-quote { - color: var(--text-secondary); -} +@media (max-width: 1279px) { + #mentorView .mentor-layout { + grid-template-columns: var(--mentor-directory-width) minmax(0, 1fr); + } -:root[data-theme="dark"] #mentorView :is(.mentor-message-label, .mentor-message small) { - color: var(--text-tertiary); + #mentorView .mentor-profile-panel { + display: none; + } } diff --git a/frontend/pages/mentor/page.html b/frontend/pages/mentor/page.html index b3c78ad..40b6a94 100644 --- a/frontend/pages/mentor/page.html +++ b/frontend/pages/mentor/page.html @@ -1,72 +1,89 @@ - 问师仅对会员开放开通会员后可使用游资思维模型进行对话。会员状态可从顶部账号标识进入。 + 问师仅对会员开放开通会员后可使用思维模型进行对话。会员状态可从顶部账号标识进入。 问师 - 向思维模型请教 · -- - - - - 全部 - A级 - B级 - C级 - + 与不同交易思维模型持续对话 · -- - -