x1001000 Claude commited on
Commit
95e671d
·
1 Parent(s): 7067f30

Remove has_stock_etf feature and add system prompt modal

Browse files

- Remove has_stock_etf checkbox, logic, and ETF URL generation
- Add system prompt modal UI positioned on left side of screen
- Add 📝 button in chat header to open system prompt viewer
- Implement /system-prompt API endpoint to return current prompt
- Extract system prompt building logic into reusable function
- Add responsive design for mobile devices
- Apply changes to both embedded (chat-widget.js) and standalone (chat-widget.html) versions

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

Files changed (3) hide show
  1. api_server.py +124 -79
  2. chat-widget.html +186 -8
  3. chat-widget.js +186 -8
api_server.py CHANGED
@@ -48,7 +48,6 @@ PRICE = {
48
  }
49
 
50
  SITE_LANGUAGES = ['繁體中文', '简体中文', 'English']
51
- LANGUAGE_PROMPTS = ['- 使用繁體中文', '- 使用简体中文', '- Use English.']
52
  SUBDOMAINS = ['www', 'sc', 'en']
53
  LANG_ROUTES = ['zh-tw', 'zh-cn', 'en-001']
54
 
@@ -74,7 +73,6 @@ class ConfigModel(BaseModel):
74
  has_quickie: bool = True
75
  has_blog: bool = True
76
  has_edm: bool = True
77
- has_stock_etf: bool = True
78
  has_hc: bool = True
79
  has_google_search: bool = True
80
  thinking_model: str = 'gemini-2.5-flash-preview-05-20'
@@ -85,6 +83,13 @@ class SearchRequest(BaseModel):
85
  class SearchResponse(BaseModel):
86
  results: str
87
 
 
 
 
 
 
 
 
88
  # Global variables for token counting
89
  class TokenCounter:
90
  def __init__(self):
@@ -181,14 +186,20 @@ def generate_content(user_prompt, system_prompt, response_type, response_schema,
181
  token_counter.accumulate(response.usage_metadata, model)
182
  return response
183
 
184
- def get_user_prompt_lang(user_prompt, token_counter):
185
- system_prompt = 'Given a user query, identify its language as one of the three: zh-tw, zh-cn, other'
186
  response_type = 'application/json'
187
  response_schema = str
188
  tools = None
189
  try:
190
  response_parsed = generate_content(user_prompt, system_prompt, response_type, response_schema, tools, token_counter).parsed
191
- return {'zh-tw': 0, 'zh-cn': 1, 'other': 2}[response_parsed.lower()]
 
 
 
 
 
 
192
  except Exception as e:
193
  raise HTTPException(status_code=500, detail=f"Language detection error: {e}")
194
 
@@ -282,6 +293,87 @@ def get_retrieval_from_help_center(csv_file, user_prompt, knowledge, token_count
282
  df['html'] = htmls
283
  return df.to_json(orient='records', force_ascii=False)
284
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
  @app.post("/chat", response_model=ChatResponse)
286
  async def chat(request: ChatRequest):
287
  """Main chat endpoint"""
@@ -308,81 +400,10 @@ async def chat(request: ChatRequest):
308
  user_prompt = request.message
309
  contents.append(types.Content(role="user", parts=[types.Part.from_text(text=user_prompt)]))
310
 
311
- # Detect language
312
- site_language_idx = get_user_prompt_lang(user_prompt, token_counter)
 
313
  site_language = SITE_LANGUAGES[site_language_idx]
314
- subdomain = SUBDOMAINS[site_language_idx]
315
-
316
- # Get system prompt
317
- if SYSTEM_PROMPT_URL:
318
- try:
319
- system_prompt = requests.get(SYSTEM_PROMPT_URL).text
320
- except Exception as e:
321
- print(f"Warning: Could not fetch system prompt: {e}")
322
- system_prompt = "You are MM Madam, a helpful financial AI assistant."
323
- else:
324
- system_prompt = "You are MM Madam, a helpful financial AI assistant."
325
-
326
- # Determine prompt type
327
- user_prompt_type_pro = get_user_prompt_type(contents, token_counter)
328
-
329
- if user_prompt_type_pro:
330
- if not config.is_paid_user:
331
- system_prompt += '- 你會鼓勵用戶升級成為付費用戶就能享有完整問答服務,並且提供訂閱方案連結 \n'
332
- system_prompt += f'`https://{subdomain}.macromicro.me/subscribe` \n'
333
-
334
- # Add retrievals based on config
335
- if config.has_chart and config.is_paid_user:
336
- if retrieval := get_retrieval_from_charts_data_api('chart.csv', user_prompt, knowledge, token_counter):
337
- system_prompt += '- MM圖表的資料,當中時間序列(series)包含前值及最新數據,務必引用 \n'
338
- system_prompt += f'網址規則 `https://{subdomain}.macromicro.me/charts/{{id}}/{{slug}}` \n'
339
- system_prompt += f'```\n{retrieval}\n```\n'
340
-
341
- if config.has_quickie and config.is_paid_user:
342
- if retrieval := get_retrieval('quickie.csv', user_prompt, knowledge, token_counter):
343
- system_prompt += '- MM短評的資料 \n'
344
- system_prompt += f'網址規則 `https://{subdomain}.macromicro.me/quickie?id={{id}}` \n' if subdomain != 'en' else ''
345
- system_prompt += f'```\n{retrieval}\n```\n'
346
-
347
- if config.has_blog and config.is_paid_user:
348
- if retrieval := get_retrieval('post.csv', user_prompt, knowledge, token_counter):
349
- system_prompt += '- MM部落格的資料 \n'
350
- system_prompt += f'網址規則 `https://{subdomain}.macromicro.me/blog/{{slug}}` \n' if subdomain != 'en' else ''
351
- system_prompt += f'```\n{retrieval}\n```\n'
352
- if retrieval := get_retrieval('post_en.csv', user_prompt, knowledge, token_counter):
353
- system_prompt += '- MM英文部落格的資料 \n'
354
- system_prompt += f'網址規則 `https://{subdomain}.macromicro.me/blog/{{slug}}` \n' if subdomain == 'en' else ''
355
- system_prompt += f'```\n{retrieval}\n```\n'
356
-
357
- if config.has_edm and config.is_paid_user:
358
- if retrieval := get_retrieval('edm.csv', user_prompt, knowledge, token_counter):
359
- system_prompt += '- MM獨家報告的資料 \n'
360
- system_prompt += f'網址規則 `https://{subdomain}.macromicro.me/mails/edm/{"tc" if site_language[0] == "繁" else "sc"}/display/{{id}}` \n' if subdomain != 'en' else ''
361
- system_prompt += f'```\n{retrieval}\n```\n'
362
-
363
- if config.has_stock_etf:
364
- system_prompt += '- MM美股財報資料庫、ETF專區的網址規則 \n'
365
- system_prompt += f'美股財報資料庫 `https://{subdomain}.macromicro.me/stocks/info/{{ticker_symbol}}` \n'
366
- system_prompt += f'美國ETF專區 `https://{subdomain}.macromicro.me/etf/us/intro/{{ticker_symbol}}` \n'
367
- system_prompt += f'台灣ETF專區 `https://{subdomain}.macromicro.me/etf/tw/intro/{{ticker_symbol}}` \n'
368
-
369
- if config.has_google_search:
370
- if retrieval := get_retrieval_from_google_search(user_prompt, token_counter):
371
- system_prompt += '- 網路搜尋的資料 \n'
372
- system_prompt += f'```\n{retrieval}\n```\n'
373
- else:
374
- if config.has_hc:
375
- lang_route = LANG_ROUTES[site_language_idx]
376
- if retrieval := get_retrieval_from_help_center(f'hc/{lang_route}/_log.csv', user_prompt, knowledge, token_counter):
377
- system_prompt += '- MM幫助中心的資料 \n'
378
- system_prompt += '不要提供來信或來電的客服聯繫方式 \n'
379
- system_prompt += f'網址規則 `https://support.macromicro.me/hc/{lang_route}/articles/{{id}}` \n'
380
- system_prompt += f'```\n{retrieval}\n```\n'
381
- system_prompt += f'- MM幫助中心網址 `https://support.macromicro.me/hc/{lang_route}` \n'
382
- system_prompt += '- 若非網站客服相關問題,你會婉拒回答 \n'
383
-
384
- # Add language instruction
385
- system_prompt += LANGUAGE_PROMPTS[site_language_idx]
386
 
387
  # Generate response
388
  response_type = 'text/plain'
@@ -448,6 +469,30 @@ async def search(request: SearchRequest):
448
  traceback.print_exc() # Print full stack trace
449
  raise HTTPException(status_code=500, detail=str(e))
450
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
451
  @app.get("/")
452
  async def serve_index():
453
  """Serve the main index.html page"""
 
48
  }
49
 
50
  SITE_LANGUAGES = ['繁體中文', '简体中文', 'English']
 
51
  SUBDOMAINS = ['www', 'sc', 'en']
52
  LANG_ROUTES = ['zh-tw', 'zh-cn', 'en-001']
53
 
 
73
  has_quickie: bool = True
74
  has_blog: bool = True
75
  has_edm: bool = True
 
76
  has_hc: bool = True
77
  has_google_search: bool = True
78
  thinking_model: str = 'gemini-2.5-flash-preview-05-20'
 
83
  class SearchResponse(BaseModel):
84
  results: str
85
 
86
+ class SystemPromptRequest(BaseModel):
87
+ message: str
88
+ config: Optional[Dict[str, Any]] = {}
89
+
90
+ class SystemPromptResponse(BaseModel):
91
+ system_prompt: str
92
+
93
  # Global variables for token counting
94
  class TokenCounter:
95
  def __init__(self):
 
186
  token_counter.accumulate(response.usage_metadata, model)
187
  return response
188
 
189
+ def get_site_language_idx(user_prompt, token_counter):
190
+ system_prompt = 'Given a user query, identify its language code'
191
  response_type = 'application/json'
192
  response_schema = str
193
  tools = None
194
  try:
195
  response_parsed = generate_content(user_prompt, system_prompt, response_type, response_schema, tools, token_counter).parsed
196
+ lang_idx_map = {
197
+ 'zh': 0,
198
+ 'zh-tw': 0, 'tw': 0,
199
+ 'zh-cn': 1, 'cn': 1,
200
+ }
201
+ site_language_idx = lang_idx_map.get(response_parsed.lower(), 2)
202
+ return site_language_idx # site_language = 'English' for other languages
203
  except Exception as e:
204
  raise HTTPException(status_code=500, detail=f"Language detection error: {e}")
205
 
 
293
  df['html'] = htmls
294
  return df.to_json(orient='records', force_ascii=False)
295
 
296
+ def build_system_prompt(user_prompt, config, knowledge, token_counter):
297
+ """Build the system prompt based on user input and configuration"""
298
+ # Detect language
299
+ site_language_idx = get_site_language_idx(user_prompt, token_counter)
300
+ site_language = SITE_LANGUAGES[site_language_idx]
301
+ subdomain = SUBDOMAINS[site_language_idx]
302
+
303
+ # Get base system prompt
304
+ system_prompt = ''
305
+ system_prompt += f'- language = "{site_language}"\n'
306
+ system_prompt += f'- subdomain = "{subdomain}"\n'
307
+ if SYSTEM_PROMPT_URL:
308
+ try:
309
+ system_prompt += requests.get(SYSTEM_PROMPT_URL).text
310
+ except:
311
+ system_prompt += "You are MM Madam, a helpful financial AI assistant."
312
+ else:
313
+ system_prompt += "You are MM Madam, a helpful financial AI assistant."
314
+
315
+ # Create dummy contents for type detection
316
+ contents = [{'role': 'user', 'content': user_prompt}]
317
+ dummy_contents = []
318
+ for msg in contents:
319
+ dummy_contents.append(types.Content(
320
+ role="user" if msg['role'] == "user" else "model",
321
+ parts=[types.Part.from_text(text=msg['content'])]
322
+ ))
323
+
324
+ # Determine prompt type
325
+ user_prompt_type_pro = get_user_prompt_type(dummy_contents, token_counter)
326
+
327
+ if user_prompt_type_pro:
328
+ if not config.is_paid_user:
329
+ system_prompt += '- 你會鼓勵用戶升級成為付費用戶就能享有完整問答服務,並且提供訂閱方案連結 \n'
330
+ system_prompt += f'`https://{subdomain}.macromicro.me/subscribe` \n'
331
+
332
+ # Add retrievals based on config
333
+ if config.has_chart and config.is_paid_user:
334
+ if retrieval := get_retrieval_from_charts_data_api('chart.csv', user_prompt, knowledge, token_counter):
335
+ system_prompt += '- MM圖表的資料,當中時間序列(series)包含前值及最新數據,務必引用 \n'
336
+ system_prompt += f'網址規則 `https://{subdomain}.macromicro.me/charts/{{id}}/{{slug}}` \n'
337
+ system_prompt += f'```\n{retrieval}\n```\n'
338
+
339
+ if config.has_quickie and config.is_paid_user:
340
+ if retrieval := get_retrieval('quickie.csv', user_prompt, knowledge, token_counter):
341
+ system_prompt += '- MM短評的資料 \n'
342
+ system_prompt += f'網址規則 `https://{subdomain}.macromicro.me/quickie?id={{id}}` \n' if subdomain != 'en' else ''
343
+ system_prompt += f'```\n{retrieval}\n```\n'
344
+
345
+ if config.has_blog and config.is_paid_user:
346
+ if retrieval := get_retrieval('post.csv', user_prompt, knowledge, token_counter):
347
+ system_prompt += '- MM部落格的資料 \n'
348
+ system_prompt += f'網址規則 `https://{subdomain}.macromicro.me/blog/{{slug}}` \n' if subdomain != 'en' else ''
349
+ system_prompt += f'```\n{retrieval}\n```\n'
350
+ if retrieval := get_retrieval('post_en.csv', user_prompt, knowledge, token_counter):
351
+ system_prompt += '- MM英文部落格的資料 \n'
352
+ system_prompt += f'網址規則 `https://{subdomain}.macromicro.me/blog/{{slug}}` \n' if subdomain == 'en' else ''
353
+ system_prompt += f'```\n{retrieval}\n```\n'
354
+
355
+ if config.has_edm and config.is_paid_user:
356
+ if retrieval := get_retrieval('edm.csv', user_prompt, knowledge, token_counter):
357
+ system_prompt += '- MM獨家報告的資料 \n'
358
+ system_prompt += f'```\n{retrieval}\n```\n'
359
+
360
+ if config.has_google_search:
361
+ if retrieval := get_retrieval_from_google_search(user_prompt, token_counter):
362
+ system_prompt += '- 網路搜尋的資料 \n'
363
+ system_prompt += f'```\n{retrieval}\n```\n'
364
+ else:
365
+ if config.has_hc:
366
+ lang_route = LANG_ROUTES[site_language_idx]
367
+ if retrieval := get_retrieval_from_help_center(f'hc/{lang_route}/_log.csv', user_prompt, knowledge, token_counter):
368
+ system_prompt += '- MM幫助中心的資料 \n'
369
+ system_prompt += '不要提供來信或來電的客服聯繫方式 \n'
370
+ system_prompt += f'網址規則 `https://support.macromicro.me/hc/{lang_route}/articles/{{id}}` \n'
371
+ system_prompt += f'```\n{retrieval}\n```\n'
372
+ system_prompt += f'- MM幫助中心網址 `https://support.macromicro.me/hc/{lang_route}` \n'
373
+ system_prompt += '- 若非網站客服相關問題,你會婉拒回答 \n'
374
+
375
+ return system_prompt
376
+
377
  @app.post("/chat", response_model=ChatResponse)
378
  async def chat(request: ChatRequest):
379
  """Main chat endpoint"""
 
400
  user_prompt = request.message
401
  contents.append(types.Content(role="user", parts=[types.Part.from_text(text=user_prompt)]))
402
 
403
+ # Build system prompt using the extracted function
404
+ system_prompt = build_system_prompt(user_prompt, config, knowledge, token_counter)
405
+ site_language_idx = get_site_language_idx(user_prompt, token_counter)
406
  site_language = SITE_LANGUAGES[site_language_idx]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
 
408
  # Generate response
409
  response_type = 'text/plain'
 
469
  traceback.print_exc() # Print full stack trace
470
  raise HTTPException(status_code=500, detail=str(e))
471
 
472
+ @app.post("/system-prompt", response_model=SystemPromptResponse)
473
+ async def get_system_prompt(request: SystemPromptRequest):
474
+ """Get the system prompt that would be used for a given message and config"""
475
+ token_counter = TokenCounter()
476
+
477
+ try:
478
+ print(f"Received system prompt request: {request.message[:50]}...") # Debug log
479
+ # Parse configuration
480
+ config = ConfigModel(**request.config) if request.config else ConfigModel()
481
+
482
+ # Get knowledge
483
+ knowledge = get_knowledge()
484
+
485
+ # Build system prompt
486
+ system_prompt = build_system_prompt(request.message, config, knowledge, token_counter)
487
+
488
+ return SystemPromptResponse(system_prompt=system_prompt)
489
+
490
+ except Exception as e:
491
+ print(f"System prompt error: {str(e)}") # Debug log
492
+ import traceback
493
+ traceback.print_exc() # Print full stack trace
494
+ raise HTTPException(status_code=500, detail=str(e))
495
+
496
  @app.get("/")
497
  async def serve_index():
498
  """Serve the main index.html page"""
chat-widget.html CHANGED
@@ -325,6 +325,109 @@
325
  margin-bottom: 20px;
326
  }
327
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
  /* Mobile responsive */
329
  @media (max-width: 768px) {
330
  .mm-chat-widget {
@@ -338,6 +441,16 @@
338
  bottom: 70px;
339
  right: -10px;
340
  }
 
 
 
 
 
 
 
 
 
 
341
  }
342
  </style>
343
  </head>
@@ -350,6 +463,7 @@
350
  <h3>👩🏻‍💼 MM Madam</h3>
351
  </div>
352
  <div>
 
353
  <button class="mm-config-toggle" id="mmConfigToggle">⚙️</button>
354
  <button class="mm-chat-close" id="mmChatClose">×</button>
355
  </div>
@@ -387,12 +501,6 @@
387
  📮 MM獨家報告
388
  </label>
389
  </div>
390
- <div class="mm-config-item">
391
- <label>
392
- <input type="checkbox" id="hasStockEtf" checked>
393
- 📈 MM美股財報、ETF專區
394
- </label>
395
- </div>
396
  <div class="mm-config-item">
397
  <label>
398
  <input type="checkbox" id="hasHc" checked>
@@ -437,6 +545,20 @@
437
  </div>
438
  </div>
439
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
440
  <script>
441
  class MMChatWidget {
442
  constructor(config = {}) {
@@ -460,6 +582,10 @@
460
  this.configToggle = document.getElementById('mmConfigToggle');
461
  this.configPanel = document.getElementById('mmConfigPanel');
462
  this.configBack = document.getElementById('mmConfigBack');
 
 
 
 
463
 
464
  // Config elements
465
  this.configElements = {
@@ -468,7 +594,6 @@
468
  hasQuickie: document.getElementById('hasQuickie'),
469
  hasBlog: document.getElementById('hasBlog'),
470
  hasEdm: document.getElementById('hasEdm'),
471
- hasStockEtf: document.getElementById('hasStockEtf'),
472
  hasHc: document.getElementById('hasHc'),
473
  hasSearch: document.getElementById('hasSearch'),
474
  hasMemory: document.getElementById('hasMemory')
@@ -481,6 +606,13 @@
481
  this.sendBtn.addEventListener('click', () => this.sendMessage());
482
  this.configToggle.addEventListener('click', () => this.toggleConfig());
483
  this.configBack.addEventListener('click', () => this.toggleConfig());
 
 
 
 
 
 
 
484
 
485
  this.input.addEventListener('keydown', (e) => {
486
  if (e.key === 'Enter' && !e.shiftKey) {
@@ -529,12 +661,59 @@
529
  this.isOpen = false;
530
  this.panel.classList.remove('open');
531
  this.configPanel.classList.remove('open');
 
532
  }
533
 
534
  toggleConfig() {
535
  this.configPanel.classList.toggle('open');
536
  }
537
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
538
  getConfig() {
539
  return {
540
  is_paid_user: this.configElements.isPaidUser.checked,
@@ -542,7 +721,6 @@
542
  has_quickie: this.configElements.hasQuickie.checked,
543
  has_blog: this.configElements.hasBlog.checked,
544
  has_edm: this.configElements.hasEdm.checked,
545
- has_stock_etf: this.configElements.hasStockEtf.checked,
546
  has_hc: this.configElements.hasHc.checked,
547
  has_search: this.configElements.hasSearch.checked,
548
  has_memory: this.configElements.hasMemory.checked,
 
325
  margin-bottom: 20px;
326
  }
327
 
328
+ .mm-system-prompt-modal {
329
+ position: fixed;
330
+ top: 0;
331
+ left: 0;
332
+ width: 100vw;
333
+ height: 100vh;
334
+ background: rgba(0, 0, 0, 0.5);
335
+ z-index: 20000;
336
+ display: none;
337
+ align-items: center;
338
+ justify-content: flex-start;
339
+ padding-left: 50px;
340
+ }
341
+
342
+ .mm-system-prompt-modal.open {
343
+ display: flex;
344
+ }
345
+
346
+ .mm-system-prompt-content {
347
+ background: white;
348
+ width: 600px;
349
+ max-width: 90vw;
350
+ height: 80vh;
351
+ border-radius: 12px;
352
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
353
+ display: flex;
354
+ flex-direction: column;
355
+ overflow: hidden;
356
+ }
357
+
358
+ .mm-system-prompt-header {
359
+ background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
360
+ color: white;
361
+ padding: 20px 25px;
362
+ display: flex;
363
+ align-items: center;
364
+ justify-content: space-between;
365
+ }
366
+
367
+ .mm-system-prompt-header h3 {
368
+ margin: 0;
369
+ font-size: 18px;
370
+ font-weight: 600;
371
+ }
372
+
373
+ .mm-system-prompt-close {
374
+ background: none;
375
+ border: none;
376
+ color: white;
377
+ cursor: pointer;
378
+ font-size: 20px;
379
+ padding: 0;
380
+ width: 30px;
381
+ height: 30px;
382
+ border-radius: 50%;
383
+ display: flex;
384
+ align-items: center;
385
+ justify-content: center;
386
+ transition: background 0.2s ease;
387
+ }
388
+
389
+ .mm-system-prompt-close:hover {
390
+ background: rgba(255, 255, 255, 0.2);
391
+ }
392
+
393
+ .mm-system-prompt-body {
394
+ flex: 1;
395
+ padding: 25px;
396
+ overflow-y: auto;
397
+ background: #f8f9fa;
398
+ }
399
+
400
+ .mm-system-prompt-text {
401
+ background: white;
402
+ border: 1px solid #e0e0e0;
403
+ border-radius: 8px;
404
+ padding: 20px;
405
+ font-family: Monaco, Consolas, monospace;
406
+ font-size: 12px;
407
+ line-height: 1.5;
408
+ white-space: pre-wrap;
409
+ word-wrap: break-word;
410
+ color: #333;
411
+ max-height: 100%;
412
+ overflow-y: auto;
413
+ }
414
+
415
+ .mm-system-prompt-toggle {
416
+ background: none;
417
+ border: none;
418
+ color: white;
419
+ cursor: pointer;
420
+ font-size: 16px;
421
+ padding: 5px;
422
+ margin-left: 5px;
423
+ border-radius: 4px;
424
+ transition: background 0.2s ease;
425
+ }
426
+
427
+ .mm-system-prompt-toggle:hover {
428
+ background: rgba(255, 255, 255, 0.2);
429
+ }
430
+
431
  /* Mobile responsive */
432
  @media (max-width: 768px) {
433
  .mm-chat-widget {
 
441
  bottom: 70px;
442
  right: -10px;
443
  }
444
+
445
+ .mm-system-prompt-modal {
446
+ padding-left: 10px;
447
+ padding-right: 10px;
448
+ }
449
+
450
+ .mm-system-prompt-content {
451
+ width: 100%;
452
+ height: 90vh;
453
+ }
454
  }
455
  </style>
456
  </head>
 
463
  <h3>👩🏻‍💼 MM Madam</h3>
464
  </div>
465
  <div>
466
+ <button class="mm-system-prompt-toggle" id="mmSystemPromptToggle">📝</button>
467
  <button class="mm-config-toggle" id="mmConfigToggle">⚙️</button>
468
  <button class="mm-chat-close" id="mmChatClose">×</button>
469
  </div>
 
501
  📮 MM獨家報告
502
  </label>
503
  </div>
 
 
 
 
 
 
504
  <div class="mm-config-item">
505
  <label>
506
  <input type="checkbox" id="hasHc" checked>
 
545
  </div>
546
  </div>
547
 
548
+ <div class="mm-system-prompt-modal" id="mmSystemPromptModal">
549
+ <div class="mm-system-prompt-content">
550
+ <div class="mm-system-prompt-header">
551
+ <h3>📝 System Prompt</h3>
552
+ <button class="mm-system-prompt-close" id="mmSystemPromptClose">×</button>
553
+ </div>
554
+ <div class="mm-system-prompt-body">
555
+ <div class="mm-system-prompt-text" id="mmSystemPromptText">
556
+ Loading system prompt...
557
+ </div>
558
+ </div>
559
+ </div>
560
+ </div>
561
+
562
  <script>
563
  class MMChatWidget {
564
  constructor(config = {}) {
 
582
  this.configToggle = document.getElementById('mmConfigToggle');
583
  this.configPanel = document.getElementById('mmConfigPanel');
584
  this.configBack = document.getElementById('mmConfigBack');
585
+ this.systemPromptToggle = document.getElementById('mmSystemPromptToggle');
586
+ this.systemPromptModal = document.getElementById('mmSystemPromptModal');
587
+ this.systemPromptClose = document.getElementById('mmSystemPromptClose');
588
+ this.systemPromptText = document.getElementById('mmSystemPromptText');
589
 
590
  // Config elements
591
  this.configElements = {
 
594
  hasQuickie: document.getElementById('hasQuickie'),
595
  hasBlog: document.getElementById('hasBlog'),
596
  hasEdm: document.getElementById('hasEdm'),
 
597
  hasHc: document.getElementById('hasHc'),
598
  hasSearch: document.getElementById('hasSearch'),
599
  hasMemory: document.getElementById('hasMemory')
 
606
  this.sendBtn.addEventListener('click', () => this.sendMessage());
607
  this.configToggle.addEventListener('click', () => this.toggleConfig());
608
  this.configBack.addEventListener('click', () => this.toggleConfig());
609
+ this.systemPromptToggle.addEventListener('click', () => this.toggleSystemPrompt());
610
+ this.systemPromptClose.addEventListener('click', () => this.closeSystemPrompt());
611
+ this.systemPromptModal.addEventListener('click', (e) => {
612
+ if (e.target === this.systemPromptModal) {
613
+ this.closeSystemPrompt();
614
+ }
615
+ });
616
 
617
  this.input.addEventListener('keydown', (e) => {
618
  if (e.key === 'Enter' && !e.shiftKey) {
 
661
  this.isOpen = false;
662
  this.panel.classList.remove('open');
663
  this.configPanel.classList.remove('open');
664
+ this.systemPromptModal.classList.remove('open');
665
  }
666
 
667
  toggleConfig() {
668
  this.configPanel.classList.toggle('open');
669
  }
670
 
671
+ async toggleSystemPrompt() {
672
+ const isOpen = this.systemPromptModal.classList.contains('open');
673
+ if (isOpen) {
674
+ this.closeSystemPrompt();
675
+ } else {
676
+ await this.openSystemPrompt();
677
+ }
678
+ }
679
+
680
+ async openSystemPrompt() {
681
+ this.systemPromptModal.classList.add('open');
682
+ await this.loadSystemPrompt();
683
+ }
684
+
685
+ closeSystemPrompt() {
686
+ this.systemPromptModal.classList.remove('open');
687
+ }
688
+
689
+ async loadSystemPrompt() {
690
+ try {
691
+ this.systemPromptText.textContent = 'Loading system prompt...';
692
+
693
+ const response = await fetch(`${this.apiUrl}/system-prompt`, {
694
+ method: 'POST',
695
+ headers: {
696
+ 'Content-Type': 'application/json',
697
+ },
698
+ body: JSON.stringify({
699
+ message: this.input.value.trim() || 'Sample message',
700
+ config: this.getConfig()
701
+ })
702
+ });
703
+
704
+ if (!response.ok) {
705
+ throw new Error(`HTTP error! status: ${response.status}`);
706
+ }
707
+
708
+ const data = await response.json();
709
+ this.systemPromptText.textContent = data.system_prompt;
710
+
711
+ } catch (error) {
712
+ console.error('System prompt error:', error);
713
+ this.systemPromptText.textContent = 'Error loading system prompt. Please try again.';
714
+ }
715
+ }
716
+
717
  getConfig() {
718
  return {
719
  is_paid_user: this.configElements.isPaidUser.checked,
 
721
  has_quickie: this.configElements.hasQuickie.checked,
722
  has_blog: this.configElements.hasBlog.checked,
723
  has_edm: this.configElements.hasEdm.checked,
 
724
  has_hc: this.configElements.hasHc.checked,
725
  has_search: this.configElements.hasSearch.checked,
726
  has_memory: this.configElements.hasMemory.checked,
chat-widget.js CHANGED
@@ -393,6 +393,109 @@
393
  background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
394
  }
395
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
396
  @media (max-width: 768px) {
397
  .mm-chat-widget {
398
  bottom: 10px;
@@ -405,6 +508,16 @@
405
  bottom: 70px;
406
  right: -10px;
407
  }
 
 
 
 
 
 
 
 
 
 
408
  }
409
  `;
410
 
@@ -447,6 +560,7 @@
447
  <h3>👩🏻‍💼 MM Madam</h3>
448
  </div>
449
  <div>
 
450
  <button class="mm-config-toggle" id="mmConfigToggle">⚙️</button>
451
  <button class="mm-chat-close" id="mmChatClose">×</button>
452
  </div>
@@ -484,12 +598,6 @@
484
  📮 MM獨家報告
485
  </label>
486
  </div>
487
- <div class="mm-config-item">
488
- <label>
489
- <input type="checkbox" id="hasStockEtf" checked>
490
- 📈 MM美股財報、ETF專區
491
- </label>
492
- </div>
493
  <div class="mm-config-item">
494
  <label>
495
  <input type="checkbox" id="hasHc" checked>
@@ -533,6 +641,20 @@
533
  </div>
534
  </div>
535
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
536
  </div>
537
  `;
538
 
@@ -551,6 +673,10 @@
551
  this.configPanel = document.getElementById('mmConfigPanel');
552
  this.configBack = document.getElementById('mmConfigBack');
553
  this.modeSwitch = document.getElementById('mmModeSwitch');
 
 
 
 
554
 
555
  this.configElements = {
556
  isPaidUser: document.getElementById('isPaidUser'),
@@ -558,7 +684,6 @@
558
  hasQuickie: document.getElementById('hasQuickie'),
559
  hasBlog: document.getElementById('hasBlog'),
560
  hasEdm: document.getElementById('hasEdm'),
561
- hasStockEtf: document.getElementById('hasStockEtf'),
562
  hasHc: document.getElementById('hasHc'),
563
  hasGoogleSearch: document.getElementById('hasGoogleSearch')
564
  };
@@ -572,6 +697,13 @@
572
  this.sendBtn.addEventListener('click', () => this.sendMessage());
573
  this.configToggle.addEventListener('click', () => this.toggleConfig());
574
  this.configBack.addEventListener('click', () => this.toggleConfig());
 
 
 
 
 
 
 
575
 
576
  this.input.addEventListener('keydown', (e) => {
577
  if (e.key === 'Enter' && !e.shiftKey) {
@@ -658,6 +790,7 @@
658
  this.isOpen = false;
659
  this.panel.classList.remove('open');
660
  this.configPanel.classList.remove('open');
 
661
  }
662
 
663
  toggleConfig() {
@@ -674,6 +807,52 @@
674
  }
675
  }
676
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
677
  getConfig() {
678
  return {
679
  is_paid_user: this.configElements.isPaidUser.checked,
@@ -681,7 +860,6 @@
681
  has_quickie: this.configElements.hasQuickie.checked,
682
  has_blog: this.configElements.hasBlog.checked,
683
  has_edm: this.configElements.hasEdm.checked,
684
- has_stock_etf: this.configElements.hasStockEtf.checked,
685
  has_hc: this.configElements.hasHc.checked,
686
  has_google_search: this.configElements.hasGoogleSearch.checked
687
  };
 
393
  background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
394
  }
395
 
396
+ .mm-system-prompt-modal {
397
+ position: fixed;
398
+ top: 0;
399
+ left: 0;
400
+ width: 100vw;
401
+ height: 100vh;
402
+ background: rgba(0, 0, 0, 0.5);
403
+ z-index: 20000;
404
+ display: none;
405
+ align-items: center;
406
+ justify-content: flex-start;
407
+ padding-left: 50px;
408
+ }
409
+
410
+ .mm-system-prompt-modal.open {
411
+ display: flex;
412
+ }
413
+
414
+ .mm-system-prompt-content {
415
+ background: white;
416
+ width: 600px;
417
+ max-width: 90vw;
418
+ height: 80vh;
419
+ border-radius: 12px;
420
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
421
+ display: flex;
422
+ flex-direction: column;
423
+ overflow: hidden;
424
+ }
425
+
426
+ .mm-system-prompt-header {
427
+ background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
428
+ color: white;
429
+ padding: 20px 25px;
430
+ display: flex;
431
+ align-items: center;
432
+ justify-content: space-between;
433
+ }
434
+
435
+ .mm-system-prompt-header h3 {
436
+ margin: 0;
437
+ font-size: 18px;
438
+ font-weight: 600;
439
+ }
440
+
441
+ .mm-system-prompt-close {
442
+ background: none;
443
+ border: none;
444
+ color: white;
445
+ cursor: pointer;
446
+ font-size: 20px;
447
+ padding: 0;
448
+ width: 30px;
449
+ height: 30px;
450
+ border-radius: 50%;
451
+ display: flex;
452
+ align-items: center;
453
+ justify-content: center;
454
+ transition: background 0.2s ease;
455
+ }
456
+
457
+ .mm-system-prompt-close:hover {
458
+ background: rgba(255, 255, 255, 0.2);
459
+ }
460
+
461
+ .mm-system-prompt-body {
462
+ flex: 1;
463
+ padding: 25px;
464
+ overflow-y: auto;
465
+ background: #f8f9fa;
466
+ }
467
+
468
+ .mm-system-prompt-text {
469
+ background: white;
470
+ border: 1px solid #e0e0e0;
471
+ border-radius: 8px;
472
+ padding: 20px;
473
+ font-family: Monaco, Consolas, monospace;
474
+ font-size: 12px;
475
+ line-height: 1.5;
476
+ white-space: pre-wrap;
477
+ word-wrap: break-word;
478
+ color: #333;
479
+ max-height: 100%;
480
+ overflow-y: auto;
481
+ }
482
+
483
+ .mm-system-prompt-toggle {
484
+ background: none;
485
+ border: none;
486
+ color: white;
487
+ cursor: pointer;
488
+ font-size: 16px;
489
+ padding: 5px;
490
+ margin-left: 5px;
491
+ border-radius: 4px;
492
+ transition: background 0.2s ease;
493
+ }
494
+
495
+ .mm-system-prompt-toggle:hover {
496
+ background: rgba(255, 255, 255, 0.2);
497
+ }
498
+
499
  @media (max-width: 768px) {
500
  .mm-chat-widget {
501
  bottom: 10px;
 
508
  bottom: 70px;
509
  right: -10px;
510
  }
511
+
512
+ .mm-system-prompt-modal {
513
+ padding-left: 10px;
514
+ padding-right: 10px;
515
+ }
516
+
517
+ .mm-system-prompt-content {
518
+ width: 100%;
519
+ height: 90vh;
520
+ }
521
  }
522
  `;
523
 
 
560
  <h3>👩🏻‍💼 MM Madam</h3>
561
  </div>
562
  <div>
563
+ <button class="mm-system-prompt-toggle" id="mmSystemPromptToggle">📝</button>
564
  <button class="mm-config-toggle" id="mmConfigToggle">⚙️</button>
565
  <button class="mm-chat-close" id="mmChatClose">×</button>
566
  </div>
 
598
  📮 MM獨家報告
599
  </label>
600
  </div>
 
 
 
 
 
 
601
  <div class="mm-config-item">
602
  <label>
603
  <input type="checkbox" id="hasHc" checked>
 
641
  </div>
642
  </div>
643
  </div>
644
+
645
+ <div class="mm-system-prompt-modal" id="mmSystemPromptModal">
646
+ <div class="mm-system-prompt-content">
647
+ <div class="mm-system-prompt-header">
648
+ <h3>📝 System Prompt</h3>
649
+ <button class="mm-system-prompt-close" id="mmSystemPromptClose">��</button>
650
+ </div>
651
+ <div class="mm-system-prompt-body">
652
+ <div class="mm-system-prompt-text" id="mmSystemPromptText">
653
+ Loading system prompt...
654
+ </div>
655
+ </div>
656
+ </div>
657
+ </div>
658
  </div>
659
  `;
660
 
 
673
  this.configPanel = document.getElementById('mmConfigPanel');
674
  this.configBack = document.getElementById('mmConfigBack');
675
  this.modeSwitch = document.getElementById('mmModeSwitch');
676
+ this.systemPromptToggle = document.getElementById('mmSystemPromptToggle');
677
+ this.systemPromptModal = document.getElementById('mmSystemPromptModal');
678
+ this.systemPromptClose = document.getElementById('mmSystemPromptClose');
679
+ this.systemPromptText = document.getElementById('mmSystemPromptText');
680
 
681
  this.configElements = {
682
  isPaidUser: document.getElementById('isPaidUser'),
 
684
  hasQuickie: document.getElementById('hasQuickie'),
685
  hasBlog: document.getElementById('hasBlog'),
686
  hasEdm: document.getElementById('hasEdm'),
 
687
  hasHc: document.getElementById('hasHc'),
688
  hasGoogleSearch: document.getElementById('hasGoogleSearch')
689
  };
 
697
  this.sendBtn.addEventListener('click', () => this.sendMessage());
698
  this.configToggle.addEventListener('click', () => this.toggleConfig());
699
  this.configBack.addEventListener('click', () => this.toggleConfig());
700
+ this.systemPromptToggle.addEventListener('click', () => this.toggleSystemPrompt());
701
+ this.systemPromptClose.addEventListener('click', () => this.closeSystemPrompt());
702
+ this.systemPromptModal.addEventListener('click', (e) => {
703
+ if (e.target === this.systemPromptModal) {
704
+ this.closeSystemPrompt();
705
+ }
706
+ });
707
 
708
  this.input.addEventListener('keydown', (e) => {
709
  if (e.key === 'Enter' && !e.shiftKey) {
 
790
  this.isOpen = false;
791
  this.panel.classList.remove('open');
792
  this.configPanel.classList.remove('open');
793
+ this.systemPromptModal.classList.remove('open');
794
  }
795
 
796
  toggleConfig() {
 
807
  }
808
  }
809
 
810
+ async toggleSystemPrompt() {
811
+ const isOpen = this.systemPromptModal.classList.contains('open');
812
+ if (isOpen) {
813
+ this.closeSystemPrompt();
814
+ } else {
815
+ await this.openSystemPrompt();
816
+ }
817
+ }
818
+
819
+ async openSystemPrompt() {
820
+ this.systemPromptModal.classList.add('open');
821
+ await this.loadSystemPrompt();
822
+ }
823
+
824
+ closeSystemPrompt() {
825
+ this.systemPromptModal.classList.remove('open');
826
+ }
827
+
828
+ async loadSystemPrompt() {
829
+ try {
830
+ this.systemPromptText.textContent = 'Loading system prompt...';
831
+
832
+ const response = await fetch(`${this.apiUrl}/system-prompt`, {
833
+ method: 'POST',
834
+ headers: {
835
+ 'Content-Type': 'application/json',
836
+ },
837
+ body: JSON.stringify({
838
+ message: this.input.value.trim() || 'Sample message',
839
+ config: this.getConfig()
840
+ })
841
+ });
842
+
843
+ if (!response.ok) {
844
+ throw new Error(`HTTP error! status: ${response.status}`);
845
+ }
846
+
847
+ const data = await response.json();
848
+ this.systemPromptText.textContent = data.system_prompt;
849
+
850
+ } catch (error) {
851
+ console.error('System prompt error:', error);
852
+ this.systemPromptText.textContent = 'Error loading system prompt. Please try again.';
853
+ }
854
+ }
855
+
856
  getConfig() {
857
  return {
858
  is_paid_user: this.configElements.isPaidUser.checked,
 
860
  has_quickie: this.configElements.hasQuickie.checked,
861
  has_blog: this.configElements.hasBlog.checked,
862
  has_edm: this.configElements.hasEdm.checked,
 
863
  has_hc: this.configElements.hasHc.checked,
864
  has_google_search: this.configElements.hasGoogleSearch.checked
865
  };