|
<!DOCTYPE html> |
|
<html lang="zh-CN"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>AI股票分析系统</title> |
|
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet"> |
|
</head> |
|
<body class="bg-gray-100"> |
|
|
|
|
|
{% if announcement %} |
|
<div id="announcement-container" class="fixed top-4 right-4 max-w-md z-50 animate-fade-in-down"> |
|
<div class="bg-blue-50 border-l-4 border-blue-500 p-4 rounded-lg shadow-lg"> |
|
<div class="flex"> |
|
<div class="flex-shrink-0"> |
|
<svg class="h-5 w-5 text-blue-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> |
|
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" /> |
|
</svg> |
|
</div> |
|
<div class="ml-3 pr-8"> |
|
<p class="text-sm text-blue-700 whitespace-pre-line" id="announcement-text"></p> |
|
<p class="text-xs text-gray-500 mt-1" id="announcement-timer"></p> |
|
</div> |
|
<button onclick="closeAnnouncement()" class="absolute top-2 right-2 text-gray-400 hover:text-gray-600"> |
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> |
|
</svg> |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
|
|
const announcementText = `{{ announcement }}`; |
|
const urlRegex = /(https?:\/\/[^\s]+)/g; |
|
document.getElementById('announcement-text').innerHTML = announcementText.replace( |
|
urlRegex, |
|
'<a href="$1" target="_blank" class="text-blue-600 hover:text-blue-800 underline">$1</a>' |
|
); |
|
|
|
|
|
const autoCloseTime = 5; |
|
let remainingTime = autoCloseTime; |
|
const timerElement = document.getElementById('announcement-timer'); |
|
|
|
|
|
function updateTimer() { |
|
timerElement.textContent = `${remainingTime}秒后自动关闭`; |
|
if (remainingTime <= 0) { |
|
closeAnnouncement(); |
|
} else { |
|
remainingTime--; |
|
setTimeout(updateTimer, 1000); |
|
} |
|
} |
|
|
|
|
|
updateTimer(); |
|
|
|
|
|
function closeAnnouncement() { |
|
const container = document.getElementById('announcement-container'); |
|
if(!container){ |
|
return; |
|
} |
|
container.style.opacity = '0'; |
|
container.style.transform = 'translateY(-10px)'; |
|
container.style.transition = 'all 0.3s ease-out'; |
|
setTimeout(() => { |
|
container.remove(); |
|
}, 300); |
|
} |
|
</script> |
|
{% endif %} |
|
|
|
<div class="container mx-auto px-4 py-8"> |
|
<h1 class="text-3xl font-bold mb-8 text-center">AI股票分析系统</h1> |
|
|
|
|
|
<div class="max-w-4xl mx-auto mb-8"> |
|
<div class="bg-white rounded-lg shadow-md p-6"> |
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-6"> |
|
|
|
<div class="text-center"> |
|
<p class="text-gray-600 mb-2">当前时间</p> |
|
<p id="currentTime" class="text-2xl font-bold text-gray-800"></p> |
|
</div> |
|
|
|
|
|
<div class="text-center"> |
|
<p class="text-gray-600 mb-2">A股市场</p> |
|
<p id="cnMarketStatus" class="text-lg font-medium"></p> |
|
<p id="cnMarketTimer" class="text-sm text-gray-500 mt-1"></p> |
|
</div> |
|
|
|
|
|
<div class="text-center"> |
|
<p class="text-gray-600 mb-2">港股市场</p> |
|
<p id="hkMarketStatus" class="text-lg font-medium"></p> |
|
<p id="hkMarketTimer" class="text-sm text-gray-500 mt-1"></p> |
|
</div> |
|
|
|
|
|
<div class="text-center"> |
|
<p class="text-gray-600 mb-2">美股市场</p> |
|
<p id="usMarketStatus" class="text-lg font-medium"></p> |
|
<p id="usMarketTimer" class="text-sm text-gray-500 mt-1"></p> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
|
|
function updateMarketTime() { |
|
const now = new Date(); |
|
|
|
|
|
document.getElementById('currentTime').textContent = |
|
now.toLocaleTimeString('zh-CN', { hour12: false }); |
|
|
|
|
|
const cnOptions = { timeZone: 'Asia/Shanghai', hour12: false }; |
|
const cnTime = now.toLocaleString('zh-CN', cnOptions); |
|
const cnHour = new Date(cnTime).getHours(); |
|
const cnMinute = new Date(cnTime).getMinutes(); |
|
|
|
|
|
const cnMarketOpen = (cnHour === 9 && cnMinute >= 30) || (cnHour === 10) || |
|
(cnHour === 11 && cnMinute <= 30) || |
|
(cnHour >= 13 && cnHour < 15); |
|
|
|
updateMarketStatus('cn', cnMarketOpen, cnHour, cnMinute, 9, 30, 15, 0); |
|
|
|
|
|
const hkMarketOpen = (cnHour === 9 && cnMinute >= 30) || |
|
(cnHour === 10) || (cnHour === 11) || |
|
(cnHour >= 13 && cnHour < 16); |
|
|
|
updateMarketStatus('hk', hkMarketOpen, cnHour, cnMinute, 9, 30, 16, 0); |
|
|
|
|
|
const usOptions = { timeZone: 'America/New_York', hour12: false }; |
|
const usTime = now.toLocaleString('zh-CN', usOptions); |
|
const usHour = new Date(usTime).getHours(); |
|
const usMinute = new Date(usTime).getMinutes(); |
|
|
|
|
|
const usMarketOpen = (usHour >= 9 && usHour < 16) || |
|
(usHour === 16 && usMinute === 0); |
|
|
|
updateMarketStatus('us', usMarketOpen, usHour, usMinute, 9, 30, 16, 0); |
|
} |
|
|
|
|
|
function updateMarketStatus(market, isOpen, currentHour, currentMinute, openHour, openMinute, closeHour, closeMinute) { |
|
const element = document.getElementById(`${market}MarketStatus`); |
|
const timer = document.getElementById(`${market}MarketTimer`); |
|
|
|
if (isOpen) { |
|
element.textContent = '交易中'; |
|
element.className = 'text-lg font-medium text-green-600'; |
|
|
|
|
|
const now = new Date(); |
|
const closeTime = new Date(now); |
|
closeTime.setHours(closeHour, closeMinute, 0); |
|
|
|
const timeToClose = closeTime - now; |
|
const hours = Math.floor(timeToClose / 3600000); |
|
const minutes = Math.floor((timeToClose % 3600000) / 60000); |
|
|
|
if (hours >= 0 && minutes >= 0) { |
|
timer.textContent = `距离收盘还有 ${hours}小时${minutes}分钟`; |
|
} |
|
} else { |
|
element.textContent = '已休市'; |
|
element.className = 'text-lg font-medium text-gray-600'; |
|
|
|
|
|
const now = new Date(); |
|
const nextOpen = new Date(now); |
|
|
|
if (market === 'us') { |
|
|
|
const usTime = new Date(now.toLocaleString('en-US', { timeZone: 'America/New_York' })); |
|
const usHour = usTime.getHours(); |
|
|
|
|
|
if (usHour >= closeHour) { |
|
nextOpen.setDate(nextOpen.getDate() + 1); |
|
} |
|
|
|
|
|
nextOpen.setHours(openHour + 12, openMinute, 0); |
|
} else { |
|
|
|
if (currentHour >= closeHour) { |
|
nextOpen.setDate(nextOpen.getDate() + 1); |
|
} |
|
nextOpen.setHours(openHour, openMinute, 0); |
|
} |
|
|
|
const timeToOpen = nextOpen - now; |
|
const hours = Math.floor(timeToOpen / 3600000); |
|
const minutes = Math.floor((timeToOpen % 3600000) / 60000); |
|
|
|
|
|
if (hours >= 0 && minutes >= 0) { |
|
timer.textContent = `距离开盘还有 ${hours}小时${minutes}分钟`; |
|
} else { |
|
timer.textContent = '计算开盘时间中...'; |
|
} |
|
} |
|
} |
|
|
|
|
|
setInterval(updateMarketTime, 1000); |
|
updateMarketTime(); |
|
</script> |
|
|
|
<div class="max-w-4xl mx-auto"> |
|
|
|
<div class="bg-white p-6 rounded-lg shadow-md"> |
|
<h2 class="text-xl font-semibold mb-4">股票批量分析</h2> |
|
|
|
|
|
<div class="mb-6 border-b pb-6"> |
|
<div class="flex items-center justify-between mb-4"> |
|
<h3 class="text-lg font-medium text-gray-700">API配置</h3> |
|
<button id="toggleApiConfig" class="text-blue-600 hover:text-blue-800 text-sm flex items-center"> |
|
<span id="toggleApiConfigText">显示配置</span> |
|
<svg id="toggleApiConfigIcon" class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path> |
|
</svg> |
|
</button> |
|
</div> |
|
|
|
<div id="apiConfigPanel" class="hidden space-y-4"> |
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> |
|
<div> |
|
<label for="apiUrl" class="block text-sm font-medium text-gray-700 mb-1">API URL</label> |
|
<input type="text" id="apiUrl" |
|
class="w-full p-2 border rounded bg-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500" |
|
placeholder="例如: https://api.openai.com" |
|
value="{{ default_api_url }}" |
|
oninput="updateFormattedUrl(this.value)"> |
|
<p id="formattedUrl" class="mt-1 text-sm text-gray-500 break-all"></p> |
|
</div> |
|
<div> |
|
<label for="apiModel" class="block text-sm font-medium text-gray-700 mb-1">API 模型</label> |
|
<input type="text" id="apiModel" |
|
class="w-full p-2 border rounded bg-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500" |
|
placeholder="例如: gpt-3.5-turbo" |
|
value="{{ default_api_model }}"> |
|
</div> |
|
</div> |
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> |
|
<div> |
|
<label for="apiKey" class="block text-sm font-medium text-gray-700 mb-1">API Key</label> |
|
<input type="password" id="apiKey" |
|
class="w-full p-2 border rounded bg-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500" |
|
placeholder="输入您的API Key"> |
|
<p class="mt-1 text-sm text-gray-500">如不填写,将使用系统默认配置</p> |
|
</div> |
|
<div> |
|
<label for="apiTimeout" class="block text-sm font-medium text-gray-700 mb-1">API 超时时间 (秒)</label> |
|
<input type="number" id="apiTimeout" |
|
class="w-full p-2 border rounded bg-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500" |
|
placeholder="例如: 60" |
|
value="{{ default_api_timeout }}" min="1" max="300"> |
|
<p class="mt-1 text-sm text-gray-500">请求超时时间,默认60秒</p> |
|
</div> |
|
</div> |
|
<div class="flex justify-between"> |
|
<div class="flex items-center"> |
|
<input type="checkbox" id="saveApiConfig" class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"> |
|
<label for="saveApiConfig" class="ml-2 block text-sm text-gray-700">保存配置到本地</label> |
|
</div> |
|
<div> |
|
<button id="resetApiConfig" class="text-gray-600 hover:text-gray-800 text-sm mr-3"> |
|
重置为默认 |
|
</button> |
|
<button id="testApiConfig" class="bg-blue-100 text-blue-700 px-3 py-1 rounded hover:bg-blue-200 text-sm"> |
|
测试连接 |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="mb-4"> |
|
<label for="marketType" class="block text-sm font-medium text-gray-700 mb-2"> |
|
选择市场类型 |
|
</label> |
|
<select id="marketType" onchange="handleMarketTypeChange()" |
|
class="w-full p-2 border rounded bg-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"> |
|
<option value="A">A股</option> |
|
<option value="HK">港股</option> |
|
<option value="US">美股</option> |
|
<option value="ETF">ETF基金</option> |
|
<option value="LOF">LOF基金</option> |
|
</select> |
|
</div> |
|
|
|
|
|
<div id="searchContainer" class="mb-4 hidden"> |
|
<div class="relative"> |
|
<input type="text" |
|
id="searchInput" |
|
class="w-full p-2 border rounded bg-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500" |
|
placeholder="输入名称回车搜索" |
|
onkeydown="handleKeyDown(event)"> |
|
<div id="searchLoading" class="absolute right-3 top-2.5 hidden"> |
|
<svg class="animate-spin h-5 w-5 text-blue-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> |
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> |
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> |
|
</svg> |
|
</div> |
|
<div id="searchResults" class="absolute z-10 w-full mt-1 bg-white border rounded-md shadow-lg hidden max-h-80 overflow-y-auto"> |
|
</div> |
|
</div> |
|
<div id="searchError" class="hidden absolute z-10 w-full mt-1 p-3 bg-red-50 text-red-600 rounded-md border border-red-200"> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<script> |
|
|
|
function debounce(func, wait) { |
|
let timeout; |
|
return function (...args) { |
|
clearTimeout(timeout); |
|
timeout = setTimeout(() => { |
|
func.apply(this, args); |
|
}, wait); |
|
}; |
|
} |
|
|
|
function handleKeyDown(event) { |
|
|
|
if (event.key === 'Enter' && !event.isComposing) { |
|
event.preventDefault(); |
|
const keyword = event.target.value.trim(); |
|
if (keyword) { |
|
debouncedSearch(keyword); |
|
} |
|
} |
|
} |
|
|
|
|
|
const debouncedSearch = debounce(async (keyword) => { |
|
if (!keyword) { |
|
document.getElementById('searchResults').classList.add('hidden'); |
|
document.getElementById('searchError').classList.add('hidden'); |
|
return; |
|
} |
|
|
|
const marketType = document.getElementById('marketType').value; |
|
document.getElementById('searchLoading').classList.remove('hidden'); |
|
document.getElementById('searchError').classList.add('hidden'); |
|
|
|
try { |
|
let endpoint = ''; |
|
if (marketType === 'US') { |
|
endpoint = '/search_us_stocks'; |
|
} else if (['ETF', 'LOF'].includes(marketType)) { |
|
endpoint = '/search_funds'; |
|
} |
|
|
|
const response = await fetch(`${endpoint}?market_type=${marketType}&keyword=${encodeURIComponent(keyword)}`); |
|
const data = await response.json(); |
|
|
|
if (!response.ok) { |
|
throw new Error(data.error || '搜索失败'); |
|
} |
|
|
|
displaySearchResults(data.results, marketType); |
|
} catch (error) { |
|
console.error('搜索出错:', error); |
|
const errorDiv = document.getElementById('searchError'); |
|
errorDiv.textContent = `搜索出错: ${error.message}`; |
|
errorDiv.classList.remove('hidden'); |
|
document.getElementById('searchResults').classList.add('hidden'); |
|
} finally { |
|
document.getElementById('searchLoading').classList.add('hidden'); |
|
} |
|
}, 500); |
|
|
|
</script> |
|
<div class="mb-4"> |
|
<label for="batchStocks" class="block text-sm font-medium text-gray-700 mb-2"> |
|
输入代码 |
|
</label> |
|
<textarea id="batchStocks" |
|
class="w-full p-2 border rounded h-32" |
|
placeholder="输入代码,支持多个代码(用回车或逗号分隔)"></textarea> |
|
</div> |
|
|
|
|
|
<script> |
|
let searchTimeout; |
|
|
|
function handleMarketTypeChange() { |
|
const marketType = document.getElementById('marketType').value; |
|
const searchContainer = document.getElementById('searchContainer'); |
|
const searchInput = document.getElementById('searchInput'); |
|
const batchStocks = document.getElementById('batchStocks'); |
|
|
|
|
|
searchContainer.classList.toggle('hidden', !['US', 'ETF', 'LOF'].includes(marketType)); |
|
|
|
|
|
if (marketType === 'US') { |
|
searchInput.placeholder = '输入美股名称回车搜索(中文和英文都试下)'; |
|
} else if (marketType === 'ETF') { |
|
searchInput.placeholder = '输入ETF基金名称回车搜索'; |
|
} else if (marketType === 'LOF') { |
|
searchInput.placeholder = '输入LOF基金名称回车搜索'; |
|
} |
|
|
|
|
|
document.getElementById('searchResults').innerHTML = ''; |
|
searchInput.value = ''; |
|
|
|
|
|
batchStocks.value = ''; |
|
} |
|
|
|
function debounceSearch(event) { |
|
clearTimeout(searchTimeout); |
|
searchTimeout = setTimeout(() => { |
|
searchUsStocks(event.target.value); |
|
}, 300); |
|
} |
|
|
|
function displaySearchResults(results, marketType) { |
|
const resultsDiv = document.getElementById('searchResults'); |
|
|
|
if (!results || results.length === 0) { |
|
resultsDiv.classList.add('hidden'); |
|
return; |
|
} |
|
|
|
let html = '<div class="divide-y divide-gray-100">'; |
|
results.forEach(item => { |
|
let rightContent = ''; |
|
if (marketType === 'US') { |
|
rightContent = ` |
|
<div class="font-medium">$${item.price}</div> |
|
<div class="text-sm text-gray-500">市值: ${formatMarketValue(item.market_value)}</div> |
|
`; |
|
} else { |
|
rightContent = ` |
|
<div class="font-medium">¥${item.price}</div> |
|
<div class="text-sm text-gray-500"> |
|
${item.discount_rate ? `折价率: ${item.discount_rate}%` : ''} |
|
</div> |
|
`; |
|
} |
|
|
|
html += ` |
|
<div class="px-4 py-2 hover:bg-gray-100 cursor-pointer flex justify-between items-center" |
|
onclick="selectStock('${item.symbol}')"> |
|
<div> |
|
<div class="font-medium">${item.name}</div> |
|
<div class="text-sm text-gray-500">${item.symbol}</div> |
|
</div> |
|
<div class="text-right"> |
|
${rightContent} |
|
</div> |
|
</div> |
|
`; |
|
}); |
|
html += '</div>'; |
|
|
|
resultsDiv.innerHTML = html; |
|
resultsDiv.classList.remove('hidden'); |
|
} |
|
|
|
function formatMarketValue(value) { |
|
if (value >= 1e12) { |
|
return (value / 1e12).toFixed(2) + '万亿'; |
|
} else if (value >= 1e8) { |
|
return (value / 1e8).toFixed(2) + '亿'; |
|
} else if (value >= 1e4) { |
|
return (value / 1e4).toFixed(2) + '万'; |
|
} |
|
return value.toString(); |
|
} |
|
|
|
function selectStock(symbol) { |
|
const textarea = document.getElementById('batchStocks'); |
|
const currentValue = textarea.value.trim(); |
|
|
|
|
|
|
|
textarea.value = currentValue |
|
? currentValue + '\n' + symbol |
|
: symbol; |
|
|
|
|
|
document.getElementById('searchInput').value = ''; |
|
document.getElementById('searchResults').classList.add('hidden'); |
|
} |
|
|
|
|
|
document.addEventListener('click', function(event) { |
|
const searchResults = document.getElementById('searchResults'); |
|
const searchInput = document.getElementById('searchInput'); |
|
|
|
if (!searchResults.contains(event.target) && |
|
!searchInput.contains(event.target)) { |
|
searchResults.classList.add('hidden'); |
|
} |
|
}); |
|
</script> |
|
<button id="analyzeBtn" onclick="analyzeStocks()" |
|
class="w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700 flex items-center justify-center"> |
|
<span>开始分析</span> |
|
<div id="loadingSpinner" class="hidden ml-2"> |
|
<svg class="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> |
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> |
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> |
|
</svg> |
|
</div> |
|
</button> |
|
</div> |
|
|
|
|
|
<div id="results" class="mt-8"> |
|
<div class="flex justify-between items-center mb-6"> |
|
<h2 class="text-2xl font-bold text-gray-800">分析结果</h2> |
|
<button onclick="copyAnalysisResults()" |
|
class="flex items-center text-blue-600 hover:text-blue-700"> |
|
<svg class="w-5 h-5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" |
|
d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"/> |
|
</svg> |
|
复制分析结果 |
|
</button> |
|
<button onclick="downloadPDF()" |
|
class="flex items-center text-blue-600 hover:text-blue-700"> |
|
<svg class="w-5 h-5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" |
|
d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"/> |
|
</svg> |
|
下载分析结果 |
|
</button> |
|
</div> |
|
<div id="resultContent" class="space-y-8"></div> |
|
</div> |
|
|
|
<script> |
|
function copyAnalysisResults() { |
|
const resultContent = document.getElementById('resultContent'); |
|
if (!resultContent.textContent.trim()) { |
|
alert('暂无分析结果可复制'); |
|
return; |
|
} |
|
|
|
|
|
let copyText = ''; |
|
const results = resultContent.querySelectorAll('.bg-white'); |
|
|
|
results.forEach(result => { |
|
|
|
const stockCode = result.querySelector('h3').textContent.trim(); |
|
copyText += `股票代码:${stockCode}\n`; |
|
|
|
|
|
const indicators = result.querySelectorAll('.flex.justify-between'); |
|
indicators.forEach(indicator => { |
|
const label = indicator.querySelector('.text-gray-600').textContent; |
|
const value = indicator.querySelector('.font-medium').textContent; |
|
copyText += `${label}:${value}\n`; |
|
}); |
|
|
|
|
|
const aiAnalysis = result.querySelector('.prose').textContent; |
|
copyText += `\nAI分析:\n${aiAnalysis}\n`; |
|
|
|
copyText += '\n----------------------------------------\n\n'; |
|
}); |
|
|
|
|
|
const textarea = document.createElement('textarea'); |
|
textarea.value = copyText; |
|
document.body.appendChild(textarea); |
|
textarea.select(); |
|
document.execCommand('copy'); |
|
document.body.removeChild(textarea); |
|
|
|
|
|
alert('分析结果已复制到剪贴板'); |
|
} |
|
</script> |
|
<script> |
|
let isAnalyzing = false; |
|
let stockAnalysisData = {}; |
|
|
|
async function analyzeStocks() { |
|
if (isAnalyzing) return; |
|
|
|
const stockInput = document.getElementById('batchStocks').value.trim(); |
|
const marketType = document.getElementById('marketType').value; |
|
const analyzeBtn = document.getElementById('analyzeBtn'); |
|
const loadingSpinner = document.getElementById('loadingSpinner'); |
|
const resultContent = document.getElementById('resultContent'); |
|
|
|
|
|
const apiUrl = document.getElementById('apiUrl').value.trim(); |
|
const apiKey = document.getElementById('apiKey').value.trim(); |
|
const apiModel = document.getElementById('apiModel').value.trim(); |
|
const apiTimeout = document.getElementById('apiTimeout').value.trim(); |
|
|
|
if (!stockInput) { |
|
alert('请输入代码'); |
|
return; |
|
} |
|
|
|
const stockCodes = stockInput.split(/[\n,]/) |
|
.map(code => code.trim()) |
|
.filter(code => code.length > 0); |
|
|
|
try { |
|
isAnalyzing = true; |
|
analyzeBtn.disabled = true; |
|
loadingSpinner.classList.remove('hidden'); |
|
analyzeBtn.querySelector('span').textContent = '分析中...'; |
|
|
|
|
|
resultContent.innerHTML = ''; |
|
stockAnalysisData = {}; |
|
|
|
|
|
const resultsContainer = document.createElement('div'); |
|
resultsContainer.className = 'space-y-6'; |
|
resultContent.appendChild(resultsContainer); |
|
|
|
|
|
const response = await fetch('/analyze', { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json', |
|
}, |
|
body: JSON.stringify({ |
|
stock_codes: stockCodes, |
|
market_type: marketType, |
|
api_url: apiUrl, |
|
api_key: apiKey, |
|
api_model: apiModel, |
|
api_timeout: apiTimeout |
|
}) |
|
}); |
|
|
|
if (!response.ok) { |
|
const errorData = await response.json(); |
|
throw new Error(errorData.error || '分析失败'); |
|
} |
|
|
|
|
|
const reader = response.body.getReader(); |
|
const decoder = new TextDecoder(); |
|
let buffer = ''; |
|
|
|
|
|
while (true) { |
|
const { done, value } = await reader.read(); |
|
if (done) break; |
|
|
|
|
|
buffer += decoder.decode(value, { stream: true }); |
|
|
|
|
|
const lines = buffer.split('\n'); |
|
buffer = lines.pop() || ''; |
|
|
|
for (const line of lines) { |
|
if (line.trim() === '') continue; |
|
|
|
try { |
|
const chunk = JSON.parse(line); |
|
handleStreamChunk(chunk, resultsContainer, marketType); |
|
} catch (e) { |
|
console.error('解析流数据出错:', e, line); |
|
} |
|
} |
|
} |
|
|
|
|
|
if (buffer.trim()) { |
|
try { |
|
const chunk = JSON.parse(buffer); |
|
handleStreamChunk(chunk, resultsContainer, marketType); |
|
} catch (e) { |
|
console.error('解析最后一行数据出错:', e, buffer); |
|
} |
|
} |
|
|
|
} catch (error) { |
|
console.error('请求失败:', error); |
|
resultContent.innerHTML = ` |
|
<div class="p-6 bg-yellow-50 text-yellow-600 rounded-lg text-center"> |
|
分析出错:${error.message} |
|
</div> |
|
`; |
|
} finally { |
|
isAnalyzing = false; |
|
analyzeBtn.disabled = false; |
|
loadingSpinner.classList.add('hidden'); |
|
analyzeBtn.querySelector('span').textContent = '开始分析'; |
|
} |
|
} |
|
|
|
|
|
function handleStreamChunk(chunk, container, marketType) { |
|
|
|
if (chunk.stream_type) { |
|
console.log('开始流式分析:', chunk); |
|
return; |
|
} |
|
|
|
|
|
const stockCode = chunk.stock_code; |
|
|
|
|
|
if (chunk.error) { |
|
|
|
let errorCard = document.getElementById(`error-${stockCode}`); |
|
|
|
|
|
let errorMessage = chunk.error; |
|
|
|
|
|
errorMessage = errorMessage.replace(/获取股票数据失败: /g, ''); |
|
|
|
|
|
let formattedError = errorMessage; |
|
|
|
|
|
if (errorMessage.includes('无效的A股股票代码格式')) { |
|
formattedError = `<strong>股票代码格式错误</strong>: ${stockCode} 不是有效的A股代码<br> |
|
<small>A股代码应以0、3、6、688或8开头</small>`; |
|
} else if (errorMessage.includes('股票代码格式错误')) { |
|
formattedError = `<strong>股票代码格式错误</strong>: ${errorMessage.split(':')[1] || errorMessage}`; |
|
} else if (errorMessage.includes('数据为空')) { |
|
formattedError = `<strong>数据获取失败</strong>: 未找到股票 ${stockCode} 的交易数据`; |
|
} |
|
|
|
if (!errorCard) { |
|
errorCard = document.createElement('div'); |
|
errorCard.id = `error-${stockCode}`; |
|
errorCard.className = 'bg-red-50 p-4 rounded-lg text-red-600 mb-4 flex items-start'; |
|
|
|
|
|
errorCard.innerHTML = ` |
|
<div class="mr-3 flex-shrink-0"> |
|
<svg class="h-5 w-5 text-red-500" fill="currentColor" viewBox="0 0 20 20"> |
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path> |
|
</svg> |
|
</div> |
|
<div> |
|
<p class="text-sm font-medium">股票 ${stockCode} 分析失败</p> |
|
<p class="mt-1 text-sm">${formattedError}</p> |
|
</div> |
|
`; |
|
container.appendChild(errorCard); |
|
} else { |
|
errorCard.innerHTML = ` |
|
<div class="mr-3 flex-shrink-0"> |
|
<svg class="h-5 w-5 text-red-500" fill="currentColor" viewBox="0 0 20 20"> |
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path> |
|
</svg> |
|
</div> |
|
<div> |
|
<p class="text-sm font-medium">股票 ${stockCode} 分析失败</p> |
|
<p class="mt-1 text-sm">${formattedError}</p> |
|
</div> |
|
`; |
|
} |
|
return; |
|
} |
|
|
|
|
|
if (!chunk.ai_analysis_chunk) { |
|
|
|
stockAnalysisData[stockCode] = { |
|
...chunk, |
|
ai_analysis: '' |
|
}; |
|
|
|
|
|
createStockCard(stockCode, container, marketType); |
|
return; |
|
} |
|
|
|
|
|
if (chunk.ai_analysis_chunk) { |
|
|
|
if (!stockAnalysisData[stockCode]) { |
|
stockAnalysisData[stockCode] = { |
|
stock_code: stockCode, |
|
ai_analysis: '' |
|
}; |
|
} |
|
|
|
|
|
stockAnalysisData[stockCode].ai_analysis += chunk.ai_analysis_chunk; |
|
|
|
|
|
updateAIAnalysisDisplay(stockCode); |
|
} |
|
} |
|
|
|
|
|
function createStockCard(stockCode, container, marketType) { |
|
const result = stockAnalysisData[stockCode]; |
|
if (!result) return; |
|
|
|
|
|
const currencySymbol = (() => { |
|
switch(marketType) { |
|
case 'US': |
|
return '$'; |
|
case 'HK': |
|
return 'HK$'; |
|
case 'A': |
|
default: |
|
return '¥'; |
|
} |
|
})(); |
|
|
|
|
|
let stockCard = document.getElementById(`stock-card-${stockCode}`); |
|
|
|
if (!stockCard) { |
|
stockCard = document.createElement('div'); |
|
stockCard.id = `stock-card-${stockCode}`; |
|
stockCard.className = 'bg-white rounded-lg shadow-lg overflow-hidden'; |
|
container.appendChild(stockCard); |
|
} |
|
|
|
stockCard.innerHTML = ` |
|
<!-- 头部信息 --> |
|
<div class="bg-gradient-to-r from-blue-600 to-blue-700 px-6 py-4"> |
|
<h3 class="text-xl font-bold text-white"> |
|
${result.stock_code} |
|
</h3> |
|
</div> |
|
|
|
<!-- 主要指标 --> |
|
<div class="p-6"> |
|
<!-- 修改网格布局:移动端单列显示,平板及以上为两列 --> |
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-6"> |
|
<div class="space-y-3"> |
|
<div class="flex justify-between items-center p-3 bg-gray-50 rounded"> |
|
<span class="text-gray-600">分析时间</span> |
|
<span class="font-medium">${result.analysis_date}</span> |
|
</div> |
|
<div class="flex justify-between items-center p-3 bg-gray-50 rounded"> |
|
<span class="text-gray-600">当前价格</span> |
|
<span class="font-medium">${currencySymbol}${result.price.toFixed(2)}</span> |
|
</div> |
|
<div class="flex justify-between items-center p-3 bg-gray-50 rounded"> |
|
<span class="text-gray-600">价格变动</span> |
|
<span class="font-medium ${result.price_change >= 0 ? 'text-red-500' : 'text-green-500'}"> |
|
${result.price_change.toFixed(2)}% |
|
</span> |
|
</div> |
|
</div> |
|
<div class="space-y-3 mt-4 sm:mt-0"> |
|
<div class="flex justify-between items-center p-3 bg-gray-50 rounded"> |
|
<span class="text-gray-600">综合评分</span> |
|
<span class="font-medium text-blue-600">${result.score}分</span> |
|
</div> |
|
<div class="flex justify-between items-center p-3 bg-gray-50 rounded"> |
|
<span class="text-gray-600">投资建议</span> |
|
<span class="font-medium text-purple-600">${result.recommendation}</span> |
|
</div> |
|
<div class="flex justify-between items-center p-3 bg-gray-50 rounded"> |
|
<span class="text-gray-600">RSI指标</span> |
|
<span class="font-medium">${result.rsi !== null ? result.rsi.toFixed(2) : '暂无数据'}</span> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<!-- AI分析部分,增加上边距防止内容粘连 --> |
|
<div class="mt-6"> |
|
<h4 class="text-lg font-semibold text-gray-800 mb-3">AI分析</h4> |
|
<div id="ai-analysis-${stockCode}" class="prose prose-red max-w-none bg-gray-50 p-4 rounded-lg relative"> |
|
<!-- 加载动画 --> |
|
<div class="ai-analysis-loading flex flex-col items-center justify-center py-8"> |
|
<div class="typing-animation mb-3"> |
|
<span></span> |
|
<span></span> |
|
<span></span> |
|
</div> |
|
<p class="text-gray-500 text-sm">AI正在思考分析中...</p> |
|
</div> |
|
<!-- 实际内容容器,增加上边距 --> |
|
<div class="ai-analysis-content hidden mt-4"></div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<!-- 免责声明 --> |
|
<div class="mt-6 border-t border-gray-100 pt-4"> |
|
<div class="bg-red-50 p-4 rounded-lg"> |
|
<p class="text-sm text-red-800 font-semibold mb-1">声明:</p> |
|
<p class="text-sm text-red-600">本分析仅基于技术指标和历史数据,不构成投资建议。股市有风险,投资需谨慎。</p> |
|
</div> |
|
</div> |
|
</div> |
|
`; |
|
} |
|
|
|
|
|
function updateAIAnalysisDisplay(stockCode) { |
|
const analysisElement = document.getElementById(`ai-analysis-${stockCode}`); |
|
if (analysisElement && stockAnalysisData[stockCode]) { |
|
const loadingElement = analysisElement.querySelector('.ai-analysis-loading'); |
|
const contentElement = analysisElement.querySelector('.ai-analysis-content'); |
|
|
|
|
|
if (stockAnalysisData[stockCode].ai_analysis) { |
|
|
|
const parsedContent = marked.parse(stockAnalysisData[stockCode].ai_analysis); |
|
|
|
|
|
const isFirstUpdate = contentElement.classList.contains('hidden'); |
|
|
|
|
|
if (isFirstUpdate) { |
|
contentElement.innerHTML = parsedContent; |
|
contentElement.classList.remove('hidden'); |
|
contentElement.classList.add('fade-in'); |
|
|
|
|
|
setTimeout(() => { |
|
loadingElement.style.display = 'none'; |
|
}, 300); |
|
} else { |
|
|
|
const currentLength = contentElement.textContent.length; |
|
|
|
|
|
contentElement.innerHTML = parsedContent; |
|
|
|
|
|
const allTextNodes = getAllTextNodes(contentElement); |
|
let totalLength = 0; |
|
|
|
for (const node of allTextNodes) { |
|
totalLength += node.textContent.length; |
|
if (totalLength > currentLength) { |
|
|
|
const newTextSpan = document.createElement('span'); |
|
newTextSpan.className = 'new-text'; |
|
node.parentNode.insertBefore(newTextSpan, node); |
|
newTextSpan.appendChild(node); |
|
break; |
|
} |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
function getAllTextNodes(element) { |
|
const textNodes = []; |
|
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false); |
|
|
|
let node; |
|
while (node = walker.nextNode()) { |
|
textNodes.push(node); |
|
} |
|
|
|
return textNodes; |
|
} |
|
|
|
|
|
function displayResults(results) { |
|
const resultContent = document.getElementById('resultContent'); |
|
|
|
|
|
resultContent.innerHTML = ''; |
|
stockAnalysisData = {}; |
|
|
|
|
|
const resultsContainer = document.createElement('div'); |
|
resultsContainer.className = 'space-y-6'; |
|
resultContent.appendChild(resultsContainer); |
|
|
|
if (!results || results.length === 0) { |
|
resultsContainer.innerHTML = '<div class="p-6 bg-yellow-50 text-yellow-600 rounded-lg text-center">没有分析结果</div>'; |
|
return; |
|
} |
|
|
|
|
|
const marketType = document.getElementById('marketType').value; |
|
|
|
|
|
results.forEach(result => { |
|
stockAnalysisData[result.stock_code] = result; |
|
createStockCard(result.stock_code, resultsContainer, marketType); |
|
updateAIAnalysisDisplay(result.stock_code); |
|
}); |
|
|
|
|
|
addMarkdownStyles(); |
|
} |
|
|
|
</script> |
|
|
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> |
|
|
|
|
|
<script> |
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
|
const toggleApiConfig = document.getElementById('toggleApiConfig'); |
|
const apiConfigPanel = document.getElementById('apiConfigPanel'); |
|
const toggleApiConfigText = document.getElementById('toggleApiConfigText'); |
|
const toggleApiConfigIcon = document.getElementById('toggleApiConfigIcon'); |
|
const apiUrl = document.getElementById('apiUrl'); |
|
const apiKey = document.getElementById('apiKey'); |
|
const apiModel = document.getElementById('apiModel'); |
|
const apiTimeout = document.getElementById('apiTimeout'); |
|
const saveApiConfig = document.getElementById('saveApiConfig'); |
|
const resetApiConfig = document.getElementById('resetApiConfig'); |
|
const testApiConfig = document.getElementById('testApiConfig'); |
|
|
|
|
|
loadApiConfig(); |
|
|
|
|
|
toggleApiConfig.addEventListener('click', function() { |
|
apiConfigPanel.classList.toggle('hidden'); |
|
|
|
if (apiConfigPanel.classList.contains('hidden')) { |
|
toggleApiConfigText.textContent = '显示配置'; |
|
toggleApiConfigIcon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>'; |
|
} else { |
|
toggleApiConfigText.textContent = '隐藏配置'; |
|
toggleApiConfigIcon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"></path>'; |
|
} |
|
}); |
|
|
|
|
|
resetApiConfig.addEventListener('click', function() { |
|
apiUrl.value = '{{ default_api_url }}'; |
|
apiKey.value = ''; |
|
apiModel.value = '{{ default_api_model }}'; |
|
apiTimeout.value = '{{ default_api_timeout }}'; |
|
saveApiConfig.checked = false; |
|
|
|
|
|
localStorage.removeItem('apiConfig'); |
|
|
|
alert('已重置为默认配置'); |
|
}); |
|
|
|
|
|
testApiConfig.addEventListener('click', async function() { |
|
const url = apiUrl.value.trim(); |
|
const key = apiKey.value.trim(); |
|
const model = apiModel.value.trim(); |
|
const timeout = apiTimeout.value.trim(); |
|
|
|
if (!url) { |
|
alert('请输入API URL'); |
|
return; |
|
} |
|
|
|
if (!key) { |
|
alert('请输入API Key'); |
|
return; |
|
} |
|
|
|
try { |
|
testApiConfig.disabled = true; |
|
testApiConfig.textContent = '测试中...'; |
|
|
|
const response = await fetch('/test_api_connection', { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json', |
|
}, |
|
body: JSON.stringify({ |
|
api_url: url, |
|
api_key: key, |
|
api_model: model, |
|
api_timeout: timeout |
|
}) |
|
}); |
|
|
|
const result = await response.json(); |
|
|
|
if (result.success) { |
|
alert(result.message); |
|
|
|
|
|
if (saveApiConfig.checked) { |
|
saveApiConfigToLocalStorage(); |
|
} |
|
} else { |
|
alert(result.message); |
|
} |
|
} catch (error) { |
|
alert(error.message); |
|
} finally { |
|
testApiConfig.disabled = false; |
|
testApiConfig.textContent = '测试连接'; |
|
} |
|
}); |
|
|
|
|
|
[apiUrl, apiKey, apiModel, apiTimeout].forEach(input => { |
|
input.addEventListener('change', function() { |
|
if (saveApiConfig.checked) { |
|
saveApiConfigToLocalStorage(); |
|
} |
|
}); |
|
}); |
|
|
|
|
|
saveApiConfig.addEventListener('change', function() { |
|
if (this.checked) { |
|
saveApiConfigToLocalStorage(); |
|
} else { |
|
localStorage.removeItem('apiConfig'); |
|
} |
|
}); |
|
}); |
|
|
|
|
|
function saveApiConfigToLocalStorage() { |
|
const apiConfig = { |
|
url: document.getElementById('apiUrl').value.trim(), |
|
model: document.getElementById('apiModel').value.trim(), |
|
key: document.getElementById('apiKey').value.trim(), |
|
timeout: document.getElementById('apiTimeout').value.trim(), |
|
saveEnabled: true |
|
}; |
|
|
|
localStorage.setItem('apiConfig', JSON.stringify(apiConfig)); |
|
} |
|
|
|
|
|
function loadApiConfig() { |
|
const savedConfig = localStorage.getItem('apiConfig'); |
|
|
|
if (savedConfig) { |
|
try { |
|
const config = JSON.parse(savedConfig); |
|
|
|
if (config.url) document.getElementById('apiUrl').value = config.url; |
|
if (config.model) document.getElementById('apiModel').value = config.model; |
|
if (config.key) document.getElementById('apiKey').value = config.key; |
|
if (config.timeout) document.getElementById('apiTimeout').value = config.timeout; |
|
|
|
document.getElementById('saveApiConfig').checked = config.saveEnabled || false; |
|
} catch (error) { |
|
console.error('加载API配置时出错:', error); |
|
} |
|
} |
|
} |
|
</script> |
|
<script> |
|
|
|
function addMarkdownStyles() { |
|
|
|
if (!document.getElementById('markdown-styles')) { |
|
const style = document.createElement('style'); |
|
style.id = 'markdown-styles'; |
|
style.textContent = ` |
|
.prose h1 { font-size: 1.5em; margin-top: 1em; margin-bottom: 0.5em; font-weight: bold; } |
|
.prose h2 { font-size: 1.3em; margin-top: 1em; margin-bottom: 0.5em; font-weight: bold; } |
|
.prose h3 { font-size: 1.1em; margin-top: 1em; margin-bottom: 0.5em; font-weight: bold; } |
|
.prose p { margin-bottom: 1em; line-height: 1.6; } |
|
.prose ul { list-style-type: disc; padding-left: 1.5em; margin-bottom: 1em; } |
|
.prose ol { list-style-type: decimal; padding-left: 1.5em; margin-bottom: 1em; } |
|
.prose li { margin-bottom: 0.5em; } |
|
.prose strong { font-weight: 600; color: #1a56db; } |
|
.prose em { font-style: italic; } |
|
.prose blockquote { border-left: 4px solid #e5e7eb; padding-left: 1em; margin: 1em 0; color: #4b5563; } |
|
.prose code { background-color: #f3f4f6; padding: 0.2em 0.4em; border-radius: 0.25em; font-size: 0.9em; } |
|
|
|
/* 打字机动画样式 */ |
|
.typing-animation { |
|
display: flex; |
|
align-items: center; |
|
} |
|
|
|
.typing-animation span { |
|
height: 10px; |
|
width: 10px; |
|
margin: 0 2px; |
|
background-color: #3b82f6; |
|
border-radius: 50%; |
|
display: inline-block; |
|
animation: typing 1.5s infinite ease-in-out; |
|
} |
|
|
|
.typing-animation span:nth-child(1) { |
|
animation-delay: 0s; |
|
} |
|
|
|
.typing-animation span:nth-child(2) { |
|
animation-delay: 0.3s; |
|
} |
|
|
|
.typing-animation span:nth-child(3) { |
|
animation-delay: 0.6s; |
|
} |
|
|
|
@keyframes typing { |
|
0% { transform: scale(1); opacity: 0.7; } |
|
50% { transform: scale(1.5); opacity: 1; } |
|
100% { transform: scale(1); opacity: 0.7; } |
|
} |
|
|
|
/* 内容淡入效果 */ |
|
.ai-analysis-content { |
|
transition: opacity 0.3s ease; |
|
} |
|
|
|
.ai-analysis-content.fade-in { |
|
opacity: 0; |
|
animation: fadeIn 0.5s forwards; |
|
} |
|
|
|
@keyframes fadeIn { |
|
from { opacity: 0; } |
|
to { opacity: 1; } |
|
} |
|
|
|
/* 高亮新增文本效果 */ |
|
.new-text { |
|
background-color: rgba(59, 130, 246, 0.1); |
|
animation: highlightFade 2s forwards; |
|
} |
|
|
|
@keyframes highlightFade { |
|
from { background-color: rgba(59, 130, 246, 0.1); } |
|
to { background-color: transparent; } |
|
} |
|
`; |
|
document.head.appendChild(style); |
|
} |
|
} |
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
addMarkdownStyles(); |
|
}); |
|
</script> |
|
</body> |
|
</html> |
|
|
|
<script> |
|
function updateFormattedUrl(url) { |
|
const formattedUrlElement = document.getElementById('formattedUrl'); |
|
if (!url) { |
|
formattedUrlElement.textContent = ''; |
|
return; |
|
} |
|
|
|
let formattedUrl; |
|
if (url.endsWith('/')) { |
|
formattedUrl = `${url}chat/completions`; |
|
} else if (url.endsWith('#')) { |
|
formattedUrl = url.replace("#", ""); |
|
} else { |
|
formattedUrl = `${url}/v1/chat/completions`; |
|
} |
|
|
|
formattedUrlElement.innerHTML = ` |
|
<span class="font-medium">/结尾忽略v1版本,#结尾强制使用输入地址:</span><br> |
|
<span class="text-blue-600">${formattedUrl}</span> |
|
`; |
|
} |
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
const apiUrl = document.getElementById('apiUrl'); |
|
if (apiUrl.value) { |
|
updateFormattedUrl(apiUrl.value); |
|
} |
|
}); |
|
|
|
function downloadPDF(){ |
|
const aigc = document.querySelector("think").innerText; |
|
const filename = document.querySelector("div.bg-gradient-to-r.from-blue-600.to-blue-700.px-6.py-4").innerText; |
|
if(aigc!=undefined && aigc!=''){ |
|
fetch("/generate_pdf", { |
|
method: "POST", |
|
headers: { |
|
"Content-Type": "application/json" |
|
}, |
|
body: JSON.stringify({"text":aigc,"filename":filename}) |
|
}) |
|
.then(response => response.blob()) |
|
.then(blob => { |
|
const url = window.URL.createObjectURL(blob); |
|
const a = document.createElement("a"); |
|
a.href = url; |
|
a.download = filename+".pdf"; |
|
document.body.appendChild(a); |
|
a.click(); |
|
a.remove(); |
|
}) |
|
.catch(error => console.error("Error:", error)); |
|
} |
|
} |
|
</script> |