Compare commits

..

No commits in common. "30e954d89fc04d7524894eddc83854ff36e6cb5d" and "2a5e471ddb8cc7d996258c56afb4f48a463574eb" have entirely different histories.

6 changed files with 30 additions and 85 deletions

View File

@ -236,10 +236,7 @@ class ExportPDFRequest(BaseModel):
async def export_chat_pdf(request: ExportPDFRequest, db: AsyncSession = Depends(get_db)): async def export_chat_pdf(request: ExportPDFRequest, db: AsyncSession = Depends(get_db)):
"""Export selected chat sessions to PDF""" """Export selected chat sessions to PDF"""
from sqlalchemy import select, desc, asc from sqlalchemy import select, desc, asc
from datetime import datetime, timezone, timedelta from datetime import datetime
# Timezone definition (Shanghai)
shanghai_tz = timezone(timedelta(hours=8))
if not request.session_ids: if not request.session_ids:
raise HTTPException(status_code=400, detail="No sessions selected") raise HTTPException(status_code=400, detail="No sessions selected")
@ -263,13 +260,7 @@ async def export_chat_pdf(request: ExportPDFRequest, db: AsyncSession = Depends(
messages_html = "" messages_html = ""
for log in logs: for log in logs:
# Convert timestamp to Shanghai time timestamp = log.timestamp.strftime('%Y-%m-%d %H:%M:%S') if log.timestamp else ""
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']) response_html = markdown.markdown(log.response or "", extensions=['tables', 'fenced_code'])
# Add context info (Stock Code / Session) to the header since they are mixed # Add context info (Stock Code / Session) to the header since they are mixed
@ -284,6 +275,7 @@ async def export_chat_pdf(request: ExportPDFRequest, db: AsyncSession = Depends(
</div> </div>
<div class="message-sub-meta"> <div class="message-sub-meta">
<span class="session-id">Session: {session_info}</span> <span class="session-id">Session: {session_info}</span>
<span class="model">Model: {log.model}</span>
</div> </div>
<div class="message-content"> <div class="message-content">
{response_html} {response_html}
@ -292,8 +284,6 @@ async def export_chat_pdf(request: ExportPDFRequest, db: AsyncSession = Depends(
''' '''
# Complete HTML # Complete HTML
now_shanghai = datetime.now(shanghai_tz)
# Updated font-family stack to include common Linux/Windows CJK fonts # Updated font-family stack to include common Linux/Windows CJK fonts
complete_html = f''' complete_html = f'''
<!DOCTYPE html> <!DOCTYPE html>
@ -393,7 +383,7 @@ async def export_chat_pdf(request: ExportPDFRequest, db: AsyncSession = Depends(
</head> </head>
<body> <body>
<h1>AI 研究讨论导出</h1> <h1>AI 研究讨论导出</h1>
<p style="text-align: center; color: #666; margin-bottom: 30px;">导出时间: {now_shanghai.strftime('%Y-%m-%d %H:%M:%S')}</p> <p style="text-align: center; color: #666; margin-bottom: 30px;">导出时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
{messages_html} {messages_html}
</body> </body>
</html> </html>
@ -405,13 +395,7 @@ async def export_chat_pdf(request: ExportPDFRequest, db: AsyncSession = Depends(
pdf_path = tmp_file.name pdf_path = tmp_file.name
HTML(string=complete_html).write_pdf(pdf_path) HTML(string=complete_html).write_pdf(pdf_path)
# Determine filename prefix based on the most common stock code in logs filename = f"AI研究讨论汇总_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
# 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) filename_encoded = quote(filename)
return FileResponse( return FileResponse(

View File

@ -343,17 +343,6 @@ async def download_report_pdf(report_id: int, db: AsyncSession = Depends(get_db)
""" """
# Complete PDF HTML # 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""" complete_html = f"""
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
@ -526,7 +515,8 @@ async def download_report_pdf(report_id: int, db: AsyncSession = Depends(get_db)
<div class="cover"> <div class="cover">
<h1>{report.company_name}</h1> <h1>{report.company_name}</h1>
<div class="meta">{report.market} {report.symbol}</div> <div class="meta">{report.market} {report.symbol}</div>
<div class="meta">分析日期: {report_time.strftime('%Y年%m月%d')}</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> </div>
<div class="section"> <div class="section">
@ -548,8 +538,7 @@ async def download_report_pdf(report_id: int, db: AsyncSession = Depends(get_db)
HTML(string=complete_html).write_pdf(pdf_path) HTML(string=complete_html).write_pdf(pdf_path)
# Return the PDF file # Return the PDF file
# Filename format: Company_Symbol_Date_Report.pdf filename = f"{report.company_name}_{report.symbol}_分析报告.pdf"
filename = f"{report.company_name}_{report.symbol}_{download_date_str}_分析报告.pdf"
# Use RFC 5987 encoding for non-ASCII filenames # Use RFC 5987 encoding for non-ASCII filenames
filename_encoded = quote(filename) filename_encoded = quote(filename)

View File

@ -75,14 +75,7 @@ export default function AnalysisPage({ params }: { params: Promise<{ id: string
const url = window.URL.createObjectURL(blob) const url = window.URL.createObjectURL(blob)
const a = document.createElement('a') const a = document.createElement('a')
a.href = url 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) document.body.appendChild(a)
a.click() a.click()
window.URL.revokeObjectURL(url) window.URL.revokeObjectURL(url)

View File

@ -250,30 +250,11 @@ export function HistoryView() {
body: JSON.stringify({ session_ids: Array.from(selectedForExport) }) body: JSON.stringify({ session_ids: Array.from(selectedForExport) })
}) })
if (!response.ok) throw new Error("导出失败") 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 blob = await response.blob()
const url = window.URL.createObjectURL(blob) const url = window.URL.createObjectURL(blob)
const a = document.createElement("a") const a = document.createElement("a")
a.href = url a.href = url
a.download = filename a.download = `讨论历史_${new Date().toISOString().slice(0, 10)}.pdf`
document.body.appendChild(a) document.body.appendChild(a)
a.click() a.click()
document.body.removeChild(a) document.body.removeChild(a)

View File

@ -32,10 +32,9 @@ function SearchResultItem({ result, onSelect }: { result: SearchResult, onSelect
const defaultSource = "Bloomberg" const defaultSource = "Bloomberg"
const [source, setSource] = useState(defaultSource) const [source, setSource] = useState(defaultSource)
const [symbol, setSymbol] = useState(result.symbol) // Editable symbol state const [symbol, setSymbol] = useState(result.symbol) // Editable symbol state
const [market, setMarket] = useState(result.market) // Editable market state
// 根据市场获取可用数据源列表 // 根据市场获取可用数据源列表
const availableSources = market === "CN" ? DATA_SOURCES.CN : DATA_SOURCES.GLOBAL const availableSources = result.market === "CN" ? DATA_SOURCES.CN : DATA_SOURCES.GLOBAL
return ( return (
<Card className="hover:bg-accent/50 transition-colors h-full flex flex-col"> <Card className="hover:bg-accent/50 transition-colors h-full flex flex-col">
@ -55,13 +54,9 @@ function SearchResultItem({ result, onSelect }: { result: SearchResult, onSelect
/> />
</div> </div>
</div> </div>
<div className="shrink-0 w-12"> <Badge variant="secondary" className="shrink-0 text-xs font-mono h-6">
<Input {result.market}
value={market} </Badge>
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>
</div> </div>
@ -83,7 +78,7 @@ function SearchResultItem({ result, onSelect }: { result: SearchResult, onSelect
</Select> </Select>
</div> </div>
<Button <Button
onClick={() => onSelect({ ...result, symbol, market }, source)} onClick={() => onSelect({ ...result, symbol }, source)}
size="sm" size="sm"
className="h-8 px-3 shrink-0" className="h-8 px-3 shrink-0"
> >

View File

@ -12,25 +12,28 @@ interface StockChartProps {
export function StockChart({ symbol, market }: StockChartProps) { export function StockChart({ symbol, market }: StockChartProps) {
const getTradingViewUrl = () => { const getTradingViewUrl = () => {
// 对于特定亚洲市场,需要显式指定交易所前缀 let exchange = "NASDAQ"
let tvSymbol = symbol
if (market === "CH" || market === "CN") { if (market === "CH" || market === "CN") {
let exchange = "SSE" // Default Shanghai if (symbol.startsWith("6")) exchange = "SSE"
if (symbol.startsWith("0") || symbol.startsWith("3")) exchange = "SZSE" else if (symbol.startsWith("0") || symbol.startsWith("3")) exchange = "SZSE"
else if (symbol.startsWith("4") || symbol.startsWith("8")) exchange = "BSE" else if (symbol.startsWith("4") || symbol.startsWith("8")) exchange = "BSE"
return `https://cn.tradingview.com/chart/?symbol=${exchange}:${symbol}`
} else if (market === "HK") { } else if (market === "HK") {
// 港股去重前面的0 exchange = "HKEX"
const tvSymbol = parseInt(symbol).toString() // Ensure no leading zeros for int conversion check
return `https://cn.tradingview.com/chart/?symbol=HKEX:${tvSymbol}` tvSymbol = parseInt(symbol).toString()
} else if (market === "JP") { } else if (market === "JP") {
return `https://cn.tradingview.com/chart/?symbol=TSE:${symbol}` exchange = "TSE"
} else if (market === "VN") { } else if (market === "VN") {
return `https://cn.tradingview.com/chart/?symbol=HOSE:${symbol}` exchange = "HOSE"
} else {
// US
exchange = "NASDAQ" // Default fallback
} }
// 对于其他市场 (US, BZ, UK 等),直接传递 Symbol 让 TradingView 自动匹配 const fullSymbol = `${exchange}:${tvSymbol}`
// 例如 "PETR4" -> Bovespa, "C" -> NYSE, "AAPL" -> NASDAQ return `https://cn.tradingview.com/chart/?symbol=${fullSymbol}`
return `https://cn.tradingview.com/chart/?symbol=${symbol}`
} }
useEffect(() => { useEffect(() => {