Spaces:
Runtime error
Runtime error
Upload 3 files
Browse files- index.html +44 -0
- script.js +174 -0
- style.css +232 -0
index.html
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh-Hant">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>預測詐騙訊息</title>
|
| 7 |
+
<link rel="stylesheet" href="style.css"><!-- 引入外部 CSS 樣式 -->
|
| 8 |
+
<script src="script.js"></script> <!-- 引入 JavaScript 檔案 -->
|
| 9 |
+
</head>
|
| 10 |
+
<body>
|
| 11 |
+
<h1>檢查可疑訊息</h1><!-- <h1>字體最大標題,最小<h6>-->
|
| 12 |
+
|
| 13 |
+
<div class="main-container">
|
| 14 |
+
|
| 15 |
+
<!-- 使用者輸入區塊。textarea文字輸入的區塊。placeholder文字尚未輸入時,預設的顯示內容,無法在網頁端編輯。maxlength限制最大自數-->
|
| 16 |
+
<section id="input_area" class="panel">
|
| 17 |
+
<textarea id="predict_info" placeholder="請輸入內容 (最多5000字)" maxlength="5000" ></textarea>
|
| 18 |
+
<div class="button-group">
|
| 19 |
+
<button id="detect_button" type="submit">檢測</button><!---->
|
| 20 |
+
<button id="clear_button" type="reset">清除</button>
|
| 21 |
+
|
| 22 |
+
<!-- 圖片上傳與檢測功能 -->
|
| 23 |
+
<div class="image-upload-group" style="margin-top: 20px;">
|
| 24 |
+
<label for="imageInput">或上傳圖片進行詐騙檢測:</label><br>
|
| 25 |
+
<input type="file" id="imageInput" accept="image/*" />
|
| 26 |
+
<button id="image_button" type="button" style="margin-left: 10px;">圖片檢測</button>
|
| 27 |
+
</div>
|
| 28 |
+
|
| 29 |
+
</div>
|
| 30 |
+
</section>
|
| 31 |
+
<!-- 顯示模型預測結果的區塊 -->
|
| 32 |
+
<section id="output_area" class="panel">
|
| 33 |
+
<h2>檢測結果</h2>
|
| 34 |
+
<!--<p>是一般的文字標籤。<strong>包住的文字會變粗體</strong>。span行內元素(inline),這裡用途顯示後端輸出內容-->
|
| 35 |
+
<p><strong>是否為詐騙訊息:</strong> <span id="is_scam">待檢測</span></p>
|
| 36 |
+
<p><strong>模型預測可疑度:</strong> <span id="confidence_score">待檢測</span></p>
|
| 37 |
+
<p><strong>可疑詞句分析:</strong></p>
|
| 38 |
+
<div id="suspicious_phrases"><!--div和span一樣功用,差別在div能自動換行-->
|
| 39 |
+
<p>請輸入訊息並點擊「檢測」按鈕。</p>
|
| 40 |
+
</div>
|
| 41 |
+
</section>
|
| 42 |
+
</div>
|
| 43 |
+
</body>
|
| 44 |
+
</html>
|
script.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// frontend/script.js
|
| 2 |
+
// 當 DOM (文件物件模型) 完全載入後才執行裡面的程式碼
|
| 3 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 4 |
+
// 取得 HTML 元素:輸入、按鈕、顯示區塊。document.getElementById('對應的html元素id')
|
| 5 |
+
const inputTextArea = document.getElementById('predict_info'); // 輸入訊息的文字區域
|
| 6 |
+
const inputButton = document.getElementById('detect_button'); // 檢測按鈕
|
| 7 |
+
const clearButton = document.getElementById('clear_button'); // 清除按鈕
|
| 8 |
+
|
| 9 |
+
// 取得圖片上傳欄位與圖片按鈕
|
| 10 |
+
const imageInput = document.getElementById('imageInput');
|
| 11 |
+
const imageButton = document.getElementById('image_button');
|
| 12 |
+
|
| 13 |
+
// 取得顯示結果的 HTML 元素
|
| 14 |
+
const normalOrScam = document.getElementById('is_scam'); // 顯示正常或詐騙
|
| 15 |
+
const confidenceScoreSpan = document.getElementById('confidence_score'); // 顯示模型預測可信度
|
| 16 |
+
const suspiciousPhrasesDiv = document.getElementById('suspicious_phrases'); // 顯示可疑詞句列表
|
| 17 |
+
|
| 18 |
+
/*
|
| 19 |
+
後端 FastAPI API 的 URL
|
| 20 |
+
在開發階段,通常是 http://127.0.0.1:8000 或 http://localhost:8000
|
| 21 |
+
請根據你實際運行 FastAPI 的位址和 Port 進行設定
|
| 22 |
+
*/
|
| 23 |
+
const API_URL = "http://127.0.0.1:8000/predict";
|
| 24 |
+
const API_IMAGE_URL = "http://127.0.0.1:8000/predict-image"
|
| 25 |
+
|
| 26 |
+
// --- 檢測按鈕點擊事件監聽器 ---
|
| 27 |
+
// 當檢測按鈕被點擊時,執行非同步函數
|
| 28 |
+
//addEventListener('click', async () => {...})
|
| 29 |
+
// click 是一種 DOM 事件,代表使用者點擊按鈕。
|
| 30 |
+
// async 是「非同步函數」的關鍵字,允許你在函數中使用 await。它讓你可以像同步一樣撰寫非同步程式(例如網路請求)。
|
| 31 |
+
|
| 32 |
+
inputButton.addEventListener('click', async () => {
|
| 33 |
+
const message = inputTextArea.value.trim(); //.value取得<textarea>的輸入內容。
|
| 34 |
+
//.trim()刪掉文字開頭和結尾的空白,避免誤判空訊息。
|
| 35 |
+
// 檢查輸入框是否為空
|
| 36 |
+
if (message.length === 0) {//alert("輸出內容")是瀏覽器內建的彈出提示。視窗屬於 JavaScript 提供的「視覺提示方法」
|
| 37 |
+
alert('請輸入您想檢測的訊息內容。'); // alert彈出提示
|
| 38 |
+
return; // 終止函數執行
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
// 顯示載入中的狀態,給使用者視覺回饋
|
| 42 |
+
normalOrScam.textContent = '檢測中...'; //.textContent->插入純文字
|
| 43 |
+
normalOrScam.style.color = 'gray'; //styly.color改變顏色,style可以想為UI設計
|
| 44 |
+
confidenceScoreSpan.textContent = '計算中...';//.innerHTML->插入 HTML 語法
|
| 45 |
+
suspiciousPhrasesDiv.innerHTML = '<p>正在分析訊息,請稍候...</p>';//<></>大部分html語法長這樣
|
| 46 |
+
|
| 47 |
+
try {//try...catch處理程式錯誤。
|
| 48 |
+
//fetch(API_URL, {...})。API_URL第17段的變數。method:為html方法,POST送出請求。headers告訴伺服器傳送的資料格式是什麼。
|
| 49 |
+
//這段是用 fetch 來呼叫後端 API,送出 POST 請求:
|
| 50 |
+
const response = await fetch(API_URL, {
|
| 51 |
+
method: 'POST', // 指定 HTTP 方法為 POST
|
| 52 |
+
headers: { // 告訴伺服器發送的資料是 JSON 格式。選JSON原因:
|
| 53 |
+
//我們使用前端(JavaScript)與後端(Python FastAPI)是兩種不同的語言,而JSON是前後端通訊的「共通語言」,交換最通用、最方便、最安全的格式。
|
| 54 |
+
'Content-Type': 'application/json'
|
| 55 |
+
},
|
| 56 |
+
//把JavaScript物件{text:message}轉換成JSON格式字串,字串作為請求的主體 (body)
|
| 57 |
+
body: JSON.stringify({ text: message}),
|
| 58 |
+
});
|
| 59 |
+
|
| 60 |
+
// 檢查 HTTP 回應是否成功 (例如:狀態碼 200 OK)
|
| 61 |
+
if (!response.ok) { // 如果回應狀態碼不是 2xx (成功),則拋出錯誤
|
| 62 |
+
//建立一個錯誤對象並丟出來,強制跳到catch{}區塊。response.status:HTTP狀態碼(如500)。
|
| 63 |
+
throw new Error(`伺服器錯誤: ${response.status} ${response.statusText} `);
|
| 64 |
+
}
|
| 65 |
+
// 變數data,儲存後端成功回傳的資料。
|
| 66 |
+
const data = await response.json();
|
| 67 |
+
// 因為後端回傳的欄位名稱status、confidence、suspicious_keywords跟傳進去的參數一致,所以拿的到後端回傳的資料。
|
| 68 |
+
updateResults( //呼叫function,分別對應function的
|
| 69 |
+
data.status, //isScam,輸出正常或詐騙
|
| 70 |
+
data.confidence, //confidence,輸出信心值
|
| 71 |
+
data.suspicious_keywords,//suspiciousParts,可疑關鍵字
|
| 72 |
+
data.highlighted_text
|
| 73 |
+
);
|
| 74 |
+
} catch (error) {// 捕獲並處理任何在 fetch 過程中發生的錯誤 (例如網路問題、CORS 錯誤)
|
| 75 |
+
|
| 76 |
+
console.error('訊息檢測失敗:', error);// 在開發者工具的控制台顯示錯誤
|
| 77 |
+
alert(`訊息檢測失敗,請檢查後端服務是否運行或網路連線。\n錯誤詳情: ${error.message}`); // 彈出錯誤提示
|
| 78 |
+
resetResults(); // 將介面恢復到初始狀態
|
| 79 |
+
}
|
| 80 |
+
});
|
| 81 |
+
|
| 82 |
+
imageButton.addEventListener('click', async()=>{
|
| 83 |
+
const file = imageInput.files[0]; //取得上傳相片
|
| 84 |
+
if (!file){
|
| 85 |
+
alert("請先選擇圖片");
|
| 86 |
+
return;
|
| 87 |
+
}
|
| 88 |
+
// 顯示載入中提示
|
| 89 |
+
normalOrScam.textContent = '圖片分析中...';
|
| 90 |
+
normalOrScam.style.color = 'gray';
|
| 91 |
+
confidenceScoreSpan.textContent = '計算中...';
|
| 92 |
+
suspiciousPhrasesDiv.innerHTML = '<p>正在從圖片擷取文字與分析中...</p>';
|
| 93 |
+
|
| 94 |
+
try{
|
| 95 |
+
const formData = new FormData();
|
| 96 |
+
formData.append("file", file); // 附加圖片檔案給後端
|
| 97 |
+
const response = await fetch(API_IMAGE_URL,{
|
| 98 |
+
method : "POST",
|
| 99 |
+
body : formData
|
| 100 |
+
});
|
| 101 |
+
if (!response.ok){
|
| 102 |
+
throw new Error(`圖片分析失敗: ${response.status} ${response.statusText}`);
|
| 103 |
+
}
|
| 104 |
+
const data = await response.json();
|
| 105 |
+
updateResults(
|
| 106 |
+
data.status,
|
| 107 |
+
data.confidence,
|
| 108 |
+
data.suspicious_keywords,
|
| 109 |
+
data.highlighted_text
|
| 110 |
+
)
|
| 111 |
+
}catch(error) {
|
| 112 |
+
console.error("圖片上傳失敗",error);
|
| 113 |
+
alert("圖片分析失敗")
|
| 114 |
+
resetResults();
|
| 115 |
+
}
|
| 116 |
+
});
|
| 117 |
+
|
| 118 |
+
function escapeRegExp(string) {
|
| 119 |
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
| 120 |
+
}
|
| 121 |
+
/*
|
| 122 |
+
function highlightSuspiciousWords(text, suspiciousParts) {
|
| 123 |
+
let highlighted = text;
|
| 124 |
+
suspiciousParts.forEach(word => {
|
| 125 |
+
if (word.length < 2) return; // 避免標記太短詞(如單個字或符號)
|
| 126 |
+
const pattern = new RegExp(escapeRegExp(word), 'g');
|
| 127 |
+
highlighted = highlighted.replace(pattern, `<span class="highlight">${word}</span>`);
|
| 128 |
+
});
|
| 129 |
+
return highlighted;
|
| 130 |
+
}
|
| 131 |
+
*/
|
| 132 |
+
// --- 清除按鈕點擊事件監聽器 ---
|
| 133 |
+
// 當清除按鈕被點擊時,執行函數
|
| 134 |
+
clearButton.addEventListener('click', () => {
|
| 135 |
+
inputTextArea.value = '';// 清空輸入框內容
|
| 136 |
+
resetResults(); // 重置顯示結果
|
| 137 |
+
});
|
| 138 |
+
|
| 139 |
+
// --- 清除按鈕點擊事件監聽器 ---
|
| 140 |
+
// 當清除按鈕被點擊時,執行函數
|
| 141 |
+
clearButton.addEventListener('click', () => {
|
| 142 |
+
inputTextArea.value = '';// 清空輸入框內容
|
| 143 |
+
resetResults(); // 重置顯示結果
|
| 144 |
+
});
|
| 145 |
+
|
| 146 |
+
/**這是 JSDoc 註解格式,給開發者與編輯器看的,不會執行。
|
| 147 |
+
* 更新結果顯示的輔助函數
|
| 148 |
+
* @param {string} isScam - 是否為詐騙訊息 (從後端獲取)(原始要得"@"param {string} isScam )
|
| 149 |
+
* @param {number} confidence - 模型預測可信度 (從後端獲取)
|
| 150 |
+
* @param {string[]} suspiciousParts - 可疑詞句陣列 (從後端獲取)
|
| 151 |
+
*/
|
| 152 |
+
|
| 153 |
+
//回傳輸出給index.html顯示
|
| 154 |
+
function updateResults(isScam, confidence, suspiciousParts, highlightedText) {
|
| 155 |
+
normalOrScam.textContent = isScam;
|
| 156 |
+
confidenceScoreSpan.textContent = confidence;
|
| 157 |
+
|
| 158 |
+
if (confidence < 15) {
|
| 159 |
+
suspiciousPhrasesDiv.innerHTML = '<p>此訊息為低風險,未發現可疑詞句。</p>';
|
| 160 |
+
} else {
|
| 161 |
+
suspiciousPhrasesDiv.innerHTML = highlightedText;
|
| 162 |
+
}
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
/**
|
| 166 |
+
* 重置所有顯示結果為初始狀態的輔助函數
|
| 167 |
+
*/
|
| 168 |
+
function resetResults() {
|
| 169 |
+
normalOrScam.textContent = '待檢測';
|
| 170 |
+
normalOrScam.style.color = 'inherit'; // 恢復預設顏色
|
| 171 |
+
confidenceScoreSpan.textContent = '待檢測';
|
| 172 |
+
suspiciousPhrasesDiv.innerHTML = '<p>請輸入訊息並點擊「檢測!」按鈕。</p>';
|
| 173 |
+
}
|
| 174 |
+
});
|
style.css
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* style.css */
|
| 2 |
+
|
| 3 |
+
body {
|
| 4 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 5 |
+
margin: 0; /* 將 body 的 margin 設為 0,讓內容可以更貼近邊緣 */
|
| 6 |
+
padding: 20px; /* 內邊距留點空間 */
|
| 7 |
+
background-color: #f4f7f6;
|
| 8 |
+
color: #333;
|
| 9 |
+
line-height: 1.6;
|
| 10 |
+
display: flex; /* 讓 body 成為 flex 容器 */
|
| 11 |
+
flex-direction: column; /* 內容垂直排列 */
|
| 12 |
+
min-height: 100vh; /* 讓 body 至少佔滿整個視窗高度 */
|
| 13 |
+
align-items: center; /* 讓 h1 居中 */
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
.highlight {
|
| 17 |
+
background-color: #fc9c9cd5;
|
| 18 |
+
color: #000000;
|
| 19 |
+
font-weight: bold;
|
| 20 |
+
padding: 2px 4px;
|
| 21 |
+
border-radius: 4px;
|
| 22 |
+
}
|
| 23 |
+
.yellow-highlight {
|
| 24 |
+
background-color: #ffeb3b85; /* 半透明黃色 */
|
| 25 |
+
color: #000000;
|
| 26 |
+
font-weight: bold;
|
| 27 |
+
padding: 2px 4px;
|
| 28 |
+
border-radius: 4px;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
.red-highlight {
|
| 32 |
+
background-color: #fc9c9cd5;
|
| 33 |
+
color: #000000;
|
| 34 |
+
font-weight: bold;
|
| 35 |
+
padding: 2px 4px;
|
| 36 |
+
border-radius: 4px;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
h1 {
|
| 40 |
+
color: #2c3e50;
|
| 41 |
+
text-align: center;
|
| 42 |
+
margin-bottom: 30px; /* 增加標題下方的間距 */
|
| 43 |
+
font-size: 2.5em; /* 讓標題更大一點 */
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
h2 { /* 針對檢測結果的 h2 */
|
| 47 |
+
color: #2c3e50;
|
| 48 |
+
text-align: center;
|
| 49 |
+
margin-top: 0; /* 移除頂部 margin,讓它更靠近 panel 頂部 */
|
| 50 |
+
margin-bottom: 20px;
|
| 51 |
+
font-size: 1.8em;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
/* --- 主容器 Flexbox 佈局 --- */
|
| 55 |
+
.main-container {
|
| 56 |
+
display: flex; /* 啟用 Flexbox */
|
| 57 |
+
flex-direction: row; /* 預設就是 row,讓子元素水平排列 */
|
| 58 |
+
gap: 30px; /* 左右兩個 panel 之間的間距 */
|
| 59 |
+
width: 100%; /* 佔滿可用寬度 */
|
| 60 |
+
max-width: 1200px; /* 設定最大寬度,避免在寬螢幕上過於分散 */
|
| 61 |
+
justify-content: center; /* 內容居中 */
|
| 62 |
+
flex-wrap: wrap; /* 當螢幕太小時,允許換行 */
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
.panel {
|
| 66 |
+
background-color: #ffffff;
|
| 67 |
+
padding: 30px; /* 增加內邊距 */
|
| 68 |
+
border-radius: 8px;
|
| 69 |
+
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1); /* 更明顯的陰影 */
|
| 70 |
+
flex: 1; /* 讓兩個 panel 平均分配空間 */
|
| 71 |
+
min-width: 380px; /* 設定每個 panel 的最小寬度,避免縮得太小 */
|
| 72 |
+
box-sizing: border-box; /* 確保 padding 和 border 不會增加元素總寬度 */
|
| 73 |
+
display: flex; /* 讓 panel 內部內容也是 flex 容器 */
|
| 74 |
+
flex-direction: column; /* 內部內容垂直排列 */
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
#input_area {
|
| 78 |
+
/* 特定於 input_area 的樣式,如果需要 */
|
| 79 |
+
align-items: center; /* 讓輸入框和按鈕在 input_area 中居中 */
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
textarea {
|
| 85 |
+
width: 100%; /* 佔滿 panel 寬度 */
|
| 86 |
+
height: 250px; /* 增加高度 */
|
| 87 |
+
padding: 15px;
|
| 88 |
+
margin-bottom: 25px; /* 增加與按鈕的間距 */
|
| 89 |
+
border: 1px solid #ddd;
|
| 90 |
+
border-radius: 5px;
|
| 91 |
+
font-size: 1.1rem; /* 稍微放大字體 */
|
| 92 |
+
box-sizing: border-box;
|
| 93 |
+
resize: vertical;
|
| 94 |
+
outline: none; /* 移除 focus 時的藍色邊框 */
|
| 95 |
+
transition: border-color 0.3s ease;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
textarea:focus {
|
| 99 |
+
border-color: #4CAF50; /* focus 時邊框變色 */
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
.button-group {
|
| 104 |
+
display: flex;
|
| 105 |
+
gap: 20px; /* 按鈕間距 */
|
| 106 |
+
justify-content: center; /* 按鈕在 group 內部居中 */
|
| 107 |
+
width: 100%; /* 佔滿寬度 */
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
button {
|
| 111 |
+
padding: 12px 30px; /* 稍微增加按鈕大小 */
|
| 112 |
+
font-size: 1.1rem;
|
| 113 |
+
cursor: pointer;
|
| 114 |
+
border: none;
|
| 115 |
+
border-radius: 5px;
|
| 116 |
+
transition: background-color 0.3s ease, transform 0.2s ease; /* 增加 transform 過渡效果 */
|
| 117 |
+
font-weight: bold; /* 字體加粗 */
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
button[type="submit"] {
|
| 121 |
+
background-color: #4CAF50;
|
| 122 |
+
color: white;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
button[type="submit"]:hover {
|
| 126 |
+
background-color: #45a049;
|
| 127 |
+
transform: translateY(-2px); /* 懸停時向上輕微移動 */
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
button[type="reset"] {
|
| 131 |
+
background-color: #f44336;
|
| 132 |
+
color: white;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
button[type="reset"]:hover {
|
| 136 |
+
background-color: #da190b;
|
| 137 |
+
transform: translateY(-2px);
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
#output_area p {
|
| 142 |
+
font-size: 1.15rem; /* 稍微放大結果文字 */
|
| 143 |
+
margin-bottom: 12px;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
#output_area strong {
|
| 147 |
+
color: #555;
|
| 148 |
+
font-weight: bold;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
#is_scam, #confidence_score {
|
| 152 |
+
font-weight: bold; /* 結果狀態字體加粗 */
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
#suspicious_phrases {
|
| 156 |
+
background-color: #fffafa; /* 給可疑詞句區塊一個淺色背景 */
|
| 157 |
+
border: 1px dashed #e0baba; /* 虛線邊框 */
|
| 158 |
+
padding: 15px;
|
| 159 |
+
border-radius: 5px;
|
| 160 |
+
margin-top: 15px;
|
| 161 |
+
min-height: 80px; /* 確保高度,避免內容少時高度變化 */
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
#suspicious_phrases ul {
|
| 165 |
+
list-style-type: '🚨 '; /* 使用表情符號作為列表標記 */
|
| 166 |
+
padding-left: 20px;
|
| 167 |
+
margin: 0; /* 移除預設 margin */
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
#suspicious_phrases li {
|
| 171 |
+
margin-bottom: 8px;
|
| 172 |
+
color: #c0392b;
|
| 173 |
+
font-weight: 500;
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
#suspicious_phrases p {
|
| 177 |
+
font-style: italic;
|
| 178 |
+
color: #666;
|
| 179 |
+
margin: 0; /* 移除預設 margin */
|
| 180 |
+
}
|
| 181 |
+
label[for="modeSelect"] {
|
| 182 |
+
display: block;
|
| 183 |
+
margin-bottom: 8px;
|
| 184 |
+
font-weight: bold;
|
| 185 |
+
color: #495057;
|
| 186 |
+
font-size: 15px;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
#modeSelect {
|
| 190 |
+
width: 100%;
|
| 191 |
+
padding: 10px;
|
| 192 |
+
font-size: 15px;
|
| 193 |
+
border-radius: 6px;
|
| 194 |
+
border: 1px solid #adb5bd;
|
| 195 |
+
background-color: #ffffff;
|
| 196 |
+
margin-bottom: 20px;
|
| 197 |
+
transition: 0.2s;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
#modeSelect:focus {
|
| 201 |
+
border-color: #74c0fc;
|
| 202 |
+
box-shadow: 0 0 0 0.1rem rgba(116, 192, 252, 0.25);
|
| 203 |
+
outline: none;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
/* --- 響應式設計:當螢幕較小時,垂直排列 --- */
|
| 208 |
+
@media (max-width: 768px) {
|
| 209 |
+
.main-container {
|
| 210 |
+
flex-direction: column; /* 小螢幕時改為垂直堆疊 */
|
| 211 |
+
gap: 20px; /* 垂直間距 */
|
| 212 |
+
padding: 0 15px; /* 左右邊距 */
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
.panel {
|
| 216 |
+
flex: none; /* 取消 flex 比例,讓他們各自佔據 100% 寬度 */
|
| 217 |
+
width: 100%;
|
| 218 |
+
max-width: none; /* 移除最大寬度限制 */
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
h1 {
|
| 222 |
+
font-size: 2em;
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
h2 {
|
| 226 |
+
font-size: 1.5em;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
textarea {
|
| 230 |
+
height: 200px;
|
| 231 |
+
}
|
| 232 |
+
}
|