diff --git a/backend/routes/ai_analysis.py b/backend/routes/ai_analysis.py index 8d9dec7..a6070c3 100644 --- a/backend/routes/ai_analysis.py +++ b/backend/routes/ai_analysis.py @@ -52,6 +52,22 @@ def _can_trigger(trade_date: str) -> tuple[bool, str]: conn.close() +@router.get("/ai-analysis/latest", summary="最新 AI 分析报告") +async def get_latest_report(): + conn = get_connection() + try: + row = conn.execute( + "SELECT * FROM ai_reports ORDER BY trade_date DESC, id DESC LIMIT 1" + ).fetchone() + if not row: + raise HTTPException(status_code=404, detail="暂无分析报告") + item = dict_from_row(row) + item["toolsUsed"] = json.loads(item.pop("tools_used") or "[]") + return JSONResponse({"data": item}, headers=_NO_CACHE_HEADERS) + finally: + conn.close() + + @router.get("/ai-analysis", summary="AI 分析报告列表") async def list_reports(): conn = get_connection() diff --git a/index.html b/index.html index 12239a8..f0a525f 100755 --- a/index.html +++ b/index.html @@ -24,14 +24,14 @@ diff --git a/package.json b/package.json index 95cb844..2d05e12 100755 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "react-markdown": "^10.1.0", "react-resizable-panels": "^4.12.1", "recharts": "^2.15.4", + "remark-gfm": "^4.0.1", "sonner": "^2.0.7", "tailwind-merge": "^3.6.0", "tailwindcss": "^4.3.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 43e9e2e..2972884 100755 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -155,6 +155,9 @@ importers: recharts: specifier: ^2.15.4 version: 2.15.4(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + remark-gfm: + specifier: ^4.0.1 + version: 4.0.1 sonner: specifier: ^2.0.7 version: 2.0.8(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -1893,6 +1896,10 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + estree-util-is-identifier-name@3.0.0: resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} @@ -2137,14 +2144,38 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + marked@16.3.0: resolution: {integrity: sha512-K3UxuKu6l6bmA5FUwYho8CfJBlsUWAooKtdGgMcERSpF7gcBUrCGsLH7wDaaNOzwq18JzSUDyoEb/YsrqMac3w==} engines: {node: '>= 20'} hasBin: true + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + mdast-util-from-markdown@2.0.3: resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + mdast-util-mdx-expression@2.0.1: resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} @@ -2172,6 +2203,27 @@ packages: micromark-core-commonmark@2.0.0: resolution: {integrity: sha512-jThOz/pVmAYUtkroV3D5c1osFXAMv9e0ypGDOIZuCeAe91/sD6BoE2Sjzt30yuXtwOYUmySOhMas/PVyh02itA==} + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + micromark-factory-destination@2.0.0: resolution: {integrity: sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==} @@ -2393,12 +2445,18 @@ packages: react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + remark-parse@11.0.0: resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} remark-rehype@11.1.2: resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + robust-predicates@3.0.3: resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} @@ -4378,6 +4436,8 @@ snapshots: escalade@3.2.0: {} + escape-string-regexp@5.0.0: {} + estree-util-is-identifier-name@3.0.0: {} eventemitter3@4.0.1: {} @@ -4567,8 +4627,17 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + markdown-table@3.0.4: {} + marked@16.3.0: {} + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + mdast-util-from-markdown@2.0.3: dependencies: '@types/mdast': 4.0.4 @@ -4586,6 +4655,63 @@ snapshots: transitivePeerDependencies: - supports-color + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.0.0 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.0.0 + micromark-util-normalize-identifier: 2.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.3 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.0.0 + transitivePeerDependencies: + - supports-color + mdast-util-mdx-expression@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 @@ -4701,6 +4827,64 @@ snapshots: micromark-util-symbol: 2.0.0 micromark-util-types: 2.0.0 + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.0.0 + micromark-util-sanitize-uri: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.0 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.0.0 + micromark-util-normalize-identifier: 2.0.0 + micromark-util-sanitize-uri: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.0 + micromark-util-classify-character: 2.0.0 + micromark-util-resolve-all: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.0 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.0 + micromark-util-types: 2.0.0 + micromark-factory-destination@2.0.0: dependencies: micromark-util-character: 2.0.0 @@ -4983,6 +5167,17 @@ snapshots: tiny-invariant: 1.3.3 victory-vendor: 36.9.2 + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.0 + transitivePeerDependencies: + - supports-color + remark-parse@11.0.0: dependencies: '@types/mdast': 4.0.4 @@ -5000,6 +5195,12 @@ snapshots: unified: 11.0.0 vfile: 6.0.3 + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.0.0 + unified: 11.0.0 + robust-predicates@3.0.3: {} rollup@4.62.2: diff --git a/src/components/Mermaid.tsx b/src/components/Mermaid.tsx index b7480bf..e3e1996 100644 --- a/src/components/Mermaid.tsx +++ b/src/components/Mermaid.tsx @@ -3,14 +3,14 @@ import mermaid from "mermaid"; mermaid.initialize({ startOnLoad: false, - theme: "dark", + theme: "default", themeVariables: { - primaryColor: "#21262d", - primaryTextColor: "#e6edf3", - primaryBorderColor: "#30363d", - lineColor: "#58a6ff", - secondaryColor: "#161b22", - tertiaryColor: "#0d1117", + primaryColor: "#f0f0f0", + primaryTextColor: "#333333", + primaryBorderColor: "#cccccc", + lineColor: "#3366cc", + secondaryColor: "#e8e8e8", + tertiaryColor: "#ffffff", fontFamily: "inherit", fontSize: "14px", }, @@ -37,7 +37,7 @@ export function Mermaid({ chart }: MermaidProps) { if (error) { return ( -
+{error}); @@ -46,7 +46,7 @@ export function Mermaid({ chart }: MermaidProps) { return ( ); diff --git a/src/components/ThemeToggle.tsx b/src/components/ThemeToggle.tsx new file mode 100644 index 0000000..11e139c --- /dev/null +++ b/src/components/ThemeToggle.tsx @@ -0,0 +1,58 @@ +import { useState, useRef, useEffect } from "react"; +import { Sun, Moon, Monitor } from "lucide-react"; +import { useTheme } from "../lib/use-theme"; + +export function ThemeToggle() { + const { theme, setTheme } = useTheme(); + const [open, setOpen] = useState(false); + const ref = useRef(null); + + useEffect(() => { + if (!open) return; + const handler = (e: MouseEvent) => { + if (ref.current && !ref.current.contains(e.target as Node)) { + setOpen(false); + } + }; + document.addEventListener("mousedown", handler); + return () => document.removeEventListener("mousedown", handler); + }, [open]); + + const iconMap = { light: Sun, dark: Moon, system: Monitor }; + const CurrentIcon = iconMap[theme]; + + return ( + + + + {open && ( ++ ); +} diff --git a/src/lib/ai-analysis-api.ts b/src/lib/ai-analysis-api.ts index 10164af..4abb672 100644 --- a/src/lib/ai-analysis-api.ts +++ b/src/lib/ai-analysis-api.ts @@ -17,6 +17,13 @@ export interface AiReport { created_at: string; } +export async function fetchAiLatestReport(): Promise+ {([ + { value: "light" as const, icon: Sun, label: "浅色" }, + { value: "dark" as const, icon: Moon, label: "深色" }, + { value: "system" as const, icon: Monitor, label: "自动" }, + ]).map(({ value, icon: Icon, label }) => ( + + ))} ++ )} +{ + const resp = await fetch(`${API_BASE}/api/ai-analysis/latest`); + if (!resp.ok) throw new Error(`请求失败 (${resp.status})`); + const result = await resp.json(); + return result.data; +} + export async function fetchAiReports(): Promise { const resp = await fetch(`${API_BASE}/api/ai-analysis`); if (!resp.ok) throw new Error(`请求失败 (${resp.status})`); diff --git a/src/lib/use-theme.ts b/src/lib/use-theme.ts new file mode 100644 index 0000000..fb67525 --- /dev/null +++ b/src/lib/use-theme.ts @@ -0,0 +1,56 @@ +import { useState, useEffect, useCallback } from "react"; + +type Theme = "light" | "dark" | "system"; + +function getSystemTheme(): "light" | "dark" { + if (typeof window === "undefined") return "light"; + return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; +} + +function getResolvedTheme(theme: Theme): "light" | "dark" { + return theme === "system" ? getSystemTheme() : theme; +} + +function applyTheme(resolved: "light" | "dark") { + const root = document.documentElement; + root.classList.remove("light", "dark"); + root.classList.add(resolved); + root.setAttribute("data-theme", resolved); +} + +export function useTheme() { + const [theme, setThemeState] = useState (() => { + if (typeof window === "undefined") return "system"; + return (localStorage.getItem("theme") as Theme) || "system"; + }); + + const [resolved, setResolved] = useState<"light" | "dark">(() => getResolvedTheme(theme)); + + const setTheme = useCallback((newTheme: Theme) => { + setThemeState(newTheme); + localStorage.setItem("theme", newTheme); + const r = getResolvedTheme(newTheme); + setResolved(r); + applyTheme(r); + }, []); + + // 初始应用 + useEffect(() => { + applyTheme(resolved); + }, []); + + // 监听系统主题变化 + useEffect(() => { + if (theme !== "system") return; + const mq = window.matchMedia("(prefers-color-scheme: dark)"); + const handler = () => { + const r = getSystemTheme(); + setResolved(r); + applyTheme(r); + }; + mq.addEventListener("change", handler); + return () => mq.removeEventListener("change", handler); + }, [theme]); + + return { theme, resolved, setTheme }; +} diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 56f50ef..1133c03 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -19,8 +19,6 @@ import { Route as IndexRouteImport } from './routes/index' import { Route as ThemeCodeRouteImport } from './routes/theme.$code' import { Route as StockCodeRouteImport } from './routes/stock.$code' import { Route as ShareCodeRouteImport } from './routes/share.$code' -import { Route as AiAnalysisIndexRouteImport } from './routes/ai-analysis._index' -import { Route as AiAnalysisIdRouteImport } from './routes/ai-analysis.$id' const ThemesRoute = ThemesRouteImport.update({ id: '/themes', @@ -72,38 +70,27 @@ const ShareCodeRoute = ShareCodeRouteImport.update({ path: '/share/$code', getParentRoute: () => rootRouteImport, } as any) -const AiAnalysisIndexRoute = AiAnalysisIndexRouteImport.update({ - id: '/_index', - getParentRoute: () => AiAnalysisRoute, -} as any) -const AiAnalysisIdRoute = AiAnalysisIdRouteImport.update({ - id: '/$id', - path: '/$id', - getParentRoute: () => AiAnalysisRoute, -} as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute - '/ai-analysis': typeof AiAnalysisRouteWithChildren + '/ai-analysis': typeof AiAnalysisRoute '/core-stocks': typeof CoreStocksRoute '/dashboard': typeof DashboardRoute '/hot-map': typeof HotMapRoute '/theme-history': typeof ThemeHistoryRoute '/themes': typeof ThemesRoute - '/ai-analysis/$id': typeof AiAnalysisIdRoute '/share/$code': typeof ShareCodeRoute '/stock/$code': typeof StockCodeRoute '/theme/$code': typeof ThemeCodeRoute } export interface FileRoutesByTo { '/': typeof IndexRoute - '/ai-analysis': typeof AiAnalysisRouteWithChildren + '/ai-analysis': typeof AiAnalysisRoute '/core-stocks': typeof CoreStocksRoute '/dashboard': typeof DashboardRoute '/hot-map': typeof HotMapRoute '/theme-history': typeof ThemeHistoryRoute '/themes': typeof ThemesRoute - '/ai-analysis/$id': typeof AiAnalysisIdRoute '/share/$code': typeof ShareCodeRoute '/stock/$code': typeof StockCodeRoute '/theme/$code': typeof ThemeCodeRoute @@ -111,14 +98,12 @@ export interface FileRoutesByTo { export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute - '/ai-analysis': typeof AiAnalysisRouteWithChildren + '/ai-analysis': typeof AiAnalysisRoute '/core-stocks': typeof CoreStocksRoute '/dashboard': typeof DashboardRoute '/hot-map': typeof HotMapRoute '/theme-history': typeof ThemeHistoryRoute '/themes': typeof ThemesRoute - '/ai-analysis/$id': typeof AiAnalysisIdRoute - '/ai-analysis/_index': typeof AiAnalysisIndexRoute '/share/$code': typeof ShareCodeRoute '/stock/$code': typeof StockCodeRoute '/theme/$code': typeof ThemeCodeRoute @@ -133,7 +118,6 @@ export interface FileRouteTypes { | '/hot-map' | '/theme-history' | '/themes' - | '/ai-analysis/$id' | '/share/$code' | '/stock/$code' | '/theme/$code' @@ -146,7 +130,6 @@ export interface FileRouteTypes { | '/hot-map' | '/theme-history' | '/themes' - | '/ai-analysis/$id' | '/share/$code' | '/stock/$code' | '/theme/$code' @@ -159,8 +142,6 @@ export interface FileRouteTypes { | '/hot-map' | '/theme-history' | '/themes' - | '/ai-analysis/$id' - | '/ai-analysis/_index' | '/share/$code' | '/stock/$code' | '/theme/$code' @@ -168,7 +149,7 @@ export interface FileRouteTypes { } export interface RootRouteChildren { IndexRoute: typeof IndexRoute - AiAnalysisRoute: typeof AiAnalysisRouteWithChildren + AiAnalysisRoute: typeof AiAnalysisRoute CoreStocksRoute: typeof CoreStocksRoute DashboardRoute: typeof DashboardRoute HotMapRoute: typeof HotMapRoute @@ -251,40 +232,12 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ShareCodeRouteImport parentRoute: typeof rootRouteImport } - '/ai-analysis/_index': { - id: '/ai-analysis/_index' - path: '' - fullPath: '/ai-analysis' - preLoaderRoute: typeof AiAnalysisIndexRouteImport - parentRoute: typeof AiAnalysisRoute - } - '/ai-analysis/$id': { - id: '/ai-analysis/$id' - path: '/$id' - fullPath: '/ai-analysis/$id' - preLoaderRoute: typeof AiAnalysisIdRouteImport - parentRoute: typeof AiAnalysisRoute - } } } -interface AiAnalysisRouteChildren { - AiAnalysisIdRoute: typeof AiAnalysisIdRoute - AiAnalysisIndexRoute: typeof AiAnalysisIndexRoute -} - -const AiAnalysisRouteChildren: AiAnalysisRouteChildren = { - AiAnalysisIdRoute: AiAnalysisIdRoute, - AiAnalysisIndexRoute: AiAnalysisIndexRoute, -} - -const AiAnalysisRouteWithChildren = AiAnalysisRoute._addFileChildren( - AiAnalysisRouteChildren, -) - const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, - AiAnalysisRoute: AiAnalysisRouteWithChildren, + AiAnalysisRoute: AiAnalysisRoute, CoreStocksRoute: CoreStocksRoute, DashboardRoute: DashboardRoute, HotMapRoute: HotMapRoute, diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 0be30f3..bf75026 100755 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -1,6 +1,7 @@ import * as React from 'react' import { Outlet, createRootRoute } from '@tanstack/react-router' import { Toaster } from 'sonner' +import { ThemeToggle } from '../components/ThemeToggle' export const Route = createRootRoute({ component: RootComponent, @@ -11,6 +12,9 @@ function RootComponent() { ) } diff --git a/src/routes/ai-analysis.$id.tsx b/src/routes/ai-analysis.$id.tsx deleted file mode 100644 index b8073a3..0000000 --- a/src/routes/ai-analysis.$id.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import * as React from "react"; -import { Link, createFileRoute } from "@tanstack/react-router"; -import { ArrowLeft, BrainCircuit, Clock, Loader2, AlertCircle, RefreshCw, Wrench } from "lucide-react"; -import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import Markdown from "react-markdown"; -import { Mermaid } from "../components/Mermaid"; -import { fetchAiReport, regenerateAiReport } from "../lib/ai-analysis-api"; -import { Button } from "../components/ui/button"; -import { Card, CardContent } from "../components/ui/card"; -import { toast } from "sonner"; - -export const Route = createFileRoute("/ai-analysis/$id")({ - component: AiReportDetailPage, -}); - -function AiReportDetailPage() { - const { id } = Route.useParams(); - const queryClient = useQueryClient(); - const reportId = Number(id); - - const { data: report, isLoading, isError, refetch } = useQuery({ - queryKey: ["ai-report", reportId], - queryFn: () => fetchAiReport(reportId), - staleTime: 60_000, - retry: false, - }); - - const regenerateMutation = useMutation({ - mutationFn: () => regenerateAiReport(reportId), - onSuccess: () => { - toast.success("重新生成完成"); - queryClient.invalidateQueries({ queryKey: ["ai-report", reportId] }); - queryClient.invalidateQueries({ queryKey: ["ai-reports"] }); - }, - onError: (err: Error) => { - toast.error(err.message || "重新生成失败"); - }, - }); - - return ( - + ++ -- ); -} diff --git a/src/routes/ai-analysis._index.tsx b/src/routes/ai-analysis._index.tsx deleted file mode 100644 index 1dc63c6..0000000 --- a/src/routes/ai-analysis._index.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import * as React from "react"; -import { Link, createFileRoute } from "@tanstack/react-router"; -import { Clock, Zap, Loader2, AlertCircle, BrainCircuit } from "lucide-react"; -import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { fetchAiReports, triggerAiAnalysis } from "../lib/ai-analysis-api"; -import { Button } from "../components/ui/button"; -import { Card, CardContent } from "../components/ui/card"; -import { toast } from "sonner"; - -export const Route = createFileRoute("/ai-analysis/_index")({ - component: AiAnalysisIndex, -}); - -function AiAnalysisIndex() { - const queryClient = useQueryClient(); - - const { data: reports, isLoading, isError, refetch } = useQuery({ - queryKey: ["ai-reports"], - queryFn: fetchAiReports, - staleTime: 30_000, - retry: false, - }); - - const triggerMutation = useMutation({ - mutationFn: triggerAiAnalysis, - onSuccess: () => { - toast.success("AI 分析已开始,请稍候..."); - queryClient.invalidateQueries({ queryKey: ["ai-reports"] }); - }, - onError: (err: Error) => { - toast.error(err.message || "触发失败"); - }, - }); - - return ( - <> -- {/* Header */} ---- - {/* Content */} - {isLoading ? ( -- -- -- - -
-- {report?.title || "AI 分析报告"} - -- ) : isError ? ( -- 加载中...
--- ) : report ? ( - <> - {/* Meta Info */} -- 报告加载失败
- -- - - {/* Report Content */} -- -- --- {report.created_at} - - {report.model} - {report.tokens_used?.toLocaleString()} tokens - {report.toolsUsed && report.toolsUsed.length > 0 && ( - - - {report.toolsUsed.length} 个工具 - - )} - - - - {/* Disclaimer */} -- -; - } - return ( - - {children} -- ); - }, - }} - > - {report.content} - -- 本报告由 AI 生成,仅供参考,不构成投资建议 -
- > - ) : null} -- -- - {isLoading ? ( --- ) : isError ? ( -- 加载中...
--- ) : reports && reports.length > 0 ? ( -- 数据加载失败
- -- {reports.map((report) => ( -- ) : ( -- - ))} -- ----- - 查看 → - -{report.title}
- {report.summary && ( -{report.summary}
- )} -- --- {report.created_at} - - {report.model} - {report.tokens_used?.toLocaleString()} tokens - -- )} - > - ); -} diff --git a/src/routes/ai-analysis.tsx b/src/routes/ai-analysis.tsx index c9c0013..1acd9af 100644 --- a/src/routes/ai-analysis.tsx +++ b/src/routes/ai-analysis.tsx @@ -1,28 +1,140 @@ import * as React from "react"; -import { Link, Outlet, createFileRoute } from "@tanstack/react-router"; -import { ArrowLeft, BrainCircuit } from "lucide-react"; +import { Link, createFileRoute } from "@tanstack/react-router"; +import { ArrowLeft, Clock, Loader2, AlertCircle, RefreshCw, Wrench, Zap, Calendar } from "lucide-react"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import Markdown from "react-markdown"; +import remarkGfm from "remark-gfm"; +import { Mermaid } from "../components/Mermaid"; +import { fetchAiLatestReport, triggerAiAnalysis, fetchAiReport } from "../lib/ai-analysis-api"; +import { Button } from "../components/ui/button"; +import { Card, CardContent } from "../components/ui/card"; +import { toast } from "sonner"; export const Route = createFileRoute("/ai-analysis")({ - component: AiAnalysisLayout, + component: AiAnalysisPage, }); -function AiAnalysisLayout() { +function AiAnalysisPage() { + const queryClient = useQueryClient(); + + const { data: report, isLoading, isError, refetch } = useQuery({ + queryKey: ["ai-report-latest"], + queryFn: fetchAiLatestReport, + retry: false, + }); + + const triggerMutation = useMutation({ + mutationFn: triggerAiAnalysis, + onSuccess: () => { + toast.success("AI 分析已开始,请稍候..."); + queryClient.invalidateQueries({ queryKey: ["ai-report-latest"] }); + }, + onError: (err: Error) => { + toast.error(err.message || "触发失败"); + }, + }); + return ( -- 暂无分析报告
-点击"立即分析"生成今日报告
--++); diff --git a/src/routes/dashboard.tsx b/src/routes/dashboard.tsx index f53e69a..6b63a0e 100644 --- a/src/routes/dashboard.tsx +++ b/src/routes/dashboard.tsx @@ -40,17 +40,17 @@ function DashboardPage() { }, [autoRefresh, refetch]); return ( -{/* Header */} -- -+ ); + }, + code({ className, children, ...props }) { + const match = /language-(\w+)/.exec(className || ""); + if (match && match[1] === "mermaid") { + return- - -
+- AI 市场分析 - +- {/* Child routes render here */} -+ +++ + +
++ AI 收盘分析 + + {/* Content */} + {isLoading ? ( + ++ ) : isError || !report ? ( ++ 加载中...
+++ ) : ( + <> + {/* Meta Info */} ++ 暂无分析报告
+点击"立即分析"生成今日报告
++ + + {/* Report Content */} ++ ++ +++ {report.created_at} + + {report.model} + {report.tokens_used?.toLocaleString()} tokens + {report.toolsUsed && report.toolsUsed.length > 0 && ( + + + {report.toolsUsed.length} 个工具 + + )} + + + + {children}
+; + } + return ( + + {children} ++ ); + }, + }} + > + {report.content} + + + + + {/* Disclaimer */} ++ 本报告由 AI 生成,仅供参考,不构成投资建议 +
+ > + )}+{/* ── 顶栏 ── */} -+ - +-市场看板
+{data?.updateTime && ( 行情时间 {data.updateTime} )} @@ -59,13 +59,13 @@ function DashboardPage() { type="checkbox" checked={autoRefresh} onChange={(e) => setAutoRefresh(e.target.checked)} - className="w-3 h-3 rounded border-[#30363d] bg-[#161b22] accent-[#58a6ff]" + className="w-3 h-3 rounded border-border bg-muted accent-primary" /> 自动刷新