实现AI左右互博
This commit is contained in:
parent
63916f9b24
commit
a6f2895dad
499
Prompt/0问题集.md
Normal file
499
Prompt/0问题集.md
Normal file
@ -0,0 +1,499 @@
|
||||
# 公司简介 (Company Profile)
|
||||
## 一、 公司概览
|
||||
* 简要介绍公司的性质、核心业务领域及其在行业中的定位。
|
||||
* 提炼并阐述公司的核心价值理念。
|
||||
## 二、 主营业务
|
||||
* 详细描述公司主要的**产品或服务**。
|
||||
* **重要提示**:如果能获取到公司最新的官方**年报**或**财务报告**,请从中提取各主要产品/服务线的**收入金额**和其占公司总收入的**百分比**。请**明确标注数据来源**(例如:"数据来源于XX年年度报告")。
|
||||
* **严格禁止**编造或估算任何财务数据。若无法找到公开、准确的财务数据,请**不要**在这一点中提及具体金额或比例,仅描述业务内容。
|
||||
## 三、 发展历程
|
||||
* 以时间线或关键事件的形式,概述公司自成立以来的主要**里程碑事件**、重大发展阶段、战略转型或重要成就。
|
||||
## 四、 核心团队
|
||||
* 介绍公司**主要管理层和核心技术团队成员**。
|
||||
* 对于每位核心成员,提供其**职务、主要工作履历、教育背景**以及**担任该职位的起始时间**。
|
||||
* 如果公开可查,可补充其**出生年份**。
|
||||
## 五、 供应链
|
||||
* 描述公司的**主要原材料、部件或服务来源**以及各种原材料、部件或服务在总采购金额中的占比。
|
||||
* 如果公开信息中包含,请列出**主要供应商名称**,并**明确其在总采购金额中的大致占比**。
|
||||
* 描述采购模式。
|
||||
## 六、 主要客户及销售模式
|
||||
* 阐明公司的**销售模式**(例如:直销、经销、线上销售、代理等)。
|
||||
* 列出公司的**主要客户群体**或**代表性大客户**。
|
||||
* 如果公开信息中包含,请标明**主要客户(或前五大客户)的销售额占公司总销售额的比例**。若无此数据,则仅描述客户类型。
|
||||
## 七、 未来展望
|
||||
* 基于公司**公开的官方声明、管理层访谈或战略规划**,总结公司未来的发展方向、战略目标、重点项目或市场预期。请确保此部分内容有可靠的信息来源支持。
|
||||
|
||||
# 基本面分析 (Fundamental Analysis)
|
||||
## 公司基本面分析 (Fundamental Analysis)
|
||||
|
||||
### 护城河与核心竞争力
|
||||
- 公司通过何种独有优势(如品牌、技术、成本、网络效应、牌照等)获取超额利润?
|
||||
- 该护城河是在增强、维持还是在削弱?请提供论据。
|
||||
|
||||
### 管理层与公司治理
|
||||
- **管理能力**:管理层过往的战略决策和执行能力如何?是否有卓越的业界声誉?
|
||||
- **股东回报**:管理层及大股东是否珍惜股权价值?(分析历史上的增持/减持行为、分红派息政策、是否存在损害小股东利益的体外资产等)
|
||||
- **激励与目标**:公司的经营目标是长期主义还是短期化?管理层的激励机制(如股权激励、考核指标)是否与长期战略目标一致?
|
||||
|
||||
### 企业文化与财务政策
|
||||
- 公司是否有独特且可观察到的企业文化?(例如:创新文化、成本控制文化等)
|
||||
- 公司的财务政策(如资本结构、现金流管理、投资策略)与同行业公司相比有何显著特点?是激进还是保守?
|
||||
|
||||
### 发展历程与战略规划
|
||||
- 梳理公司发展史上的关键事件、重大业务转型或里程碑。
|
||||
- 公司是否有清晰的长期战略目标(未来5-10年)?计划成为一家什么样的企业?
|
||||
|
||||
## 业务与市场分析 (Business & Market Analysis)
|
||||
|
||||
### 产品与客户价值
|
||||
- 公司为客户提供什么核心产品/服务?其核心价值点是什么?客户为何选择公司的产品而非竞争对手的?
|
||||
- 产品的更新迭代是颠覆性的还是渐进积累型的?分析产品历年的产量、价格及销量变化,并探讨其背后的驱动因素。
|
||||
|
||||
### 市场需求与景气度
|
||||
- 客户所处行业的需求是趋势性的高增长,还是周期性波动?或是两者结合?当前处于何种阶段?
|
||||
- 目标客户群体的经营状况和现金流是否健康?
|
||||
|
||||
### 议价能力与客户关系
|
||||
- 公司对下游客户的议价能力强弱如何?(结合应收账款周转天数、账龄结构、毛利率等数据进行佐证)
|
||||
- 公司与核心客户的关系是否稳定?客户对公司的评价如何(例如:客户忠诚度、满意度)?
|
||||
|
||||
## 竞争格局分析 (Competitive Landscape Analysis)
|
||||
|
||||
### 竞争对手画像
|
||||
- 列出公司的主要竞争对手,并分析各自的优势与劣势。
|
||||
- 公司的竞争对手是在增多还是减少?行业进入壁垒是在增高还是降低?
|
||||
- 是否存在潜在的跨界竞争者?
|
||||
|
||||
## 供应链与外部关系 (Supply Chain & External Relations)
|
||||
|
||||
### 供应链议价能力
|
||||
- 公司对上游供应商的议价能力如何?(结合应付账款周转天数、采购成本控制等数据进行佐证)
|
||||
- 核心供应商的经营是否稳定?供应链是否存在集中度过高的风险?
|
||||
|
||||
### 金融机构关系与融资需求
|
||||
- 公司与金融机构的关系如何?融资渠道是否通畅?
|
||||
- 公司未来的发展是否依赖于大规模的债务或股权融资?
|
||||
|
||||
## 监管环境与政策风险 (Regulatory Environment & Policy Risks)
|
||||
### 公司所处行业是否存在重要的监管部门?主要的监管政策有哪些?
|
||||
### 监管政策是否稳定?未来可能发生哪些重大变化?对公司有何潜在影响?
|
||||
### 公司是否具备影响或适应监管政策变化的能力?
|
||||
|
||||
# 核心能力分析 (Core Capabilities)
|
||||
## 一、技术能力评估 (Technical Capabilities)
|
||||
|
||||
### 1.1 研发体系与创新能力
|
||||
- **研发投入强度**:近3-5年研发费用占营收比例的变化趋势?与行业平均水平对比如何?
|
||||
- **研发团队规模**:研发人员数量及占比?核心技术人员的学历结构和专业背景?
|
||||
- **创新产出**:专利申请和授权数量(发明专利、实用新型、外观设计)?核心技术的专利壁垒强度?
|
||||
- **技术储备**:是否有前瞻性技术布局?在研项目的技术难度和商业化前景?
|
||||
|
||||
### 1.2 核心技术与工艺
|
||||
- **关键技术**:公司掌握哪些核心技术?这些技术的先进性如何?(国际领先/国内领先/行业平均)
|
||||
- **技术来源**:核心技术是自主研发、引进消化还是合作开发?技术自主可控程度?
|
||||
- **工艺优势**:生产工艺的独特性和难以复制性?良品率、生产效率等关键指标?
|
||||
- **技术迭代**:技术更新换代的速度?公司是否能持续保持技术领先?
|
||||
|
||||
### 1.3 数字化与信息化能力
|
||||
- **数字化转型**:公司在生产、管理、营销等环节的数字化程度?
|
||||
- **数据资产**:是否积累了有价值的数据资产?数据的应用场景和变现能力?
|
||||
- **IT基础设施**:信息系统的先进性和稳定性?是否有自主开发的核心系统?
|
||||
|
||||
## 二、运营能力评估 (Operational Capabilities)
|
||||
|
||||
### 2.1 生产制造能力
|
||||
- **产能规模**:现有产能及产能利用率?扩产计划和可行性?
|
||||
- **质量控制**:质量管理体系(如ISO认证)?产品合格率和客户投诉率?
|
||||
- **成本控制**:单位成本的变化趋势?成本控制的主要手段和效果?
|
||||
- **柔性制造**:能否快速响应市场需求变化?定制化生产能力如何?
|
||||
|
||||
### 2.2 供应链管理能力
|
||||
- **采购管理**:供应商管理体系的成熟度?原材料库存周转效率?
|
||||
- **物流配送**:物流网络的覆盖范围和响应速度?物流成本占比?
|
||||
- **库存管理**:存货周转天数的变化趋势?是否存在库存积压或短缺风险?
|
||||
- **供应链韧性**:应对供应链中断的能力?(如疫情、贸易摩擦等极端情况)
|
||||
|
||||
### 2.3 质量与安全管理
|
||||
- **质量认证**:获得了哪些国际/国内质量认证?认证的含金量?
|
||||
- **安全生产**:近年是否发生重大安全事故?安全投入和管理措施?
|
||||
- **环保合规**:环保设施投入?是否存在环保违规记录?
|
||||
|
||||
## 三、市场能力评估 (Market Capabilities)
|
||||
|
||||
### 3.1 品牌与营销能力
|
||||
- **品牌价值**:品牌知名度和美誉度?品牌估值(如有)?
|
||||
- **营销投入**:销售费用占比及变化趋势?营销ROI如何?
|
||||
- **渠道能力**:销售渠道的广度和深度?线上线下渠道的协同效应?
|
||||
- **客户触达**:获客成本(CAC)和客户生命周期价值(LTV)?客户留存率?
|
||||
|
||||
### 3.2 客户服务能力
|
||||
- **服务体系**:售前、售中、售后服务的完整性?
|
||||
- **响应速度**:客户问题的平均响应时间和解决时间?
|
||||
- **客户满意度**:NPS(净推荐值)或其他客户满意度指标?
|
||||
- **增值服务**:是否提供超越产品本身的增值服务?
|
||||
|
||||
### 3.3 市场洞察与快速响应
|
||||
- **市场研究**:是否有专门的市场研究团队?对市场趋势的预判能力?
|
||||
- **产品迭代**:从市场需求到产品上市的周期?新产品成功率?
|
||||
- **危机应对**:面对市场突发事件(如竞品降价、政策变化)的应对速度和效果?
|
||||
|
||||
## 四、组织能力评估 (Organizational Capabilities)
|
||||
|
||||
### 4.1 人才体系
|
||||
- **人才结构**:员工的学历结构、年龄结构、专业背景?
|
||||
- **核心人才**:核心技术/管理人才的稳定性?近年核心人才流失情况?
|
||||
- **薪酬竞争力**:人均薪酬水平与行业对比?股权激励覆盖范围?
|
||||
- **人才培养**:是否有完善的培训体系?内部晋升机制是否顺畅?
|
||||
|
||||
### 4.2 组织架构与管理效率
|
||||
- **组织结构**:组织架构是否适应业务发展需要?决策链条长度?
|
||||
- **管理效率**:人均创收、人均利润等效率指标?与同行对比?
|
||||
- **跨部门协同**:部门间协作效率?是否存在部门墙?
|
||||
- **变革能力**:组织对战略调整的响应速度?历史上重大变革的成功案例?
|
||||
|
||||
### 4.3 企业文化与价值观
|
||||
- **文化特征**:公司倡导什么样的企业文化?(创新、执行、客户导向等)
|
||||
- **文化落地**:企业文化是否真正影响员工行为?员工认同度?
|
||||
- **价值观一致性**:管理层言行与公司价值观的一致性?
|
||||
- **文化竞争力**:企业文化是否成为吸引人才和凝聚团队的核心要素?
|
||||
|
||||
## 五、战略能力评估 (Strategic Capabilities)
|
||||
|
||||
### 5.1 战略规划与执行
|
||||
- **战略清晰度**:公司是否有明确的长期战略目标?战略是否可衡量?
|
||||
- **战略一致性**:各业务单元的战略是否与公司整体战略一致?
|
||||
- **执行能力**:历史上制定的战略目标达成率?
|
||||
- **战略调整**:是否能根据内外部环境变化及时调整战略?
|
||||
|
||||
### 5.2 资源配置能力
|
||||
- **资本配置**:资本开支的方向和效率?ROIC(投入资本回报率)的变化?
|
||||
- **业务组合**:多元化业务的协同效应?是否存在需要剥离的低效业务?
|
||||
- **资源聚焦**:是否将资源集中在核心业务和优势领域?
|
||||
|
||||
### 5.3 生态构建能力
|
||||
- **产业链地位**:在产业链中的位置?是否有产业链整合能力?
|
||||
- **生态伙伴**:与上下游、同业、跨界企业的合作关系?
|
||||
- **平台效应**:是否构建了具有网络效应的平台?平台的开放性和吸引力?
|
||||
- **标准制定**:是否参与或主导行业标准的制定?
|
||||
|
||||
## 六、学习与进化能力 (Learning & Evolution)
|
||||
|
||||
### 6.1 知识管理
|
||||
- **知识沉淀**:是否有系统的知识管理体系?核心知识是否文档化?
|
||||
- **经验传承**:老员工的经验如何传递给新员工?
|
||||
- **最佳实践**:是否能快速识别和推广内部最佳实践?
|
||||
|
||||
### 6.2 组织学习能力
|
||||
- **外部学习**:是否积极学习行业标杆和跨界优秀企业?
|
||||
- **试错文化**:是否鼓励创新和容忍失败?
|
||||
- **快速迭代**:能否通过小步快跑的方式快速验证新想法?
|
||||
|
||||
### 6.3 自我革新能力
|
||||
- **业务创新**:是否能主动淘汰落后业务,开拓新业务?
|
||||
- **流程优化**:是否持续优化内部流程,提升效率?
|
||||
- **技术升级**:是否能主动拥抱新技术,避免被颠覆?
|
||||
|
||||
## 七、综合评估与能力矩阵
|
||||
|
||||
### 7.1 核心能力识别
|
||||
- 列出公司最核心的3-5项能力,并说明为什么这些能力是核心竞争力的来源。
|
||||
- 这些核心能力是否具有**稀缺性、难以模仿性、不可替代性**?
|
||||
|
||||
### 7.2 能力短板分析
|
||||
- 识别公司在上述各维度中的明显短板。
|
||||
- 这些短板是否会成为公司发展的瓶颈?公司是否有计划弥补?
|
||||
|
||||
### 7.3 能力演进趋势
|
||||
- 公司的核心能力在过去3-5年是在增强还是削弱?
|
||||
- 未来3-5年,哪些能力需要重点建设?
|
||||
|
||||
### 7.4 能力与战略匹配度
|
||||
- 公司现有的能力体系是否能支撑其战略目标的实现?
|
||||
- 是否存在能力与战略不匹配的情况?(如战略激进但能力不足,或能力过剩但战略保守)
|
||||
|
||||
# 看涨分析 (Bull Case)
|
||||
## 一、 深度挖掘:公司的隐藏资产与未被市场充分定价的价值
|
||||
|
||||
### 1.1 资产负债表之外的价值 (Off-Balance Sheet Value)
|
||||
- **无形资产**:公司是否拥有未被充分计价的核心技术专利、软件著作权、特许经营权或强大的品牌价值?请量化或举例说明其潜在商业价值。
|
||||
- **数据资产**:公司是否积累了具有巨大潜在价值的用户或行业数据?这些数据未来可能的变现途径是什么?
|
||||
|
||||
### 1.2 低估的实体或股权资产 (Undervalued Physical or Equity Assets)
|
||||
- **土地/物业重估**:公司持有的土地、房产等固定资产,其当前市场公允价值是否远超账面价值?
|
||||
- **子公司/投资价值**:公司旗下是否有快速增长但未被市场充分关注的子公司或有价值的长期股权投资?分析其独立估值的潜力。
|
||||
|
||||
### 1.3 运营中的“隐形冠军” (Operational "Hidden Champions")
|
||||
- 公司是否存在独特的、难以复制的生产工艺、供应链管理能力或运营效率优势,而这些优势尚未完全体现在当前的利润率中?
|
||||
|
||||
## 二、 护城河的加深:竞争优势的动态强化分析
|
||||
|
||||
### 2.1 护城河的动态演变:是静态还是在拓宽?
|
||||
- 论证公司的核心护城河(例如:网络效应、转换成本、成本优势、技术壁垒)在未来几年将如何被**强化**而非削弱。请提供具体证据(如:研发投入的持续增长、客户续约率的提升、市场份额的扩大等)。
|
||||
|
||||
### 2.2 技术与创新壁垒的领先优势
|
||||
- 公司的研发投入和创新产出,如何确保其在未来3-5年内保持对竞争对手的技术代差或领先地位?
|
||||
- 是否有即将商业化的“杀手级”新产品或新技术?
|
||||
|
||||
### 2.3 品牌与客户粘性的正反馈循环
|
||||
- 公司的品牌价值或客户关系如何形成一个正反馈循环(即:强品牌带来高议价能力 -> 高利润投入研发/营销 -> 品牌更强)?
|
||||
- 客户为何难以转向竞争对手?分析其高昂的转换成本。
|
||||
|
||||
## 三、 长期景气度:行业未来3年以上的持续增长动力
|
||||
|
||||
### 3.1 长期需求驱动力(Demand-Side Drivers)
|
||||
- 驱动行业增长的核心动力是短期的周期性复苏,还是长期的结构性变迁(如:技术革命、消费升级、国产替代、政策驱动)?请深入论证。
|
||||
- 行业的市场渗透率是否仍有巨大提升空间?分析未来市场规模(TAM)的扩张潜力。
|
||||
|
||||
### 3.2 供给侧格局优化(Supply-Side Dynamics)
|
||||
- 行业供给侧是否出现集中度提升、落后产能出清的趋势?这是否意味着龙头企业的定价权和盈利能力将持续增强?
|
||||
- 行业的进入壁垒是否在显著提高(如:技术、资金、资质壁垒),从而限制新竞争者的涌入?
|
||||
|
||||
### 3.3 关键催化剂(Key Catalysts)
|
||||
- 未来1-2年内,是否存在可以显著提升公司估值或盈利的潜在催化剂事件(如:新产品发布、重要政策落地、海外市场突破等)?
|
||||
|
||||
# 看跌分析 (Bear Case)
|
||||
## 一、 护城河的侵蚀:竞争优势的脆弱性分析 (Moat Erosion: Vulnerability of Competitive Advantages)
|
||||
|
||||
### 1.1 现有护城河的潜在威胁
|
||||
- 公司的核心护城河(技术、品牌、成本等)是否面临被颠覆的风险?(例如:新技术的出现、竞争对手的模仿或价格战)
|
||||
- 客户的转换成本是否真的足够高?是否存在某些因素(如行业标准化)可能降低客户的转换壁垒?
|
||||
|
||||
### 1.2 竞争格局的恶化
|
||||
- 是否有新的、强大的“跨界”竞争者进入市场?
|
||||
- 行业是否从“蓝海”变为“红海”?分析导致竞争加剧的因素(如:产能过剩、产品同质化)。
|
||||
- 竞争对手的哪些战略举动可能对公司构成致命打击?
|
||||
|
||||
## 二、 隐藏的负债与风险:资产负债表之外的“地雷” (Hidden Liabilities & Risks: Off-Balance Sheet "Mines")
|
||||
|
||||
### 2.1 潜在的财务风险
|
||||
- 公司是否存在大量的或有负债、对外担保或未入表的债务?
|
||||
- 公司的现金流健康状况是否脆弱?分析其经营现金流能否覆盖资本开支和债务利息,尤其是在收入下滑的情况下。
|
||||
- 应收账款或存货是否存在潜在的暴雷风险?(分析其账龄、周转率和减值计提的充分性)
|
||||
|
||||
### 2.2 运营与管理风险
|
||||
- 公司是否对单一供应商、单一客户或单一市场存在过度依赖?
|
||||
- 公司是否存在“关键人物风险”?创始团队或核心技术人员的离开会对公司造成多大影响?
|
||||
- 公司的企业文化或治理结构是否存在可能导致重大决策失误的缺陷?
|
||||
|
||||
## 三、 行业逆风与最坏情况分析 (Industry Headwinds & Worst-Case Scenario)
|
||||
|
||||
### 3.1 行业天花板与需求逆转
|
||||
- 行业渗透率是否已接近饱和?未来的增长空间是否被高估?
|
||||
- 驱动行业增长的核心因素是否可持续?是否存在可能导致需求突然逆转的黑天鹅事件(如:政策突变、技术路线改变、消费者偏好转移)?
|
||||
|
||||
### 3.2 价值链上的压力传导
|
||||
- 上游供应商的议价能力是否在增强,从而挤压公司利润空间?
|
||||
- 下游客户的需求是否在萎缩,或者客户的财务状况是否在恶化?
|
||||
|
||||
### 3.3 最坏情况压力测试 (Worst-Case Stress Test)
|
||||
- **情景假设**:假设行业需求下滑30%,或主要竞争对手发起价格战,公司的收入、利润和现金流会受到多大冲击?
|
||||
- **破产风险评估**:在这种极端情况下,公司是否有足够的现金储备和融资能力来度过危机?公司的生存底线在哪里?
|
||||
|
||||
### 3.4 价值底线评估:清算价值分析 (Bottom-Line Valuation: Liquidation Value Analysis)
|
||||
- **核心假设**:在公司被迫停止经营并清算的极端情况下,其资产的真实变现价值是多少?
|
||||
- **资产逐项折价**:请对资产负债表中的主要科目进行折价估算。例如:
|
||||
- *现金及等价物*:按100%计算。
|
||||
- *应收账款*:根据账龄和客户质量,估计一个合理的回收率(如50%-80%)。
|
||||
- *存货*:根据存货类型(原材料、产成品)和市场状况,估计一个变现折扣(如30%-70%)。
|
||||
- *固定资产(厂房、设备)*:估计其二手市场的变现价值,通常远低于账面净值。
|
||||
- *无形资产/商誉*:大部分在清算时价值归零。
|
||||
- **负债计算**:公司的总负债(包括所有表内及表外负债)需要被优先偿还。
|
||||
- **清算价值估算**:计算**(折价后的总资产 - 总负债)/ 总股本**,得出每股清算价值。这是公司价值的绝对底线。
|
||||
|
||||
## 四、 估值陷阱分析 (Valuation Trap Analysis)
|
||||
### 4.1 增长预期的证伪
|
||||
- 当前的高估值是否隐含了过于乐观的增长预期?论证这些预期为何可能无法实现。
|
||||
- 市场是否忽略了公司盈利能力的周期性,而将其误判为长期成长性?
|
||||
|
||||
### 4.2 资产质量重估
|
||||
- 公司的资产(尤其是商誉、无形资产)是否存在大幅减值的风险?
|
||||
- 公司的真实盈利能力(扣除非经常性损益后)是否低于报表利润?
|
||||
|
||||
# 内部人与机构动向分析 (Insider & Institutional)
|
||||
## 一、 内部人动向分析 (Insider Activity Analysis)
|
||||
|
||||
### 1.1 核心高管交易 (Key Executive Transactions)
|
||||
- **公开市场买卖**:近6-12个月,公司的核心高管(CEO, CFO等)是否有在公开市场**主动买入**或**卖出**自家股票?
|
||||
- **交易动机解读**:
|
||||
- **买入**:买入的金额、次数以及当时股价所处的位置?(*通常,高管在股价下跌后主动增持,被视为强烈的看多信号*)
|
||||
- **卖出**:是出于个人资金需求(如纳税)的一次性小额卖出,还是持续、大量的减持?是否在股价历史高位附近减持?
|
||||
- **期权行权**:高管行使期权后,是选择继续持有股票,还是立即在市场卖出?
|
||||
|
||||
### 1.2 大股东与董事会成员动向 (Major Shareholder & Director Activity)
|
||||
- 持股5%以上的大股东或董事会成员,近期的整体趋势是增持还是减持?
|
||||
- 是否存在关键股东(如创始人、战略投资者)的持股比例发生重大变化?
|
||||
|
||||
### 1.3 内部人持股的总体趋势
|
||||
- 综合来看,内部人近半年的行为释放了什么样的集体信号?是信心增强、信心减弱,还是无明显趋势?
|
||||
|
||||
## 二、 机构投资者动向分析 (Institutional Investor Activity Analysis)
|
||||
|
||||
### 2.1 机构持股的总体变化
|
||||
- **持股比例**:机构投资者的总持股占流通股的比例,在最近几个季度是上升还是下降?
|
||||
- **股东数量**:持有该公司股票的机构总数是在增加还是减少?(*数量增加通常意味着市场关注度的提升*)
|
||||
|
||||
### 2.2 顶级机构的进出 (Top-Tier Institution Moves)
|
||||
- **十大机构股东**:当前最大的机构股东有哪些?在最近一个报告期,它们是“增持”、“减持”、“新进”或“清仓”?
|
||||
- **“聪明钱”的踪迹**:是否有以长期价值投资著称的知名基金(如高瓴、景林、Fidelity等)新进入了股东名单,或者大幅增持?
|
||||
- 反之,是否有顶级机构在清仓式卖出?
|
||||
|
||||
### 2.3 机构观点的“一致性”
|
||||
- 从机构的整体行为来看,市场主流机构对该公司的看法是趋于一致(大家都在买或都在卖),还是存在巨大分歧?
|
||||
|
||||
## 三、 综合研判:“聪明钱”的信号 (Synthesized Verdict: The "Smart Money" Signal)
|
||||
|
||||
### 3.1 信号的一致性与背离
|
||||
- 内部人和机构投资者的行动方向是否一致?(*例如:内部人增持的同时,顶级机构也在建仓,这是一个极强的看多信号*)
|
||||
- “聪明钱”的动向是否与当前市场情绪或股价走势相背离?(*例如:在散户普遍悲观、股价下跌时,内部人和机构却在持续买入*)
|
||||
|
||||
### 3.2 最终结论
|
||||
- 综合来看,在未来3-6个月,来自“聪明钱”的资金流向是可能成为股价的**顺风**(Tailwind)还是**逆风**(Headwind)?
|
||||
|
||||
# 市场情绪分析 (Market Sentiment)
|
||||
## 一、 当前市场主流叙事与估值定位 (Current Market Narrative & Valuation Positioning)
|
||||
|
||||
### 1.1 市场的主流故事线是什么?
|
||||
- 综合近期(1-3个月内)的新闻报道和券商研报,当前市场在为这家公司讲述一个什么样的“故事”?是“困境反转”、“AI赋能”、“周期复苏”还是“增长放缓”?
|
||||
- 这个主流故事线是在近期被强化了,还是开始出现动摇?
|
||||
|
||||
### 1.2 当前估值反映了什么预期?
|
||||
- 公司当前的估值水平(如市盈率P/E、市净率P/B)在历史和行业中处于什么位置(高位、中位、低位)?
|
||||
- 这个估值水平背后,市场“计价”了什么样的增长率、利润率或成功预期?*(例如:市场普遍预期其新业务明年将贡献30%的收入增长)*
|
||||
|
||||
## 二、 情绪分歧点:多空双方的核心博弈 (Points of Disagreement: The Core Bull vs. Bear Debate)
|
||||
|
||||
### 2.1 关键分歧一:[例如:新产品的市场前景]
|
||||
- **看多者认为**:[陈述看多方的核心理由,并引用支持性新闻或数据]
|
||||
- **看空者认为**:[陈述看空方的核心理由,并引用支持性新闻或数据]
|
||||
|
||||
### 2.2 关键分歧二:[例如:监管政策的影响]
|
||||
- **看多者认为**:[陈述看多方的核心理由,并引用支持性新闻或数据]
|
||||
- **看空者认为**:[陈述看空方的核心理由,并引用支持性新闻或数据]
|
||||
|
||||
### 2.3 市场资金的态度
|
||||
- 近期是否有知名的机构投资者在增持或减持?
|
||||
- 股票的卖空比例是否有显著变化?这反映了什么情绪?
|
||||
|
||||
## 三、 情绪变化的潜在驱动力 (Potential Drivers of Sentiment Change)
|
||||
|
||||
### 3.1 近期(未来1-3个月)的关键催化剂
|
||||
- 列出未来短期内可能打破当前市场情绪平衡的关键事件。(例如:即将发布的财报、行业重要会议、新产品发布会、重要的宏观数据公布等)
|
||||
- 这些事件的结果将如何分别验证或证伪当前多/空双方的逻辑?
|
||||
|
||||
### 3.2 识别“预期差”
|
||||
- 当前市场最可能“过度乐观”的点是什么?
|
||||
- 当前市场最可能“过度悲观”的点是什么?
|
||||
- 未来什么样的信息出现,会最大程度地修复这种预期差,并引发股价剧烈波动?
|
||||
|
||||
# 股价催化剂分析 (Price Action Catalysts)
|
||||
## 一、 近期关键新闻梳理与解读 (Recent Key News Flow & Interpretation)
|
||||
- **新闻事件1:[日期] [新闻标题]**
|
||||
- *来源:[例如:公司官网公告 / 彭博社]*
|
||||
- **事件概述**:[简要概括新闻内容]
|
||||
- **市场初步反应**:[事件发生后,股价和成交量有何变化?]
|
||||
- **深层解读**:[该新闻是孤立事件,还是某个趋势的延续?它暗示了公司基本面的何种变化?]
|
||||
- **新闻事件2:[日期] [新闻标题]**
|
||||
- ... (以此类推)
|
||||
|
||||
## 二、 正面催化剂预判 (Potential Positive Catalysts)
|
||||
|
||||
### 2.1 确定性较高的催化剂 (High-Probability Catalysts)
|
||||
- **催化剂名称**:[例如:新一代产品发布]
|
||||
- **预期时间窗口**:[例如:预计在下个月的行业大会上]
|
||||
- **触发逻辑**:[为什么这件事会成为股价的正面驱动力?它会如何改善市场预期?]
|
||||
- **需观察的信号**:[需要看到什么具体信息(如产品性能参数、预订单数量)才能确认催化剂的有效性?]
|
||||
|
||||
### 2.2 潜在的“黑天鹅”式利好 (Potential "Black Swan" Positives)
|
||||
- **催化剂名称**:[例如:意外获得海外市场准入 / 竞争对手出现重大失误]
|
||||
- **触发逻辑**:[描述这种小概率但影响巨大的利好事件及其可能性]
|
||||
- **需观察的信号**:[哪些先行指标或行业动态可能预示着这种事件的发生?]
|
||||
|
||||
## 三、 负面催化剂预判 (Potential Negative Catalysts)
|
||||
|
||||
### 3.1 确定性较高的风险 (High-Probability Risks)
|
||||
- **催化剂名称**:[例如:关键专利到期 / 主要客户合同续约谈判]
|
||||
- **预期时间窗口**:[例如:本季度末]
|
||||
- **触发逻辑**:[为什么这件事可能对股价造成负面冲击?]
|
||||
- **需观察的信号**:[需要关注哪些数据或公告来判断风险是否会兑现?]
|
||||
|
||||
### 3.2 潜在的“黑天鹅”式风险 (Potential "Black Swan" Risks)
|
||||
- **催化剂名称**:[例如:突发性的行业监管收紧 / 供应链“断链”风险]
|
||||
- **触发逻辑**:[描述这种小概率但影响巨大的风险事件]
|
||||
- **需观察的信号**:[哪些蛛丝马迹可能预示着风险的临近?]
|
||||
|
||||
## 四、 综合预判:下一个股价拐点 (Synthesis: The Next Inflection Point)
|
||||
|
||||
### 4.1 **核心博弈点**:综合以上分析,当前市场最关注、最可能率先发生的多空催化剂是什么?
|
||||
### 4.2 **拐点预测**:基于当前信息,下一个可能改变股价趋势的关键时间点或事件最有可能是什么?
|
||||
### 4.3 **关键验证指标**:在那个拐点到来之前,我们应该把注意力集中在哪个/哪些最关键的数据或信息上?
|
||||
|
||||
|
||||
# 交易策略分析 (Trading Strategy)
|
||||
## 一、 当前价格走势与结构分析 (Current Price Action & Structure Analysis)
|
||||
|
||||
### 1.1 趋势与动能
|
||||
- **当前趋势**:股价目前处于明确的上升、下降还是盘整趋势中?(*参考:关键均线系统,如MA20, MA60, MA120的排列状态*)
|
||||
- **关键水平**:当前最重要的支撑位和阻力位分别在哪里?这些是历史高低点、均线位置还是成交密集区?
|
||||
- **量价关系**:近期的成交量与价格波动是否匹配?是否存在“价升量增”的健康上涨或“价跌量增”的恐慌抛售?
|
||||
|
||||
### 1.2 图表形态
|
||||
- 近期是否形成了关键的K线形态?(例如:突破性阳线、反转信号)
|
||||
- 是否存在经典的图表形态?(例如:头肩底、W底、收敛三角形、箱体震荡)
|
||||
|
||||
## 二、 市场体量与赔率计算 (Market Capacity & Risk/Reward Calculation)
|
||||
|
||||
### 2.1 上涨空间评估 (Upside Potential)
|
||||
- 如果向上突破关键阻力位,下一个或几个现实的**目标价位**在哪里?(*参考:前期高点、斐波那契扩展位、形态测量目标*)
|
||||
- **潜在回报率**:从当前价格到主要目标价位的潜在上涨百分比是多少?
|
||||
|
||||
### 2.2 风险评估与止损设置 (Downside Risk & Stop-Loss)
|
||||
- 如果交易逻辑被证伪,一个清晰、有效的**止损价位**应该设在哪里?(*参考:关键支撑位下方、上升趋势线下方*)
|
||||
- **潜在风险率**:从当前价格到止损价位的潜在下跌百分比是多少?
|
||||
|
||||
### 2.3 赔率分析 (Risk/Reward Ratio)
|
||||
- 计算**风险回报比**(= 潜在回报率 / 潜在风险率)。这个比率是否具有吸引力?(*专业交易者通常要求至少大于 2:1 或 3:1*)
|
||||
- **市场体量**:该股的日均成交额是否足够大,能够容纳计划中的资金进出而不会造成显著的冲击成本?
|
||||
|
||||
## 三、 增长路径:“双击”可能性评估 (Growth Path: "Dual-Click" Potential)
|
||||
|
||||
### 3.1 基本面驱动力 (Fundamental Momentum)
|
||||
- 近期是否有或将要有**基本面催化剂**来支撑股价上涨?(*参考《股价催化剂分析》的结论,如:超预期的财报、新产品成功、行业政策利好*)
|
||||
- 这个基本面利好是能提供“一次性”的脉冲,还是能开启一个“持续性”的盈利增长周期?
|
||||
|
||||
### 3.2 资金面驱动力 (Capital Momentum)
|
||||
- 是否有证据表明**增量资金**正在流入?(*参考:成交量的持续放大、机构投资者的增持报告、龙虎榜数据*)
|
||||
- 该股所属的板块或赛道,当前是否受到市场主流资金的青睐?
|
||||
|
||||
### 3.3 “双击”可能性综合评估
|
||||
- 综合来看,公司出现“**业绩超预期(基本面)+ 估值提升(资金面)**”双击局面的可能性有多大?
|
||||
- 触发“双击”的关键信号可能是什么?(例如:在发布亮眼财报后,股价以放量涨停的方式突破关键阻力位)
|
||||
|
||||
## 四、 交易计划总结 (Actionable Trading Plan)
|
||||
### 4.1 **入场信号**:[具体的入场条件。例如:日线收盘价站上 {阻力位A} 并且成交量放大至 {数值X} 以上]
|
||||
### 4.2 **止损策略**:[具体的止损条件。例如:日线收盘价跌破 {支撑位B}]
|
||||
### 4.3 **止盈策略**:[具体的目标位和操作。例如:在 {目标位C} 止盈50%,剩余仓位跟踪止盈]
|
||||
### 4.4 **仓位管理**:[基于赔率和确定性,建议的初始仓位是多少?]
|
||||
|
||||
# 最终投资决策 (Final Decision)
|
||||
## 一、 核心矛盾与预期差 (Core Contradiction & Expectation Gap)
|
||||
### 1.1 **当前的核心矛盾是什么?** 综合所有分析,当前多空双方争论的、最核心的、最关键的一个问题是什么?(例如:是“高估值下的成长故事”与“宏观逆风下的业绩担忧”之间的矛盾?还是“革命性产品”与“商业化落地不确定性”之间的矛盾?)
|
||||
### 1.2 **最大的预期差在哪里?** 我们认为市场在哪一个关键点上可能犯了最大的错误?是我们比市场更乐观,还是更悲观?具体体现在哪个方面?
|
||||
|
||||
## 二、 拐点的临近度与关键信号 (Proximity to Inflection Point & Key Signals)
|
||||
### 2.1 **拐点是否临近?** 能够解决上述“核心矛盾”的关键催化剂事件,是否即将发生?(参考新闻和催化剂分析)
|
||||
### 2.2 **我们需要验证什么?** 在拐点到来之前,我们需要密切跟踪和验证的、最关键的1-2个数据或信号是什么?(例如:是新产品的预订单数量,还是下一个季度的毛利率指引?)
|
||||
|
||||
## 三、 综合投资论点 (Synthesized Investment Thesis)
|
||||
### 3.1 **质量与价值(基本面 & 看跌风险)**:这家公司的“质量”如何?它的护城河是否足够深厚,能够在最坏的情况下提供足够的安全边际(清算价值)?
|
||||
### 3.2 **成长与赔率(看涨 & 交易分析)**:如果看涨逻辑兑现,潜在的回报空间有多大?当前的交易结构是否提供了有吸引力的风险回报比?
|
||||
### 3.3 **情绪与资金(市场情绪 & 聪明钱)**:当前的市场情绪是助力还是阻力?“聪明钱”的流向是在支持还是反对我们的判断?
|
||||
### 3.4 **时机与催化剂(新闻分析)**:现在是合适的扣动扳机的时间点吗?还是需要等待某个关键催化剂的出现?
|
||||
|
||||
## 四、 最终决策与评级 (Final Decision & Rating)
|
||||
### 4.1 **投资结论**:[明确给出:**买入 / 增持 / 观望 / 减持 / 卖出**]
|
||||
### 4.2 **核心投资逻辑**:[用一句话总结本次决策的核心理由]
|
||||
### 4.3 **值得参与度评分**:**[请打分, 1-10分]**
|
||||
- *(评分标准:1-3分=机会不佳;4-6分=值得观察;7-8分=良好机会,建议配置;9-10分=极佳机会,应重点配置)*
|
||||
### 4.4 **关注时间维度**:**[请选择:紧急 / 中期 / 长期]**
|
||||
- *(评级标准:**紧急**=关键拐点预计在1个月内;**中期**=关键拐点预计在1-6个月;**长期**=需要持续跟踪6个月以上)*
|
||||
|
||||
169
backend/app/api/chat_routes.py
Normal file
169
backend/app/api/chat_routes.py
Normal file
@ -0,0 +1,169 @@
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from fastapi.responses import StreamingResponse
|
||||
import json
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
import logging
|
||||
import time
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.services.analysis_service import get_genai_client
|
||||
from google.genai import types
|
||||
from app.database import get_db
|
||||
from app.models import LLMUsageLog
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ChatMessage(BaseModel):
|
||||
role: str
|
||||
content: str
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
messages: List[ChatMessage]
|
||||
model: str
|
||||
system_prompt: Optional[str] = None
|
||||
use_google_search: bool = False
|
||||
session_id: Optional[str] = None
|
||||
|
||||
@router.post("/chat")
|
||||
async def chat_with_ai(request: ChatRequest, db: AsyncSession = Depends(get_db)):
|
||||
"""AI Chat Endpoint with Logging (Streaming)"""
|
||||
try:
|
||||
client = get_genai_client()
|
||||
|
||||
# Prepare History and Config
|
||||
history = []
|
||||
# Exclude the last message as it will be sent via send_message
|
||||
if len(request.messages) > 1:
|
||||
for msg in request.messages[:-1]:
|
||||
history.append(types.Content(
|
||||
role="user" if msg.role == "user" else "model",
|
||||
parts=[types.Part(text=msg.content)]
|
||||
))
|
||||
|
||||
last_message = request.messages[-1].content if request.messages else ""
|
||||
|
||||
model_name = request.model or "gemini-2.5-flash"
|
||||
|
||||
# Search Configuration & System Prompt
|
||||
tools = []
|
||||
if request.use_google_search:
|
||||
tools.append(types.Tool(google_search=types.GoogleSearch()))
|
||||
|
||||
config = types.GenerateContentConfig(
|
||||
tools=tools if tools else None,
|
||||
temperature=0.1,
|
||||
system_instruction=request.system_prompt
|
||||
)
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
async def generate():
|
||||
full_response_text = ""
|
||||
grounding_data = None
|
||||
prompt_tokens = 0
|
||||
completion_tokens = 0
|
||||
total_tokens = 0
|
||||
|
||||
try:
|
||||
# Initialize Chat
|
||||
chat = client.chats.create(
|
||||
model=model_name,
|
||||
history=history,
|
||||
config=config
|
||||
)
|
||||
|
||||
# Streaming Call
|
||||
response_stream = chat.send_message_stream(last_message)
|
||||
|
||||
for chunk in response_stream:
|
||||
if chunk.text:
|
||||
full_response_text += chunk.text
|
||||
data = json.dumps({"type": "content", "content": chunk.text})
|
||||
yield f"{data}\n"
|
||||
|
||||
# Accumulate Metadata (will be complete in the last chunk usually, or usage_metadata)
|
||||
if chunk.usage_metadata:
|
||||
prompt_tokens = chunk.usage_metadata.prompt_token_count or 0
|
||||
completion_tokens = chunk.usage_metadata.candidates_token_count or 0
|
||||
total_tokens = chunk.usage_metadata.total_token_count or 0
|
||||
|
||||
if chunk.candidates and chunk.candidates[0].grounding_metadata:
|
||||
gm = chunk.candidates[0].grounding_metadata
|
||||
# We only expect one final grounding metadata object
|
||||
if gm.search_entry_point or gm.grounding_chunks:
|
||||
grounding_obj = {}
|
||||
if gm.search_entry_point:
|
||||
grounding_obj["searchEntryPoint"] = {"renderedContent": gm.search_entry_point.rendered_content}
|
||||
|
||||
if gm.grounding_chunks:
|
||||
grounding_obj["groundingChunks"] = []
|
||||
for g_chunk in gm.grounding_chunks:
|
||||
if g_chunk.web:
|
||||
grounding_obj["groundingChunks"].append({
|
||||
"web": {
|
||||
"uri": g_chunk.web.uri,
|
||||
"title": g_chunk.web.title
|
||||
}
|
||||
})
|
||||
if gm.web_search_queries:
|
||||
grounding_obj["webSearchQueries"] = gm.web_search_queries
|
||||
|
||||
grounding_data = grounding_obj # Save final metadata
|
||||
|
||||
# End of stream actions
|
||||
end_time = time.time()
|
||||
response_time = end_time - start_time
|
||||
|
||||
# Send Metadata Chunk
|
||||
if grounding_data:
|
||||
yield f"{json.dumps({'type': 'metadata', 'groundingMetadata': grounding_data})}\n"
|
||||
|
||||
# Send Usage Chunk
|
||||
usage_data = {
|
||||
"prompt_tokens": prompt_tokens,
|
||||
"completion_tokens": completion_tokens,
|
||||
"total_tokens": total_tokens,
|
||||
"response_time": response_time
|
||||
}
|
||||
yield f"{json.dumps({'type': 'usage', 'usage': usage_data})}\n"
|
||||
|
||||
# Log to Database (Async)
|
||||
# Note: creating a new session here because the outer session might be closed or not thread-safe in generator?
|
||||
# Actually, we can use the injected `db` session but we must be careful.
|
||||
# Since we are inside an async generator, awaiting db calls is fine.
|
||||
try:
|
||||
# Reconstruct prompt for logging
|
||||
logged_prompt = ""
|
||||
if request.system_prompt:
|
||||
logged_prompt += f"System: {request.system_prompt}\n\n"
|
||||
for msg in request.messages: # Log full history including last message
|
||||
logged_prompt += f"{msg.role}: {msg.content}\n"
|
||||
|
||||
log_entry = LLMUsageLog(
|
||||
model=model_name,
|
||||
prompt=logged_prompt,
|
||||
response=full_response_text,
|
||||
response_time=response_time,
|
||||
used_google_search=request.use_google_search,
|
||||
session_id=request.session_id,
|
||||
prompt_tokens=prompt_tokens,
|
||||
completion_tokens=completion_tokens,
|
||||
total_tokens=total_tokens
|
||||
)
|
||||
db.add(log_entry)
|
||||
await db.commit()
|
||||
except Exception as db_err:
|
||||
logger.error(f"Failed to log LLM usage: {db_err}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Stream generation error: {e}")
|
||||
err_json = json.dumps({"type": "error", "error": str(e)})
|
||||
yield f"{err_json}\n"
|
||||
|
||||
return StreamingResponse(generate(), media_type="application/x-ndjson")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Chat error: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@ -14,6 +14,9 @@ import io
|
||||
import tempfile
|
||||
from urllib.parse import quote
|
||||
from bs4 import BeautifulSoup
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional, Dict, Any
|
||||
from app.services.analysis_service import get_genai_client
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@ -547,4 +550,39 @@ async def download_report_pdf(report_id: int, db: AsyncSession = Depends(get_db)
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"生成PDF失败: {str(e)}")
|
||||
class ChatMessage(BaseModel):
|
||||
role: str
|
||||
content: str
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
messages: List[ChatMessage]
|
||||
model: str
|
||||
system_prompt: Optional[str] = None
|
||||
|
||||
@router.post("/chat")
|
||||
async def chat_with_ai(request: ChatRequest):
|
||||
try:
|
||||
client = get_genai_client()
|
||||
|
||||
# Prepare content for Gemini
|
||||
# Gemini 2.0 Flash expects simple string or list of contents
|
||||
# We need to format the conversation history and system prompt
|
||||
|
||||
full_prompt = ""
|
||||
if request.system_prompt:
|
||||
full_prompt += f"System: {request.system_prompt}\n\n"
|
||||
|
||||
for msg in request.messages:
|
||||
role_label = "User" if msg.role == "user" else "Model"
|
||||
full_prompt += f"{role_label}: {msg.content}\n"
|
||||
|
||||
full_prompt += "Model: " # Prompt for completion
|
||||
|
||||
response = client.models.generate_content(
|
||||
model=request.model,
|
||||
contents=full_prompt
|
||||
)
|
||||
|
||||
return {"response": response.text}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@ -6,6 +6,7 @@ import sys
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
|
||||
# 配置日志系统 - 在最开始配置,确保所有模块都能使用
|
||||
logging.basicConfig(
|
||||
@ -25,7 +26,7 @@ if str(SRC_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(SRC_DIR))
|
||||
|
||||
# 导入新路由
|
||||
from app.api import data_routes, analysis_routes
|
||||
from app.api import data_routes, analysis_routes, chat_routes
|
||||
|
||||
load_dotenv()
|
||||
|
||||
@ -53,6 +54,7 @@ app.add_middleware(
|
||||
# 挂载新的 API 路由
|
||||
app.include_router(data_routes.router, prefix="/api")
|
||||
app.include_router(analysis_routes.router, prefix="/api")
|
||||
app.include_router(chat_routes.router, prefix="/api")
|
||||
|
||||
@app.get("/")
|
||||
def read_root():
|
||||
@ -94,22 +96,61 @@ async def get_config_compat(db: AsyncSession = Depends(get_db)):
|
||||
try:
|
||||
result = await db.execute(select(Setting))
|
||||
settings = result.scalars().all()
|
||||
config_map = {}
|
||||
|
||||
if settings:
|
||||
return {setting.key: setting.value for setting in settings}
|
||||
# 返回默认配置
|
||||
return {
|
||||
"ai_model": "gemini-2.0-flash",
|
||||
config_map = {setting.key: setting.value for setting in settings}
|
||||
|
||||
# Compatibility: Map common UPPERCASE keys to lowercase for frontend
|
||||
if "AI_MODEL" in config_map and "ai_model" not in config_map:
|
||||
config_map["ai_model"] = config_map["AI_MODEL"]
|
||||
if "AVAILABLE_MODELS" in config_map and "available_models" not in config_map:
|
||||
config_map["available_models"] = config_map["AVAILABLE_MODELS"]
|
||||
if "AI_DISCUSSION_ROLES" in config_map and "ai_discussion_roles" not in config_map:
|
||||
config_map["ai_discussion_roles"] = config_map["AI_DISCUSSION_ROLES"]
|
||||
if "AI_QUESTION_LIBRARY" in config_map and "ai_question_library" not in config_map:
|
||||
config_map["ai_question_library"] = config_map["AI_QUESTION_LIBRARY"]
|
||||
|
||||
# Ensure available_models is present
|
||||
if "available_models" not in config_map:
|
||||
# Default models list as requested
|
||||
import json
|
||||
default_models = [
|
||||
"gemini-2.0-flash",
|
||||
"gemini-2.5-flash",
|
||||
"gemini-3-flash-preview",
|
||||
"gemini-3-pro-preview"
|
||||
]
|
||||
config_map["available_models"] = json.dumps(default_models)
|
||||
|
||||
# Ensure defaults for other keys exist if not in DB
|
||||
defaults = {
|
||||
"ai_model": "gemini-2.5-flash",
|
||||
"data_source_cn": "Tushare",
|
||||
"data_source_hk": "iFinD",
|
||||
"data_source_us": "iFinD",
|
||||
"data_source_jp": "iFinD",
|
||||
"data_source_vn": "iFinD"
|
||||
}
|
||||
|
||||
for k, v in defaults.items():
|
||||
if k not in config_map:
|
||||
config_map[k] = v
|
||||
|
||||
return config_map
|
||||
|
||||
except Exception as e:
|
||||
# 如果表不存在,返回默认配置
|
||||
print(f"Config read error (using defaults): {e}")
|
||||
import json
|
||||
return {
|
||||
"ai_model": "gemini-2.0-flash",
|
||||
"ai_model": "gemini-2.5-flash",
|
||||
"available_models": json.dumps([
|
||||
"gemini-2.0-flash",
|
||||
"gemini-2.5-flash",
|
||||
"gemini-3-flash-preview",
|
||||
"gemini-3-pro-preview"
|
||||
]),
|
||||
"data_source_cn": "Tushare",
|
||||
"data_source_hk": "iFinD",
|
||||
"data_source_us": "iFinD",
|
||||
@ -231,5 +272,8 @@ async def search_stock(request: StockSearchRequest):
|
||||
elapsed = time.time() - start_time
|
||||
logger.error(f"❌ [搜索] 搜索失败: {e}, 耗时: {elapsed:.2f}秒")
|
||||
print(f"Search error: {e}")
|
||||
# 返回空结果而不是错误,避免前端崩溃
|
||||
return []
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Chat Endpoint - Configured via app.api.chat_routes
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
|
||||
@ -2,10 +2,14 @@
|
||||
SQLAlchemy Models for FA3 Refactored Architecture
|
||||
新架构的数据库 ORM 模型
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, String, Text, TIMESTAMP, Boolean, JSON, ForeignKey
|
||||
from sqlalchemy import Column, Integer, String, Text, TIMESTAMP, Boolean, JSON, ForeignKey, Float
|
||||
from sqlalchemy.orm import declarative_base, relationship
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
# ... (existing code)
|
||||
|
||||
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
class Company(Base):
|
||||
@ -119,3 +123,25 @@ class Setting(Base):
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Setting(key={self.key}, value={self.value[:50]})>"
|
||||
|
||||
|
||||
class LLMUsageLog(Base):
|
||||
"""LLM 调用日志表"""
|
||||
__tablename__ = 'llm_usage_logs'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
timestamp = Column(TIMESTAMP, server_default=func.now())
|
||||
model = Column(String(50), nullable=False)
|
||||
prompt = Column(Text, nullable=False)
|
||||
response = Column(Text, nullable=True)
|
||||
response_time = Column(Float, nullable=True) # Seconds
|
||||
used_google_search = Column(Boolean, default=False)
|
||||
session_id = Column(String, nullable=True)
|
||||
|
||||
# Token Usage
|
||||
prompt_tokens = Column(Integer, default=0)
|
||||
completion_tokens = Column(Integer, default=0)
|
||||
total_tokens = Column(Integer, default=0)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<LLMUsageLog(id={self.id}, model={self.model}, tokens={self.total_tokens})>"
|
||||
|
||||
25
backend/check_db_logs.py
Normal file
25
backend/check_db_logs.py
Normal file
@ -0,0 +1,25 @@
|
||||
import asyncio
|
||||
from sqlalchemy import select, desc
|
||||
from app.database import get_db, SessionLocal
|
||||
from app.models import LLMUsageLog
|
||||
|
||||
async def check_logs():
|
||||
async with SessionLocal() as session:
|
||||
result = await session.execute(
|
||||
select(LLMUsageLog).order_by(desc(LLMUsageLog.timestamp)).limit(5)
|
||||
)
|
||||
logs = result.scalars().all()
|
||||
|
||||
print(f"Found {len(logs)} recent logs:")
|
||||
for log in logs:
|
||||
print("-" * 50)
|
||||
print(f"ID: {log.id}")
|
||||
print(f"Time: {log.timestamp}")
|
||||
print(f"Model: {log.model}")
|
||||
print(f"Used Search: {log.used_google_search}")
|
||||
print(f"Prompt (first 50): {log.prompt[:50]}...")
|
||||
print(f"Response (first 50): {log.response[:50]}..." if log.response else "No Response")
|
||||
# Note: We don't store grounding metadata in the DB currently, only in the API return.
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(check_logs())
|
||||
24
backend/debug_settings.py
Normal file
24
backend/debug_settings.py
Normal file
@ -0,0 +1,24 @@
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add backend to path
|
||||
sys.path.append('/Users/xucheng/git.qubit.ltd/FA3/backend')
|
||||
|
||||
from app.legacy.database_old import AsyncSessionLocal
|
||||
from app.legacy.models_old import Setting
|
||||
from sqlalchemy import select
|
||||
|
||||
async def main():
|
||||
async with AsyncSessionLocal() as session:
|
||||
result = await session.execute(select(Setting))
|
||||
settings = result.scalars().all()
|
||||
for s in settings:
|
||||
print(f"Key: {s.key}, Value: {s.value}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
26
backend/debug_settings_pg.py
Normal file
26
backend/debug_settings_pg.py
Normal file
@ -0,0 +1,26 @@
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add backend to path
|
||||
sys.path.append('/Users/xucheng/git.qubit.ltd/FA3/backend')
|
||||
|
||||
from app.database import SessionLocal
|
||||
from app.models import Setting
|
||||
from sqlalchemy import select
|
||||
|
||||
async def main():
|
||||
async with SessionLocal() as session:
|
||||
result = await session.execute(select(Setting))
|
||||
settings = result.scalars().all()
|
||||
print("--- Postgres Settings ---")
|
||||
for s in settings:
|
||||
print(f"Key: {s.key}, Value: {s.value}")
|
||||
print("-------------------------")
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
@ -2625,3 +2625,184 @@ JSON_END
|
||||
2026-01-12 20:39:54,960 - app.clients.bloomberg_client - INFO - ✅ Completed processing for 600600 CH Equity
|
||||
2026-01-12 20:39:54,960 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=83, Msg=Bloomberg data sync complete, Progress=90%
|
||||
2026-01-12 20:39:56,366 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=83, Msg=Bloomberg 数据同步完成, Progress=100%
|
||||
2026-01-12 21:08:47,060 - google_genai.models - INFO - AFC is enabled with max remote calls: 10.
|
||||
2026-01-12 21:08:48,909 - httpx - INFO - HTTP Request: POST https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent "HTTP/1.1 200 OK"
|
||||
2026-01-12 21:20:11,057 - google_genai.models - INFO - AFC is enabled with max remote calls: 10.
|
||||
2026-01-12 21:20:22,679 - httpx - INFO - HTTP Request: POST https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash-preview:generateContent "HTTP/1.1 200 OK"
|
||||
2026-01-12 21:25:52,842 - google_genai.models - INFO - AFC is enabled with max remote calls: 10.
|
||||
2026-01-12 21:26:04,452 - httpx - INFO - HTTP Request: POST https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent "HTTP/1.1 200 OK"
|
||||
2026-01-12 21:26:40,755 - google_genai.models - INFO - AFC is enabled with max remote calls: 10.
|
||||
2026-01-12 21:27:00,349 - httpx - INFO - HTTP Request: POST https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent "HTTP/1.1 200 OK"
|
||||
2026-01-12 22:18:53,449 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=84, Msg=正在初始化数据获取..., Progress=0%
|
||||
2026-01-12 22:18:54,487 - app.clients.bloomberg_client - INFO - Connecting to Jupyter at http://192.168.3.161:8888...
|
||||
2026-01-12 22:18:56,587 - app.clients.bloomberg_client - INFO - ✅ Authentication successful.
|
||||
2026-01-12 22:18:56,619 - app.clients.bloomberg_client - INFO - ✅ Found existing kernel: bc27f3b1-b028-434a-99fa-c1cad4495a87 (remote_env)
|
||||
2026-01-12 22:18:57,677 - app.clients.bloomberg_client - INFO - ✅ WebSocket connected.
|
||||
2026-01-12 22:18:57,677 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=84, Msg=数据源连接成功, Progress=10%
|
||||
2026-01-12 22:18:58,977 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=84, Msg=正在连接 Bloomberg 终端..., Progress=20%
|
||||
2026-01-12 22:18:59,243 - app.clients.bloomberg_client - INFO - 🚀 Starting fetch for: 6301 JP Equity
|
||||
2026-01-12 22:18:59,244 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=84, Msg=Starting Bloomberg session..., Progress=20%
|
||||
2026-01-12 22:18:59,887 - app.clients.bloomberg_client - INFO - Using forced currency: CNY
|
||||
2026-01-12 22:18:59,888 - app.clients.bloomberg_client - INFO - Fetching Basic Data...
|
||||
2026-01-12 22:18:59,888 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=84, Msg=Fetching Company Basic Info..., Progress=27%
|
||||
2026-01-12 22:19:01,204 - app.clients.bloomberg_client - INFO - REMOTE RAW OUTPUT: JSON_START
|
||||
[{"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:19:02", "currency": "CNY", "indicator": "company_name", "value": "KOMATSU LTD", "value_date": "2026-01-12 22:19:02"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:19:02", "currency": "CNY", "indicator": "pe_ratio", "value": "11.538204871538206", "value_date": "2026-01-12 22:19:02"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:19:02", "currency": "CNY", "indicator": "pb_ratio", "value": "1.4637777168999744", "value_date": "2026-01-12 22:19:02"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:19:02", "currency": "CNY", "indicator": "Rev_Abroad", "value": "86.05716369679564", "value_date": "2026-01-12 22:19:02"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:19:02", "currency": "CNY", "indicator": "dividend_yield", "value": "3.894351262772315", "value_date": "2026-01-12 22:19:02"}, {"Company_code": "6301 JP Equity", "update_date": "2026
|
||||
2026-01-12 22:19:01,205 - app.clients.bloomberg_client - INFO - ✅ Parsed 6 items from remote.
|
||||
2026-01-12 22:19:01,205 - app.clients.bloomberg_client - INFO - DEBUG: basic_data before save: [{'Company_code': '6301 JP Equity', 'update_date': '2026-01-12 22:19:02', 'currency': 'CNY', 'indicator': 'company_name', 'value': 'KOMATSU LTD', 'value_date': '2026-01-12 22:19:02'}, {'Company_code': '6301 JP Equity', 'update_date': '2026-01-12 22:19:02', 'currency': 'CNY', 'indicator': 'pe_ratio', 'value': '11.538204871538206', 'value_date': '2026-01-12 22:19:02'}, {'Company_code': '6301 JP Equity', 'update_date': '2026-01-12 22:19:02', 'currency': 'CNY', 'indicator': 'pb_ratio', 'value': '1.4637777168999744', 'value_date': '2026-01-12 22:19:02'}, {'Company_code': '6301 JP Equity', 'update_date': '2026-01-12 22:19:02', 'currency': 'CNY', 'indicator': 'Rev_Abroad', 'value': '86.05716369679564', 'value_date': '2026-01-12 22:19:02'}, {'Company_code': '6301 JP Equity', 'update_date': '2026-01-12 22:19:02', 'currency': 'CNY', 'indicator': 'dividend_yield', 'value': '3.894351262772315', 'value_date': '2026-01-12 22:19:02'}, {'Company_code': '6301 JP Equity', 'update_date': '2026-01-12 22:19:02', 'currency': 'CNY', 'indicator': 'market_cap', 'value': '213059.5081', 'value_date': '2026-01-12 22:19:02'}]
|
||||
2026-01-12 22:19:02,735 - app.clients.bloomberg_client - INFO - ✅ Saved 6 records to database.
|
||||
2026-01-12 22:19:02,736 - app.clients.bloomberg_client - INFO - Fetching Currency Data...
|
||||
2026-01-12 22:19:02,736 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=84, Msg=正在获取货币指标 (CNY)..., Progress=41%
|
||||
2026-01-12 22:19:04,552 - app.clients.bloomberg_client - INFO - REMOTE RAW OUTPUT: JSON_START
|
||||
[{"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:19:03", "currency": "CNY", "indicator": "Revenue", "value": "98388.5158", "value_date": "2016-03-31"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:19:03", "currency": "CNY", "indicator": "Net_Income", "value": "7289.1658", "value_date": "2016-03-31"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:19:03", "currency": "CNY", "indicator": "Cash_From_Operating", "value": "16953.5985", "value_date": "2016-03-31"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:19:03", "currency": "CNY", "indicator": "Capital_Expenditure", "value": "-8830.1561", "value_date": "2016-03-31"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:19:03", "currency": "CNY", "indicator": "Free_Cash_Flow", "value": "8123.4424", "value_date": "2016-03-31"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:19:03", "currency": "CNY", "indicator": "Dividends_
|
||||
2026-01-12 22:19:04,553 - app.clients.bloomberg_client - INFO - ✅ Parsed 248 items from remote.
|
||||
2026-01-12 22:19:28,949 - app.clients.bloomberg_client - INFO - ✅ Saved 248 records to database.
|
||||
2026-01-12 22:19:28,951 - app.clients.bloomberg_client - INFO - Fetching Non-Currency Data...
|
||||
2026-01-12 22:19:28,951 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=84, Msg=正在获取非货币指标..., Progress=55%
|
||||
2026-01-12 22:19:30,386 - app.clients.bloomberg_client - INFO - REMOTE RAW OUTPUT: JSON_START
|
||||
[{"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:19:29", "currency": "CNY", "indicator": "ROE", "value": "9.0222", "value_date": "2016-03-31"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:19:29", "currency": "CNY", "indicator": "ROA", "value": "5.0776", "value_date": "2016-03-31"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:19:29", "currency": "CNY", "indicator": "ROCE", "value": "10.4918", "value_date": "2016-03-31"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:19:29", "currency": "CNY", "indicator": "Gross_Margin", "value": "29.0675", "value_date": "2016-03-31"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:19:29", "currency": "CNY", "indicator": "EBITDA_margin", "value": "17.3073", "value_date": "2016-03-31"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:19:29", "currency": "CNY", "indicator": "Net_Profit_Margin", "value": "7.4086", "value_date": "2016-
|
||||
2026-01-12 22:19:30,387 - app.clients.bloomberg_client - INFO - ✅ Parsed 200 items from remote.
|
||||
2026-01-12 22:19:45,636 - app.clients.bloomberg_client - INFO - ✅ Saved 200 records to database.
|
||||
2026-01-12 22:19:45,638 - app.clients.bloomberg_client - INFO - Fetching Price Data (Aligned)...
|
||||
2026-01-12 22:19:45,638 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=84, Msg=正在获取价格指标..., Progress=69%
|
||||
2026-01-12 22:19:46,248 - app.clients.bloomberg_client - INFO - Found 10 revenue reporting dates. Fetching aligned price data...
|
||||
2026-01-12 22:20:01,288 - app.clients.bloomberg_client - INFO - REMOTE RAW OUTPUT: JSON_START
|
||||
[{"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:19:46", "currency": "CNY", "indicator": "Last_Price", "value": "208.47187", "value_date": "2025-03-31"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:19:46", "currency": "CNY", "indicator": "Market_Cap", "value": "198246.9783", "value_date": "2025-03-31"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:19:46", "currency": "CNY", "indicator": "Dividend_Yield", "value": "4.4124", "value_date": "2025-03-31"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:19:46", "currency": "CNY", "indicator": "Last_Price", "value": "211.10676", "value_date": "2024-03-31"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:19:46", "currency": "CNY", "indicator": "Market_Cap", "value": "205578.0066", "value_date": "2024-03-31"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:19:46", "currency": "CNY", "indicator": "Dividend_Yield", "value": "
|
||||
2026-01-12 22:20:01,289 - app.clients.bloomberg_client - INFO - ✅ Parsed 30 items from remote.
|
||||
2026-01-12 22:20:04,481 - app.clients.bloomberg_client - INFO - ✅ Saved 30 records to database.
|
||||
2026-01-12 22:20:04,482 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=84, Msg=Finalizing data..., Progress=82%
|
||||
2026-01-12 22:20:05,789 - app.clients.bloomberg_client - INFO - ✅ Cleanup and View Refresh completed.
|
||||
2026-01-12 22:20:05,789 - app.clients.bloomberg_client - INFO - ✅ Completed processing for 6301 JP Equity
|
||||
2026-01-12 22:20:05,789 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=84, Msg=Bloomberg data sync complete, Progress=90%
|
||||
2026-01-12 22:20:06,785 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=84, Msg=Bloomberg 数据同步完成, Progress=100%
|
||||
2026-01-12 22:20:17,197 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=85, Msg=正在初始化数据获取..., Progress=0%
|
||||
2026-01-12 22:20:17,758 - app.clients.bloomberg_client - INFO - Connecting to Jupyter at http://192.168.3.161:8888...
|
||||
2026-01-12 22:20:17,985 - app.clients.bloomberg_client - INFO - ✅ Authentication successful.
|
||||
2026-01-12 22:20:18,466 - app.clients.bloomberg_client - INFO - ✅ Found existing kernel: bc27f3b1-b028-434a-99fa-c1cad4495a87 (remote_env)
|
||||
2026-01-12 22:20:18,628 - app.clients.bloomberg_client - INFO - ✅ WebSocket connected.
|
||||
2026-01-12 22:20:18,629 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=85, Msg=数据源连接成功, Progress=10%
|
||||
2026-01-12 22:20:19,398 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=85, Msg=正在连接 Bloomberg 终端..., Progress=20%
|
||||
2026-01-12 22:20:20,286 - app.clients.bloomberg_client - INFO - 🚀 Starting fetch for: 6301 JP Equity
|
||||
2026-01-12 22:20:20,286 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=85, Msg=Starting Bloomberg session..., Progress=20%
|
||||
2026-01-12 22:20:20,882 - app.clients.bloomberg_client - INFO - Using auto-detected currency: JPY
|
||||
2026-01-12 22:20:20,883 - app.clients.bloomberg_client - INFO - Fetching Basic Data...
|
||||
2026-01-12 22:20:20,883 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=85, Msg=Fetching Company Basic Info..., Progress=27%
|
||||
2026-01-12 22:20:22,541 - app.clients.bloomberg_client - INFO - REMOTE RAW OUTPUT: JSON_START
|
||||
[{"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:20:23", "currency": "JPY", "indicator": "company_name", "value": "KOMATSU LTD", "value_date": "2026-01-12 22:20:23"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:20:23", "currency": "JPY", "indicator": "pe_ratio", "value": "11.538204871538206", "value_date": "2026-01-12 22:20:23"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:20:23", "currency": "JPY", "indicator": "pb_ratio", "value": "1.4637777168999744", "value_date": "2026-01-12 22:20:23"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:20:23", "currency": "JPY", "indicator": "Rev_Abroad", "value": "86.05716369679564", "value_date": "2026-01-12 22:20:23"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:20:23", "currency": "JPY", "indicator": "dividend_yield", "value": "3.894351262772315", "value_date": "2026-01-12 22:20:23"}, {"Company_code": "6301 JP Equity", "update_date": "2026
|
||||
2026-01-12 22:20:22,541 - app.clients.bloomberg_client - INFO - ✅ Parsed 6 items from remote.
|
||||
2026-01-12 22:20:22,541 - app.clients.bloomberg_client - INFO - DEBUG: basic_data before save: [{'Company_code': '6301 JP Equity', 'update_date': '2026-01-12 22:20:23', 'currency': 'JPY', 'indicator': 'company_name', 'value': 'KOMATSU LTD', 'value_date': '2026-01-12 22:20:23'}, {'Company_code': '6301 JP Equity', 'update_date': '2026-01-12 22:20:23', 'currency': 'JPY', 'indicator': 'pe_ratio', 'value': '11.538204871538206', 'value_date': '2026-01-12 22:20:23'}, {'Company_code': '6301 JP Equity', 'update_date': '2026-01-12 22:20:23', 'currency': 'JPY', 'indicator': 'pb_ratio', 'value': '1.4637777168999744', 'value_date': '2026-01-12 22:20:23'}, {'Company_code': '6301 JP Equity', 'update_date': '2026-01-12 22:20:23', 'currency': 'JPY', 'indicator': 'Rev_Abroad', 'value': '86.05716369679564', 'value_date': '2026-01-12 22:20:23'}, {'Company_code': '6301 JP Equity', 'update_date': '2026-01-12 22:20:23', 'currency': 'JPY', 'indicator': 'dividend_yield', 'value': '3.894351262772315', 'value_date': '2026-01-12 22:20:23'}, {'Company_code': '6301 JP Equity', 'update_date': '2026-01-12 22:20:23', 'currency': 'JPY', 'indicator': 'market_cap', 'value': '4825676.7959', 'value_date': '2026-01-12 22:20:23'}]
|
||||
2026-01-12 22:20:24,221 - app.clients.bloomberg_client - INFO - ✅ Saved 6 records to database.
|
||||
2026-01-12 22:20:24,222 - app.clients.bloomberg_client - INFO - Fetching Currency Data...
|
||||
2026-01-12 22:20:24,222 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=85, Msg=正在获取货币指标 (JPY)..., Progress=41%
|
||||
2026-01-12 22:20:25,706 - app.clients.bloomberg_client - INFO - REMOTE RAW OUTPUT: JSON_START
|
||||
[{"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:20:24", "currency": "JPY", "indicator": "Revenue", "value": "1854964.0", "value_date": "2016-03-31"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:20:24", "currency": "JPY", "indicator": "Net_Income", "value": "137426.0", "value_date": "2016-03-31"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:20:24", "currency": "JPY", "indicator": "Cash_From_Operating", "value": "319634.0", "value_date": "2016-03-31"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:20:24", "currency": "JPY", "indicator": "Capital_Expenditure", "value": "-166479.0", "value_date": "2016-03-31"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:20:24", "currency": "JPY", "indicator": "Free_Cash_Flow", "value": "153155.0", "value_date": "2016-03-31"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:20:24", "currency": "JPY", "indicator": "Dividends_Paid",
|
||||
2026-01-12 22:20:25,707 - app.clients.bloomberg_client - INFO - ✅ Parsed 248 items from remote.
|
||||
2026-01-12 22:20:46,304 - app.clients.bloomberg_client - INFO - ✅ Saved 248 records to database.
|
||||
2026-01-12 22:20:46,307 - app.clients.bloomberg_client - INFO - Fetching Non-Currency Data...
|
||||
2026-01-12 22:20:46,307 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=85, Msg=正在获取非货币指标..., Progress=55%
|
||||
2026-01-12 22:20:48,044 - app.clients.bloomberg_client - INFO - REMOTE RAW OUTPUT: JSON_START
|
||||
[{"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:20:46", "currency": "JPY", "indicator": "ROE", "value": "9.0222", "value_date": "2016-03-31"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:20:46", "currency": "JPY", "indicator": "ROA", "value": "5.0776", "value_date": "2016-03-31"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:20:46", "currency": "JPY", "indicator": "ROCE", "value": "10.4918", "value_date": "2016-03-31"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:20:46", "currency": "JPY", "indicator": "Gross_Margin", "value": "29.0675", "value_date": "2016-03-31"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:20:46", "currency": "JPY", "indicator": "EBITDA_margin", "value": "17.3073", "value_date": "2016-03-31"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:20:46", "currency": "JPY", "indicator": "Net_Profit_Margin", "value": "7.4086", "value_date": "2016-
|
||||
2026-01-12 22:20:48,045 - app.clients.bloomberg_client - INFO - ✅ Parsed 200 items from remote.
|
||||
2026-01-12 22:21:02,961 - app.clients.bloomberg_client - INFO - ✅ Saved 200 records to database.
|
||||
2026-01-12 22:21:02,964 - app.clients.bloomberg_client - INFO - Fetching Price Data (Aligned)...
|
||||
2026-01-12 22:21:02,965 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=85, Msg=正在获取价格指标..., Progress=69%
|
||||
2026-01-12 22:21:05,078 - app.clients.bloomberg_client - INFO - Found 10 revenue reporting dates. Fetching aligned price data...
|
||||
2026-01-12 22:21:18,531 - app.clients.bloomberg_client - INFO - REMOTE RAW OUTPUT: JSON_START
|
||||
[{"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:21:05", "currency": "JPY", "indicator": "Last_Price", "value": "4306.0", "value_date": "2025-03-31"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:21:05", "currency": "JPY", "indicator": "Market_Cap", "value": "4094804.1347", "value_date": "2025-03-31"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:21:05", "currency": "JPY", "indicator": "Dividend_Yield", "value": "4.4124", "value_date": "2025-03-31"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:21:05", "currency": "JPY", "indicator": "Last_Price", "value": "4423.0", "value_date": "2024-03-31"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:21:05", "currency": "JPY", "indicator": "Market_Cap", "value": "4307164.3723", "value_date": "2024-03-31"}, {"Company_code": "6301 JP Equity", "update_date": "2026-01-12 22:21:05", "currency": "JPY", "indicator": "Dividend_Yield", "value": "3.77
|
||||
2026-01-12 22:21:18,535 - app.clients.bloomberg_client - INFO - ✅ Parsed 30 items from remote.
|
||||
2026-01-12 22:21:22,636 - app.clients.bloomberg_client - INFO - ✅ Saved 30 records to database.
|
||||
2026-01-12 22:21:22,637 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=85, Msg=Finalizing data..., Progress=82%
|
||||
2026-01-12 22:21:24,568 - app.clients.bloomberg_client - INFO - ✅ Cleanup and View Refresh completed.
|
||||
2026-01-12 22:21:24,569 - app.clients.bloomberg_client - INFO - ✅ Completed processing for 6301 JP Equity
|
||||
2026-01-12 22:21:24,570 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=85, Msg=Bloomberg data sync complete, Progress=90%
|
||||
2026-01-12 22:21:25,136 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=85, Msg=Bloomberg 数据同步完成, Progress=100%
|
||||
2026-01-13 09:19:34,085 - google_genai.models - INFO - AFC is enabled with max remote calls: 10.
|
||||
2026-01-13 09:19:40,592 - httpx - INFO - HTTP Request: POST https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent "HTTP/1.1 200 OK"
|
||||
2026-01-13 09:32:36,415 - google_genai.models - INFO - AFC is enabled with max remote calls: 10.
|
||||
2026-01-13 09:32:53,486 - httpx - INFO - HTTP Request: POST https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent "HTTP/1.1 200 OK"
|
||||
2026-01-13 09:33:12,180 - google_genai.models - INFO - AFC is enabled with max remote calls: 10.
|
||||
2026-01-13 09:33:28,715 - httpx - INFO - HTTP Request: POST https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent "HTTP/1.1 200 OK"
|
||||
2026-01-13 09:36:46,297 - google_genai.models - INFO - AFC is enabled with max remote calls: 10.
|
||||
2026-01-13 09:36:59,091 - httpx - INFO - HTTP Request: POST https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent "HTTP/1.1 200 OK"
|
||||
2026-01-13 10:23:19,850 - google_genai.models - INFO - AFC is enabled with max remote calls: 10.
|
||||
2026-01-13 10:23:34,015 - httpx - INFO - HTTP Request: POST https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash-preview:generateContent "HTTP/1.1 200 OK"
|
||||
2026-01-13 10:24:01,616 - google_genai.models - INFO - AFC is enabled with max remote calls: 10.
|
||||
2026-01-13 10:24:16,031 - httpx - INFO - HTTP Request: POST https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash-preview:generateContent "HTTP/1.1 200 OK"
|
||||
2026-01-13 10:29:57,190 - app.main - INFO - 🔍 [搜索] 开始搜索股票: 理文造纸
|
||||
2026-01-13 10:29:57,291 - app.main - INFO - 🤖 [搜索-LLM] 调用 gemini-2.5-flash 进行股票搜索
|
||||
2026-01-13 10:29:57,292 - google_genai.models - INFO - AFC is enabled with max remote calls: 10.
|
||||
2026-01-13 10:30:03,475 - httpx - INFO - HTTP Request: POST https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent "HTTP/1.1 200 OK"
|
||||
2026-01-13 10:30:03,489 - app.main - INFO - ✅ [搜索-LLM] 模型响应完成, 耗时: 6.20秒, Tokens: prompt=167, completion=120, total=287
|
||||
2026-01-13 10:30:03,490 - app.main - INFO - ✅ [搜索] 搜索完成, 找到 2 个结果, 总耗时: 6.30秒
|
||||
2026-01-13 10:30:09,631 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=86, Msg=正在初始化数据获取..., Progress=0%
|
||||
2026-01-13 10:30:10,234 - app.clients.bloomberg_client - INFO - Connecting to Jupyter at http://192.168.3.161:8888...
|
||||
2026-01-13 10:30:10,514 - app.clients.bloomberg_client - INFO - ✅ Authentication successful.
|
||||
2026-01-13 10:30:10,544 - app.clients.bloomberg_client - INFO - ✅ Found existing kernel: bc27f3b1-b028-434a-99fa-c1cad4495a87 (remote_env)
|
||||
2026-01-13 10:30:10,761 - app.clients.bloomberg_client - INFO - ✅ WebSocket connected.
|
||||
2026-01-13 10:30:10,761 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=86, Msg=数据源连接成功, Progress=10%
|
||||
2026-01-13 10:30:11,228 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=86, Msg=正在连接 Bloomberg 终端..., Progress=20%
|
||||
2026-01-13 10:30:12,031 - app.clients.bloomberg_client - INFO - 🚀 Starting fetch for: 02314 HK Equity
|
||||
2026-01-13 10:30:12,031 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=86, Msg=Starting Bloomberg session..., Progress=20%
|
||||
2026-01-13 10:30:12,786 - app.clients.bloomberg_client - INFO - Using auto-detected currency: HKD
|
||||
2026-01-13 10:30:12,786 - app.clients.bloomberg_client - INFO - Fetching Basic Data...
|
||||
2026-01-13 10:30:12,786 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=86, Msg=Fetching Company Basic Info..., Progress=27%
|
||||
2026-01-13 10:30:14,623 - app.clients.bloomberg_client - INFO - REMOTE RAW OUTPUT: JSON_START
|
||||
[{"Company_code": "02314 HK Equity", "update_date": "2026-01-13 10:30:14", "currency": "HKD", "indicator": "company_name", "value": "LEE & MAN PAPER MANUFACTURIN", "value_date": "2026-01-13 10:30:14"}, {"Company_code": "02314 HK Equity", "update_date": "2026-01-13 10:30:14", "currency": "HKD", "indicator": "pe_ratio", "value": "10.646742709315328", "value_date": "2026-01-13 10:30:14"}, {"Company_code": "02314 HK Equity", "update_date": "2026-01-13 10:30:14", "currency": "HKD", "indicator": "pb_ratio", "value": "0.5032843258204789", "value_date": "2026-01-13 10:30:14"}, {"Company_code": "02314 HK Equity", "update_date": "2026-01-13 10:30:14", "currency": "HKD", "indicator": "dividend_yield", "value": "3.293768588442477", "value_date": "2026-01-13 10:30:14"}, {"Company_code": "02314 HK Equity", "update_date": "2026-01-13 10:30:14", "currency": "HKD", "indicator": "IPO_date", "value": "2003-09-26", "value_date": "2026-01-13 10:30:14"}, {"Company_code": "02314 HK Equity", "updat
|
||||
2026-01-13 10:30:14,623 - app.clients.bloomberg_client - INFO - ✅ Parsed 6 items from remote.
|
||||
2026-01-13 10:30:14,624 - app.clients.bloomberg_client - INFO - DEBUG: basic_data before save: [{'Company_code': '02314 HK Equity', 'update_date': '2026-01-13 10:30:14', 'currency': 'HKD', 'indicator': 'company_name', 'value': 'LEE & MAN PAPER MANUFACTURIN', 'value_date': '2026-01-13 10:30:14'}, {'Company_code': '02314 HK Equity', 'update_date': '2026-01-13 10:30:14', 'currency': 'HKD', 'indicator': 'pe_ratio', 'value': '10.646742709315328', 'value_date': '2026-01-13 10:30:14'}, {'Company_code': '02314 HK Equity', 'update_date': '2026-01-13 10:30:14', 'currency': 'HKD', 'indicator': 'pb_ratio', 'value': '0.5032843258204789', 'value_date': '2026-01-13 10:30:14'}, {'Company_code': '02314 HK Equity', 'update_date': '2026-01-13 10:30:14', 'currency': 'HKD', 'indicator': 'dividend_yield', 'value': '3.293768588442477', 'value_date': '2026-01-13 10:30:14'}, {'Company_code': '02314 HK Equity', 'update_date': '2026-01-13 10:30:14', 'currency': 'HKD', 'indicator': 'IPO_date', 'value': '2003-09-26', 'value_date': '2026-01-13 10:30:14'}, {'Company_code': '02314 HK Equity', 'update_date': '2026-01-13 10:30:14', 'currency': 'HKD', 'indicator': 'market_cap', 'value': '14517.1005', 'value_date': '2026-01-13 10:30:14'}]
|
||||
2026-01-13 10:30:16,139 - app.clients.bloomberg_client - INFO - ✅ Saved 6 records to database.
|
||||
2026-01-13 10:30:16,140 - app.clients.bloomberg_client - INFO - Fetching Currency Data...
|
||||
2026-01-13 10:30:16,140 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=86, Msg=正在获取货币指标 (HKD)..., Progress=41%
|
||||
2026-01-13 10:30:17,030 - app.clients.bloomberg_client - INFO - REMOTE RAW OUTPUT: JSON_START
|
||||
[{"Company_code": "02314 HK Equity", "update_date": "2026-01-13 10:30:16", "currency": "HKD", "indicator": "Revenue", "value": "18341.677", "value_date": "2016-12-31"}, {"Company_code": "02314 HK Equity", "update_date": "2026-01-13 10:30:16", "currency": "HKD", "indicator": "Net_Income", "value": "2862.743", "value_date": "2016-12-31"}, {"Company_code": "02314 HK Equity", "update_date": "2026-01-13 10:30:16", "currency": "HKD", "indicator": "Cash_From_Operating", "value": "3939.552", "value_date": "2016-12-31"}, {"Company_code": "02314 HK Equity", "update_date": "2026-01-13 10:30:16", "currency": "HKD", "indicator": "Capital_Expenditure", "value": "-3737.839", "value_date": "2016-12-31"}, {"Company_code": "02314 HK Equity", "update_date": "2026-01-13 10:30:16", "currency": "HKD", "indicator": "Free_Cash_Flow", "value": "201.713", "value_date": "2016-12-31"}, {"Company_code": "02314 HK Equity", "update_date": "2026-01-13 10:30:16", "currency": "HKD", "indicator": "Dividends_P
|
||||
2026-01-13 10:30:17,031 - app.clients.bloomberg_client - INFO - ✅ Parsed 213 items from remote.
|
||||
2026-01-13 10:30:29,114 - app.clients.bloomberg_client - INFO - ✅ Saved 213 records to database.
|
||||
2026-01-13 10:30:29,116 - app.clients.bloomberg_client - INFO - Fetching Non-Currency Data...
|
||||
2026-01-13 10:30:29,116 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=86, Msg=正在获取非货币指标..., Progress=55%
|
||||
2026-01-13 10:30:30,169 - app.clients.bloomberg_client - INFO - REMOTE RAW OUTPUT: JSON_START
|
||||
[{"Company_code": "02314 HK Equity", "update_date": "2026-01-13 10:30:29", "currency": "HKD", "indicator": "ROE", "value": "16.8104", "value_date": "2016-12-31"}, {"Company_code": "02314 HK Equity", "update_date": "2026-01-13 10:30:29", "currency": "HKD", "indicator": "ROA", "value": "8.1103", "value_date": "2016-12-31"}, {"Company_code": "02314 HK Equity", "update_date": "2026-01-13 10:30:29", "currency": "HKD", "indicator": "ROCE", "value": "11.3233", "value_date": "2016-12-31"}, {"Company_code": "02314 HK Equity", "update_date": "2026-01-13 10:30:29", "currency": "HKD", "indicator": "Gross_Margin", "value": "22.2473", "value_date": "2016-12-31"}, {"Company_code": "02314 HK Equity", "update_date": "2026-01-13 10:30:29", "currency": "HKD", "indicator": "EBITDA_margin", "value": "18.996", "value_date": "2016-12-31"}, {"Company_code": "02314 HK Equity", "update_date": "2026-01-13 10:30:29", "currency": "HKD", "indicator": "Net_Profit_Margin", "value": "15.6079", "value_date":
|
||||
2026-01-13 10:30:30,170 - app.clients.bloomberg_client - INFO - ✅ Parsed 163 items from remote.
|
||||
2026-01-13 10:30:39,430 - app.clients.bloomberg_client - INFO - ✅ Saved 163 records to database.
|
||||
2026-01-13 10:30:39,431 - app.clients.bloomberg_client - INFO - Fetching Price Data (Aligned)...
|
||||
2026-01-13 10:30:39,432 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=86, Msg=正在获取价格指标..., Progress=69%
|
||||
2026-01-13 10:30:39,741 - app.clients.bloomberg_client - INFO - Found 9 revenue reporting dates. Fetching aligned price data...
|
||||
2026-01-13 10:30:50,248 - app.clients.bloomberg_client - INFO - REMOTE RAW OUTPUT: JSON_START
|
||||
[{"Company_code": "02314 HK Equity", "update_date": "2026-01-13 10:30:39", "currency": "HKD", "indicator": "Last_Price", "value": "2.41", "value_date": "2024-12-31"}, {"Company_code": "02314 HK Equity", "update_date": "2026-01-13 10:30:39", "currency": "HKD", "indicator": "Market_Cap", "value": "10350.95", "value_date": "2024-12-31"}, {"Company_code": "02314 HK Equity", "update_date": "2026-01-13 10:30:39", "currency": "HKD", "indicator": "Dividend_Yield", "value": "5.1037", "value_date": "2024-12-31"}, {"Company_code": "02314 HK Equity", "update_date": "2026-01-13 10:30:39", "currency": "HKD", "indicator": "Last_Price", "value": "2.29", "value_date": "2023-12-31"}, {"Company_code": "02314 HK Equity", "update_date": "2026-01-13 10:30:39", "currency": "HKD", "indicator": "Market_Cap", "value": "9868.9038", "value_date": "2023-12-31"}, {"Company_code": "02314 HK Equity", "update_date": "2026-01-13 10:30:39", "currency": "HKD", "indicator": "Dividend_Yield", "value": "2.5328",
|
||||
2026-01-13 10:30:50,249 - app.clients.bloomberg_client - INFO - ✅ Parsed 27 items from remote.
|
||||
2026-01-13 10:30:52,276 - app.clients.bloomberg_client - INFO - ✅ Saved 27 records to database.
|
||||
2026-01-13 10:30:52,278 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=86, Msg=Finalizing data..., Progress=82%
|
||||
2026-01-13 10:30:53,556 - app.clients.bloomberg_client - INFO - ✅ Cleanup and View Refresh completed.
|
||||
2026-01-13 10:30:53,557 - app.clients.bloomberg_client - INFO - ✅ Completed processing for 02314 HK Equity
|
||||
2026-01-13 10:30:53,557 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=86, Msg=Bloomberg data sync complete, Progress=90%
|
||||
2026-01-13 10:30:53,866 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=86, Msg=Bloomberg 数据同步完成, Progress=100%
|
||||
2026-01-13 10:33:28,211 - google_genai.models - INFO - AFC is enabled with max remote calls: 10.
|
||||
2026-01-13 10:33:48,399 - httpx - INFO - HTTP Request: POST https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash-preview:generateContent "HTTP/1.1 200 OK"
|
||||
2026-01-13 10:36:42,054 - google_genai.models - INFO - AFC is enabled with max remote calls: 10.
|
||||
2026-01-13 10:37:00,757 - httpx - INFO - HTTP Request: POST https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash-preview:generateContent "HTTP/1.1 200 OK"
|
||||
2026-01-13 10:48:46,989 - app.api.chat_routes - ERROR - Stream generation error: Models.generate_content() got an unexpected keyword argument 'stream'
|
||||
2026-01-13 10:49:06,199 - app.api.chat_routes - ERROR - Stream generation error: Models.generate_content() got an unexpected keyword argument 'stream'
|
||||
2026-01-13 10:50:51,922 - google_genai.models - INFO - AFC is enabled with max remote calls: 10.
|
||||
2026-01-13 10:51:04,963 - httpx - INFO - HTTP Request: POST https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:streamGenerateContent?alt=sse "HTTP/1.1 200 OK"
|
||||
2026-01-13 10:51:31,643 - google_genai.models - INFO - AFC is enabled with max remote calls: 10.
|
||||
2026-01-13 10:51:41,555 - httpx - INFO - HTTP Request: POST https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:streamGenerateContent?alt=sse "HTTP/1.1 200 OK"
|
||||
2026-01-13 11:12:30,678 - google_genai.models - INFO - AFC is enabled with max remote calls: 10.
|
||||
2026-01-13 11:12:36,515 - httpx - INFO - HTTP Request: POST https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash-preview:streamGenerateContent?alt=sse "HTTP/1.1 200 OK"
|
||||
2026-01-13 11:13:32,305 - google_genai.models - INFO - AFC is enabled with max remote calls: 10.
|
||||
2026-01-13 11:13:39,439 - httpx - INFO - HTTP Request: POST https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash-preview:streamGenerateContent?alt=sse "HTTP/1.1 200 OK"
|
||||
2026-01-13 11:15:42,762 - google_genai.models - INFO - AFC is enabled with max remote calls: 10.
|
||||
2026-01-13 11:15:54,798 - httpx - INFO - HTTP Request: POST https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:streamGenerateContent?alt=sse "HTTP/1.1 200 OK"
|
||||
2026-01-13 11:17:56,437 - google_genai.models - INFO - AFC is enabled with max remote calls: 10.
|
||||
2026-01-13 11:18:03,073 - httpx - INFO - HTTP Request: POST https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash-preview:streamGenerateContent?alt=sse "HTTP/1.1 200 OK"
|
||||
2026-01-13 13:46:38,919 - google_genai.models - INFO - AFC is enabled with max remote calls: 10.
|
||||
2026-01-13 13:46:45,337 - httpx - INFO - HTTP Request: POST https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:streamGenerateContent?alt=sse "HTTP/1.1 200 OK"
|
||||
2026-01-13 13:55:22,566 - google_genai.models - INFO - AFC is enabled with max remote calls: 10.
|
||||
2026-01-13 13:55:30,494 - httpx - INFO - HTTP Request: POST https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash-preview:streamGenerateContent?alt=sse "HTTP/1.1 200 OK"
|
||||
2026-01-13 13:55:51,512 - google_genai.models - INFO - AFC is enabled with max remote calls: 10.
|
||||
2026-01-13 13:56:04,646 - httpx - INFO - HTTP Request: POST https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:streamGenerateContent?alt=sse "HTTP/1.1 200 OK"
|
||||
2026-01-13 13:57:43,472 - google_genai.models - INFO - AFC is enabled with max remote calls: 10.
|
||||
2026-01-13 13:57:51,321 - httpx - INFO - HTTP Request: POST https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash-preview:streamGenerateContent?alt=sse "HTTP/1.1 200 OK"
|
||||
2026-01-13 13:59:03,716 - google_genai.models - INFO - AFC is enabled with max remote calls: 10.
|
||||
2026-01-13 13:59:17,415 - httpx - INFO - HTTP Request: POST https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash-preview:streamGenerateContent?alt=sse "HTTP/1.1 200 OK"
|
||||
2026-01-13 14:07:07,996 - google_genai.models - INFO - AFC is enabled with max remote calls: 10.
|
||||
2026-01-13 14:07:21,494 - httpx - INFO - HTTP Request: POST https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:streamGenerateContent?alt=sse "HTTP/1.1 200 OK"
|
||||
2026-01-13 14:07:48,005 - google_genai.models - INFO - AFC is enabled with max remote calls: 10.
|
||||
2026-01-13 14:08:04,297 - httpx - INFO - HTTP Request: POST https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:streamGenerateContent?alt=sse "HTTP/1.1 200 OK"
|
||||
|
||||
30
frontend/package-lock.json
generated
30
frontend/package-lock.json
generated
@ -14,6 +14,7 @@
|
||||
"@radix-ui/react-progress": "^1.1.8",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@ -1897,6 +1898,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-switch": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz",
|
||||
"integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||
"@radix-ui/react-use-previous": "1.1.1",
|
||||
"@radix-ui/react-use-size": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs": {
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
"@radix-ui/react-progress": "^1.1.8",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
|
||||
@ -37,14 +37,17 @@ export default function ConfigPage() {
|
||||
|
||||
if (loading) return <div className="p-8 flex justify-center"><Loader2 className="animate-spin" /></div>
|
||||
|
||||
const prompts = [
|
||||
"company_profile", "fundamental_analysis", "insider_analysis", "bullish_analysis", "bearish_analysis"
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-10 space-y-8 max-w-4xl">
|
||||
<h1 className="text-3xl font-bold">配置</h1>
|
||||
|
||||
{message && (
|
||||
<div className="fixed top-4 right-4 z-50 bg-primary text-primary-foreground px-4 py-2 rounded-md shadow-lg transition-all duration-300">
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>系统设置</CardTitle>
|
||||
@ -60,7 +63,9 @@ export default function ConfigPage() {
|
||||
onChange={(e) => setConfig({ ...config, "GEMINI_API_KEY": e.target.value })}
|
||||
placeholder="****************"
|
||||
/>
|
||||
<Button onClick={() => handleSave("GEMINI_API_KEY", config["GEMINI_API_KEY"])}>保存</Button>
|
||||
<Button onClick={() => handleSave("GEMINI_API_KEY", config["GEMINI_API_KEY"])} disabled={saving}>
|
||||
{saving ? <Loader2 className="w-4 h-4 animate-spin" /> : "保存"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -79,7 +84,9 @@ export default function ConfigPage() {
|
||||
))}
|
||||
<option value="custom">自定义模型...</option>
|
||||
</select>
|
||||
<Button onClick={() => handleSave("AI_MODEL", config["AI_MODEL"])}>保存</Button>
|
||||
<Button onClick={() => handleSave("AI_MODEL", config["AI_MODEL"])} disabled={saving}>
|
||||
{saving ? <Loader2 className="w-4 h-4 animate-spin" /> : "保存"}
|
||||
</Button>
|
||||
</div>
|
||||
{config["AI_MODEL"] === "custom" && (
|
||||
<div className="mt-2">
|
||||
@ -99,38 +106,124 @@ export default function ConfigPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>分析提示词</CardTitle>
|
||||
<CardDescription>自定义每个分析步骤使用的提示词。</CardDescription>
|
||||
<CardTitle>AI 讨论角色</CardTitle>
|
||||
<CardDescription>配置 AI 研究讨论界面中可用的角色。</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{prompts.map(key => (
|
||||
<div key={key} className="space-y-2">
|
||||
<Label className="capitalize">{key.replace(/_/g, " ")}</Label>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Textarea
|
||||
className="min-h-[150px] font-mono text-sm"
|
||||
value={config[`PROMPT_${key.toUpperCase()}`] || ""}
|
||||
onChange={(e) => setConfig({ ...config, [`PROMPT_${key.toUpperCase()}`]: e.target.value })}
|
||||
placeholder={`Enter prompt for ${key}...`}
|
||||
<AiRolesEditor
|
||||
initialRoles={config["ai_discussion_roles"]}
|
||||
onSave={(roles) => handleSave("ai_discussion_roles", JSON.stringify(roles))}
|
||||
/>
|
||||
<Button
|
||||
className="w-fit"
|
||||
variant="outline"
|
||||
onClick={() => handleSave(`PROMPT_${key.toUpperCase()}`, config[`PROMPT_${key.toUpperCase()}`])}
|
||||
>
|
||||
保存 {key.replace(/_/g, " ")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{message && <div className="fixed bottom-4 right-4 bg-primary text-primary-foreground px-4 py-2 rounded shadow-lg animate-in fade-in slide-in-from-bottom-5">
|
||||
{message.replace('Saved', '已保存').replace('Error saving settings', '保存设置时出错')}
|
||||
</div>}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>问题库配置</CardTitle>
|
||||
<CardDescription>
|
||||
配置 AI 讨论的问题库。支持 Markdown 格式,层级结构如下:<br />
|
||||
# 一级分类<br />
|
||||
## 二级分类<br />
|
||||
### 具体问题标题<br />
|
||||
问题内容...
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Textarea
|
||||
className="min-h-[300px] font-mono text-sm"
|
||||
value={config["ai_question_library"] || ""}
|
||||
onChange={(e) => setConfig({ ...config, "ai_question_library": e.target.value })}
|
||||
placeholder="# 财务分析 ## 盈利能力 ### 净利润增长 请分析该公司过去3年的净利润增长趋势..."
|
||||
/>
|
||||
<Button onClick={() => handleSave("ai_question_library", config["ai_question_library"] || "")} disabled={saving}>
|
||||
{saving && <Loader2 className="mr-2 w-4 h-4 animate-spin" />}
|
||||
保存问题库
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
|
||||
function AiRolesEditor({ initialRoles, onSave }: { initialRoles?: string, onSave: (roles: any[]) => void }) {
|
||||
const [roles, setRoles] = useState<any[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (initialRoles) {
|
||||
try {
|
||||
const parsed = JSON.parse(initialRoles)
|
||||
if (Array.isArray(parsed)) setRoles(parsed)
|
||||
} catch (e) {
|
||||
console.error("Failed to parse roles", e)
|
||||
}
|
||||
} else {
|
||||
// Default roles if nothing saved
|
||||
setRoles([
|
||||
{ id: "fundamental", name: "基本面分析师", prompt: "你是一位专业的股票基本面分析师..." },
|
||||
{ id: "market", name: "市场分析师", prompt: "你是一位敏锐的市场分析师..." }
|
||||
])
|
||||
}
|
||||
}, [initialRoles])
|
||||
|
||||
const updateRole = (index: number, key: string, value: string) => {
|
||||
const newRoles = [...roles]
|
||||
newRoles[index] = { ...newRoles[index], [key]: value }
|
||||
setRoles(newRoles)
|
||||
}
|
||||
|
||||
const addRole = () => {
|
||||
const id = `custom_${Date.now()}`
|
||||
setRoles([...roles, { id, name: "新角色", prompt: "你是一位..." }])
|
||||
}
|
||||
|
||||
const removeRole = (index: number) => {
|
||||
const newRoles = roles.filter((_, i) => i !== index)
|
||||
setRoles(newRoles)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4">
|
||||
{roles.map((role, idx) => (
|
||||
<div key={role.id || idx} className="p-4 border rounded-lg space-y-3 bg-muted/20">
|
||||
<div className="flex justify-between items-start">
|
||||
<Label className="mt-2">角色 #{idx + 1}</Label>
|
||||
<Button variant="ghost" size="sm" onClick={() => removeRole(idx)} className="text-destructive h-8 px-2">删除</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>名称</Label>
|
||||
<Input
|
||||
value={role.name}
|
||||
onChange={(e) => updateRole(idx, 'name', e.target.value)}
|
||||
/>
|
||||
<Label>ID (Unique)</Label>
|
||||
<Input
|
||||
value={role.id}
|
||||
onChange={(e) => updateRole(idx, 'id', e.target.value)}
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2 space-y-2">
|
||||
<Label>Prompt</Label>
|
||||
<Textarea
|
||||
value={role.prompt}
|
||||
onChange={(e) => updateRole(idx, 'prompt', e.target.value)}
|
||||
className="min-h-[100px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={addRole}>添加新角色</Button>
|
||||
<Button onClick={() => onSave(roles)}>保存所有角色配置</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ import { BloombergView } from "@/components/bloomberg-view"
|
||||
import { HeaderPortal } from "@/components/header-portal"
|
||||
import { AppSidebar } from "@/components/app-sidebar"
|
||||
import { StockChart } from "@/components/stock-chart"
|
||||
import { AiDiscussionView } from "@/components/ai-discussion-view"
|
||||
|
||||
export default function Home() {
|
||||
const searchParams = useSearchParams()
|
||||
@ -145,6 +146,19 @@ export default function Home() {
|
||||
)
|
||||
}
|
||||
|
||||
// AI Discussion View
|
||||
if (currentView === "ai-research") {
|
||||
return (
|
||||
<div className="h-full">
|
||||
<AiDiscussionView
|
||||
companyName={selectedCompany.company_name}
|
||||
symbol={selectedCompany.symbol}
|
||||
market={selectedCompany.market}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Financial Data View (Default fallback)
|
||||
return (
|
||||
<div className="w-full pl-2 pr-32 py-6 space-y-6">
|
||||
|
||||
644
frontend/src/components/ai-discussion-view.tsx
Normal file
644
frontend/src/components/ai-discussion-view.tsx
Normal file
@ -0,0 +1,644 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect, useRef } from "react"
|
||||
import { HeaderPortal } from "@/components/header-portal"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
// Removed missing ScrollArea import
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Send, Bot, User, Trash2, ArrowLeft, ArrowRight } from "lucide-react"
|
||||
// Removed missing Dialog imports
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import ReactMarkdown from "react-markdown"
|
||||
import remarkGfm from "remark-gfm"
|
||||
|
||||
interface Message {
|
||||
role: "user" | "model"
|
||||
content: string
|
||||
groundingMetadata?: {
|
||||
searchEntryPoint?: { renderedContent: string }
|
||||
groundingChunks?: Array<{ web?: { uri: string; title: string } }>
|
||||
webSearchQueries?: string[]
|
||||
}
|
||||
}
|
||||
|
||||
interface RoleConfig {
|
||||
id: string
|
||||
name: string
|
||||
prompt: string
|
||||
}
|
||||
|
||||
const DEFAULT_ROLES: RoleConfig[] = [
|
||||
{ id: "fundamental", name: "基本面分析师", prompt: "你是一位专业的股票基本面分析师,专注于分析公司的财务报表、盈利能力和竞争优势。" },
|
||||
{ id: "market", name: "市场分析师", prompt: "你是一位敏锐的市场分析师,关注宏观经济、行业趋势和市场情绪对股价的影响。" },
|
||||
{ id: "bullish", name: "长期看涨分析师", prompt: "你通过乐观的视角分析公司,寻找长期的增长潜力和投资价值,强调公司的优势和机会。" },
|
||||
{ id: "bearish", name: "短期风险分析师", prompt: "你通过审慎的视角分析公司,专注于识别潜在的风险、估值过高和短期利空因素。" },
|
||||
{ id: "info", name: "市场信息分析师", prompt: "你专注于搜集和解读最新的市场新闻、公告和传闻,提供客观的信息汇总。" }
|
||||
]
|
||||
|
||||
export function AiDiscussionView({ companyName, symbol, market }: { companyName: string, symbol: string, market: string }) {
|
||||
const [roles, setRoles] = useState<RoleConfig[]>([])
|
||||
const [availableModels, setAvailableModels] = useState<string[]>(["gemini-2.0-flash", "gemini-1.5-pro", "gemini-1.5-flash"])
|
||||
const [questionLibrary, setQuestionLibrary] = useState<any>(null)
|
||||
|
||||
// Left Chat State
|
||||
const [leftRole, setLeftRole] = useState("")
|
||||
const [leftModel, setLeftModel] = useState("gemini-2.0-flash")
|
||||
const [leftMessages, setLeftMessages] = useState<Message[]>([])
|
||||
const [leftInput, setLeftInput] = useState("")
|
||||
const [leftLoading, setLeftLoading] = useState(false)
|
||||
const [leftGoogleSearch, setLeftGoogleSearch] = useState(false)
|
||||
const [leftSessionId, setLeftSessionId] = useState(crypto.randomUUID())
|
||||
|
||||
// Right Chat State
|
||||
const [rightRole, setRightRole] = useState("")
|
||||
const [rightModel, setRightModel] = useState("gemini-2.0-flash")
|
||||
const [rightMessages, setRightMessages] = useState<Message[]>([])
|
||||
const [rightInput, setRightInput] = useState("")
|
||||
const [rightLoading, setRightLoading] = useState(false)
|
||||
const [rightGoogleSearch, setRightGoogleSearch] = useState(false)
|
||||
const [rightSessionId, setRightSessionId] = useState(crypto.randomUUID())
|
||||
|
||||
// Load config on mount
|
||||
useEffect(() => {
|
||||
fetch("/api/config")
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
let currentRoles = DEFAULT_ROLES;
|
||||
|
||||
// Parse Roles
|
||||
if (data.ai_discussion_roles) {
|
||||
try {
|
||||
const parsed = JSON.parse(data.ai_discussion_roles)
|
||||
if (Array.isArray(parsed) && parsed.length > 0) {
|
||||
currentRoles = parsed
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to parse roles config", e)
|
||||
}
|
||||
}
|
||||
|
||||
setRoles(currentRoles)
|
||||
|
||||
// Initialize selected roles if they are empty or invalid
|
||||
if (currentRoles.length > 0) {
|
||||
setLeftRole(currentRoles[0].id)
|
||||
if (currentRoles.length > 1) {
|
||||
setRightRole(currentRoles[1].id)
|
||||
} else {
|
||||
setRightRole(currentRoles[0].id)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse AI Model preferences (Default Model)
|
||||
if (data.ai_model) {
|
||||
setLeftModel(data.ai_model)
|
||||
setRightModel(data.ai_model)
|
||||
}
|
||||
|
||||
// Parse Available Models List
|
||||
if (data.available_models) {
|
||||
try {
|
||||
const parsedModels = JSON.parse(data.available_models)
|
||||
if (Array.isArray(parsedModels) && parsedModels.length > 0) {
|
||||
setAvailableModels(parsedModels)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to parse available_models config", e)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse Question Library
|
||||
if (data.ai_question_library) {
|
||||
setQuestionLibrary(parseQuestionLibrary(data.ai_question_library))
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Failed to fetch config", err)
|
||||
// Fallback to defaults on error
|
||||
setRoles(DEFAULT_ROLES)
|
||||
setLeftRole(DEFAULT_ROLES[0].id)
|
||||
setRightRole(DEFAULT_ROLES[1].id)
|
||||
})
|
||||
}, [])
|
||||
|
||||
const parseQuestionLibrary = (markdown: string) => {
|
||||
const lines = markdown.split('\n');
|
||||
const library: any = {};
|
||||
let currentL1: string | null = null;
|
||||
let currentL2: string | null = null;
|
||||
let currentL3: string | null = null;
|
||||
let currentContent: string[] = [];
|
||||
|
||||
const saveContent = () => {
|
||||
if (currentL1 && currentL2) {
|
||||
// If L3 is missing, use L2 as the question title (support 2-level structure)
|
||||
const l3 = currentL3 || currentL2;
|
||||
|
||||
if (!library[currentL1]) library[currentL1] = {};
|
||||
if (!library[currentL1][currentL2]) library[currentL1][currentL2] = {};
|
||||
|
||||
// If content is empty, use the question title as content
|
||||
const content = currentContent.length > 0 ? currentContent.join('\n').trim() : l3;
|
||||
if (content && l3) {
|
||||
library[currentL1][currentL2][l3] = content;
|
||||
}
|
||||
}
|
||||
currentContent = [];
|
||||
}
|
||||
|
||||
lines.forEach(line => {
|
||||
if (line.startsWith('# ')) {
|
||||
saveContent();
|
||||
currentL1 = line.substring(2).trim();
|
||||
currentL2 = null;
|
||||
currentL3 = null;
|
||||
} else if (line.startsWith('## ')) {
|
||||
saveContent();
|
||||
currentL2 = line.substring(3).trim();
|
||||
currentL3 = null;
|
||||
} else if (line.startsWith('### ')) {
|
||||
saveContent();
|
||||
currentL3 = line.substring(4).trim();
|
||||
} else {
|
||||
// Collect content if at least L1 and L2 are defined
|
||||
if (currentL1 && currentL2) {
|
||||
currentContent.push(line);
|
||||
}
|
||||
}
|
||||
});
|
||||
saveContent(); // Save last item
|
||||
return library;
|
||||
}
|
||||
|
||||
const sendMessage = async (
|
||||
messages: Message[],
|
||||
setMessages: (msgs: Message[]) => void,
|
||||
role: string,
|
||||
model: string,
|
||||
useGoogleSearch: boolean,
|
||||
sessionId: string,
|
||||
setLoading: (l: boolean) => void
|
||||
) => {
|
||||
setLoading(true)
|
||||
|
||||
const systemPrompt = roles.find(r => r.id === role)?.prompt
|
||||
? `${roles.find(r => r.id === role)?.prompt}\n\n当前分析对象:${companyName} (${symbol}, ${market})。`
|
||||
: `分析对象:${companyName}`
|
||||
|
||||
let botMsg: Message = { role: "model", content: "" }
|
||||
// Add empty bot message immediately (will update it incrementally)
|
||||
const historyWithBot = [...messages, botMsg]
|
||||
setMessages(historyWithBot)
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/chat", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
messages: messages,
|
||||
model: model,
|
||||
system_prompt: systemPrompt,
|
||||
use_google_search: useGoogleSearch,
|
||||
session_id: sessionId
|
||||
})
|
||||
})
|
||||
|
||||
if (!res.ok) throw new Error("API Request failed")
|
||||
if (!res.body) throw new Error("No response body")
|
||||
|
||||
const reader = res.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let done = false
|
||||
let accumulatedContent = ""
|
||||
|
||||
while (!done) {
|
||||
const { value, done: doneReading } = await reader.read()
|
||||
done = doneReading
|
||||
if (value) {
|
||||
const chunk = decoder.decode(value, { stream: true })
|
||||
const lines = chunk.split('\n')
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.trim()) {
|
||||
try {
|
||||
const data = JSON.parse(line)
|
||||
if (data.type === 'content') {
|
||||
accumulatedContent += data.content
|
||||
botMsg = { ...botMsg, content: accumulatedContent }
|
||||
// Update last message
|
||||
setMessages([...messages, botMsg])
|
||||
} else if (data.type === 'metadata') {
|
||||
botMsg = { ...botMsg, groundingMetadata: data.groundingMetadata }
|
||||
setMessages([...messages, botMsg])
|
||||
} else if (data.type === 'error') {
|
||||
console.error("Stream error:", data.error)
|
||||
botMsg = { ...botMsg, content: botMsg.content + "\n[Error: " + data.error + "]" }
|
||||
setMessages([...messages, botMsg])
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Failed to parse chunk", line, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
setMessages([...messages, { role: "model", content: "Error: Failed to get response." }])
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const [leftPendingQuote, setLeftPendingQuote] = useState<string | null>(null)
|
||||
const [rightPendingQuote, setRightPendingQuote] = useState<string | null>(null)
|
||||
|
||||
// ... existing Load config logic ...
|
||||
|
||||
const handleSendLeft = (text: string) => {
|
||||
if (!text.trim() && !leftPendingQuote) return
|
||||
|
||||
let finalContent = text
|
||||
if (leftPendingQuote) {
|
||||
finalContent = `> ${leftPendingQuote}\n\n${text}`
|
||||
setLeftPendingQuote(null)
|
||||
}
|
||||
|
||||
const newMsg: Message = { role: 'user', content: finalContent }
|
||||
const newHistory = [...leftMessages, newMsg]
|
||||
setLeftMessages(newHistory)
|
||||
sendMessage(newHistory, setLeftMessages, leftRole, leftModel, leftGoogleSearch, leftSessionId, setLeftLoading)
|
||||
setLeftInput("")
|
||||
}
|
||||
|
||||
const handleSendRight = (text: string) => {
|
||||
if (!text.trim() && !rightPendingQuote) return
|
||||
|
||||
let finalContent = text
|
||||
if (rightPendingQuote) {
|
||||
finalContent = `> ${rightPendingQuote}\n\n${text}`
|
||||
setRightPendingQuote(null)
|
||||
}
|
||||
|
||||
const newMsg: Message = { role: 'user', content: finalContent }
|
||||
const newHistory = [...rightMessages, newMsg]
|
||||
setRightMessages(newHistory)
|
||||
sendMessage(newHistory, setRightMessages, rightRole, rightModel, rightGoogleSearch, rightSessionId, setRightLoading)
|
||||
setRightInput("")
|
||||
}
|
||||
|
||||
const handleCrossToRight = (content: string) => {
|
||||
setRightPendingQuote(content)
|
||||
setRightInput("请针对以上观点进行点评:")
|
||||
}
|
||||
|
||||
const handleCrossToLeft = (content: string) => {
|
||||
setLeftPendingQuote(content)
|
||||
setLeftInput("请针对以上观点进行点评:")
|
||||
}
|
||||
|
||||
// Clear Handler - Regenerate Session ID
|
||||
const handleClearLeft = () => {
|
||||
setLeftMessages([])
|
||||
setLeftSessionId(crypto.randomUUID())
|
||||
}
|
||||
|
||||
const handleClearRight = () => {
|
||||
setRightMessages([])
|
||||
setRightSessionId(crypto.randomUUID())
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col p-4 pb-18 gap-4 relative">
|
||||
<HeaderPortal>
|
||||
<div className="flex items-center gap-2 mr-4 border-r pr-4">
|
||||
<span className="font-bold whitespace-nowrap">AI 研究讨论</span>
|
||||
<Badge variant="outline" className="font-mono">{companyName}</Badge>
|
||||
<Badge className="text-xs">{symbol}</Badge>
|
||||
<Badge variant="secondary" className="text-xs">{market}</Badge>
|
||||
</div>
|
||||
</HeaderPortal>
|
||||
|
||||
<div className="flex-1 grid grid-cols-2 gap-4 h-full overflow-hidden">
|
||||
{/* Left Pane */}
|
||||
<ChatPane
|
||||
side="left"
|
||||
roles={roles}
|
||||
models={availableModels}
|
||||
selectedRole={leftRole}
|
||||
setSelectedRole={setLeftRole}
|
||||
selectedModel={leftModel}
|
||||
setSelectedModel={setLeftModel}
|
||||
messages={leftMessages}
|
||||
input={leftInput}
|
||||
setInput={setLeftInput}
|
||||
loading={leftLoading}
|
||||
questionLibrary={questionLibrary}
|
||||
useGoogleSearch={leftGoogleSearch}
|
||||
setUseGoogleSearch={setLeftGoogleSearch}
|
||||
onSend={() => handleSendLeft(leftInput)}
|
||||
onClear={handleClearLeft}
|
||||
onCrossCritique={handleCrossToRight}
|
||||
pendingQuote={leftPendingQuote}
|
||||
onClearQuote={() => setLeftPendingQuote(null)}
|
||||
/>
|
||||
|
||||
{/* Right Pane */}
|
||||
<ChatPane
|
||||
side="right"
|
||||
roles={roles}
|
||||
models={availableModels}
|
||||
selectedRole={rightRole}
|
||||
setSelectedRole={setRightRole}
|
||||
selectedModel={rightModel}
|
||||
setSelectedModel={setRightModel}
|
||||
messages={rightMessages}
|
||||
input={rightInput}
|
||||
setInput={setRightInput}
|
||||
loading={rightLoading}
|
||||
questionLibrary={questionLibrary}
|
||||
useGoogleSearch={rightGoogleSearch}
|
||||
setUseGoogleSearch={setRightGoogleSearch}
|
||||
onSend={() => handleSendRight(rightInput)}
|
||||
onClear={handleClearRight}
|
||||
onCrossCritique={handleCrossToLeft}
|
||||
pendingQuote={rightPendingQuote}
|
||||
onClearQuote={() => setRightPendingQuote(null)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ChatPane({
|
||||
side, roles, models, selectedRole, setSelectedRole, selectedModel, setSelectedModel,
|
||||
messages, input, setInput, loading, onSend, onClear, questionLibrary, useGoogleSearch, setUseGoogleSearch, onCrossCritique,
|
||||
pendingQuote, onClearQuote
|
||||
}: any) {
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
// ... existing state ...
|
||||
const [qL1, setQL1] = useState("")
|
||||
const [qL2, setQL2] = useState("")
|
||||
const [qL3, setQL3] = useState("")
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight
|
||||
}
|
||||
}, [messages, loading])
|
||||
|
||||
const handleQuestionSelect = (l3: string) => {
|
||||
if (questionLibrary && qL1 && qL2) {
|
||||
if (l3 === "__ALL__") {
|
||||
const l2Obj = questionLibrary[qL1]?.[qL2]
|
||||
if (l2Obj) {
|
||||
const allContent = Object.values(l2Obj).join("\n\n")
|
||||
setInput(allContent)
|
||||
}
|
||||
} else {
|
||||
const content = questionLibrary[qL1]?.[qL2]?.[l3]
|
||||
if (content) {
|
||||
setInput(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
setQL3("") // Reset last selection to allow re-selection
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="flex flex-col h-full overflow-hidden shadow-sm pt-1 pb-0 border-2 border-muted/20">
|
||||
<CardHeader className="p-1 pb-1 border-b bg-muted/10 h-12 flex">
|
||||
{/* ... existing header content ... */}
|
||||
<div className="flex gap-2 items-center">
|
||||
<Select value={selectedRole} onValueChange={setSelectedRole}>
|
||||
<SelectTrigger className="w-[160px] h-6 text-xs font-medium focus:ring-0 focus:ring-offset-0">
|
||||
<SelectValue placeholder="选择角色" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{roles.map((r: any) => (
|
||||
<SelectItem key={r.id} value={r.id} className="text-xs">
|
||||
{r.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select value={selectedModel} onValueChange={setSelectedModel}>
|
||||
<SelectTrigger className="w-[130px] h-6 text-xs font-mono text-muted-foreground focus:ring-0 focus:ring-offset-0">
|
||||
<SelectValue placeholder="选择模型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{models.map((m: string) => (
|
||||
<SelectItem key={m} value={m} className="text-xs font-mono">
|
||||
{m}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<div className="flex items-center space-x-2 border-l pl-2 ml-2 h-6">
|
||||
<Switch id={`gs-${side}`} checked={useGoogleSearch} onCheckedChange={setUseGoogleSearch} className="h-3 w-6 scale-75 origin-left" />
|
||||
<Label htmlFor={`gs-${side}`} className="text-[10px] text-muted-foreground whitespace-nowrap cursor-pointer">
|
||||
Google Search
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex-1" />
|
||||
<Button variant="ghost" size="icon" onClick={onClear} className="h-6 w-6 text-muted-foreground hover:text-destructive">
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 p-0 flex flex-col overflow-hidden">
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4" ref={scrollRef}>
|
||||
{/* ... messages rendering ... */}
|
||||
{messages.length === 0 && (
|
||||
<div className="text-center text-muted-foreground text-sm mt-10 opacity-50">
|
||||
选择角色并开始讨论...
|
||||
</div>
|
||||
)}
|
||||
{messages.map((m: Message, i: number) => (
|
||||
<div key={i} className={`flex gap-3 ${m.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
||||
{m.role === 'model' && <div className="w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900 flex items-center justify-center flex-shrink-0">
|
||||
<Bot className="w-5 h-5 text-blue-600 dark:text-blue-300" />
|
||||
</div>}
|
||||
|
||||
<div className={`rounded-lg p-3 text-sm max-w-[85%] ${m.role === 'user'
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted/50 border'
|
||||
}`}>
|
||||
{m.role === 'model' ? (
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none break-words">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
pre: ({ node, className, ...props }) => <pre className={`overflow-auto w-full my-2 bg-black/10 dark:bg-black/30 p-2 rounded ${className || ''}`} {...props} />,
|
||||
code: ({ node, className, children, ...props }) => {
|
||||
const match = /language-(\w+)/.exec(className || '')
|
||||
return <code className={`${className || ''} bg-black/5 dark:bg-white/10 rounded px-1`} {...props}>{children}</code>
|
||||
}
|
||||
}}
|
||||
>
|
||||
{m.content}
|
||||
</ReactMarkdown>
|
||||
|
||||
{/* Grounding Metadata Display */}
|
||||
{m.groundingMetadata && (
|
||||
<div className="mt-3 pt-3 border-t text-xs">
|
||||
{/* Citations */}
|
||||
{m.groundingMetadata.groundingChunks && m.groundingMetadata.groundingChunks.length > 0 && (
|
||||
<div className="mb-2">
|
||||
<div className="font-semibold mb-1 text-muted-foreground">参考来源:</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{m.groundingMetadata.groundingChunks.map((chunk: any, idx: number) => chunk.web ? (
|
||||
<a
|
||||
key={idx}
|
||||
href={chunk.web.uri}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="bg-muted px-2 py-1 rounded hover:bg-muted/80 text-blue-500 underline truncate max-w-[200px]"
|
||||
title={chunk.web.title}
|
||||
>
|
||||
{chunk.web.title || chunk.web.uri}
|
||||
</a>
|
||||
) : null)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Search Suggestions (Search Entry Point) */}
|
||||
{m.groundingMetadata.searchEntryPoint && (
|
||||
<div className="mt-2">
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: m.groundingMetadata.searchEntryPoint.renderedContent }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="whitespace-pre-wrap">{m.content}</div>
|
||||
)}
|
||||
{m.role === 'model' && (
|
||||
<div className="flex justify-end mt-2 pt-2 border-t border-dashed border-gray-200 dark:border-gray-700">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 px-2 text-xs text-muted-foreground hover:text-primary gap-1"
|
||||
onClick={() => onCrossCritique && onCrossCritique(m.content)}
|
||||
title={side === 'left' ? "发送给右侧点评" : "发送给左侧点评"}
|
||||
>
|
||||
{side === 'left' ? "点评" : null}
|
||||
{side === 'left' ? <ArrowRight className="w-3 h-3" /> : <ArrowLeft className="w-3 h-3" />}
|
||||
{side === 'right' ? "点评" : null}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{m.role === 'user' && <div className="w-8 h-8 rounded-full bg-slate-100 dark:bg-slate-800 flex items-center justify-center flex-shrink-0">
|
||||
<User className="w-5 h-5 text-slate-600 dark:text-slate-300" />
|
||||
</div>}
|
||||
</div>
|
||||
))}
|
||||
{loading && (
|
||||
<div className="flex gap-3 justify-start">
|
||||
<div className="w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900 flex items-center justify-center">
|
||||
<Bot className="w-5 h-5 text-blue-600 dark:text-blue-300" />
|
||||
</div>
|
||||
<div className="rounded-lg p-3 bg-muted/50 border text-sm text-muted-foreground animate-pulse">
|
||||
分析中...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Question Library Selector */}
|
||||
{questionLibrary && Object.keys(questionLibrary).length > 0 && (
|
||||
<div className="px-3 pt-2 bg-background border-t flex gap-2">
|
||||
<Select value={qL1} onValueChange={(v) => { setQL1(v); setQL2(""); setQL3(""); }}>
|
||||
<SelectTrigger className="h-7 text-xs w-[120px]">
|
||||
<SelectValue placeholder="问题分类" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.keys(questionLibrary).map(k => (
|
||||
<SelectItem key={k} value={k} className="text-xs">{k}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{qL1 && questionLibrary[qL1] && (
|
||||
<Select value={qL2} onValueChange={(v) => { setQL2(v); setQL3(""); }}>
|
||||
<SelectTrigger className="h-7 text-xs w-[120px]">
|
||||
<SelectValue placeholder="子分类" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.keys(questionLibrary[qL1]).map(k => (
|
||||
<SelectItem key={k} value={k} className="text-xs">{k}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
|
||||
{qL2 && questionLibrary[qL1]?.[qL2] && (
|
||||
<Select value={qL3} onValueChange={(v) => { setQL3(""); handleQuestionSelect(v); }}>
|
||||
<SelectTrigger className="h-7 text-xs flex-1">
|
||||
<SelectValue placeholder="选择问题..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__ALL__" className="font-bold text-primary">【发送本节整合内容】</SelectItem>
|
||||
{Object.keys(questionLibrary[qL1][qL2]).map(k => (
|
||||
<SelectItem key={k} value={k} className="text-xs">{k}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-3 bg-background flex flex-col gap-2">
|
||||
{pendingQuote && (
|
||||
<div className="relative bg-muted/60 border-l-4 border-primary/50 p-2 rounded-r text-xs text-muted-foreground flex justify-between items-start group animate-in slide-in-from-bottom-2 fade-in duration-200">
|
||||
<div className="line-clamp-2 italic pr-6 select-none">
|
||||
{pendingQuote}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-4 w-4 absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
onClick={onClearQuote}
|
||||
>
|
||||
<span className="sr-only">Close</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="lucide lucide-x h-3 w-3"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<form
|
||||
className="flex gap-2"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
onSend()
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
placeholder="输入消息..."
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button type="submit" size="sm" disabled={loading || !input.trim()}>
|
||||
<Send className="w-4 h-4" />
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
|
||||
import { Home, LineChart, BarChart3, Search } from "lucide-react"
|
||||
import { Home, LineChart, BarChart3, Search, MessageSquare } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
@ -15,6 +15,7 @@ export function AppSidebar({ activeTab, onTabChange, className, hasSelectedCompa
|
||||
{ id: "home", label: "首页", icon: Home, disabled: false },
|
||||
{ id: "financial", label: "财务数据", icon: BarChart3, disabled: !hasSelectedCompany },
|
||||
{ id: "chart", label: "股价图", icon: LineChart, disabled: !hasSelectedCompany },
|
||||
{ id: "ai-research", label: "AI研究讨论", icon: MessageSquare, disabled: !hasSelectedCompany },
|
||||
]
|
||||
|
||||
return (
|
||||
|
||||
29
frontend/src/components/ui/switch.tsx
Normal file
29
frontend/src/components/ui/switch.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
))
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||
|
||||
export { Switch }
|
||||
Loading…
Reference in New Issue
Block a user