feat: refresh button and prompt settings
This commit is contained in:
@ -268,6 +268,7 @@ HTML_TEMPLATE = r"""<!DOCTYPE html>
|
||||
<option value="priority">Sort: Priority</option>
|
||||
</select>
|
||||
<button id="toggleExpand">Toggle Expand All</button>
|
||||
<button id="refreshData" style="background:#3b82f6;color:white;border:none;" onclick="window.refreshDataAPI()">🔄 Refresh</button>
|
||||
</div>
|
||||
|
||||
<div class="stats" id="statsDisplay"></div>
|
||||
@ -276,11 +277,71 @@ HTML_TEMPLATE = r"""<!DOCTYPE html>
|
||||
<div id="tree-container"></div>
|
||||
</div>
|
||||
|
||||
<div id="settingsModal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.5); z-index:9999; align-items:center; justify-content:center;">
|
||||
<div style="background:white; padding:24px; border-radius:12px; width:600px; max-width:90%; max-height:90vh; overflow-y:auto; box-shadow:0 10px 25px rgba(0,0,0,0.2); max-height: 80vh;">
|
||||
<h2 style="margin-top:0;">配置 AI 参数与 Prompts</h2>
|
||||
|
||||
<label style="display:block; margin-top:12px; font-weight:bold;">API Key</label>
|
||||
<input type="text" id="settingApiKey" style="width:100%; padding:8px; margin-top:4px; border:1px solid #ccc; border-radius:4px; box-sizing: border-box;" />
|
||||
|
||||
<label style="display:block; margin-top:12px; font-weight:bold;">API Base URL <span style="font-weight:normal; font-size:12px; color:#64748b;">(如 https://api.openai.com/v1)</span></label>
|
||||
<input type="text" id="settingApiUrl" style="width:100%; padding:8px; margin-top:4px; border:1px solid #ccc; border-radius:4px; box-sizing: border-box;" />
|
||||
|
||||
<label style="display:block; margin-top:12px; font-weight:bold;">Model <span style="font-weight:normal; font-size:12px; color:#64748b;">(如 gpt-4o, deepseek-chat)</span></label>
|
||||
<input type="text" id="settingApiModel" style="width:100%; padding:8px; margin-top:4px; border:1px solid #ccc; border-radius:4px; box-sizing: border-box;" />
|
||||
|
||||
<label style="display:block; margin-top:12px; font-weight:bold;">System Prompt</label>
|
||||
<textarea id="settingSystemPrompt" rows="3" style="width:100%; padding:8px; margin-top:4px; border:1px solid #ccc; border-radius:4px; font-family:inherit; box-sizing: border-box;"></textarea>
|
||||
|
||||
<label style="display:block; margin-top:12px; font-weight:bold;">Tab Prompt (Tab AI Summary)</label>
|
||||
<textarea id="settingTabPrompt" rows="5" style="width:100%; padding:8px; margin-top:4px; border:1px solid #ccc; border-radius:4px; font-family:inherit; box-sizing: border-box;"></textarea>
|
||||
|
||||
<label style="display:block; margin-top:12px; font-weight:bold;">Global Prompt (Global AI Summary)</label>
|
||||
<textarea id="settingGlobalPrompt" rows="5" style="width:100%; padding:8px; margin-top:4px; border:1px solid #ccc; border-radius:4px; font-family:inherit; box-sizing: border-box;"></textarea>
|
||||
|
||||
<div style="margin-top:20px; display:flex; justify-content:flex-end; gap:12px;">
|
||||
<button onclick="document.getElementById('settingsModal').style.display='none'" style="padding:8px 16px; border:1px solid #ccc; background:white; cursor:pointer; border-radius:6px;">取消</button>
|
||||
<button onclick="window.saveSettings()" style="padding:8px 16px; border:none; background:#3b82f6; color:white; cursor:pointer; border-radius:6px;">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const defaultSystemPrompt = '你是一位资深项目负责人,擅长做简洁、高价值的工作汇报,突出结果与业务价值,而非技术细节。';
|
||||
const defaultTabPrompt = `请将以下多个任务整理汇报摘要:\n要求:\n1. 每个任务用3-4行总结(须明确提及该任务相关的 TargetCustomers 目标客户信息)\n2. 最后增加【整体情况总结】:\n- 完成情况(完成/进行中)\n- 当前重点方向(包含核心目标客户的总体进展)\n\n输出结构:\n(多个任务总结)\n\n【整体情况总结】`;
|
||||
const defaultGlobalPrompt = `请将以下来自多个部门/维度的表格任务进行全局大总结:\n要求:\n1. 按照不同的 Tab (即数据来源) 独立提取核心进展与问题。\n2. 特别留意并提炼出 TargetCustomers (目标客户) 的主要推行情况。\n3. 最后给出一个【全局统筹结论】,指出哪些方向正常,哪些可能存在延期或需要重点关注。\n4. 尽量言简意赅,不要只是把每一条数据罗列一遍,要有跨表分析和高层次的提炼。`;
|
||||
|
||||
const tabData = {RECORDS_JSON}; // Array of {name, records}
|
||||
let rawRecords = [];
|
||||
let records = [];
|
||||
|
||||
window.refreshDataAPI = async function() {
|
||||
const btn = document.getElementById('refreshData');
|
||||
if (!btn) return;
|
||||
const originalText = btn.innerHTML;
|
||||
btn.innerHTML = '⏳ Pulling latest Feishu data (may take tens of seconds)...';
|
||||
btn.disabled = true;
|
||||
try {
|
||||
const res = await fetch('/api/refresh', {method: 'GET'});
|
||||
if (!res.ok) {
|
||||
let msg = res.statusText;
|
||||
try {
|
||||
const data = await res.json();
|
||||
if(data.message) msg = data.message;
|
||||
} catch(e) {}
|
||||
alert('Refresh failed: ' + msg + '\\nIt might be a timeout connecting to Feishu, please try again later.');
|
||||
} else {
|
||||
alert('Refresh successful! The page will reload to display the latest data.');
|
||||
window.location.href = '/';
|
||||
}
|
||||
} catch(e) {
|
||||
alert('Refresh request error: ' + e.message + '\\nIf using local files, please ensure the backend server.py is running.');
|
||||
} finally {
|
||||
btn.innerHTML = originalText;
|
||||
btn.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
function extractText(val) {
|
||||
if (Array.isArray(val) && val.length > 0) return val[0].text || '';
|
||||
return val ? String(val) : '';
|
||||
@ -695,24 +756,36 @@ window.configAI = function() {
|
||||
let currentKey = localStorage.getItem('ai_api_key') || '{DEFAULT_AI_KEY}';
|
||||
let currentUrl = localStorage.getItem('ai_base_url') || '{DEFAULT_AI_URL}';
|
||||
let currentModel = localStorage.getItem('ai_model') || '{DEFAULT_AI_MODEL}';
|
||||
let currentSys = localStorage.getItem('ai_sys_prompt') || defaultSystemPrompt;
|
||||
let currentTab = localStorage.getItem('ai_tab_prompt') || defaultTabPrompt;
|
||||
let currentGlobal = localStorage.getItem('ai_global_prompt') || defaultGlobalPrompt;
|
||||
|
||||
let key = prompt('请输入您的 API Key (无需则留空或取消):', currentKey);
|
||||
if (key === null) return;
|
||||
let url = prompt('请输入 API Base URL (例如 https://api.openai.com/v1 或兼容地址):', currentUrl);
|
||||
if (url === null) return;
|
||||
let model = prompt('请输入模型名称 (例如 gpt-4o, deepseek-chat):', currentModel);
|
||||
if (model === null) return;
|
||||
document.getElementById('settingApiKey').value = currentKey;
|
||||
document.getElementById('settingApiUrl').value = currentUrl;
|
||||
document.getElementById('settingApiModel').value = currentModel;
|
||||
document.getElementById('settingSystemPrompt').value = currentSys;
|
||||
document.getElementById('settingTabPrompt').value = currentTab;
|
||||
document.getElementById('settingGlobalPrompt').value = currentGlobal;
|
||||
|
||||
localStorage.setItem('ai_api_key', key);
|
||||
localStorage.setItem('ai_base_url', url);
|
||||
localStorage.setItem('ai_model', model);
|
||||
alert('AI 配置已成功保存在浏览器本地!');
|
||||
document.getElementById('settingsModal').style.display = 'flex';
|
||||
};
|
||||
|
||||
window.saveSettings = function() {
|
||||
localStorage.setItem('ai_api_key', document.getElementById('settingApiKey').value);
|
||||
localStorage.setItem('ai_base_url', document.getElementById('settingApiUrl').value);
|
||||
localStorage.setItem('ai_model', document.getElementById('settingApiModel').value);
|
||||
localStorage.setItem('ai_sys_prompt', document.getElementById('settingSystemPrompt').value);
|
||||
localStorage.setItem('ai_tab_prompt', document.getElementById('settingTabPrompt').value);
|
||||
localStorage.setItem('ai_global_prompt', document.getElementById('settingGlobalPrompt').value);
|
||||
|
||||
document.getElementById('settingsModal').style.display = 'none';
|
||||
};
|
||||
|
||||
async function fetchAndStreamAI(promptText, box) {
|
||||
let key = localStorage.getItem('ai_api_key') || '{DEFAULT_AI_KEY}';
|
||||
let url = localStorage.getItem('ai_base_url') || '{DEFAULT_AI_URL}';
|
||||
let model = localStorage.getItem('ai_model') || '{DEFAULT_AI_MODEL}';
|
||||
let sysPrompt = localStorage.getItem('ai_sys_prompt') || defaultSystemPrompt;
|
||||
|
||||
if (!key && url.includes('openai.com')) {
|
||||
alert('系统检测到您未配置 API Key,请先进行配置!');
|
||||
@ -738,7 +811,7 @@ async function fetchAndStreamAI(promptText, box) {
|
||||
body: JSON.stringify({
|
||||
model: model,
|
||||
messages: [
|
||||
{ role: 'system', content: '你是一位资深项目负责人,擅长做简洁、高价值的工作汇报,突出结果与业务价值,而非技术细节。' },
|
||||
{ role: 'system', content: sysPrompt },
|
||||
{ role: 'user', content: promptText }
|
||||
],
|
||||
stream: true,
|
||||
@ -804,17 +877,9 @@ window.requestAISummary = function() {
|
||||
TargetCustomers: m.targetCustomers || "无"
|
||||
}));
|
||||
|
||||
const promptText = `请将以下多个任务整理汇报摘要:
|
||||
要求:
|
||||
1. 每个任务用3-4行总结(须明确提及该任务相关的 TargetCustomers 目标客户信息)
|
||||
2. 最后增加【整体情况总结】:
|
||||
- 完成情况(完成/进行中)
|
||||
- 当前重点方向(包含核心目标客户的总体进展)
|
||||
let tabPrompt = localStorage.getItem('ai_tab_prompt') || defaultTabPrompt;
|
||||
const promptText = `${tabPrompt}
|
||||
|
||||
输出结构:
|
||||
(多个任务总结)
|
||||
|
||||
【整体情况总结】
|
||||
- 数据如下:
|
||||
${JSON.stringify(tasksForAI, null, 2)}`;
|
||||
|
||||
@ -859,12 +924,8 @@ window.requestGlobalSummary = function() {
|
||||
return;
|
||||
}
|
||||
|
||||
const promptText = `请将以下来自多个部门/维度的表格任务进行全局大总结:
|
||||
要求:
|
||||
1. 按照不同的 Tab (即数据来源) 独立提取核心进展与问题。
|
||||
2. 特别留意并提炼出 TargetCustomers (目标客户) 的主要推行情况。
|
||||
3. 最后给出一个【全局统筹结论】,指出哪些方向正常,哪些可能存在延期或需要重点关注。
|
||||
4. 尽量言简意赅,不要只是把每一条数据罗列一遍,要有跨表分析和高层次的提炼。
|
||||
let globalPrompt = localStorage.getItem('ai_global_prompt') || defaultGlobalPrompt;
|
||||
const promptText = `${globalPrompt}
|
||||
|
||||
- 数据如下:
|
||||
${JSON.stringify(allTasks, null, 2)}`;
|
||||
|
||||
File diff suppressed because one or more lines are too long
42
server.py
42
server.py
@ -6,6 +6,7 @@ import logging
|
||||
import time
|
||||
import os
|
||||
import sys
|
||||
import urllib.parse
|
||||
|
||||
# Import functions from existing scripts
|
||||
from export_web_view import HTML_TEMPLATE, build_html
|
||||
@ -14,7 +15,6 @@ import api
|
||||
import config
|
||||
|
||||
PORT = int(os.getenv("PORT", 18080))
|
||||
CACHE_DURATION = 60 # Cache data for 60 seconds
|
||||
|
||||
# Globals to store cached HTML
|
||||
cached_html = None
|
||||
@ -28,15 +28,17 @@ class FeishuHandler(http.server.SimpleHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
global cached_html, last_fetch_time
|
||||
|
||||
parsed_path = urllib.parse.urlparse(self.path)
|
||||
|
||||
# Serve dynamic HTML at root or outline_view.html
|
||||
if self.path == '/' or self.path == '/outline_view.html':
|
||||
if parsed_path.path == '/' or parsed_path.path == '/outline_view.html':
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'text/html; charset=utf-8')
|
||||
self.end_headers()
|
||||
|
||||
current_time = time.time()
|
||||
if cached_html is None or (current_time - last_fetch_time) > CACHE_DURATION:
|
||||
logging.info("Fetching fresh records from API (Cache expired or missing)...")
|
||||
if cached_html is None:
|
||||
logging.info("Fetching fresh records from API (Cache missing)...")
|
||||
try:
|
||||
client = api.Client(config.LARK_HOST)
|
||||
access_token = client.get_tenant_access_token(config.APP_ID, config.APP_SECRET)
|
||||
@ -45,13 +47,35 @@ class FeishuHandler(http.server.SimpleHTTPRequestHandler):
|
||||
|
||||
cached_html = final_html.encode('utf-8')
|
||||
last_fetch_time = current_time
|
||||
logging.info(f"Successfully generated new HTML view. Cached for {CACHE_DURATION}s.")
|
||||
logging.info("Successfully generated new HTML view.")
|
||||
except Exception as e:
|
||||
logging.error(f"Error fetching data: {e}")
|
||||
if cached_html is None:
|
||||
error_msg = f"<h1>Internal Server Error: Could not fetch data</h1><p>{e}</p>".encode('utf-8')
|
||||
self.wfile.write(error_msg)
|
||||
return
|
||||
error_msg = f"<h1>Internal Server Error: Could not fetch data</h1><p>{e}</p>".encode('utf-8')
|
||||
self.wfile.write(error_msg)
|
||||
return
|
||||
|
||||
# Write response
|
||||
self.wfile.write(cached_html)
|
||||
elif parsed_path.path == '/api/refresh':
|
||||
logging.info("Ajax refresh requested. Fetching fresh records from API...")
|
||||
try:
|
||||
client = api.Client(config.LARK_HOST)
|
||||
access_token = client.get_tenant_access_token(config.APP_ID, config.APP_SECRET)
|
||||
|
||||
final_html = build_html(client, access_token)
|
||||
cached_html = final_html.encode('utf-8')
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'application/json; charset=utf-8')
|
||||
self.end_headers()
|
||||
self.wfile.write(b'{"status": "ok"}')
|
||||
except Exception as e:
|
||||
logging.error(f"Ajax Error fetching data: {e}")
|
||||
self.send_response(500)
|
||||
self.send_header('Content-type', 'application/json; charset=utf-8')
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({"status": "error", "message": str(e)}).encode('utf-8'))
|
||||
return
|
||||
|
||||
# Write response
|
||||
self.wfile.write(cached_html)
|
||||
|
||||
Reference in New Issue
Block a user