Compare commits
No commits in common. "30e954d89fc04d7524894eddc83854ff36e6cb5d" and "2a5e471ddb8cc7d996258c56afb4f48a463574eb" have entirely different histories.
30e954d89f
...
2a5e471ddb
@ -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(
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -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(() => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user