feat: AI分析第三档——海外期货章节、报告分卡片渲染、红绿着色、期号;LLM改流式+分段生成绕开网关120s超时
This commit is contained in:
+109
-35
@@ -4,6 +4,7 @@ import { ArrowLeft, Clock, Loader2, AlertCircle, Wrench, Calendar } from "lucide
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import Markdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import type { Components } from "react-markdown";
|
||||
import { Mermaid } from "../components/Mermaid";
|
||||
import { fetchAiLatestReport } from "../lib/ai-analysis-api";
|
||||
import { Card, CardContent } from "../components/ui/card";
|
||||
@@ -12,6 +13,51 @@ export const Route = createFileRoute("/ai-analysis")({
|
||||
component: AiAnalysisPage,
|
||||
});
|
||||
|
||||
/** A股惯例红涨绿跌:只给带显式 +/- 号的数字着色(+2.3% / -1.2亿 / +56万...),无符号数字语义不明保持默认 */
|
||||
const SIGNED_NUM_RE = /([+-]\d+(?:\.\d+)?(?:%|亿|万亿|万)?)/g;
|
||||
|
||||
function colorizeText(text: string): React.ReactNode[] {
|
||||
return text.split(SIGNED_NUM_RE).map((part, i) => {
|
||||
if (part.startsWith("+") || part.startsWith("-")) {
|
||||
const val = parseFloat(part);
|
||||
if (!Number.isNaN(val) && val !== 0) {
|
||||
if (val > 0) return <span key={i} className="text-red-500">{part}</span>;
|
||||
return <span key={i} className="text-green-600">{part}</span>;
|
||||
}
|
||||
}
|
||||
return <React.Fragment key={i}>{part}</React.Fragment>;
|
||||
});
|
||||
}
|
||||
|
||||
function colorizeChildren(children: React.ReactNode): React.ReactNode {
|
||||
return React.Children.map(children, (child) => {
|
||||
if (typeof child === "string") return colorizeText(child);
|
||||
if (React.isValidElement(child)) {
|
||||
const kids = (child.props as { children?: React.ReactNode }).children;
|
||||
if (kids != null) {
|
||||
return React.cloneElement(
|
||||
child as React.ReactElement<{ children?: React.ReactNode }>,
|
||||
{},
|
||||
colorizeChildren(kids),
|
||||
);
|
||||
}
|
||||
}
|
||||
return child;
|
||||
});
|
||||
}
|
||||
|
||||
/** 按 "## " 二级标题拆分报告为多张卡片;首段(# 标题 + 定调引用块)单独一张 */
|
||||
function splitReport(content: string): { intro: string; sections: { title: string; body: string }[] } {
|
||||
const parts = content.split(/\n(?=## )/);
|
||||
const sections = parts.slice(1).map((p) => {
|
||||
const nl = p.indexOf("\n");
|
||||
const title = (nl === -1 ? p : p.slice(0, nl)).replace(/^##\s*/, "").trim();
|
||||
const body = nl === -1 ? "" : p.slice(nl + 1).trim();
|
||||
return { title, body };
|
||||
});
|
||||
return { intro: parts[0].trim(), sections };
|
||||
}
|
||||
|
||||
function AiAnalysisPage() {
|
||||
const { data: report, isLoading, isError } = useQuery({
|
||||
queryKey: ["ai-report-latest"],
|
||||
@@ -19,6 +65,30 @@ function AiAnalysisPage() {
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const mdComponents: Components = {
|
||||
table({ children, ...props }) {
|
||||
return (
|
||||
<div className="overflow-x-auto -mx-3 sm:mx-0 px-3 sm:px-0">
|
||||
<table {...props}>{children}</table>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
td({ children, ...props }) {
|
||||
return <td {...props}>{colorizeChildren(children)}</td>;
|
||||
},
|
||||
code({ className, children, ...props }) {
|
||||
const match = /language-(\w+)/.exec(className || "");
|
||||
if (match && match[1] === "mermaid") {
|
||||
return <Mermaid chart={String(children).replace(/\n$/, "")} />;
|
||||
}
|
||||
return (
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<div className="max-w-4xl mx-auto px-3 sm:px-4 py-4 sm:py-6">
|
||||
@@ -30,6 +100,9 @@ function AiAnalysisPage() {
|
||||
<h1 className="text-lg sm:text-xl md:text-2xl font-bold flex items-center gap-2">
|
||||
<Calendar className="h-5 w-5 text-primary" />
|
||||
AI 收盘分析
|
||||
{report?.issue_number ? (
|
||||
<span className="text-xs font-normal text-muted-foreground">总第 {report.issue_number} 期</span>
|
||||
) : null}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -81,44 +154,45 @@ function AiAnalysisPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Report Content */}
|
||||
<Card className="bg-card border-border">
|
||||
<CardContent className="p-3 sm:p-4 md:p-6 ai-report-content">
|
||||
<Markdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
table({ children, ...props }) {
|
||||
return (
|
||||
<div className="overflow-x-auto -mx-3 sm:mx-0 px-3 sm:px-0">
|
||||
<table {...props}>{children}</table>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
code({ className, children, ...props }) {
|
||||
const match = /language-(\w+)/.exec(className || "");
|
||||
if (match && match[1] === "mermaid") {
|
||||
return <Mermaid chart={String(children).replace(/\n$/, "")} />;
|
||||
}
|
||||
return (
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
}}
|
||||
>
|
||||
{report.content}
|
||||
</Markdown>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Disclaimer */}
|
||||
<p className="text-xs text-muted-foreground/60 text-center mt-4 sm:mt-6">
|
||||
本报告由 AI 生成,仅供参考,不构成投资建议
|
||||
</p>
|
||||
{/* 报告正文:按 ## 章节分卡片渲染 */}
|
||||
{(() => {
|
||||
const { intro, sections } = splitReport(report.content);
|
||||
return (
|
||||
<div className="space-y-3 sm:space-y-4">
|
||||
{intro && (
|
||||
<Card className="bg-card border-border">
|
||||
<CardContent className="p-3 sm:p-4 md:p-6 ai-report-content">
|
||||
<Markdown remarkPlugins={[remarkGfm]} components={mdComponents}>
|
||||
{intro}
|
||||
</Markdown>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
{sections.map((sec) => (
|
||||
<Card key={sec.title} className="bg-card border-border overflow-hidden">
|
||||
<div className="px-3 sm:px-4 md:px-6 pt-3 sm:pt-4 pb-2 sm:pb-3 border-b border-border/60">
|
||||
<h2 className="text-base sm:text-lg font-semibold">{sec.title}</h2>
|
||||
</div>
|
||||
<CardContent className="p-3 sm:p-4 md:p-6 ai-report-content">
|
||||
<Markdown remarkPlugins={[remarkGfm]} components={mdComponents}>
|
||||
{sec.body}
|
||||
</Markdown>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Disclaimer */}
|
||||
<p className="text-xs text-muted-foreground/60 text-center mt-4 sm:mt-6">
|
||||
本报告由 AI 生成,仅供参考,不构成投资建议
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AiAnalysisPage;
|
||||
|
||||
Reference in New Issue
Block a user