Compare commits
2 Commits
2a5e471ddb
...
30e954d89f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30e954d89f | ||
|
|
66f4914c5a |
@ -236,7 +236,10 @@ class ExportPDFRequest(BaseModel):
|
||||
async def export_chat_pdf(request: ExportPDFRequest, db: AsyncSession = Depends(get_db)):
|
||||
"""Export selected chat sessions to PDF"""
|
||||
from sqlalchemy import select, desc, asc
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
# Timezone definition (Shanghai)
|
||||
shanghai_tz = timezone(timedelta(hours=8))
|
||||
|
||||
if not request.session_ids:
|
||||
raise HTTPException(status_code=400, detail="No sessions selected")
|
||||
@ -260,7 +263,13 @@ async def export_chat_pdf(request: ExportPDFRequest, db: AsyncSession = Depends(
|
||||
|
||||
messages_html = ""
|
||||
for log in logs:
|
||||
timestamp = log.timestamp.strftime('%Y-%m-%d %H:%M:%S') if log.timestamp else ""
|
||||
# Convert timestamp to Shanghai time
|
||||
if log.timestamp:
|
||||
ts_shanghai = log.timestamp.astimezone(shanghai_tz) if log.timestamp.tzinfo else log.timestamp.replace(tzinfo=timezone.utc).astimezone(shanghai_tz)
|
||||
timestamp = ts_shanghai.strftime('%Y-%m-%d %H:%M:%S')
|
||||
else:
|
||||
timestamp = ""
|
||||
|
||||
response_html = markdown.markdown(log.response or "", extensions=['tables', 'fenced_code'])
|
||||
|
||||
# Add context info (Stock Code / Session) to the header since they are mixed
|
||||
@ -275,7 +284,6 @@ async def export_chat_pdf(request: ExportPDFRequest, db: AsyncSession = Depends(
|
||||
</div>
|
||||
<div class="message-sub-meta">
|
||||
<span class="session-id">Session: {session_info}</span>
|
||||
<span class="model">Model: {log.model}</span>
|
||||
</div>
|
||||
<div class="message-content">
|
||||
{response_html}
|
||||
@ -284,6 +292,8 @@ async def export_chat_pdf(request: ExportPDFRequest, db: AsyncSession = Depends(
|
||||
'''
|
||||
|
||||
# Complete HTML
|
||||
now_shanghai = datetime.now(shanghai_tz)
|
||||
|
||||
# Updated font-family stack to include common Linux/Windows CJK fonts
|
||||
complete_html = f'''
|
||||
<!DOCTYPE html>
|
||||
@ -383,7 +393,7 @@ async def export_chat_pdf(request: ExportPDFRequest, db: AsyncSession = Depends(
|
||||
</head>
|
||||
<body>
|
||||
<h1>AI 研究讨论导出</h1>
|
||||
<p style="text-align: center; color: #666; margin-bottom: 30px;">导出时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
|
||||
<p style="text-align: center; color: #666; margin-bottom: 30px;">导出时间: {now_shanghai.strftime('%Y-%m-%d %H:%M:%S')}</p>
|
||||
{messages_html}
|
||||
</body>
|
||||
</html>
|
||||
@ -395,7 +405,13 @@ async def export_chat_pdf(request: ExportPDFRequest, db: AsyncSession = Depends(
|
||||
pdf_path = tmp_file.name
|
||||
HTML(string=complete_html).write_pdf(pdf_path)
|
||||
|
||||
filename = f"AI研究讨论汇总_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
|
||||
# Determine filename prefix based on the most common stock code in logs
|
||||
# or just the first one found
|
||||
company_name = "AI研究讨论"
|
||||
if logs and logs[0].stock_code:
|
||||
company_name = logs[0].stock_code
|
||||
|
||||
filename = f"{company_name}_{now_shanghai.strftime('%Y%m%d')}_讨论记录.pdf"
|
||||
filename_encoded = quote(filename)
|
||||
|
||||
return FileResponse(
|
||||
|
||||
@ -343,6 +343,17 @@ async def download_report_pdf(report_id: int, db: AsyncSession = Depends(get_db)
|
||||
"""
|
||||
|
||||
# Complete PDF HTML
|
||||
# Timezone conversion for display
|
||||
from datetime import datetime, timezone, timedelta
|
||||
shanghai_tz = timezone(timedelta(hours=8))
|
||||
|
||||
# Use report creation time for the report date in PDF
|
||||
report_time = report.created_at.astimezone(shanghai_tz) if report.created_at.tzinfo else report.created_at.replace(tzinfo=timezone.utc).astimezone(shanghai_tz)
|
||||
|
||||
# Current time for filename (download date)
|
||||
now_shanghai = datetime.now(shanghai_tz)
|
||||
download_date_str = now_shanghai.strftime('%Y%m%d')
|
||||
|
||||
complete_html = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
@ -515,8 +526,7 @@ async def download_report_pdf(report_id: int, db: AsyncSession = Depends(get_db)
|
||||
<div class="cover">
|
||||
<h1>{report.company_name}</h1>
|
||||
<div class="meta">{report.market} {report.symbol}</div>
|
||||
<div class="meta">分析日期: {report.created_at.strftime('%Y年%m月%d日')}</div>
|
||||
{f'<div class="meta">AI模型: {report.ai_model}</div>' if report.ai_model else ''}
|
||||
<div class="meta">分析日期: {report_time.strftime('%Y年%m月%d日')}</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
@ -538,7 +548,8 @@ async def download_report_pdf(report_id: int, db: AsyncSession = Depends(get_db)
|
||||
HTML(string=complete_html).write_pdf(pdf_path)
|
||||
|
||||
# Return the PDF file
|
||||
filename = f"{report.company_name}_{report.symbol}_分析报告.pdf"
|
||||
# Filename format: Company_Symbol_Date_Report.pdf
|
||||
filename = f"{report.company_name}_{report.symbol}_{download_date_str}_分析报告.pdf"
|
||||
# Use RFC 5987 encoding for non-ASCII filenames
|
||||
filename_encoded = quote(filename)
|
||||
|
||||
|
||||
@ -75,7 +75,14 @@ export default function AnalysisPage({ params }: { params: Promise<{ id: string
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `${report.company_name}_${report.symbol}_分析报告.pdf`
|
||||
|
||||
// Format date as YYYYMMDD to match backend
|
||||
const date = new Date()
|
||||
const dateStr = date.getFullYear() +
|
||||
String(date.getMonth() + 1).padStart(2, '0') +
|
||||
String(date.getDate()).padStart(2, '0')
|
||||
|
||||
a.download = `${report.company_name}_${report.symbol}_${dateStr}_分析报告.pdf`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
|
||||
@ -250,11 +250,30 @@ export function HistoryView() {
|
||||
body: JSON.stringify({ session_ids: Array.from(selectedForExport) })
|
||||
})
|
||||
if (!response.ok) throw new Error("导出失败")
|
||||
|
||||
// Try to get filename from content-disposition
|
||||
let filename = `讨论历史_${new Date().toISOString().slice(0, 10)}.pdf`
|
||||
const disposition = response.headers.get('content-disposition')
|
||||
if (disposition && disposition.indexOf('attachment') !== -1) {
|
||||
const filenameRegex = /filename\*=UTF-8''([\w%\-\.]+)(?:;|$)|filename="([^"]+)"(?:;|$)|filename=([^;]+)(?:;|$)/i;
|
||||
const matches = filenameRegex.exec(disposition);
|
||||
if (matches) {
|
||||
// Try decoding the UTF-8 encoded filename
|
||||
try {
|
||||
if (matches[1]) filename = decodeURIComponent(matches[1]);
|
||||
else if (matches[2]) filename = matches[2];
|
||||
else if (matches[3]) filename = matches[3];
|
||||
} catch (e) {
|
||||
console.error("Error decoding filename:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const blob = await response.blob()
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement("a")
|
||||
a.href = url
|
||||
a.download = `讨论历史_${new Date().toISOString().slice(0, 10)}.pdf`
|
||||
a.download = filename
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
|
||||
@ -32,9 +32,10 @@ function SearchResultItem({ result, onSelect }: { result: SearchResult, onSelect
|
||||
const defaultSource = "Bloomberg"
|
||||
const [source, setSource] = useState(defaultSource)
|
||||
const [symbol, setSymbol] = useState(result.symbol) // Editable symbol state
|
||||
const [market, setMarket] = useState(result.market) // Editable market state
|
||||
|
||||
// 根据市场获取可用数据源列表
|
||||
const availableSources = result.market === "CN" ? DATA_SOURCES.CN : DATA_SOURCES.GLOBAL
|
||||
const availableSources = market === "CN" ? DATA_SOURCES.CN : DATA_SOURCES.GLOBAL
|
||||
|
||||
return (
|
||||
<Card className="hover:bg-accent/50 transition-colors h-full flex flex-col">
|
||||
@ -54,9 +55,13 @@ function SearchResultItem({ result, onSelect }: { result: SearchResult, onSelect
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="secondary" className="shrink-0 text-xs font-mono h-6">
|
||||
{result.market}
|
||||
</Badge>
|
||||
<div className="shrink-0 w-12">
|
||||
<Input
|
||||
value={market}
|
||||
onChange={(e) => setMarket(e.target.value)}
|
||||
className="h-6 text-xs font-mono px-1 text-center bg-secondary/50 border-transparent hover:border-input focus:border-input transition-colors"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -78,7 +83,7 @@ function SearchResultItem({ result, onSelect }: { result: SearchResult, onSelect
|
||||
</Select>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => onSelect({ ...result, symbol }, source)}
|
||||
onClick={() => onSelect({ ...result, symbol, market }, source)}
|
||||
size="sm"
|
||||
className="h-8 px-3 shrink-0"
|
||||
>
|
||||
|
||||
@ -12,28 +12,25 @@ interface StockChartProps {
|
||||
export function StockChart({ symbol, market }: StockChartProps) {
|
||||
|
||||
const getTradingViewUrl = () => {
|
||||
let exchange = "NASDAQ"
|
||||
let tvSymbol = symbol
|
||||
|
||||
// 对于特定亚洲市场,需要显式指定交易所前缀
|
||||
if (market === "CH" || market === "CN") {
|
||||
if (symbol.startsWith("6")) exchange = "SSE"
|
||||
else if (symbol.startsWith("0") || symbol.startsWith("3")) exchange = "SZSE"
|
||||
let exchange = "SSE" // Default Shanghai
|
||||
if (symbol.startsWith("0") || symbol.startsWith("3")) exchange = "SZSE"
|
||||
else if (symbol.startsWith("4") || symbol.startsWith("8")) exchange = "BSE"
|
||||
return `https://cn.tradingview.com/chart/?symbol=${exchange}:${symbol}`
|
||||
} else if (market === "HK") {
|
||||
exchange = "HKEX"
|
||||
// Ensure no leading zeros for int conversion check
|
||||
tvSymbol = parseInt(symbol).toString()
|
||||
// 港股去重前面的0
|
||||
const tvSymbol = parseInt(symbol).toString()
|
||||
return `https://cn.tradingview.com/chart/?symbol=HKEX:${tvSymbol}`
|
||||
} else if (market === "JP") {
|
||||
exchange = "TSE"
|
||||
return `https://cn.tradingview.com/chart/?symbol=TSE:${symbol}`
|
||||
} else if (market === "VN") {
|
||||
exchange = "HOSE"
|
||||
} else {
|
||||
// US
|
||||
exchange = "NASDAQ" // Default fallback
|
||||
return `https://cn.tradingview.com/chart/?symbol=HOSE:${symbol}`
|
||||
}
|
||||
|
||||
const fullSymbol = `${exchange}:${tvSymbol}`
|
||||
return `https://cn.tradingview.com/chart/?symbol=${fullSymbol}`
|
||||
// 对于其他市场 (US, BZ, UK 等),直接传递 Symbol 让 TradingView 自动匹配
|
||||
// 例如 "PETR4" -> Bovespa, "C" -> NYSE, "AAPL" -> NASDAQ
|
||||
return `https://cn.tradingview.com/chart/?symbol=${symbol}`
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user