feat: refresh button and prompt settings

This commit is contained in:
0Xiao0
2026-03-25 11:02:56 +08:00
parent 64684b1f48
commit 018c653267
3 changed files with 214 additions and 68 deletions

View File

@ -268,6 +268,7 @@ HTML_TEMPLATE = r"""<!DOCTYPE html>
<option value="priority">Sort: Priority</option> <option value="priority">Sort: Priority</option>
</select> </select>
<button id="toggleExpand">Toggle Expand All</button> <button id="toggleExpand">Toggle Expand All</button>
<button id="refreshData" style="background:#3b82f6;color:white;border:none;" onclick="window.refreshDataAPI()">🔄 Refresh</button>
</div> </div>
<div class="stats" id="statsDisplay"></div> <div class="stats" id="statsDisplay"></div>
@ -276,11 +277,71 @@ HTML_TEMPLATE = r"""<!DOCTYPE html>
<div id="tree-container"></div> <div id="tree-container"></div>
</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> <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} const tabData = {RECORDS_JSON}; // Array of {name, records}
let rawRecords = []; let rawRecords = [];
let records = []; 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) { function extractText(val) {
if (Array.isArray(val) && val.length > 0) return val[0].text || ''; if (Array.isArray(val) && val.length > 0) return val[0].text || '';
return val ? String(val) : ''; return val ? String(val) : '';
@ -695,24 +756,36 @@ window.configAI = function() {
let currentKey = localStorage.getItem('ai_api_key') || '{DEFAULT_AI_KEY}'; let currentKey = localStorage.getItem('ai_api_key') || '{DEFAULT_AI_KEY}';
let currentUrl = localStorage.getItem('ai_base_url') || '{DEFAULT_AI_URL}'; let currentUrl = localStorage.getItem('ai_base_url') || '{DEFAULT_AI_URL}';
let currentModel = localStorage.getItem('ai_model') || '{DEFAULT_AI_MODEL}'; 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); document.getElementById('settingApiKey').value = currentKey;
if (key === null) return; document.getElementById('settingApiUrl').value = currentUrl;
let url = prompt('请输入 API Base URL (例如 https://api.openai.com/v1 或兼容地址):', currentUrl); document.getElementById('settingApiModel').value = currentModel;
if (url === null) return; document.getElementById('settingSystemPrompt').value = currentSys;
let model = prompt('请输入模型名称 (例如 gpt-4o, deepseek-chat):', currentModel); document.getElementById('settingTabPrompt').value = currentTab;
if (model === null) return; document.getElementById('settingGlobalPrompt').value = currentGlobal;
localStorage.setItem('ai_api_key', key); document.getElementById('settingsModal').style.display = 'flex';
localStorage.setItem('ai_base_url', url); };
localStorage.setItem('ai_model', model);
alert('AI 配置已成功保存在浏览器本地!'); 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) { async function fetchAndStreamAI(promptText, box) {
let key = localStorage.getItem('ai_api_key') || '{DEFAULT_AI_KEY}'; let key = localStorage.getItem('ai_api_key') || '{DEFAULT_AI_KEY}';
let url = localStorage.getItem('ai_base_url') || '{DEFAULT_AI_URL}'; let url = localStorage.getItem('ai_base_url') || '{DEFAULT_AI_URL}';
let model = localStorage.getItem('ai_model') || '{DEFAULT_AI_MODEL}'; let model = localStorage.getItem('ai_model') || '{DEFAULT_AI_MODEL}';
let sysPrompt = localStorage.getItem('ai_sys_prompt') || defaultSystemPrompt;
if (!key && url.includes('openai.com')) { if (!key && url.includes('openai.com')) {
alert('系统检测到您未配置 API Key请先进行配置'); alert('系统检测到您未配置 API Key请先进行配置');
@ -738,7 +811,7 @@ async function fetchAndStreamAI(promptText, box) {
body: JSON.stringify({ body: JSON.stringify({
model: model, model: model,
messages: [ messages: [
{ role: 'system', content: '你是一位资深项目负责人,擅长做简洁、高价值的工作汇报,突出结果与业务价值,而非技术细节。' }, { role: 'system', content: sysPrompt },
{ role: 'user', content: promptText } { role: 'user', content: promptText }
], ],
stream: true, stream: true,
@ -804,17 +877,9 @@ window.requestAISummary = function() {
TargetCustomers: m.targetCustomers || "" TargetCustomers: m.targetCustomers || ""
})); }));
const promptText = `请将以下多个任务整理汇报摘要: let tabPrompt = localStorage.getItem('ai_tab_prompt') || defaultTabPrompt;
要求: const promptText = `${tabPrompt}
1. 每个任务用3-4行总结须明确提及该任务相关的 TargetCustomers 目标客户信息)
2. 最后增加【整体情况总结】:
- 完成情况(完成/进行中)
- 当前重点方向(包含核心目标客户的总体进展)
输出结构:
(多个任务总结)
【整体情况总结】
- 数据如下: - 数据如下:
${JSON.stringify(tasksForAI, null, 2)}`; ${JSON.stringify(tasksForAI, null, 2)}`;
@ -859,12 +924,8 @@ window.requestGlobalSummary = function() {
return; return;
} }
const promptText = `请将以下来自多个部门/维度的表格任务进行全局大总结: let globalPrompt = localStorage.getItem('ai_global_prompt') || defaultGlobalPrompt;
要求: const promptText = `${globalPrompt}
1. 按照不同的 Tab (即数据来源) 独立提取核心进展与问题。
2. 特别留意并提炼出 TargetCustomers (目标客户) 的主要推行情况。
3. 最后给出一个【全局统筹结论】,指出哪些方向正常,哪些可能存在延期或需要重点关注。
4. 尽量言简意赅,不要只是把每一条数据罗列一遍,要有跨表分析和高层次的提炼。
- 数据如下: - 数据如下:
${JSON.stringify(allTasks, null, 2)}`; ${JSON.stringify(allTasks, null, 2)}`;

File diff suppressed because one or more lines are too long

View File

@ -6,6 +6,7 @@ import logging
import time import time
import os import os
import sys import sys
import urllib.parse
# Import functions from existing scripts # Import functions from existing scripts
from export_web_view import HTML_TEMPLATE, build_html from export_web_view import HTML_TEMPLATE, build_html
@ -14,7 +15,6 @@ import api
import config import config
PORT = int(os.getenv("PORT", 18080)) PORT = int(os.getenv("PORT", 18080))
CACHE_DURATION = 60 # Cache data for 60 seconds
# Globals to store cached HTML # Globals to store cached HTML
cached_html = None cached_html = None
@ -28,15 +28,17 @@ class FeishuHandler(http.server.SimpleHTTPRequestHandler):
def do_GET(self): def do_GET(self):
global cached_html, last_fetch_time global cached_html, last_fetch_time
parsed_path = urllib.parse.urlparse(self.path)
# Serve dynamic HTML at root or outline_view.html # 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_response(200)
self.send_header('Content-type', 'text/html; charset=utf-8') self.send_header('Content-type', 'text/html; charset=utf-8')
self.end_headers() self.end_headers()
current_time = time.time() current_time = time.time()
if cached_html is None or (current_time - last_fetch_time) > CACHE_DURATION: if cached_html is None:
logging.info("Fetching fresh records from API (Cache expired or missing)...") logging.info("Fetching fresh records from API (Cache missing)...")
try: try:
client = api.Client(config.LARK_HOST) client = api.Client(config.LARK_HOST)
access_token = client.get_tenant_access_token(config.APP_ID, config.APP_SECRET) 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') cached_html = final_html.encode('utf-8')
last_fetch_time = current_time 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: except Exception as e:
logging.error(f"Error fetching data: {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')
error_msg = f"<h1>Internal Server Error: Could not fetch data</h1><p>{e}</p>".encode('utf-8') self.wfile.write(error_msg)
self.wfile.write(error_msg) return
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 # Write response
self.wfile.write(cached_html) self.wfile.write(cached_html)