From e855a16c698cd255cc63b9f29fb2b29c006ac4ce Mon Sep 17 00:00:00 2001 From: "Lv, Qi" Date: Wed, 19 Nov 2025 06:58:47 +0800 Subject: [PATCH] feat(analysis): integrate analysis templates into report workflow - Backend: Add template_id to DataRequest and AnalysisResult metadata - Frontend: Add TemplateSelector to Report page - Frontend: Refactor ReportPage to use AnalysisTemplateSets for dynamic tabs - Frontend: Strict mode for report rendering based on template_id - Cleanup: Remove legacy analysis config hooks --- .../20251118_analysis_template_integration.md | 130 ++++ .../src/app/api/financials/[...slug]/route.ts | 76 ++ frontend/src/app/config/hooks/useAiConfig.ts | 208 ------ .../src/app/config/hooks/useAnalysisConfig.ts | 228 ------ .../config/hooks/useDataSourcesConfigLogic.ts | 94 --- .../src/app/config/hooks/useSystemConfig.ts | 57 -- frontend/src/app/config/utils/yaml-helper.ts | 138 ---- .../[symbol]/components/ReportHeader.tsx | 78 +- .../[symbol]/hooks/useAnalysisRunner.ts | 65 +- frontend/src/app/report/[symbol]/page.tsx | 2 + frontend/src/components/ui/command.tsx | 160 ++++ frontend/src/components/ui/popover.tsx | 49 ++ frontend/src/hooks/useApi.ts | 79 +- frontend/src/types/index.ts | 16 + package-lock.json | 697 ++++++++++++++++++ package.json | 3 + services/api-gateway/src/api.rs | 20 +- services/common-contracts/src/lib.rs | 1 + services/common-contracts/src/provider.rs | 8 + .../finnhub-provider-service/src/finnhub.rs | 11 + .../tushare-provider-service/src/tushare.rs | 11 + 21 files changed, 1294 insertions(+), 837 deletions(-) create mode 100644 docs/3_project_management/tasks/pending/20251118_analysis_template_integration.md delete mode 100644 frontend/src/app/config/hooks/useAiConfig.ts delete mode 100644 frontend/src/app/config/hooks/useAnalysisConfig.ts delete mode 100644 frontend/src/app/config/hooks/useDataSourcesConfigLogic.ts delete mode 100644 frontend/src/app/config/hooks/useSystemConfig.ts delete mode 100644 frontend/src/app/config/utils/yaml-helper.ts create mode 100644 frontend/src/components/ui/command.tsx create mode 100644 frontend/src/components/ui/popover.tsx create mode 100644 services/common-contracts/src/provider.rs diff --git a/docs/3_project_management/tasks/pending/20251118_analysis_template_integration.md b/docs/3_project_management/tasks/pending/20251118_analysis_template_integration.md new file mode 100644 index 0000000..221ee2a --- /dev/null +++ b/docs/3_project_management/tasks/pending/20251118_analysis_template_integration.md @@ -0,0 +1,130 @@ +# 分析模板集成设计文档 + +## 1. 概述 +系统正在从单一、固定的分析配置架构向多模板架构迁移。目前,后端已支持 `AnalysisTemplateSets` 并能执行特定的模板。然而,前端在渲染报告标签页(Tabs)和触发分析时,仍然依赖于过时的 `AnalysisModulesConfig`(单一配置集)。 + +本文档概述了将“分析模板”完全集成到用户工作流中所需的变更,具体包括: +1. **触发分析**:在启动新的分析任务时选择特定的模板。 +2. **报告展示**:根据分析所使用的模板,动态渲染标签页和内容。 + +## 2. 当前状态 vs. 目标状态 + +| 功能特性 | 当前状态 | 目标状态 | +| :--- | :--- | :--- | +| **配置管理** | `useAnalysisConfig` (过时,单一模块列表) | `useAnalysisTemplateSets` (多套具名模板) | +| **触发分析** | `trigger(symbol, market)` (无模板选择) | `trigger(symbol, market, templateId)` | +| **报告标签页** | 硬编码遍历过时的 `analysis_modules` keys | 根据报告使用的**特定模板**动态生成标签页 | +| **模块名称** | 从全局默认配置获取 | 从所使用的特定模板配置中获取 | + +## 3. 详细设计 + +### 3.1. 后端变更 + +#### 3.1.1. API Gateway (`api-gateway`) +* **Endpoint**: `POST /api/data-requests` +* **变更**: 更新请求 DTO (Data Transfer Object) 以接收可选参数 `template_id: String`。 +* **逻辑**: 将此 `template_id` 通过 `GenerateReportCommand` 向下传递给 `report-generator-service`。 + +#### 3.1.2. 数据持久化 / 报告数据 +* **需求**: 前端需要知道生成某份报告时具体使用了*哪个*模板,以便正确渲染标签页(包括标题和顺序)。 +* **变更**: 确保 `GET /api/financials/...` 或 `GET /api/reports/...` 的响应数据中,在 metadata 中包含 `template_id`。 +* **实现**: 前端 `route.ts` 聚合层通过查询 `analysis-results` 获取最新的 `template_id` 并注入到 `meta` 中。 + +### 3.2. 前端变更 + +#### 3.2.1. API Hooks (`useApi.ts`) +* **`useDataRequest`**: 更新 `trigger` 函数签名: + ```typescript + trigger(symbol: string, market: string, templateId?: string) + ``` +* **`useAnalysisTemplateSets`**: 确保此 hook 可用(目前代码中已存在)。 + +#### 3.2.2. 触发 UI (报告页侧边栏 / 查询页) +* **组件**: 在“触发分析”按钮旁增加一个 `TemplateSelector` (选择框/下拉菜单)。 +* **数据源**: `useAnalysisTemplateSets`。 +* **默认值**: 自动选中第一个可用的模板,或者标记为 "default" 的模板。 + +#### 3.2.3. 报告页面 (`frontend/src/app/report/[symbol]/page.tsx`) +这是最复杂的变更部分。我们需要重构标签页(Tabs)的生成逻辑。 + +1. **移除旧逻辑**: + * 移除对 `useAnalysisConfig` (全局默认配置) 的依赖。 + * 弃用/移除 `runAnalysesSequentially` (旧的前端编排流程)。 + +2. **识别模板**: + * 从获取到的财务/报告数据中读取 `template_id` (例如 `financials.meta.template_id` 或类似位置)。 + * **Strict Mode**: 如果缺失 `template_id`,则视为严重数据错误,前端直接报错停止渲染,**绝不进行默认值回退或自动推断**。 + +3. **动态标签页**: + * 使用 `useAnalysisTemplateSets` 获取 `templateSets`。 + * 从 `templateSets[currentTemplateId].modules` 中推导出 `activeModules` 列表。 + * 遍历 `activeModules` 来生成 `TabsTrigger` 和 `TabsContent`。 + * **显示名称**: 使用 `moduleConfig.name`。 + * **排序**: 严格遵循模板中定义的顺序(或依赖顺序)。 + +### 3.3. 数据流 + +1. **用户**选择 "标准分析模板 V2" (Standard Analysis V2) 并点击 "运行"。 +2. **前端**调用 `POST /api/data-requests`,载荷为 `{ ..., template_id: "standard_v2" }`。 +3. **后端**使用 "standard_v2" 中定义的模块生成报告。 +4. **前端**轮询任务进度。 +5. **前端**获取完成的数据。数据包含元数据 `meta: { template_id: "standard_v2" }`。 +6. **前端**查询 "standard_v2" 的配置详情。 +7. **前端**渲染标签页:如 "公司简介"、"财务健康"(均来自 V2 配置)。 + +## 4. 实施步骤 + +1. **后端更新**: + * 验证 `api-gateway` 是否正确传递 `template_id`。 + * 验证报告 API 是否在 metadata 中返回 `template_id`。 + +2. **前端 - 触发**: + * 更新 `useDataRequest` hook。 + * 在 `ReportPage` 中添加 `TemplateSelector` 组件。 + +3. **前端 - 展示**: + * 重构 `ReportPage` 以使用 `templateSets`。 + * 根据报告中的 `template_id` 动态计算 `analysisTypes`。 + +## 5. 待办事项列表 (To-Do List) + +### Phase 1: 后端与接口 (Backend & API) +- [x] **1.1 更新请求 DTO (api-gateway)** + - 目标: `api-gateway` 的 `DataRequest` 结构体 + - 动作: 增加 `template_id` 字段 (Option) + - 验证: `curl` 请求带 `template_id` 能被解析 +- [x] **1.2 传递 Command (api-gateway -> report-service)** + - 目标: `GenerateReportCommand` 消息 + - 动作: 确保 `template_id` 被正确透传到消息队列或服务调用中 +- [x] **1.3 验证报告元数据 (data-persistence)** + - 目标: `GET /api/financials/...` 接口 + - 动作: 检查返回的 JSON 中 `meta` 字段是否包含 `template_id` + - 备注: 已通过 frontend `route.ts` 聚合实现 + +### Phase 2: 前端逻辑 (Frontend Logic) +- [x] **2.1 更新 API Hook** + - 文件: `frontend/src/hooks/useApi.ts` + - 动作: 修改 `useDataRequest` 的 `trigger` 方法签名,支持 `templateId` 参数 +- [x] **2.2 移除旧版依赖** + - 文件: `frontend/src/app/report/[symbol]/page.tsx` + - 动作: 移除 `useAnalysisConfig` 及相关旧版逻辑 (`runAnalysesSequentially`) + +### Phase 3: 前端界面 (Frontend UI) +- [x] **3.1 实现模板选择器** + - 文件: `frontend/src/app/report/[symbol]/page.tsx` (侧边栏) + - 动作: 添加 ` + + + + + {templateSets && Object.entries(templateSets).map(([id, set]: [string, any]) => ( + + {set.name} + + ))} + + + + +
+ + + +
+ + + + 昨日快照
- -
- - - - - - - - ); } @@ -131,8 +151,7 @@ function SnapshotItem({ label, value, loading }: { label: string; value?: string {loading ? ( - {label === '日期' && } - {label === '日期' ? '加载中...' : '-'} + ) : value ? ( value @@ -143,4 +162,3 @@ function SnapshotItem({ label, value, loading }: { label: string; value?: string ); } - diff --git a/frontend/src/app/report/[symbol]/hooks/useAnalysisRunner.ts b/frontend/src/app/report/[symbol]/hooks/useAnalysisRunner.ts index b5e69fa..44772ba 100644 --- a/frontend/src/app/report/[symbol]/hooks/useAnalysisRunner.ts +++ b/frontend/src/app/report/[symbol]/hooks/useAnalysisRunner.ts @@ -25,17 +25,47 @@ interface AnalysisRecord { export function useAnalysisRunner( financials: any, - analysisConfig: any, + financialConfig: any, normalizedMarket: string, unifiedSymbol: string, isLoading: boolean, - error: any + error: any, + templateSets: any // Added templateSets ) { + // --- Template Logic --- + const [selectedTemplateId, setSelectedTemplateId] = useState(''); + + // Set default template + useEffect(() => { + if (!selectedTemplateId && templateSets && Object.keys(templateSets).length > 0) { + const defaultId = Object.keys(templateSets).find(k => k.includes('standard') || k === 'default') || Object.keys(templateSets)[0]; + setSelectedTemplateId(defaultId); + } + }, [templateSets, selectedTemplateId]); + + const reportTemplateId = financials?.meta?.template_id; + + // Determine active template set + const activeTemplateId = (financials && reportTemplateId) ? reportTemplateId : selectedTemplateId; + + const activeTemplateSet = useMemo(() => { + if (!activeTemplateId || !templateSets) return null; + return templateSets[activeTemplateId] || null; + }, [activeTemplateId, templateSets]); + + // Derive effective analysis config from template set, falling back to global config if needed + const activeAnalysisConfig = useMemo(() => { + if (activeTemplateSet) { + return { analysis_modules: activeTemplateSet.modules }; + } + return financialConfig; // Fallback to global config (legacy behavior) + }, [activeTemplateSet, financialConfig]); + // 分析类型列表 const analysisTypes = useMemo(() => { - if (!analysisConfig?.analysis_modules) return []; - return Object.keys(analysisConfig.analysis_modules); - }, [analysisConfig]); + if (!activeAnalysisConfig?.analysis_modules) return []; + return Object.keys(activeAnalysisConfig.analysis_modules); + }, [activeAnalysisConfig]); // 分析状态管理 const [analysisStates, setAnalysisStates] = useState>({}); @@ -109,7 +139,7 @@ export function useAnalysisRunner( }, [startTime]); const retryAnalysis = async (analysisType: string) => { - if (!financials || !analysisConfig?.analysis_modules) { + if (!financials || !activeAnalysisConfig?.analysis_modules) { return; } analysisFetchedRefs.current[analysisType] = false; @@ -119,7 +149,7 @@ export function useAnalysisRunner( })); setAnalysisRecords(prev => prev.filter(record => record.type !== analysisType)); const analysisName = - analysisConfig.analysis_modules[analysisType]?.name || analysisType; + activeAnalysisConfig.analysis_modules[analysisType]?.name || analysisType; const startTimeISO = new Date().toISOString(); setCurrentAnalysisTask(analysisType); setAnalysisRecords(prev => [...prev, { @@ -186,6 +216,7 @@ export function useAnalysisRunner( setAnalysisStates(prev => ({ ...prev, [analysisType]: { + ...prev[analysisType], content: '', loading: false, error: errorMessage @@ -208,7 +239,7 @@ export function useAnalysisRunner( }; useEffect(() => { - if (isLoading || error || !financials || !analysisConfig?.analysis_modules || analysisTypes.length === 0) { + if (isLoading || error || !financials || !activeAnalysisConfig?.analysis_modules || analysisTypes.length === 0) { return; } if (isAnalysisRunningRef.current) { @@ -231,13 +262,13 @@ export function useAnalysisRunner( if (analysisFetchedRefs.current[analysisType]) { continue; } - if (!analysisFetchedRefs.current || !analysisConfig?.analysis_modules) { + if (!analysisFetchedRefs.current || !activeAnalysisConfig?.analysis_modules) { console.error("分析配置或refs未初始化,无法进行分析。"); continue; } currentAnalysisTypeRef.current = analysisType; const analysisName = - analysisConfig.analysis_modules[analysisType]?.name || analysisType; + activeAnalysisConfig.analysis_modules[analysisType]?.name || analysisType; const startTimeISO = new Date().toISOString(); setCurrentAnalysisTask(analysisType); setAnalysisRecords(prev => { @@ -360,7 +391,7 @@ export function useAnalysisRunner( } }; runAnalysesSequentially(); - }, [isLoading, error, financials, analysisConfig, analysisTypes, normalizedMarket, unifiedSymbol, startTime, manualRunKey]); + }, [isLoading, error, financials, activeAnalysisConfig, analysisTypes, normalizedMarket, unifiedSymbol, startTime, manualRunKey]); const stopAll = () => { stopRequestedRef.current = true; @@ -382,11 +413,12 @@ export function useAnalysisRunner( }; const triggerAnalysis = async () => { - const reqId = await triggerAnalysisRequest(unifiedSymbol, normalizedMarket || ''); + const reqId = await triggerAnalysisRequest(unifiedSymbol, normalizedMarket || '', selectedTemplateId); if (reqId) setRequestId(reqId); }; return { + activeAnalysisConfig, // Exported analysisTypes, analysisStates, analysisRecords, @@ -404,11 +436,8 @@ export function useAnalysisRunner( continuePending, retryAnalysis, hasRunningTask, - isAnalysisRunning: isAnalysisRunningRef.current, // 注意:这里返回的是 ref.current,可能不是响应式的。通常需要 state。 - // 但原代码中 isAnalysisRunningRef 仅用于内部逻辑控制,没有直接用于 UI 展示(除了 disabled 状态,但这通常依赖 state 变化触发重渲染)。 - // 在原 UI 中 `disabled={isAnalysisRunningRef.current}` 可能会有问题,如果仅仅 Ref 变了组件不一定会刷新。 - // 不过原代码就是这样写的。 - // 我建议可以用 hasRunningTask 来作为替代。 + isAnalysisRunning: isAnalysisRunningRef.current, + selectedTemplateId, // Exported + setSelectedTemplateId, // Exported }; } - diff --git a/frontend/src/app/report/[symbol]/page.tsx b/frontend/src/app/report/[symbol]/page.tsx index 666b085..0a8819b 100644 --- a/frontend/src/app/report/[symbol]/page.tsx +++ b/frontend/src/app/report/[symbol]/page.tsx @@ -134,3 +134,5 @@ export default function ReportPage() { ); } + + diff --git a/frontend/src/components/ui/command.tsx b/frontend/src/components/ui/command.tsx new file mode 100644 index 0000000..169359e --- /dev/null +++ b/frontend/src/components/ui/command.tsx @@ -0,0 +1,160 @@ +"use client" + +import * as React from "react" +import { Command as CommandPrimitive } from "cmdk" +import { SearchIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Command({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandDialog({ + title = "Command Palette", + description = "Search for a command to run...", + children, + ...props +}: React.ComponentProps & { + title?: string + description?: string +}) { + return ( + + {children} + + ) +} + +function CommandInput({ + className, + ...props +}: React.ComponentProps) { + return ( +
+ + +
+ ) +} + +function CommandList({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandEmpty({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandGroup({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} + diff --git a/frontend/src/components/ui/popover.tsx b/frontend/src/components/ui/popover.tsx new file mode 100644 index 0000000..96522c4 --- /dev/null +++ b/frontend/src/components/ui/popover.tsx @@ -0,0 +1,49 @@ +"use client" + +import * as React from "react" +import * as PopoverPrimitive from "@radix-ui/react-popover" + +import { cn } from "@/lib/utils" + +function Popover({ + ...props +}: React.ComponentProps) { + return +} + +function PopoverTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function PopoverContent({ + className, + align = "center", + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function PopoverAnchor({ + ...props +}: React.ComponentProps) { + return +} + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } + diff --git a/frontend/src/hooks/useApi.ts b/frontend/src/hooks/useApi.ts index da844df..ba8e03d 100644 --- a/frontend/src/hooks/useApi.ts +++ b/frontend/src/hooks/useApi.ts @@ -3,15 +3,13 @@ import { BatchFinancialDataResponse, TodaySnapshotResponse, RealTimeQuoteResponse, - AnalysisConfigResponse, LlmProvidersConfig, - AnalysisModulesConfig, AnalysisTemplateSets, // New type FinancialConfigResponse, DataSourcesConfig, + AnalysisResultDto, } from "@/types"; import { useEffect, useState } from "react"; -// Execution-step types not used currently; keep API minimal and explicit import { useConfigStore } from "@/stores/useConfigStore"; import type { SystemConfig } from "@/stores/useConfigStore"; @@ -24,7 +22,7 @@ export function useDataRequest() { const [isMutating, setIsMutating] = useState(false); const [error, setError] = useState(null); - const trigger = async (symbol: string, market: string): Promise => { + const trigger = async (symbol: string, market: string, templateId?: string): Promise => { setIsMutating(true); setError(null); try { @@ -33,7 +31,7 @@ export function useDataRequest() { headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ symbol, market }), + body: JSON.stringify({ symbol, market, template_id: templateId }), }); if (!res.ok) { @@ -79,6 +77,19 @@ export function useTaskProgress(requestId: string | null, options?: SWRConfigura }; } +// --- Analysis Results Hooks (NEW) --- + +export function useAnalysisResults(symbol?: string) { + return useSWR( + symbol ? `/api/analysis-results?symbol=${encodeURIComponent(symbol)}` : null, + fetcher, + { + refreshInterval: 5000, // Poll for new results + } + ); +} + + // --- 保留的旧Hooks (用于查询最终数据) --- export function useCompanyProfile(symbol?: string, market?: string) { @@ -139,44 +150,6 @@ export function useFinancials(market?: string, stockCode?: string, years: number ); } -export function useAnalysisConfig() { - return useSWR('/api/configs/analysis_modules', fetcher); -} - -export async function updateAnalysisConfig(config: AnalysisConfigResponse) { - const res = await fetch('/api/configs/analysis_modules', { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(config), - }); - if (!res.ok) throw new Error(await res.text()); - return res.json(); -} - -export async function generateFullAnalysis(tsCode: string, companyName: string) { - const url = `/api/financials/china/${encodeURIComponent(tsCode)}/analysis?company_name=${encodeURIComponent(companyName)}`; - const res = await fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - }); - - const text = await res.text(); - if (!res.ok) { - try { - const errorJson = JSON.parse(text); - throw new Error(errorJson.detail || text); - } catch { - throw new Error(text || `Request failed: ${res.status}`); - } - } - - try { - return JSON.parse(text); - } catch { - throw new Error('Invalid JSON response from server.'); - } -} - export function useChinaSnapshot(ts_code?: string) { return useSWR( ts_code ? `/api/financials/china/${encodeURIComponent(ts_code)}/snapshot` : null, @@ -397,26 +370,6 @@ export async function updateAnalysisTemplateSets(payload: AnalysisTemplateSets) return res.json() as Promise; } - -// --- Analysis Modules Config Hooks (OLD - DEPRECATED) --- - -export function useAnalysisModules() { - return useSWR('/api/configs/analysis_modules', fetcher); -} - -export async function updateAnalysisModules(payload: AnalysisModulesConfig) { - const res = await fetch('/api/configs/analysis_modules', { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload), - }); - if (!res.ok) { - const text = await res.text().catch(() => ''); - throw new Error(text || `HTTP ${res.status}`); - } - return res.json() as Promise; -} - // --- Data Sources Config Hooks --- export function useDataSourcesConfig() { diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 1cc5f79..8fd25ed 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -102,6 +102,7 @@ export interface FinancialMeta { api_calls_by_group: Record; current_action?: string | null; steps: StepRecord[]; + template_id?: string | null; } export interface BatchFinancialDataResponse { @@ -183,6 +184,21 @@ export interface AnalysisResponse { error?: string; } +/** + * 分析结果 DTO (Analysis Result Data Transfer Object) + * Corresponds to backend AnalysisResultDto + */ +export interface AnalysisResultDto { + id: number; + request_id: string; // UUID + symbol: string; + template_id: string; + module_id: string; + content: string; + meta_data: any; // JSON + created_at: string; // ISO8601 +} + /** * 分析配置响应接口 */ diff --git a/package-lock.json b/package-lock.json index a2b202d..1c24983 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,10 +5,559 @@ "packages": { "": { "dependencies": { + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-popover": "^1.1.15", + "cmdk": "^1.1.1", "immer": "^10.2.0", "zustand": "^5.0.8" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "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-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "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-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "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-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "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-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "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-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "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-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "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-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@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-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "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-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "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-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "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-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "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-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cmdk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/immer": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", @@ -19,6 +568,154 @@ "url": "https://opencollective.com/immer" } }, + "node_modules/react": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT", + "peer": true + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/zustand": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz", diff --git a/package.json b/package.json index 49b6d89..6918575 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,8 @@ { "dependencies": { + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-popover": "^1.1.15", + "cmdk": "^1.1.1", "immer": "^10.2.0", "zustand": "^5.0.8" } diff --git a/services/api-gateway/src/api.rs b/services/api-gateway/src/api.rs index 0644273..20c8dc0 100644 --- a/services/api-gateway/src/api.rs +++ b/services/api-gateway/src/api.rs @@ -23,6 +23,7 @@ const ANALYSIS_COMMANDS_QUEUE: &str = "analysis.commands.generate_report"; pub struct DataRequest { pub symbol: String, pub market: String, + pub template_id: Option, } #[derive(Serialize)] @@ -102,7 +103,7 @@ async fn trigger_data_fetch( let request_id = Uuid::new_v4(); let command = FetchCompanyDataCommand { request_id, - symbol: payload.symbol, + symbol: payload.symbol.clone(), market: payload.market, }; @@ -116,6 +117,23 @@ async fn trigger_data_fetch( ) .await?; + // If a template_id is provided, trigger the analysis generation workflow as well. + if let Some(template_id) = payload.template_id { + let analysis_command = GenerateReportCommand { + request_id, + symbol: payload.symbol, + template_id, + }; + info!(request_id = %request_id, template_id = %analysis_command.template_id, "Publishing analysis generation command (auto-triggered)"); + state + .nats_client + .publish( + ANALYSIS_COMMANDS_QUEUE.to_string(), + serde_json::to_vec(&analysis_command).unwrap().into(), + ) + .await?; + } + Ok(( StatusCode::ACCEPTED, Json(RequestAcceptedResponse { request_id }), diff --git a/services/common-contracts/src/lib.rs b/services/common-contracts/src/lib.rs index 14d50cc..e9050a4 100644 --- a/services/common-contracts/src/lib.rs +++ b/services/common-contracts/src/lib.rs @@ -3,5 +3,6 @@ pub mod models; pub mod observability; pub mod messages; pub mod config_models; +pub mod provider; diff --git a/services/common-contracts/src/provider.rs b/services/common-contracts/src/provider.rs new file mode 100644 index 0000000..df186e1 --- /dev/null +++ b/services/common-contracts/src/provider.rs @@ -0,0 +1,8 @@ +pub trait DataProvider { + /// Returns the name of the provider (e.g., "tushare", "yfinance") + fn name(&self) -> &str; + + /// Returns whether the provider is enabled in the configuration + fn is_enabled(&self) -> bool; +} + diff --git a/services/finnhub-provider-service/src/finnhub.rs b/services/finnhub-provider-service/src/finnhub.rs index 2382340..c6691d6 100644 --- a/services/finnhub-provider-service/src/finnhub.rs +++ b/services/finnhub-provider-service/src/finnhub.rs @@ -6,6 +6,7 @@ use crate::{ mapping::{map_financial_dtos, map_profile_dto}, }; use common_contracts::dtos::{CompanyProfileDto, TimeSeriesFinancialDto}; +use common_contracts::provider::DataProvider; use tokio; #[derive(Debug, Deserialize, Clone)] @@ -60,6 +61,16 @@ pub struct FinnhubDataProvider { client: FinnhubClient, } +impl DataProvider for FinnhubDataProvider { + fn name(&self) -> &str { + "finnhub" + } + + fn is_enabled(&self) -> bool { + true + } +} + impl FinnhubDataProvider { pub fn new(api_url: String, api_token: String) -> Self { Self { diff --git a/services/tushare-provider-service/src/tushare.rs b/services/tushare-provider-service/src/tushare.rs index 7c0adad..662930d 100644 --- a/services/tushare-provider-service/src/tushare.rs +++ b/services/tushare-provider-service/src/tushare.rs @@ -9,12 +9,23 @@ use crate::{ ts_client::TushareClient, }; use common_contracts::dtos::{CompanyProfileDto, TimeSeriesFinancialDto}; +use common_contracts::provider::DataProvider; #[derive(Clone)] pub struct TushareDataProvider { client: TushareClient, } +impl DataProvider for TushareDataProvider { + fn name(&self) -> &str { + "tushare" + } + + fn is_enabled(&self) -> bool { + true + } +} + impl TushareDataProvider { pub fn new(api_url: String, api_token: String) -> Self { Self {