分页: 4 / 10

Re: 工欲善其事,必先利其器:準備更新哈哈倉頡

发表于 : 2026年 5月 22日 20:28
ejsoon
為什麼在單字模式中,點擊一個edit-item,再點擊另一個,這個字沒有移到另一個字的前面或後面呢?


gemini pro:

代码: 全选

這是一個非常敏銳的觀察!這確實是程式碼邏輯中的一個漏洞。

這個字沒有移動的原因在於**檢測條件放錯了位置**。在原先的程式碼中,判斷兩個字能否移動的邏輯被包在一個大括號裡:它去搜尋 `mcc0string`(這是一個專門存放**詞組**的陣列)來找你點擊的字。

但在**單字模式**中,你點擊的是**單一漢字**,這些字存在於 `charDefCodeMap` 而不是 `mcc0string`。因此,程式在詞組名單裡找不到這兩個漢字,返回了 `-1`,導致整個移動邏輯直接被跳過(Bypassed),什麼事都沒發生。

為了解決這個問題,我們需要把「單字模式」與「詞組模式」的搜尋邏輯完全分開。

以下是完整的修正代碼。請替換掉你原來的 `handleItemClick` 函數:

### Update `handleItemClick`

```javascript
function handleItemClick(word) {
  if (pendingAction) {
    applyActionToWord(pendingAction, word);
    pendingAction = null;
    selectedEditItem = null;
    updateActionButtonsUI();
    return;
  }

  if (!selectedEditItem) {
    selectedEditItem = word;
    justMovedItem = null; // Clear previous just-moved styling when making a new selection
    renderEditArea();
  } else {
    if (selectedEditItem === word) {
      selectedEditItem = null;
      renderEditArea();
      return;
    }

    let oldState = getSystemState();

    if (isSingleWordMode) {
      // --- SINGLE WORD MODE LOGIC ---
      // Find which code row this word belongs to within active keys
      let targetCode = Array.from(activeEditKeys).find(k => (charDefCodeMap.get(k) || []).includes(word));
      let sourceCode = Array.from(activeEditKeys).find(k => (charDefCodeMap.get(k) || []).includes(selectedEditItem));

      if (targetCode && targetCode === sourceCode) {
        let arr = charDefCodeMap.get(targetCode);
        let charIdxA = arr.indexOf(selectedEditItem);
        let charIdxB = arr.indexOf(word);

        // Check array bounds specific to single characters
        if (charIdxA > -1 && charIdxB > -1 && charIdxA !== charIdxB) {
          // Format descriptive log: "Word A -> before/after Word B"
          let placement = charIdxA > charIdxB ? 'before' : 'after';
          saveStateForUndo(`${selectedEditItem} -> ${placement} ${word}`);

          arr.splice(charIdxA, 1);
          let newIdxB = arr.indexOf(word);
          arr.splice(charIdxA > charIdxB ? newIdxB : newIdxB + 1, 0, selectedEditItem);
          justMovedItem = selectedEditItem;

          // Rebuild %chardef block in filecontent directly to persist single word moves
          let linesFile = filecontent.split('\n');
          let chardefBeginIdx = linesFile.indexOf('%chardef begin');
          let chardefEndIdx = -1;
          let inDef = false;

          for (let i = 0; i < linesFile.length; i++) {
            if (linesFile[i] === '%chardef begin') {
              inDef = true;
              continue;
            }
            if (!inDef) continue;
            if (
              linesFile[i] === '%chardef end' ||
              linesFile[i].startsWith('x') ||
              /[^z]z/.test(linesFile[i].split('\t')[0])
            ) {
              chardefEndIdx = i;
              break;
            }
          }

          if (chardefBeginIdx > -1 && chardefEndIdx > -1) {
            let newBlock = [];
            for (let [c, chars] of charDefCodeMap) {
              chars.forEach(ch => newBlock.push(`${c}\t${ch}`));
            }
            linesFile.splice(chardefBeginIdx + 1, chardefEndIdx - chardefBeginIdx - 1, ...newBlock);
            filecontent = linesFile.join('\n');

            // Execute global processing phase to synchronize state and refresh UI structures
            processContent();
            let newState = getSystemState();
            detectAndRenderChanges(oldState, newState);
          }
          renderEditArea();
        }
      }
    } else {
      // --- PHRASE MODE LOGIC ---
      let lines = mcc0string.split('\n').filter(l => l.trim() !== '');
      let originalIdxA = lines.indexOf(selectedEditItem);
      let originalIdxB = lines.indexOf(word);

      // Check array bounds specific to phrases
      if (originalIdxA > -1 && originalIdxB > -1 && originalIdxA !== originalIdxB) {
        let placement = originalIdxA > originalIdxB ? 'before' : 'after';
        saveStateForUndo(`${selectedEditItem} -> ${placement} ${word}`);

        // Extract A from lines
        lines.splice(originalIdxA, 1);

        // Recalculate target position index for element B after A is removed
        let newIdxB = lines.indexOf(word);

        // If A was originally after B, move A to be before B
        if (originalIdxA > originalIdxB) {
          lines.splice(newIdxB, 0, selectedEditItem);
        }
        // If A was originally before B, move A to be after B
        else {
          lines.splice(newIdxB + 1, 0, selectedEditItem);
        }

        justMovedItem = selectedEditItem; // Target item now registers as just-moved
        processContent(JSON.stringify(lines));
        let newState = getSystemState();
        detectAndRenderChanges(oldState, newState);
      }
    }
    
    selectedEditItem = null; // Deselect element once movement completes
  }
}

```

Re: 工欲善其事,必先利其器:準備更新哈哈倉頡

发表于 : 2026年 5月 22日 20:55
ejsoon
簡而言之,兩個程式在載入文本文件之後,都執行了哪些函式?

代码: 全选

簡而言之,這兩個程式在載入文本文件(透過檔案上傳欄位的 `change` 事件)後,核心執行的 JavaScript 函式流程如下:

### 1. 第一個程式 (`domchar5741.html`:碼表詞組處理)

當使用者選取檔案後,主要依序執行了以下函式:

* **`handleFile(event)`**:
* 負責接收檔案物件,隱藏上傳標籤。
* 建立 `FileReader`,並在讀取完成後觸發呼叫 `processContent()`。


* **`processContent()`**(核心業務函式):
* 內部分別清空所有 Map 容器。
* 將文字切行,解析 `%chardef begin` 到碼表結束之間的字位定義,並將首碼、次碼存入 `ccharfirst` 與 `ccharsecond`。
* 篩選、去重並擷取詞組總表(`mcc0string`),再依據編碼與長度規則將詞組分類至 `mcc0` ~ `mcc4` 的各個 Map 中。
* 自動計算當前時間戳記,組合出新文字,轉換成 Blob 並更新下載連結。
* 最後將主界面分頁系統顯示出來。


* *備註:網頁中雖有名為 `resetPage()` 的函式,但它是用於清空與還原初始狀態,在初次載入檔案時不會主動執行。*

---

### 2. 第二個程式 (`domu1.html`:重碼分析工具)

當使用者選取檔案後,主要依序執行了以下函式:

* **(匿名事件監聽函式)**:
* 在 `fileInput` 的 `change` 事件監聽器中,負責更新畫面的選取狀態提示,並直接呼叫 `analyzeFile()`。


* **`analyzeFile()`**(主體異步函式):
* 使用 `file.text()` 異步讀取純文字。
* 利用正則表達式清洗行尾,並尋找 `z` 或 `x` 開頭的臨界點以確定有效範圍。
* 解析編碼並將漢字存入 `codeMap`,並完成重碼與碼長的分類統計。
* 計算魔改後綴(消重碼),將結果分別輸出到網頁上的多個 `textarea` 文本框中。
* 最後呼叫 `integrateDownload()`。


* **`integrateDownload()`**(文件整合與導出函式):
* 負責獲取當前系統時間戳記、更新 `%version`。
* 將原本碼表的漢字定義區塊替換為排序後的規格,並在 `x` 開頭行前插入魔改去重數據(`gzhaha`)。
* 轉換為 Blob 並動態生成最終的整合下載連結。



### 總結

* **第一個程式**:觸發 `handleFile` $\rightarrow$ 呼叫 `processContent`(一擊完成所有解析與下載準備)。
* **第二個程式**:觸發監聽器 $\rightarrow$ 呼叫 `analyzeFile` $\rightarrow$ 內部再呼叫 `integrateDownload`(分工完成分析與文件重組)。

Re: 工欲善其事,必先利其器:準備更新哈哈倉頡

发表于 : 2026年 5月 23日 00:57
ejsoon
把domu1.html的功能合並至domchar5741.html中,按以下要求進行:
一,將domu1.html中的analyzeFile複製過來。當文本載入時,先執行analyzeFile函式(不用生成download),之後再把結果交給processContent函式。

二,把#output和#download,擴展為一個美觀的概覽,它的項目為:單字數量(cchardefine 行數),尹末去重(開頭不是z或x,後面含有z的行數),中文標點(開頭為x但第二個字母不為z的行數),英文符號(開頭為xz的行數),詞組數量(mcc0string 行數),詞組去重(所有以z開頭的行數減去mcc0string 行數)。下載按鈕添加美觀的css。

三,在Submit右邊加上「自動、單字、詞語」的切換按鈕,默認為自動。

當處於自動模式時,如果在codeInput所輸入的是英文字母且開頭不是z,或者輸入的是一個漢字,則自動判斷是單字模式(不再在英文字母前自動加z。);如果輸入的是英文字母且開頭是z,或者輸入的是兩個以上的漢字,則自動判斷是詞語模式。

當按下enter或點擊Submit後,模式切換按鈕切到「單字」或「詞語」,這時模式將不能再改變,除非按下btnCloseEdit。

四,當處於單字模式時,如果codeInput輸入的是一個漢字,則展示這個字所在的序列的重碼情況。

例如輸入「轟」(或者jjjj),則下方將顯示:
j 都十
jj 幹艹卄廾
jjj 轉
jjjj 轟

當點擊其中一個字A,它將會高亮,點擊另一個字B,如果A在B的前面,則A將插入至B的後面;如果A在B的後面,則A將插入至B的前面。之後執行analyzeFile函式,統計數據更新,並生成download。

這些操作都將計入undo-redo,reset,操作log中。

如果codeInput輸入的是一個編碼,則這個編碼作為標題將高亮。

當一個字從一個欄目移到另一個欄目,則它的編碼將變為所在欄的標題的編碼。

當一個編碼本身沒有字(空碼),或者一開始有字,但是後來被移走了,則它下方(原本有字的地方)將出現一個用方框圍住的加號,用以其它單字再移過去。

單字模式沒有「put to the end」和「delete」。其餘跟詞語模式類似。

五,把analyzeFile中的統計功能,加至Statistics欄,其最左邊增加一個切換按鈕「重碼統計」,下方將是analyzeFile中的統計結果。「重碼統計」將是Statistics欄默認顯示的項目。

「總重碼統計」將改為:0重、1重、2重……。

「分類重碼統計」將改為:碼長1:0重、1重、2重……;碼長2:0重、1重、2重……;碼長3:0重、1重、2重……;碼長4:0重、1重、2重……。

將「重碼結果」和「去重結果」標題後面加上數量,去掉「下載」。把四個「超出字」按之前的順序合並成一個,數量也合並。

美化以上元素的css,不要用原生的html樣式。

只需給出所要修改的地方,所有的代碼和注釋都要使用英文。

代码: 全选

把domu1.html的功能合並至domchar5741.html中,按以下要求進行:
一,將domu1.html中的analyzeFile複製過來。當文本載入時,先執行analyzeFile函式(不用生成download),之後再把結果交給processContent函式。

二,把#output和#download,擴展為一個美觀的概覽,它的項目為:單字數量(cchardefine 行數),尹末去重(開頭不是z或x,後面含有z的行數),中文標點(開頭為x但第二個字母不為z的行數),英文符號(開頭為xz的行數),詞組數量(mcc0string 行數),詞組去重(所有以z開頭的行數減去mcc0string 行數)。下載按鈕添加美觀的css。

三,在Submit右邊加上「自動、單字、詞語」的切換按鈕,默認為自動。

當處於自動模式時,如果在codeInput所輸入的是英文字母且開頭不是z,或者輸入的是一個漢字,則自動判斷是單字模式(不再在英文字母前自動加z。);如果輸入的是英文字母且開頭是z,或者輸入的是兩個以上的漢字,則自動判斷是詞語模式。

當按下enter或點擊Submit後,模式切換按鈕切到「單字」或「詞語」,這時模式將不能再改變,除非按下btnCloseEdit。

四,當處於單字模式時,如果codeInput輸入的是一個漢字,則展示這個字所在的序列的重碼情況。

例如輸入「轟」(或者jjjj),則下方將顯示:
j	都十
jj	幹艹卄廾
jjj	轉
jjjj	轟

當點擊其中一個字A,它將會高亮,點擊另一個字B,如果A在B的前面,則A將插入至B的後面;如果A在B的後面,則A將插入至B的前面。之後執行analyzeFile函式,統計數據更新,並生成download。

這些操作都將計入undo-redo,reset,操作log中。

如果codeInput輸入的是一個編碼,則這個編碼作為標題將高亮。

當一個字從一個欄目移到另一個欄目,則它的編碼將變為所在欄的標題的編碼。

當一個編碼本身沒有字(空碼),或者一開始有字,但是後來被移走了,則它下方(原本有字的地方)將出現一個用方框圍住的加號,用以其它單字再移過去。

單字模式沒有「put to the end」和「delete」。其餘跟詞語模式類似。

五,把analyzeFile中的統計功能,加至Statistics欄,其最左邊增加一個切換按鈕「重碼統計」,下方將是analyzeFile中的統計結果。「重碼統計」將是Statistics欄默認顯示的項目。

「總重碼統計」將改為:0重、1重、2重……。

「分類重碼統計」將改為:碼長1:0重、1重、2重……;碼長2:0重、1重、2重……;碼長3:0重、1重、2重……;碼長4:0重、1重、2重……。

將「重碼結果」和「去重結果」標題後面加上數量,去掉「下載」。把四個「超出字」按之前的順序合並成一個,數量也合並。

美化以上元素的css,不要用原生的html樣式。

只需給出所要修改的地方,所有的代碼和注釋都要使用英文。

Re: 工欲善其事,必先利其器:準備更新哈哈倉頡

发表于 : 2026年 5月 23日 01:02
ejsoon
gemini pro:

代码: 全选

Here are the specific modifications required to merge the features from `domu1.html` into `domchar5741.html` according to your requirements. All code and comments are provided in English.

### 1. HTML Modifications

**Find this block:**

```html
    <div id="output"></div>
    <div id="download"></div>
    <br />

```

**Replace with:**

```html
    <div id="dashboardOverview" class="dashboard-grid" style="display: none;">
      <div class="stat-card">
        <div class="stat-title">Single Char Count</div>
        <div class="stat-value" id="dashCharDef">0</div>
      </div>
      <div class="stat-card">
        <div class="stat-title">Yinmo Dedup</div>
        <div class="stat-value" id="dashYinmo">0</div>
      </div>
      <div class="stat-card">
        <div class="stat-title">Chinese Punc</div>
        <div class="stat-value" id="dashChiPunc">0</div>
      </div>
      <div class="stat-card">
        <div class="stat-title">English Sym</div>
        <div class="stat-value" id="dashEngSym">0</div>
      </div>
      <div class="stat-card">
        <div class="stat-title">Phrase Count</div>
        <div class="stat-value" id="dashPhraseCount">0</div>
      </div>
      <div class="stat-card">
        <div class="stat-title">Phrase Dedup</div>
        <div class="stat-value" id="dashPhraseDedup">0</div>
      </div>
    </div>
    <div id="downloadContainer" style="display: none; margin-top: 15px;"></div>

```

**Find this block:**

```html
        <div id="inputArea">
          <label for="codeInput" style="font-weight: bold">Encode Input: </label>
          <input type="text" id="codeInput" placeholder="Enter code (e.g. zhy) or word" />
          <button id="submitCode" onclick="handleEditSubmit()">Submit</button>
        </div>

```

**Replace with:**

```html
        <div id="inputArea">
          <label for="codeInput" style="font-weight: bold">Input: </label>
          <select id="modeSelect" class="mode-dropdown">
            <option value="auto">Auto</option>
            <option value="char">Char</option>
            <option value="phrase">Phrase</option>
          </select>
          <input type="text" id="codeInput" placeholder="Enter code or word" />
          <button id="submitCode" onclick="handleEditSubmit()">Submit</button>
        </div>

```

**Find this block:**

```html
        <div class="stats-buttons" id="statsBtnContainer">
          <button class="stats-btn" id="btnMcc1All" onclick="renderStatsSection('mcc1all', '二碼高頻', this)">

```

**Replace with:**

```html
        <div class="stats-buttons" id="statsBtnContainer">
          <button class="stats-btn active" id="btnDupStats" onclick="showDupStatsSection(this)">
            重碼統計
          </button>
          <button class="stats-btn" id="btnMcc1All" onclick="renderStatsSection('mcc1all', '二碼高頻', this)">

```

### 2. CSS Modifications

**Add the following CSS rules inside your `<style>` block:**

```css
      /* Dashboard Overview Styling */
      .dashboard-grid {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
        gap: 15px;
        margin: 20px 0;
      }
      .stat-card {
        background: linear-gradient(135deg, #ffffff 0%, #f5f7fa 100%);
        border: 1px solid #e4e7eb;
        border-radius: 10px;
        padding: 15px;
        text-align: center;
        box-shadow: 0 4px 6px rgba(0,0,0,0.05);
        transition: transform 0.2s;
      }
      .stat-card:hover {
        transform: translateY(-2px);
        box-shadow: 0 6px 12px rgba(0,0,0,0.1);
      }
      .stat-title {
        font-size: 13px;
        color: #6b7280;
        margin-bottom: 8px;
        font-weight: bold;
      }
      .stat-value {
        font-size: 24px;
        color: #1f2937;
        font-weight: 900;
      }
      /* Beautiful Download Button */
      .modern-download-btn {
        display: inline-block;
        width: 100%;
        padding: 12px 24px;
        background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
        color: white !important;
        text-decoration: none;
        text-align: center;
        font-weight: bold;
        border-radius: 8px;
        box-shadow: 0 4px 15px rgba(124, 58, 237, 0.3);
        transition: all 0.3s ease;
      }
      .modern-download-btn:hover {
        background: linear-gradient(135deg, #4338ca 0%, #6d28d9 100%);
        box-shadow: 0 6px 20px rgba(124, 58, 237, 0.4);
        transform: translateY(-1px);
      }
      /* Dropdown styling */
      .mode-dropdown {
        padding: 8px;
        border-radius: 4px;
        border: 1px solid #ccc;
        font-size: 14px;
        margin-right: 6px;
        outline: none;
      }
      .mode-dropdown:disabled {
        background-color: #f3f4f6;
        cursor: not-allowed;
      }
      /* Char mode specific elements */
      .empty-slot {
        display: inline-flex;
        justify-content: center;
        align-items: center;
        width: 30px;
        height: 30px;
        margin: 4px;
        border: 2px dashed #9ca3af;
        border-radius: 4px;
        color: #9ca3af;
        cursor: pointer;
        font-weight: bold;
        background: #f9fafb;
        transition: all 0.2s;
        vertical-align: middle;
      }
      .empty-slot:hover {
        background: #e5e7eb;
        border-color: #6b7280;
        color: #4b5563;
      }
      /* Beautified Dup Stats Area */
      .dup-stats-container {
        display: flex;
        flex-direction: column;
        gap: 20px;
      }
      .dup-box {
        background: #fff;
        border: 1px solid #e5e7eb;
        border-radius: 8px;
        padding: 15px;
        box-shadow: 0 2px 4px rgba(0,0,0,0.02);
      }
      .dup-box h3 {
        margin-top: 0;
        margin-bottom: 10px;
        font-size: 16px;
        color: #374151;
        border-bottom: 2px solid #f3f4f6;
        padding-bottom: 8px;
      }
      .dup-row {
        display: flex;
        flex-wrap: wrap;
        gap: 10px;
      }
      .dup-tag {
        background: #f3f4f6;
        padding: 4px 10px;
        border-radius: 12px;
        font-size: 13px;
        color: #4b5563;
      }
      .dup-tag strong {
        color: #1f2937;
      }
      .modern-textarea {
        width: 100%;
        height: 120px;
        border: 1px solid #d1d5db;
        border-radius: 6px;
        padding: 10px;
        font-family: monospace;
        resize: vertical;
        box-sizing: border-box;
      }
      .highlighted-title {
        color: #d32f2f !important;
        background: #ffebee;
        padding: 2px 6px;
        border-radius: 4px;
      }

```

### 3. JavaScript Modifications

**Inject these Global Variables near the top of the `<script>` tag:**

```javascript
      // --- New Globals for Char Mode & Dup Stats ---
      let globalCodeMap = new Map();
      let rawCharDefLines = [];
      let activeMode = 'phrase'; // 'char' or 'phrase'
      let generatedDupStatsHTML = ''; 
      // ---------------------------------------------

```

**Find the `resetPage` function. Add these state resets:**

```javascript
      // Add inside resetPage() before mcc0update.style.display = 'none';
      globalCodeMap.clear();
      rawCharDefLines = [];
      generatedDupStatsHTML = '';
      document.getElementById('dashboardOverview').style.display = 'none';
      document.getElementById('downloadContainer').style.display = 'none';
      document.getElementById('modeSelect').disabled = false;
      document.getElementById('modeSelect').value = 'auto';

```

**Find `handleFile(event)`. Replace the reader.onload block:**

```javascript
        reader.onload = async function (e) {
          filecontent = e.target.result;
          await runAnalyzeFileAndDashboard(); // Execute analyze logic first
          processContent(); // Then pass to phrase processing
        };

```

**Insert this new function `runAnalyzeFileAndDashboard` anywhere in the script:**

```javascript
      async function runAnalyzeFileAndDashboard() {
        const lines = filecontent.split('\n').map(l => l.replace(/\r$/, ''));
        
        let inChardef = false;
        let charDefStartIndex = -1;
        let charDefEndIndex = lines.length;
        let zStartCount = 0;
        let yinmoCount = 0;
        let chiPuncCount = 0;
        let engSymCount = 0;

        globalCodeMap.clear();
        rawCharDefLines = [];

        // Parse metrics and build code map
        for (let i = 0; i < lines.length; i++) {
          const line = lines[i].trim();
          if (line === '%chardef begin') {
            inChardef = true;
            charDefStartIndex = i;
            continue;
          }
          if (!inChardef) continue;
          if (line === '%chardef end') {
            charDefEndIndex = i;
            break;
          }

          rawCharDefLines.push(line);
          const parts = line.split('\t');
          const code = parts[0];
          const char = parts.length > 1 ? parts[1].trim() : '';

          if (code) {
            // Collect code map for Char mode & Stats
            if (char && /^[a-z]{1,4}$/.test(code)) {
              if (!globalCodeMap.has(code)) globalCodeMap.set(code, []);
              globalCodeMap.get(code).push(char);
            }

            // Dashboard conditions
            if (code.startsWith('z')) zStartCount++;
            if (!code.startsWith('z') && !code.startsWith('x') && code.includes('z')) yinmoCount++;
            if (code.startsWith('x') && code[1] !== 'z' && code.length >= 2) chiPuncCount++;
            if (code.startsWith('xz')) engSymCount++;
          }
        }

        // Calculate duplicate stats logic (from domu1.html)
        const sortedCodeMap = new Map([...globalCodeMap.entries()].sort());
        const dupStats = new Map();
        for (const chars of sortedCodeMap.values()) {
          const dupCount = chars.length - 1; // 1 char = 0 dup, 2 chars = 1 dup...
          if (dupCount >= 0) dupStats.set(dupCount, (dupStats.get(dupCount) || 0) + 1);
        }

        const lenStats = [new Map(), new Map(), new Map(), new Map()];
        for (const [code, chars] of sortedCodeMap) {
          const lenIdx = Math.min(code.length - 1, 3);
          const dupCount = chars.length - 1;
          if (dupCount >= 0) {
            const map = lenStats[lenIdx];
            map.set(dupCount, (map.get(dupCount) || 0) + 1);
          }
        }

        // Build HTML for Stats Tab
        let statsHtml = `<div class="dup-stats-container">`;
        
        // Total Dup Stats
        statsHtml += `<div class="dup-box"><h3>總重碼統計 (Total Dup Stats)</h3><div class="dup-row">`;
        const maxDup = Math.max(...dupStats.keys(), 0);
        for (let i = 0; i <= maxDup; i++) {
          statsHtml += `<span class="dup-tag"><strong>${i}重:</strong> ${dupStats.get(i) || 0}</span>`;
        }
        statsHtml += `</div></div>`;

        // Category Dup Stats
        statsHtml += `<div class="dup-box"><h3>分類重碼統計 (Category Dup Stats)</h3>`;
        lenStats.forEach((map, i) => {
          const maxDupForLen = Math.max(...map.keys(), 0);
          statsHtml += `<div style="margin-bottom:8px; font-size:14px; font-weight:bold;">碼長 ${i + 1}:</div><div class="dup-row" style="margin-bottom:15px;">`;
          for (let j = 0; j <= maxDupForLen; j++) {
            statsHtml += `<span class="dup-tag"><strong>${j}重:</strong> ${map.get(j) || 0}</span>`;
          }
          statsHtml += `</div>`;
        });
        statsHtml += `</div>`;

        // Dup & Dedup Results TextAreas
        const collapseLines = [];
        const gzhahaLines = [];
        const outOfBoundsLines = [];

        for (const [code, chars] of sortedCodeMap) {
          if (chars.length > 1) collapseLines.push(`${code}\t${chars.join('')}`);
          if (chars.length <= 1) continue;
          
          const len = code.length;
          chars.forEach((char, idx) => {
            if (idx === 0) return;
            if (len === 1 || len === 2) {
              const suffix = ['z', 'zz', 'zx', 'xz'][idx - 1] || '';
              if (suffix) gzhahaLines.push(`${code}${suffix}\t${char}`);
              else outOfBoundsLines.push(`${code}\t${char}`);
            } else if (len === 3) {
              if (idx === 1) gzhahaLines.push(`${code}z\t${char}`);
              else if (idx === 2) gzhahaLines.push(`${code.slice(0, 2)}z${code.slice(2)}\t${char}`);
              else if (idx === 3) gzhahaLines.push(`${code.slice(0, 1)}z${code.slice(1)}\t${char}`);
              else outOfBoundsLines.push(`${code}\t${char}`);
            } else if (len === 4) {
              outOfBoundsLines.push(`${code}\t${char}`);
            }
          });
        }

        statsHtml += `<div class="dup-box"><h3>重碼結果 (${collapseLines.length})</h3><textarea class="modern-textarea" readonly>${collapseLines.join('\n')}</textarea></div>`;
        statsHtml += `<div class="dup-box"><h3>去重結果 (${gzhahaLines.length})</h3><textarea class="modern-textarea" readonly>${gzhahaLines.join('\n')}</textarea></div>`;
        statsHtml += `<div class="dup-box"><h3>超出字合集 (${outOfBoundsLines.length})</h3><textarea class="modern-textarea" readonly>${outOfBoundsLines.join('\n')}</textarea></div>`;
        statsHtml += `</div>`;
        
        generatedDupStatsHTML = statsHtml;

        // Pass calculated metrics to global scope for processContent to use
        window._parsedMetrics = { charDefLinesLen: rawCharDefLines.length, yinmoCount, chiPuncCount, engSymCount, zStartCount };
      }

      function showDupStatsSection(btnElement) {
        document.querySelectorAll('#statsBtnContainer .stats-btn').forEach(btn => btn.classList.remove('active'));
        if (btnElement) btnElement.classList.add('active');
        document.getElementById('statsDisplayContainer').innerHTML = generatedDupStatsHTML;
      }

```

**Find `processContent` and modify the dashboard updates:**
Replace the standard `document.getElementById('output').innerHTML = ...` injections with:

```javascript
        // Inside processContent, replace the first document.getElementById('output') with:
        document.getElementById('dashboardOverview').style.display = 'grid';
        if (window._parsedMetrics) {
          document.getElementById('dashCharDef').textContent = window._parsedMetrics.charDefLinesLen;
          document.getElementById('dashYinmo').textContent = window._parsedMetrics.yinmoCount;
          document.getElementById('dashChiPunc').textContent = window._parsedMetrics.chiPuncCount;
          document.getElementById('dashEngSym').textContent = window._parsedMetrics.engSymCount;
        }

        // ... lower down, replace the second document.getElementById('output') line with:
        document.getElementById('dashPhraseCount').textContent = mcc0Lines.length;
        if (window._parsedMetrics) {
          document.getElementById('dashPhraseDedup').textContent = window._parsedMetrics.zStartCount - mcc0Lines.length;
        }

```

**Find the download link generation in `processContent`:**

```javascript
        // Replace the download injection:
        document.getElementById('download').innerHTML =
          `<a href="${url}" download="haha_${timestamp}.txt">下載文件 (${size} bytes)</a>`;

```

**With:**

```javascript
        document.getElementById('downloadContainer').style.display = 'block';
        document.getElementById('downloadContainer').innerHTML =
          `<a href="${url}" download="haha_${timestamp}.txt" class="modern-download-btn">下載碼表 Download File (${size} bytes)</a>`;

```

**Find `handleEditSubmit()` and replace it with this mode-aware version:**

```javascript
      function handleEditSubmit() {
        let val = document.getElementById('codeInput').value.trim();
        if (!val) return;

        let selectedMode = document.getElementById('modeSelect').value;
        const isChinese = /[\u4e00-\u9fa5]/.test(val);

        // Auto logic assignment
        if (selectedMode === 'auto') {
          if ((!isChinese && !val.toLowerCase().startsWith('z')) || (isChinese && val.length === 1)) {
            selectedMode = 'char';
          } else {
            selectedMode = 'phrase';
          }
          document.getElementById('modeSelect').value = selectedMode;
        }
        
        // Lock the mode
        document.getElementById('modeSelect').disabled = true;
        activeMode = selectedMode;
        document.getElementById('codeInput').value = '';
        selectedEditItem = null;
        justMovedItem = null;
        pendingAction = null;

        if (activeMode === 'char') {
          // Initialize char mode state tracking
          if (undoStack.length === 0) {
            undoStack.push({ state: JSON.stringify(rawCharDefLines), desc: 'Baseline Char State', mode: 'char' });
          }
          currentEditCode = val.toLowerCase(); 
          // Extract base code if a character was entered
          if (isChinese) {
             let foundCode = null;
             for (const [code, chars] of globalCodeMap) {
               if (chars.includes(val)) { foundCode = code; break; }
             }
             if (foundCode) {
               // Extract prefix for displaying the sequence
               currentEditCode = foundCode[0];
             }
          }
          
          document.getElementById('inputArea').style.display = 'block';
          document.getElementById('editArea').style.display = 'block';
          document.getElementById('btnMoveEnd').style.display = 'none'; // Hide in char mode
          document.getElementById('btnDelete').style.display = 'none';  // Hide in char mode
          renderCharEditArea(currentEditCode, val);
          return;
        }

        // --- Original Phrase Mode Logic Follows ---
        document.getElementById('btnMoveEnd').style.display = 'inline-block';
        document.getElementById('btnDelete').style.display = 'inline-block';
        // ... (Keep existing code from here down to renderEditArea() for phrase mode logic)
        
        if (/^[a-zA-Z]+$/.test(val)) {
          if (val[0].toLowerCase() !== 'z') val = 'z' + val;
          val = val.substring(0, 4).toLowerCase();
          currentEditCode = val;
          currentTargetWord = '';
        } else {
          currentTargetWord = val;
          let lines = mcc0string.split('\n').filter(l => l.trim() !== '');

          if (!lines.includes(val)) {
            if (!originalSnapshotState || originalSnapshotState.size === 0) {
              originalSnapshotState = getSystemState();
            }
            saveStateForUndo(`Added new word: ${val}`);
            lines.push(val);
            processContent(JSON.stringify(lines));
          }

          let code = 'z';
          let c1 = ccharfirst.get(val[0]) || '';
          if (val.length === 1) {
            code += c1;
          } else if (val.length === 2) {
            let c2 = ccharfirst.get(val[1]) || '';
            let c3 = ccharsecond.get(val[1]) || 'x';
            code += c1 + c2 + c3;
          } else {
            let c2 = ccharfirst.get(val[1]) || '';
            let c3 = ccharfirst.get(val[2]) || '';
            code += c1 + c2 + c3;
          }
          currentEditCode = code.substring(0, 4);
        }

        if (currentEditCode.length >= 1) activeEditKeys.add('z');
        if (currentEditCode.length >= 2) activeEditKeys.add(currentEditCode.substring(0, 2));
        if (currentEditCode.length >= 3) activeEditKeys.add(currentEditCode.substring(0, 3));
        if (currentEditCode.length >= 4) activeEditKeys.add(currentEditCode);

        if (!originalSnapshotState || originalSnapshotState.size === 0) {
          originalSnapshotState = getSystemState();
        }

        document.getElementById('inputArea').style.display = 'block';
        document.getElementById('editArea').style.display = 'block';
        renderEditArea();
      }

```

**Insert this rendering logic for Char Mode anywhere:**

```javascript
      function renderCharEditArea(baseCode, targetInput) {
        let activeContainer = document.getElementById('editRows');
        let originalContainer = document.getElementById('originalRows');
        activeContainer.innerHTML = '';
        originalContainer.innerHTML = '';

        // Derive sequences (j, jj, jjj, jjjj) from the first character of the code
        const firstLetter = baseCode[0];
        const targetCodes = [firstLetter, firstLetter.repeat(2), firstLetter.repeat(3), firstLetter.repeat(4)];

        targetCodes.forEach(codeKey => {
          let rowDiv = document.createElement('div');
          rowDiv.className = 'edit-row';
          
          let title = document.createElement('div');
          title.textContent = codeKey;
          title.style.fontWeight = 'bold';
          title.style.marginBottom = '8px';
          if (codeKey === targetInput) title.className = 'highlighted-title'; // Highlight if exact code match
          rowDiv.appendChild(title);

          const charsForCode = globalCodeMap.get(codeKey) || [];
          
          if (charsForCode.length === 0) {
             // Show empty drop zone
             let emptyBtn = document.createElement('span');
             emptyBtn.className = 'empty-slot';
             emptyBtn.textContent = '+';
             emptyBtn.onclick = () => handleCharClick('+', codeKey);
             rowDiv.appendChild(emptyBtn);
          } else {
             charsForCode.forEach((char, idx) => {
               let span = document.createElement('span');
               span.textContent = char;
               span.className = `edit-item`;
               if (char === selectedEditItem) span.classList.add('selected');
               if (char === justMovedItem) span.classList.add('just-moved');
               
               // Target character highlighting
               if (char === targetInput) {
                 span.classList.add('target-word');
                 title.className = 'highlighted-title'; // Highlight title if row contains target char
               }

               span.onclick = () => handleCharClick(char, codeKey);
               rowDiv.appendChild(span);
             });
             
             // Always append an empty slot at the end of populated rows for trailing insertion
             let emptyBtn = document.createElement('span');
             emptyBtn.className = 'empty-slot';
             emptyBtn.textContent = '+';
             emptyBtn.onclick = () => handleCharClick('+', codeKey);
             rowDiv.appendChild(emptyBtn);
          }

          activeContainer.appendChild(rowDiv);
        });
      }

      function handleCharClick(clickedChar, targetCode) {
        if (!selectedEditItem) {
          if (clickedChar === '+') return; // Cannot select empty slot
          selectedEditItem = clickedChar;
          justMovedItem = null;
          renderCharEditArea(currentEditCode[0], ''); // Rerender to show selection
          return;
        }

        // Swap logic
        if (selectedEditItem === clickedChar) {
          selectedEditItem = null;
          renderCharEditArea(currentEditCode[0], '');
          return;
        }

        // Save State
        undoStack.push({ state: JSON.stringify(rawCharDefLines), desc: `${selectedEditItem} -> ${targetCode}`, mode: 'char' });
        redoStack = [];
        renderActionLogs();

        // Perform swap/move in raw lines array
        let sourceLineIdx = rawCharDefLines.findIndex(l => l.endsWith('\t' + selectedEditItem));
        if (sourceLineIdx !== -1) {
          rawCharDefLines.splice(sourceLineIdx, 1); // remove from old position
        }

        let newRowString = `${targetCode}\t${selectedEditItem}`;
        if (clickedChar === '+') {
           // Insert at the end of the block for that code, or just append
           let lastIdx = -1;
           for(let i = rawCharDefLines.length - 1; i >= 0; i--) {
             if (rawCharDefLines[i].startsWith(targetCode + '\t')) { lastIdx = i; break; }
           }
           if (lastIdx !== -1) rawCharDefLines.splice(lastIdx + 1, 0, newRowString);
           else rawCharDefLines.push(newRowString);
        } else {
           // Insert relative to clicked item
           let targetLineIdx = rawCharDefLines.findIndex(l => l === `${targetCode}\t${clickedChar}`);
           if (targetLineIdx !== -1) {
             if (sourceLineIdx > targetLineIdx) {
               rawCharDefLines.splice(targetLineIdx, 0, newRowString); // Insert before
             } else {
               rawCharDefLines.splice(targetLineIdx + 1, 0, newRowString); // Insert after
             }
           } else {
             rawCharDefLines.push(newRowString);
           }
        }

        justMovedItem = selectedEditItem;
        selectedEditItem = null;
        
        // Rebuild filecontent structure and re-process to cascade updates
        rebuildAndProcessFromCharLines();
      }

      function rebuildAndProcessFromCharLines() {
         const lines = filecontent.split('\n').map(l => l.replace(/\r$/, ''));
         const start = lines.indexOf('%chardef begin');
         const end = lines.indexOf('%chardef end');
         
         if (start !== -1 && end !== -1) {
             const newFileContentArr = [
                ...lines.slice(0, start + 1),
                ...rawCharDefLines,
                ...lines.slice(end)
             ];
             filecontent = newFileContentArr.join('\n');
             runAnalyzeFileAndDashboard(); 
             processContent(); // Refreshes UI, generates new download
             renderCharEditArea(currentEditCode[0], ''); 
         }
      }

```

**Find `handleUndo()` and `handleRedo()` and add mode branching:**

```javascript
      function handleUndo() {
        if (undoStack.length === 0) return;
        let prevState = undoStack.pop();
        
        if (prevState.mode === 'char') {
          redoStack.push({ state: JSON.stringify(rawCharDefLines), desc: `Undo: ${prevState.desc}`, mode: 'char' });
          rawCharDefLines = JSON.parse(prevState.state);
          rebuildAndProcessFromCharLines();
        } else {
          // Original phrase logic
          let currentLines = mcc0string.split('\n').filter(l => l.trim() !== '');
          redoStack.push({ lines: currentLines, desc: `Undo: ${prevState.desc}`, mode: 'phrase' });
          let oldState = getSystemState();
          processContent(JSON.stringify(prevState.lines));
          detectAndRenderChanges(oldState, getSystemState());
        }
        justMovedItem = null; 
        renderActionLogs();
      }

      function handleRedo() {
        if (redoStack.length === 0) return;
        let nextState = redoStack.pop();
        
        if (nextState.mode === 'char') {
          undoStack.push({ state: JSON.stringify(rawCharDefLines), desc: nextState.desc, mode: 'char' });
          rawCharDefLines = JSON.parse(nextState.state);
          rebuildAndProcessFromCharLines();
        } else {
          let currentLines = mcc0string.split('\n').filter(l => l.trim() !== '');
          undoStack.push({ lines: currentLines, desc: nextState.desc, mode: 'phrase' });
          let oldState = getSystemState();
          processContent(JSON.stringify(nextState.lines));
          detectAndRenderChanges(oldState, getSystemState());
        }
        justMovedItem = null; 
        renderActionLogs();
      }

```

**Find `handleEditCloseOnly()` and ensure unlock logic is present:**

```javascript
      function handleEditCloseOnly() {
        document.getElementById('inputArea').style.display = 'block';
        document.getElementById('editArea').style.display = 'none';
        document.getElementById('codeInput').value = '';
        document.getElementById('modeSelect').disabled = false; // Unlock dropdown
        // ... keep rest of handleEditCloseOnly

```

**Find `switchTab(tab)`. Add the default Stats redirect logic:**

```javascript
        if (tab === 'stats') {
          // Determine which filter button is active or fallback to Dup Stats default
          const activeBtn =
            document.querySelector('#statsBtnContainer .stats-btn.active') || document.getElementById('btnDupStats');
          if (activeBtn) {
            if (activeBtn.id === 'btnDupStats') { showDupStatsSection(activeBtn); }
            else { activeBtn.click(); }
          }
        }

```

Re: 工欲善其事,必先利其器:準備更新哈哈倉頡

发表于 : 2026年 5月 23日 01:28
ejsoon
參照domu1.js中的analyzeFile和processContent兩個函式,改進domchar5744.html:
一,當文本載入時,先執行analyzeFile函式(不用生成download),之後再把結果交給processContent函式。

二,修正overviewPanel統計數據:Yinmo Deduplication(編碼開頭不是z或x,後面含有z的行數),中文標點(編碼開頭為x但第二個字母不為z的行數),英文符號(編距開頭為xz的行數),詞組數量(mcc0string行數,這個是對的),詞組去重(所有編碼以z開頭的行數減去mcc0string行數)。

三,在Submit右邊加上「自動、單字、詞語」的切換按鈕,默認為自動。

當處於自動模式時,如果在codeInput所輸入的是英文字母且開頭不是z,或者輸入的是一個漢字,則自動判斷是單字模式;如果輸入的是英文字母且開頭是z,或者輸入的是兩個以上的漢字,則自動判斷是詞語模式。

當按下enter或點擊Submit後,模式切換按鈕切到「單字」或「詞語」,這時模式將不能再改變,除非按下btnCloseEdit。

四,當處於單字模式時,如果codeInput輸入的是一個漢字,則展示這個字所在的序列的重碼情況。

例如輸入「轟」(或者jjjj),則下方將顯示:
j 都十
jj 幹艹卄廾
jjj 轉
jjjj 轟

當點擊其中一個字A,它將會高亮,點擊另一個字B,如果A在B的前面,則A將插入至B的後面;如果A在B的後面,則A將插入至B的前面。之後執行analyzeFile函式,統計數據更新,並生成download。

這些操作都將計入undo-redo,reset,操作log中。

如果codeInput輸入的是一個編碼,則這個編碼作為標題將高亮。

當一個字從一個欄目移到另一個欄目,則它的編碼將變為所在欄的標題的編碼。

當一個編碼本身沒有字(空碼),或者一開始有字,但是後來被移走了,則它下方(原本有字的地方)將出現一個用方框圍住的加號,用以其它單字再移過去。

單字模式沒有「put to the end」和「delete」。其餘跟詞語模式類似。

只需給出所要修改的地方,所有的代碼和注釋都要使用英文。

代码: 全选

參照domu1.js中的analyzeFile和processContent兩個函式,改進domchar5744.html:
一,當文本載入時,先執行analyzeFile函式(不用生成download),之後再把結果交給processContent函式。

二,修正overviewPanel統計數據:Yinmo Deduplication(編碼開頭不是z或x,後面含有z的行數),中文標點(編碼開頭為x但第二個字母不為z的行數),英文符號(編距開頭為xz的行數),詞組數量(mcc0string行數,這個是對的),詞組去重(所有編碼以z開頭的行數減去mcc0string行數)。

三,在Submit右邊加上「自動、單字、詞語」的切換按鈕,默認為自動。

當處於自動模式時,如果在codeInput所輸入的是英文字母且開頭不是z,或者輸入的是一個漢字,則自動判斷是單字模式;如果輸入的是英文字母且開頭是z,或者輸入的是兩個以上的漢字,則自動判斷是詞語模式。

當按下enter或點擊Submit後,模式切換按鈕切到「單字」或「詞語」,這時模式將不能再改變,除非按下btnCloseEdit。

四,當處於單字模式時,如果codeInput輸入的是一個漢字,則展示這個字所在的序列的重碼情況。

例如輸入「轟」(或者jjjj),則下方將顯示:
j	都十
jj	幹艹卄廾
jjj	轉
jjjj	轟

當點擊其中一個字A,它將會高亮,點擊另一個字B,如果A在B的前面,則A將插入至B的後面;如果A在B的後面,則A將插入至B的前面。之後執行analyzeFile函式,統計數據更新,並生成download。

這些操作都將計入undo-redo,reset,操作log中。

如果codeInput輸入的是一個編碼,則這個編碼作為標題將高亮。

當一個字從一個欄目移到另一個欄目,則它的編碼將變為所在欄的標題的編碼。

當一個編碼本身沒有字(空碼),或者一開始有字,但是後來被移走了,則它下方(原本有字的地方)將出現一個用方框圍住的加號,用以其它單字再移過去。

單字模式沒有「put to the end」和「delete」。其餘跟詞語模式類似。

只需給出所要修改的地方,所有的代碼和注釋都要使用英文。

Re: 工欲善其事,必先利其器:準備更新哈哈倉頡

发表于 : 2026年 5月 23日 01:33
ejsoon
gemini pro要休息到4:52:

代码: 全选

Here are the specific modifications required to implement the requested features. Please replace the corresponding sections in your `domchar5744.html` file.

All logic, variables, and comments have been refactored in English according to your instructions.

### 1. Add the Mode Toggle to the UI

Locate the `<div id="inputArea">` block and insert the `<select id="modeSelect">` right before the Submit button.

**Replace this block:**

```html
        <div id="inputArea">
          <label for="codeInput" style="font-weight: bold">Encode Input: </label>
          <input type="text" id="codeInput" placeholder="Enter code (e.g. zhy) or word" />
          <button id="submitCode" onclick="handleEditSubmit()">Submit</button>
        </div>

```

**With this:**

```html
        <div id="inputArea">
          <label for="codeInput" style="font-weight: bold">Encode Input: </label>
          <input type="text" id="codeInput" placeholder="Enter code (e.g. zhy) or word" />
          <select id="modeSelect" style="padding: 8px; border-radius: 4px; margin-right: 6px; font-size: 14px;">
            <option value="auto">Auto</option>
            <option value="single">Single Word</option>
            <option value="phrase">Phrase</option>
          </select>
          <button id="submitCode" onclick="handleEditSubmit()">Submit</button>
        </div>

```

---

### 2. File Loading Logic Chain (Task 1)

Chain the execution so `runDomu1LogicAndGetIntegratedText` (acting as `analyzeFile`) runs first, then triggers `processContent()`.

**Replace this block:**

```javascript
      function handleFile(event) {
        const file = event.target.files[0];
        if (!file) return;

        document.getElementById('selectcinfile').classList.add('hide');

        const reader = new FileReader();
        reader.onload = function (e) {
          filecontent = e.target.result;

          // Execute domu1.html logic first, which processes stats and modifies raw text
          filecontent = runDomu1LogicAndGetIntegratedText(filecontent);

          // Execute domchar5741.html logic
          processContent();
        };
        reader.readAsText(file);
      }

```

**With this:**

```javascript
      function handleFile(event) {
        const file = event.target.files[0];
        if (!file) return;

        document.getElementById('selectcinfile').classList.add('hide');

        const reader = new FileReader();
        reader.onload = function (e) {
          filecontent = e.target.result;

          // Task 1: First execute analyzeFile logic (does not trigger download directly)
          // It modifies the content with dedup stats and bounds, returning integrated text
          filecontent = runDomu1LogicAndGetIntegratedText(filecontent);

          // Task 1: Pass the integrated result to processContent
          processContent();
        };
        reader.readAsText(file);
      }

```

---

### 3. Overview Panel Statistics Accuracy (Task 2)

Ensure the counting rules strictly match the requested parameters exactly inside the iteration loops.

**Replace this block inside `processContent()`:**

```javascript
        charDefLines.forEach(line => {
          const parts = line.split('\t');
          const code = parts[0];
          const char = parts[1];
          if (code && char) {
            cchardefine.set(char, code);
            statSingleWord++;
            if (!code.startsWith('z') && !code.startsWith('x') && code.includes('z')) statYinmo++;
            if (code.startsWith('x') && code[1] !== 'z') statChiPunc++;
            if (code.startsWith('xz')) statEngSym++;
            if (code.startsWith('z')) allZLineCount++;
          }
        });

```

**With this:**

```javascript
        charDefLines.forEach(line => {
          const parts = line.split('\t');
          const code = parts[0];
          const char = parts[1];
          if (code && char) {
            cchardefine.set(char, code);
            statSingleWord++;
            
            // Task 2: Yinmo Deduplication -> Code does not start with z or x, but contains z afterwards
            if (!code.startsWith('z') && !code.startsWith('x') && code.includes('z')) {
               statYinmo++;
            }
            
            // Task 2: Chinese Punctuation -> Code starts with x but the second letter is not z
            if (code.startsWith('x') && code.length > 1 && code[1] !== 'z') {
               statChiPunc++;
            }
            
            // Task 2: English Symbol -> Code starts exactly with xz prefix
            if (code.startsWith('xz')) {
               statEngSym++;
            }
            
            // Task 2 count for Deduplication Baseline
            if (code.startsWith('z')) {
               allZLineCount++;
            }
          }
        });

```

---

### 4. Submit & Mode Toggle Auto-Detection (Task 3 & 4)

Adjust input handling to auto-detect the user's intent, lock the UI, and generate accurate single-word structural display sequences.

**Replace the `handleEditSubmit()` function:**

```javascript
      function handleEditSubmit() {
        let val = document.getElementById('codeInput').value.trim();
        if (!val) return;
        document.getElementById('codeInput').value = '';

        selectedEditItem = null;
        justMovedItem = null;
        pendingAction = null;
        updateActionButtonsUI();

        let isEnglishCode = /^[a-zA-Z]+$/.test(val);

        // Detect Single Word Mode (English without 'z' prefix, or a single Chinese char)
        isSingleWordMode = false;
        singleWordActiveCode = '';
        currentTargetWord = '';

        if (isEnglishCode && val[0].toLowerCase() !== 'z') {
          isSingleWordMode = true;
          singleWordActiveCode = val.toLowerCase().substring(0, 4);
        } else if (!isEnglishCode && val.length === 1) {
          isSingleWordMode = true;
          currentTargetWord = val; // Set the target word to highlight the Chinese char
          // Reverse lookup code from charDefCodeMap
          for (let [code, chars] of charDefCodeMap) {
            if (chars.includes(val)) {
              singleWordActiveCode = code;
              break;
            }
          }
        }

        if (isSingleWordMode) {
          activeEditKeys.clear(); // Only show sequences for this code
          for (let i = 1; i <= singleWordActiveCode.length; i++) {
            activeEditKeys.add(singleWordActiveCode.substring(0, i));
          }
        } else {
        // ... (rest of old logic up to renderEditArea) ...

```

**With this:**

```javascript
      function handleEditSubmit() {
        let val = document.getElementById('codeInput').value.trim();
        if (!val) return;
        document.getElementById('codeInput').value = '';

        selectedEditItem = null;
        justMovedItem = null;
        pendingAction = null;

        let modeSelect = document.getElementById('modeSelect');
        let currentMode = modeSelect.value;
        let isEnglishCode = /^[a-zA-Z]+$/.test(val);

        // Task 3: Auto-detect if "auto" is selected
        if (currentMode === 'auto') {
          if ((isEnglishCode && val[0].toLowerCase() !== 'z') || (!isEnglishCode && val.length === 1)) {
            currentMode = 'single';
          } else {
            currentMode = 'phrase';
          }
        }

        // Lock the mode UI
        modeSelect.value = currentMode;
        modeSelect.disabled = true;

        isSingleWordMode = (currentMode === 'single');
        singleWordActiveCode = '';
        currentTargetWord = '';

        if (isSingleWordMode) {
          if (!isEnglishCode && val.length === 1) {
            currentTargetWord = val; // Highlight target char
            for (let [code, chars] of charDefCodeMap) {
              if (chars.includes(val)) {
                singleWordActiveCode = code;
                break;
              }
            }
          } else {
            // Target code as title if english code
            singleWordActiveCode = val.toLowerCase().substring(0, 4);
          }

          // Generate sequential lookup: j, jj, jjj, jjjj
          activeEditKeys.clear();
          let baseCode = singleWordActiveCode || 'x'; // safety fallback
          for (let i = 1; i <= baseCode.length; i++) {
            activeEditKeys.add(baseCode.substring(0, i));
          }
          
          // Task 4: Hide End & Delete Buttons
          document.getElementById('btnMoveEnd').style.display = 'none';
          document.getElementById('btnDelete').style.display = 'none';
        } else {
          // Enable phrase-specific UI buttons
          document.getElementById('btnMoveEnd').style.display = 'inline-block';
          document.getElementById('btnDelete').style.display = 'inline-block';
          
          currentTargetWord = val;
          if (isEnglishCode) {
            if (val[0].toLowerCase() !== 'z') val = 'z' + val;
            val = val.substring(0, 4).toLowerCase();
            currentEditCode = val;
            currentTargetWord = '';
          } else {
            let lines = mcc0string.split('\n').filter(l => l.trim() !== '');
            if (!lines.includes(val)) {
              if (!originalSnapshotState || originalSnapshotState.size === 0) {
                originalSnapshotState = getSystemState();
              }
              saveStateForUndo(`Added new word: ${val}`);
              lines.push(val);
              processContent(JSON.stringify(lines));
            }

            let code = 'z';
            let c1 = ccharfirst.get(val[0]) || '';
            if (val.length === 1) {
              code += c1;
            } else if (val.length === 2) {
              let c2 = ccharfirst.get(val[1]) || '';
              let c3 = ccharsecond.get(val[1]) || 'x';
              code += c1 + c2 + c3;
            } else {
              let c2 = ccharfirst.get(val[1]) || '';
              let c3 = ccharfirst.get(val[2]) || '';
              code += c1 + c2 + c3;
            }
            currentEditCode = code.substring(0, 4);
          }
          if (currentEditCode.length >= 1) activeEditKeys.add('z');
          if (currentEditCode.length >= 2) activeEditKeys.add(currentEditCode.substring(0, 2));
          if (currentEditCode.length >= 3) activeEditKeys.add(currentEditCode.substring(0, 3));
          if (currentEditCode.length >= 4) activeEditKeys.add(currentEditCode);

          if (!originalSnapshotState || originalSnapshotState.size === 0) {
            originalSnapshotState = getSystemState();
          }
        }

        updateActionButtonsUI();
        document.getElementById('inputArea').style.display = 'block';
        document.getElementById('editArea').style.display = 'block';
        renderEditArea();
      }

```

---

### 5. Restore Auto Mode on Close (Task 3)

Ensure the lock releases when editing is closed.

**Inside `handleEditCloseOnly()`:**

```javascript
      function handleEditCloseOnly() {
        document.getElementById('inputArea').style.display = 'block';
        document.getElementById('editArea').style.display = 'none';
        document.getElementById('codeInput').value = '';
        
        // Add this line to unlock mode UI
        document.getElementById('modeSelect').disabled = false;
        // Optionally revert to auto
        document.getElementById('modeSelect').value = 'auto'; 
        
        // ... (keep the rest identical)

```

---

### 6. Rendering the Empty Target Blocks `[+]` (Task 4)

Update `renderEditArea` to ensure empty slots still draw, and bind click actions passing the code constraint.

**Locate this condition in `renderEditArea()`:**

```javascript
          if (activeItems.length > 0 || originalItems.length > 0) {

```

**Replace it and its child logic block with this:**

```javascript
          // Task 4: Force render on single word mode to allow empty slot UI
          if (isSingleWordMode || activeItems.length > 0 || originalItems.length > 0) {
            let isIdentical = false;
            let activeWordsArr = activeItems.map(i => i.w);
            if (activeWordsArr.length === originalItems.length && activeWordsArr.every((v, i) => v === originalItems[i])) {
              isIdentical = true;
            }

            let activeRowDiv = document.createElement('div');
            activeRowDiv.className = 'edit-row';

            if (isIdentical && originalItems.length > 0) {
              activeRowDiv.style.backgroundColor = '#e8f5e9';
            }

            let activeTitle = document.createElement('div');
            activeTitle.textContent = label;
            activeTitle.style.fontWeight = 'bold';
            activeTitle.style.marginBottom = '8px';

            if (isSingleWordMode && singleWordActiveCode === k && !currentTargetWord) {
              activeTitle.className = 'target-code-title';
            }
            activeRowDiv.appendChild(activeTitle);

            activeItems.forEach(item => {
              let span = document.createElement('span');
              span.textContent = item.w;
              span.className = `edit-item ${item.cls}`;

              // Adjusted selected style targeting for objects in Single Mode
              if (isSingleWordMode) {
                 if (selectedEditItem && selectedEditItem.word === item.w && selectedEditItem.code === k) span.classList.add('selected');
              } else {
                 if (item.w === selectedEditItem) span.classList.add('selected');
              }
              
              if (item.w === justMovedItem) span.classList.add('just-moved'); 
              if (item.w === currentTargetWord) span.classList.add('target-word');
              if (changedWords.has(item.w)) span.classList.add('changed');

              span.onclick = () => handleItemClick(item.w, k); // Pass row code
              activeRowDiv.appendChild(span);
            });

            // Task 4: Render empty slot box target
            if (isSingleWordMode) {
              let addBtn = document.createElement('span');
              addBtn.textContent = '[+]';
              addBtn.className = 'edit-item empty-slot-btn';
              addBtn.style.border = '1px solid #aaa'; // Framed box
              addBtn.style.color = '#777';
              addBtn.style.fontFamily = 'monospace';
              if (selectedEditItem && selectedEditItem.word === '[+]' && selectedEditItem.code === k) addBtn.classList.add('selected');
              addBtn.onclick = () => handleItemClick('[+]', k);
              activeRowDiv.appendChild(addBtn);
            }

            // ... (keep the rest of the original/mobile preview logic identical)

```

---

### 7. Refactoring the Item Click and Directional Movement System (Task 4)

Replace `handleItemClick` entirely. This new version calculates structural placement indices, injects logic to re-trigger `analyzeFile`, update stats, and rebuild the text file in memory immediately upon movement completion.

**Replace the entire `handleItemClick(word)` function with this block:**

```javascript
      // Helper function to rebuild the file content after a single-word mode movement
      function rebuildFileContentAndRefresh() {
        let linesFile = filecontent.split('\n');
        let chardefBeginIdx = linesFile.indexOf('%chardef begin');
        let chardefEndIdx = -1;
        let inDef = false;

        for (let i = 0; i < linesFile.length; i++) {
          if (linesFile[i] === '%chardef begin') {
            inDef = true;
            continue;
          }
          if (!inDef) continue;
          if (linesFile[i] === '%chardef end' || linesFile[i].startsWith('x') || /[^z]z/.test(linesFile[i].split('\t')[0])) {
            chardefEndIdx = i;
            break;
          }
        }

        if (chardefBeginIdx > -1 && chardefEndIdx > -1) {
          let newBlock = [];
          for (let [c, chars] of charDefCodeMap) {
            chars.forEach(ch => newBlock.push(`${c}\t${ch}`));
          }
          linesFile.splice(chardefBeginIdx + 1, chardefEndIdx - chardefBeginIdx - 1, ...newBlock);
          filecontent = linesFile.join('\n');

          // Task 4: Re-trigger analyze logic and generate final download stats
          filecontent = runDomu1LogicAndGetIntegratedText(filecontent);
          
          let oldState = getSystemState();
          processContent();
          let newState = getSystemState();
          detectAndRenderChanges(oldState, newState);
        }
      }

      function handleItemClick(word, code = null) {
        if (pendingAction) {
          applyActionToWord(pendingAction, word);
          pendingAction = null;
          selectedEditItem = null;
          updateActionButtonsUI();
          return;
        }

        if (isSingleWordMode) {
          // --- SINGLE WORD MODE LOGIC (A -> B Relative Movement) ---
          if (!selectedEditItem) {
            selectedEditItem = { word, code };
            justMovedItem = null;
            renderEditArea();
            return;
          }

          let A = selectedEditItem;
          let B = { word, code };

          // Clicking the same target cancels selection
          if (A.word === B.word && A.code === B.code) {
            selectedEditItem = null;
            renderEditArea();
            return;
          }

          let arrA = charDefCodeMap.get(A.code) || [];
          let arrB = charDefCodeMap.get(B.code) || [];
          let idxA = arrA.indexOf(A.word);
          let idxB = B.word === '[+]' ? -1 : arrB.indexOf(B.word);

          if (idxA > -1) {
            let logMsg = B.word === '[+]' ? `${A.word} moved to empty slot ${B.code}` : `${A.word} moved relative to ${B.word}`;
            saveStateForUndo(logMsg);

            arrA.splice(idxA, 1); // Extract A from memory

            // Task 4 Logic: If A is before B -> A inserts after B. If A is after B -> A inserts before B.
            let isABeforeB = false;
            if (A.code.length < B.code.length) isABeforeB = true; // Different structural rows
            else if (A.code === B.code && idxA < idxB) isABeforeB = true; // Same row

            let targetArr = (A.code === B.code) ? arrA : arrB;
            let newIdxB = B.word === '[+]' ? targetArr.length : targetArr.indexOf(B.word);

            if (B.word === '[+]') {
              targetArr.push(A.word);
            } else {
              if (isABeforeB) {
                targetArr.splice(newIdxB + 1, 0, A.word); // Insert After
              } else {
                targetArr.splice(newIdxB, 0, A.word); // Insert Before
              }
            }

            // Task 4: Move code properties dynamically across bounds
            if (A.code !== B.code) {
               charDefCodeMap.set(A.code, arrA);
               charDefCodeMap.set(B.code, targetArr);
            }

            justMovedItem = A.word;
            selectedEditItem = null;
            rebuildFileContentAndRefresh();
            renderEditArea();
          }
        } else {
          // --- PHRASE MODE LOGIC (Original Baseline Logic) ---
          if (!selectedEditItem) {
            selectedEditItem = word;
            justMovedItem = null;
            renderEditArea();
          } else {
            if (selectedEditItem === word) {
              selectedEditItem = null;
              renderEditArea();
              return;
            }

            let oldState = getSystemState();
            let lines = mcc0string.split('\n').filter(l => l.trim() !== '');
            let originalIdxA = lines.indexOf(selectedEditItem);
            let originalIdxB = lines.indexOf(word);

            if (originalIdxA > -1 && originalIdxB > -1 && originalIdxA !== originalIdxB) {
              let placement = originalIdxA > originalIdxB ? 'before' : 'after';
              saveStateForUndo(`${selectedEditItem} -> ${placement} ${word}`);

              lines.splice(originalIdxA, 1);
              let newIdxB = lines.indexOf(word);

              if (originalIdxA > originalIdxB) {
                lines.splice(newIdxB, 0, selectedEditItem);
              } else {
                lines.splice(newIdxB + 1, 0, selectedEditItem);
              }

              justMovedItem = selectedEditItem;
              processContent(JSON.stringify(lines));
              let newState = getSystemState();
              detectAndRenderChanges(oldState, newState);
            }
            selectedEditItem = null;
          }
        }
      }

```

---

Let me know if you need to trace any specific code mapping behavior further, or if you plan to extend the duplicate stats behavior visually next!

Re: 工欲善其事,必先利其器:準備更新哈哈倉頡

发表于 : 2026年 5月 23日 02:04
ejsoon
gemini flash來吧:

代码: 全选

// --- 保持原代碼邏輯不變的單字前半區塊 ---
        let charDefStart = lines.indexOf('%chardef begin') + 1;
        let charDefEnd = lines.findIndex((line, i) => i > charDefStart && line.includes('z'));
        if (charDefEnd === -1) charDefEnd = lines.length;

        const charDefLines = lines.slice(charDefStart, charDefEnd);

        // 【新增邏輯】在不動原邏輯前提下,獲取首個含 z 的行到 %chardef end 的完整殘餘區塊
        let realCharDefEndLine = lines.indexOf('%chardef end');
        if (realCharDefEndLine === -1) realCharDefEndLine = lines.length;
        
        // 擷取從 charDefEnd(第一個含z的行)開始,到真正的 %chardef end 為止的行
        const remainingCharDefLines = lines.slice(charDefEnd, realCharDefEndLine);

        // 1. 先遍歷前半段(原代碼完全不動)
        charDefLines.forEach(line => {
          const parts = line.split('\t');
          const code = parts[0];
          const char = parts[1];
          if (code && char) {
            cchardefine.set(char, code);
            statSingleWord++;
            if (!code.startsWith('z') && !code.startsWith('x') && code.includes('z')) statYinmo++;
            if (code.startsWith('x') && code[1] !== 'z') statChiPunc++;
            if (code.startsWith('xz')) statEngSym++;
            if (code.startsWith('z')) allZLineCount++;
          }
        });

        // 2. 【新增邏輯】對另一個變數(殘餘區塊)進行逐行統計與補算,邏輯與上面完全一致
        remainingCharDefLines.forEach(line => {
          const parts = line.split('\t');
          const code = parts[0];
          const char = parts[1];
          if (code && char) {
            cchardefine.set(char, code);
            statSingleWord++;
            if (!code.startsWith('z') && !code.startsWith('x') && code.includes('z')) statYinmo++;
            if (code.startsWith('x') && code[1] !== 'z') statChiPunc++;
            if (code.startsWith('xz')) statEngSym++;
            if (code.startsWith('z')) allZLineCount++;
          }
        });

Re: 工欲善其事,必先利其器:準備更新哈哈倉頡

发表于 : 2026年 5月 23日 02:47
ejsoon

代码: 全选

// ==========================================
          // 修正:依據指定條件精準定位起始點與終點並替換
          // ==========================================
          
          let startIdx = -1;     // 起始點:第一個含 z 但非 z 開頭的行
          let insertIdx = -1;    // 終點:第一個出現 x 開頭的行

          // 1. 尋找起始點 (按照 domu1.js 的 /[^z]z/ 邏輯)
          for (let i = chardefBeginIdx + 1; i < integratedLines.length; i++) {
            if (integratedLines[i] === '%chardef end') break;
            
            const code = integratedLines[i].split('\t')[0];
            if (code && /[^z]z/.test(code)) {
              startIdx = i;
              break;
            }
          }

          // 2. 尋找終點 (第一個出現 x 開頭的行)
          for (let i = chardefBeginIdx + 1; i < integratedLines.length; i++) {
            if (integratedLines[i] === '%chardef end') {
              insertIdx = i;
              break;
            }
            
            const code = integratedLines[i].split('\t')[0];
            if (code && code.startsWith('x')) {
              insertIdx = i;
              break;
            }
          }

          // 如果沒找到符合 /[^z]z/ 的起始點,則預設將起始點設為終點 (退化為純插入)
          if (startIdx === -1) {
            startIdx = (insertIdx !== -1) ? insertIdx : integratedLines.length;
          }

          // 如果終點沒找到,則預設到檔案末尾
          if (insertIdx === -1) {
            insertIdx = integratedLines.length;
          }

          // 3. 計算舊資料行數並執行精準「替換」
          if (gzhahaLines.length > 0) {
            // 舊資料的行數 = 終點行號 - 起始點行號
            let deleteCount = Math.max(0, insertIdx - startIdx);
            
            // 從起始點開始,刪除 deleteCount 行舊數據,並填入新的 gzhahaLines
            integratedLines.splice(startIdx, deleteCount, ...gzhahaLines);
          }
        }

        return integratedLines.join('\n');
      }

Re: 工欲善其事,必先利其器:準備更新哈哈倉頡

发表于 : 2026年 5月 23日 07:34
ejsoon
你需要明確講哪些代碼要換成哪些。

代码: 全选

這裡為您明確列出「原本的代碼是什麼」**以及**「需要換成什麼新代碼」。

請在您的 `domchar5744.html` 檔案中,找到 `runDomu1LogicAndGetIntegratedText` 函式。

---

### 1. 要被換掉的「原代碼」

請在該函式的接近尾端處,找到以下這段尋找 `insertIdx` 並執行 `splice` 的舊邏輯區塊(大約在 `const gzhahaText = gzhahaLines.join('\n');` 的下方):

```javascript
          let insertIdx = integratedLines.length;
          for (let i = chardefBeginIdx + 1; i < integratedLines.length; i++) {
            if (integratedLines[i] === '%chardef end') {
              insertIdx = i;
              break;
            }
            if (integratedLines[i].startsWith('x')) {
              insertIdx = i;
              break;
            }
          }

          if (gzhahaLines.length > 0) {
            integratedLines.splice(insertIdx, 0, ...gzhahaLines);
          }

```

---

### 2. 替換後的「新代碼」

請將上方那段舊代碼**完全刪除**,並在相同位置**貼上以下新代碼**:

```javascript
          // ==========================================
          // 修正:依據指定條件精準定位起始點與終點並替換
          // ==========================================
          
          let startIdx = -1;     // 起始點:第一個含 z 但非 z 開頭的行
          let insertIdx = -1;    // 終點:第一個出現 x 開頭的行

          // 1. 尋找起始點 (完全按照 domu1.js 的 /[^z]z/ 邏輯)
          for (let i = chardefBeginIdx + 1; i < integratedLines.length; i++) {
            if (integratedLines[i] === '%chardef end') break;
            
            const code = integratedLines[i].split('\t')[0];
            if (code && /[^z]z/.test(code)) {
              startIdx = i;
              break;
            }
          }

          // 2. 尋找終點 (第一個出現 x 開頭的行)
          for (let i = chardefBeginIdx + 1; i < integratedLines.length; i++) {
            if (integratedLines[i] === '%chardef end') {
              insertIdx = i;
              break;
            }
            
            const code = integratedLines[i].split('\t')[0];
            if (code && code.startsWith('x')) {
              insertIdx = i;
              break;
            }
          }

          // 如果沒找到符合 /[^z]z/ 的起始點,則預設將起始點設為終點 (退化為純插入)
          if (startIdx === -1) {
            startIdx = (insertIdx !== -1) ? insertIdx : integratedLines.length;
          }

          // 如果終點沒找到,則預設到檔案末尾
          if (insertIdx === -1) {
            insertIdx = integratedLines.length;
          }

          // 3. 計算舊資料行數並執行精準「替換」
          if (gzhahaLines.length > 0) {
            // 舊資料的行數 = 終點行號 - 起始點行號
            let deleteCount = Math.max(0, insertIdx - startIdx);
            
            // 從起始點開始,刪除 deleteCount 行舊數據,並填入新的 gzhahaLines
            integratedLines.splice(startIdx, deleteCount, ...gzhahaLines);
          }

```

---

### 修改後的運行效果說明:

1. **起始點(`startIdx`)**:第一個迴圈會精準找到程式碼中第一個符合「內含 `z` 但非 `z` 開頭」的編碼行(即 `domu1.js` 認定的舊去重區塊起點)。
2. **終點(`insertIdx`)**:第一個出現 `x` 開頭的行(即舊去重區塊的末尾,標點符號的起點)。
3. **區塊置換**:`deleteCount` 會算出這段舊資料的精確行數,並在 `splice` 內將其刪除,同時置換上新的 `gzhahaLines`。如果原本沒有舊的去重資料,則 `deleteCount` 為 `0`,自動安全地進行純插入。

Re: 工欲善其事,必先利其器:準備更新哈哈倉頡

发表于 : 2026年 5月 23日 08:03
ejsoon
把overviewPanel歸至一行,僅需給出所要修改的地方,明確告知什麼代碼修改成什麼,所有的代碼和注釋都要使用英文。

代码: 全选

要將 `#overviewPanel` 的外觀歸至一行,我們需要調整它的 CSS 佈局樣式(由原本可能的垂直排列或預設區塊改為單行彈性佈局),並確保子元素的間距與對齊符合單行顯示。

以下是明確的代碼修改對照,所有的代碼、變數、HTML 屬性以及注釋均已轉換為英文:

### 1. CSS 樣式修改

在 `<style>` 標籤中,找到 `#overviewPanel` 的定義。

**原本的代碼(Original Code):**

```css
#overviewPanel {
    /* Might be display: none or other layouts initially */
    flex-direction: column; 
    gap: 10px;
}

```

**修改後的代碼(Modified Code):**

```css
#overviewPanel {
    display: flex;
    flex-direction: row; /* Align all stat items into a single row */
    flex-wrap: nowrap;   /* Prevent items from wrapping to the next line */
    justify-content: space-between; /* Distribute items evenly across the row */
    align-items: center; /* Center items vertically */
    gap: 15px;           /* Add horizontal spacing between stat boxes */
    width: 100%;         /* Ensure it stretches fully across the container */
}

```

---

### 2. JavaScript 顯示邏輯修改

在 `processContent` 函式的最尾端,找到控制該面板顯示的代碼。

**原本的代碼(Original Code):**

```javascript
document.getElementById('overviewPanel').style.display = 'block'; 
// Or sometimes style.display = 'flex' depending on your initial version

```

**修改後的代碼(Modified Code):**

```javascript
// Force the panel to render as a flex container to keep everything in one row
document.getElementById('overviewPanel').style.display = 'flex';

```

---

### 3. HTML 結構優化說明(選填確認)

如果您的 `.stat-box`(面板內的子方塊)原本被設定了過寬的固定寬度(例如 `width: 100%`),請在 CSS 中將子方塊的寬度改為彈性適應:

**將子方塊樣式修改為:**

```css
#overviewPanel .stat-box {
    flex: 1; /* Allow each box to grow and shrink equally in the single row */
    min-width: 120px; /* Set a reasonable minimum width to prevent text clipping */
    text-align: center;
}

```

Re: 工欲善其事,必先利其器:準備更新哈哈倉頡

发表于 : 2026年 5月 23日 08:43
ejsoon
domchar57442

現在想看一下單字模式的問題。

Re: 工欲善其事,必先利其器:準備更新哈哈倉頡

发表于 : 2026年 5月 23日 09:03
ejsoon
逐行介紹handleItemClick函式的意義,從if peningAction開始。

代码: 全选

以下是 `handleItemClick(word)` 函式中,從 `if (pendingAction)` 開始的逐行程式碼詳細介紹與其背後邏輯意義:

```javascript
function handleItemClick(word) {
  // (上方的輸入檢查與前置作業省略,從 pendingAction 開始:)

  // 【1. 檢查是否有先前點擊動作按鈕後留下的「懸安/待處理動作」(例如:Delete 或 Put to the end)】
  if (pendingAction) {
    // 如果有待處理動作,直接將該動作套用到目前點擊的單字上(執行刪除或移至末尾)
    applyActionToWord(pendingAction, word);
    
    // 動作執行完畢後,將待處理動作狀態清空
    pendingAction = null;
    
    // 將目前畫面上被選取的單字狀態清空
    selectedEditItem = null;
    
    // 重新更新畫面上動作按鈕的 UI 外觀(移除按鈕的高亮狀態)
    updateActionButtonsUI();
    
    // 結束此函式,不繼續往下執行排序邏輯
    return;
  }

  // 【2. 當沒有待處理動作時,進入標準的單字/詞組選取與搬移邏輯】
  // 如果目前「沒有」任何單字被選取(這是選取流程的第一步:選取 A)
  if (!selectedEditItem) {
    // 將目前點擊的單字記錄到全域變數中,作為被選取的目標(Item A)
    selectedEditItem = word;
    
    // 清空上一次搬移的高亮紀錄,因為現在使用者開啟了新的選取流程
    justMovedItem = null; 
    
    // 重新渲染編輯區,此時被選取的單字 A 會被加上黃色外框與底色 (.selected)
    renderEditArea();
  } 
  // 如果目前「已經有」單字被選取了(這是選取流程的第二步)
  else {
    // 如果再次點擊同一個單字(即 A 等於 B,點擊已被選取的單字)
    if (selectedEditItem === word) {
      // 視為取消選取,將選取狀態重置為空
      selectedEditItem = null;
      
      // 重新渲染編輯區,取消黃色高亮外框
      renderEditArea();
      
      // 結束函式
      return;
    }

    // 取得目前的系統狀態快照(用於稍後比對位置是否有發生變化)
    let oldState = getSystemState();

    // 【3. 進入單字模式(Single Word Mode)的順序調整邏輯】
    if (isSingleWordMode) {
      
      // 在目前所有顯示的編碼列(activeEditKeys)中,尋找點擊的目標單字 B 屬於哪一個編碼列 (targetCode)
      let targetCode = Array.from(activeEditKeys).find(k => (charDefCodeMap.get(k) || []).includes(word));
      
      // 尋找最初選取的單字 A 屬於哪一個編碼列 (sourceCode)
      let sourceCode = Array.from(activeEditKeys).find(k =>
        (charDefCodeMap.get(k) || []).includes(selectedEditItem)
      );

      // 限制條件:只有當 A 和 B 屬於「同一個編碼列」時,才能進行重新排序
      if (targetCode && targetCode === sourceCode) {
        // 取得該編碼對應的單字陣列(例如編碼 "abc" 後面的單字群)
        let arr = charDefCodeMap.get(targetCode);
        
        // 取得單字 A (選取項) 在陣列中的索引位置
        let charIdxA = arr.indexOf(selectedEditItem);
        
        // 取得單字 B (目標項) 在陣列中的索引位置
        let charIdxB = arr.indexOf(word);

        // 確保 A 和 B 都有在陣列中找到,且確定點擊的是不同的位置
        if (charIdxA > -1 && charIdxB > -1 && charIdxA !== charIdxB) {
          
          // 根據 A 與 B 的相對位置,決定紀錄格式字串是 'before' 還是 'after'
          let placement = charIdxA > charIdxB ? 'before' : 'after';
          
          // 儲存當前狀態到 Undo 歷史紀錄堆疊中,並附帶步驟描述(例如:"我 -> before 你")
          saveStateForUndo(`${selectedEditItem} -> ${placement} ${word}`);

          // 核心重排算式 (1):先將單字 A 從陣列中的原本位置抽出來(刪除 1 個元素)
          arr.splice(charIdxA, 1);
          
          // 核心重排算式 (2):因為陣列少了一個元素,重新尋找單字 B 目前的新索引位置
          let newIdxB = arr.indexOf(word);
          
          // 核心重排算式 (3):將 A 插回陣列中。
          // 如果 A 原本在 B 的右邊 (charIdxA > charIdxB),抽出 A 後 B 索引不變,將 A 插在 newIdxB 位置(B 的前面)。
          // 如果 A 原本在 B 的左邊,抽出 A 後 B 會往左遞補,所以必須插在 newIdxB + 1 的位置(B 的後面)。
          arr.splice(charIdxA > charIdxB ? newIdxB : newIdxB + 1, 0, selectedEditItem);
          
          // 將剛剛搬移的單字 A 記錄下來,以便渲染時給予特殊的紫色高亮樣式 (.just-moved)
          justMovedItem = selectedEditItem;

          // 【4. 將排序後的結果,即時同步寫回底層的 filecontent 文字檔案資料】
          // 將整份文件內容以換行符號切割成行陣列
          let linesFile = filecontent.split('\n');
          
          // 尋找 %chardef 區塊在檔案中開始的行索引
          let chardefBeginIdx = linesFile.indexOf('%chardef begin');
          let chardefEndIdx = -1;
          let inDef = false;

          // 透過迴圈尋找該 %chardef 區塊應該結束的邊界位置(遇到 end、x 開頭或包含 z 的非首位編碼列)
          for (let i = 0; i < linesFile.length; i++) {
            if (linesFile[i] === '%chardef begin') {
              inDef = true;
              continue;
            }
            if (!inDef) continue;
            if (
              linesFile[i] === '%chardef end' ||
              linesFile[i].startsWith('x') ||
              /[^z]z/.test(linesFile[i].split('\t')[0])
            ) {
              chardefEndIdx = i;
              break;
            }
          }

          // 如果成功定位了檔案中 %chardef 的起點與終點邊界
          if (chardefBeginIdx > -1 && chardefEndIdx > -1) {
            let newBlock = [];
            // 遍歷記憶體中最新的 charDefCodeMap,重新拼裝成符合表格格式的「編碼\t單字」字串陣列
            for (let [c, chars] of charDefCodeMap) {
              chars.forEach(ch => newBlock.push(`${c}\t${ch}`));
            }
            // 利用 splice 將舊的 %chardef 區塊整塊刪除,替換成剛剛拼裝好的全新排序區塊
            linesFile.splice(chardefBeginIdx + 1, chardefEndIdx - chardefBeginIdx - 1, ...newBlock);
            
            // 將行陣列重新用換行符號結合,更新全域的 filecontent 原始文字
            filecontent = linesFile.join('\n');

            // 呼叫全域核心解析函式,全面更新整個網頁的所有統計、詞組 Map 與重組下載檔案
            processContent();
            
            // 取得同步後的全新系統狀態
            let newState = getSystemState();
            
            // 比對 oldState 與 newState,找出哪些字順序變了,並將該編碼列自動展開、標記紅虛線 (.changed)
            detectAndRenderChanges(oldState, newState);
          }
          
          // 重新繪製編輯區域的 DOM 元素
          renderEditArea();
        }
      }
    } 
    // 【5. 詞組模式(Phrase Mode)的順序調整邏輯,結構與單字模式雷同】
    else {
      // 取得詞組字串陣列
      let lines = mcc0string.split('\n').filter(l => l.trim() !== '');
      
      // 尋找詞組 A 與詞組 B 的位置並進行 splice 交換計算...(此處後續為詞組搬移,與單字模式原理相同)
      let originalIdxA = lines.indexOf(selectedEditItem);
      let originalIdxB = lines.indexOf(word);

      if (originalIdxA > -1 && originalIdxB > -1 && originalIdxA !== originalIdxB) {
        let placement = originalIdxA > originalIdxB ? 'before' : 'after';
        saveStateForUndo(`${selectedEditItem} -> ${placement} ${word}`);

        lines.splice(originalIdxA, 1);
        let newIdxB = lines.indexOf(word);

        if (originalIdxA > originalIdxB) {
          lines.splice(newIdxB, 0, selectedEditItem);
        } else {
          lines.splice(newIdxB + 1, 0, selectedEditItem);
        }

        justMovedItem = selectedEditItem;
        processContent(JSON.stringify(lines));
        let newState = getSystemState();
        detectAndRenderChanges(oldState, newState);
      }
    }

    // 搬移順序完成後,重置選取狀態為 null,以便使用者點擊下一個單字進行新的一輪操作
    selectedEditItem = null; 
  }
}

```

Re: 工欲善其事,必先利其器:準備更新哈哈倉頡

发表于 : 2026年 5月 23日 09:21
ejsoon
改寫getSystemState函式,當單字模式時,它讀的應該是當前的單字信息,而不是以z開頭的mcc詞語信息。

代码: 全选

function getSystemState() {
  let state = new Map();

  if (isSingleWordMode) {
    // --- 單字模式:只讀取與目前單字狀態相關的資訊 ---
    // 遍歷所有單字編碼對應表,將當前的編碼與單字陣列複製一份存入快照狀態
    for (let [k, v] of charDefCodeMap.entries()) {
      state.set(k, [...v]);
    }
  } else {
    // --- 詞組模式:維持原本讀取 z 開頭的 mcc 詞語資訊 ---
    state.set('z', mcc0.get('z') || []);
    for (let [k, v] of mcc1all) state.set(k, v.slice(0, 5));
    for (let [k, v] of mcc2all) state.set(k, v.slice(0, 4));
    for (let [k, v] of mcc3) {
      if (!state.has(k)) state.set(k, []);
      state.get(k).push(...v);
    }
    for (let [k, v] of mcc4) {
      if (!state.has(k)) state.set(k, []);
      state.get(k).push(...v);
    }
  }
  
  return state;
}

Re: 工欲善其事,必先利其器:準備更新哈哈倉頡

发表于 : 2026年 5月 23日 09:45
ejsoon
originalSnapshotState應該如何在單字模式時正確賦值?

代码: 全选

在單字模式下,要讓 `originalSnapshotState` 正確賦值且不與詞組模式的資料混雜,最核心的觀念是:**在進入單字模式(提交查詢)的當下,就應該立刻把當時所有單字的狀態完整複製一份,作為比對的基線(Baseline),並且將舊的詞組快照完全清空。**

配合您前面提到的 `getSingleState()` 獨立函式,正確的賦值與處理邏輯應該在 **`handleEditSubmit()`** 函式中進行。

以下是正確的賦值步驟與修改方式:

### 1. 修改 `handleEditSubmit` 函式

請在 `handleEditSubmit` 內處理 `isSingleWordMode` 的分支中,加入對 `originalSnapshotState` 的初始化賦值:

```javascript
function handleEditSubmit() {
  // ... 前面的輸入框檢查與模式判定省略 ...

  if (isSingleWordMode) {
    activeEditKeys.clear();
    for (let i = 1; i <= singleWordActiveCode.length; i++) {
      activeEditKeys.add(singleWordActiveCode.substring(0, i));
    }

    // === 【核心修改:在此處為單字模式正確賦值】 ===
    // 1. 先徹底清空可能殘留的詞組快照資料
    originalSnapshotState = new Map(); 
    
    // 2. 使用專門的 getSingleState() 讀取目前所有單字的基線狀態並存入
    let singleState = getSingleState();
    for (let [k, v] of singleState.entries()) {
      originalSnapshotState.set(k, v);
    }
    // ===========================================

  } else {
    // 詞組模式原有的邏輯保持不變
    originalSnapshotState = new Map(); // 建議詞組模式進入時也清空舊的
    let sysState = getSystemState();
    for (let [k, v] of sysState.entries()) {
      originalSnapshotState.set(k, v);
    }
  }

  // 渲染畫面
  renderEditArea();
}

```

---

### 2. 同步修正 `renderEditArea` 內的「補丁寫法」

在您原本的 `renderEditArea()` 程式碼中,有一段防止報錯的「補丁防線」:

```javascript
// 原本的寫法:
if (!originalSnapshotState.has(k)) {
  originalSnapshotState.set(k, [...mapItems]);
}

```

既然我們已經在 `handleEditSubmit` 進入模式時就全面且正確地為 `originalSnapshotState` 賦值了,這段舊程式碼就可以**安全地移除**,或者改寫為純粹的防呆機制,避免它在渲染過程中又無意間寫入錯誤的即時狀態。

---

### 這樣改寫的好處是什麼?

1. **時機點正確**:使用者輸入完單字並按下 Enter(或點擊 Submit)的「那一瞬間」,當前的字典狀態就被完美凍結在 `originalSnapshotState` 中。
2. **資料隔離**:因為呼叫了 `getSingleState()`,快照裡面**完全不會存在**以 `z` 開頭的 `mcc` 詞組資訊,`detectAndRenderChanges` 在做迴圈比對時,就不會因為詞組被 `processContent()` 重新解析而發生張冠李戴、亂高亮其他行編碼的錯誤。
3. **乾淨的 Undo 基礎**:不論使用者在單字模式下如何連續搬移單字,紅虛線(`.changed`)都能精準比對出「目前順序」與「最初剛進入編輯模式時」的絕對差異。

Re: 工欲善其事,必先利其器:準備更新哈哈倉頡

发表于 : 2026年 5月 23日 10:05
ejsoon

代码: 全选

這是將所有注釋(包含中文標籤與邏輯說明)翻譯為英文後的代碼:

```javascript
function handleEditResetOnly() {
  if (!originalSnapshotState || originalSnapshotState.size === 0) return;

  if (isSingleWordMode) {
    // === [Reset Logic for Single Word Mode] ===
    
    // 1. Restore the word table in memory directly to the baseline state when entering (deep copy)
    charDefCodeMap.clear();
    for (let [k, v] of originalSnapshotState.entries()) {
      charDefCodeMap.set(k, [...v]);
    }

    // 2. Reassemble the restored word order and write it back to the underlying filecontent
    let linesFile = filecontent.split('\n');
    let chardefBeginIdx = linesFile.indexOf('%chardef begin');
    let chardefEndIdx = -1;
    let inDef = false;

    for (let i = 0; i < linesFile.length; i++) {
      if (linesFile[i] === '%chardef begin') {
        inDef = true;
        continue;
      }
      if (!inDef) continue;
      if (
        linesFile[i] === '%chardef end' ||
        linesFile[i].startsWith('x') ||
        /[^z]z/.test(linesFile[i].split('\t')[0])
      ) {
        chardefEndIdx = i;
        break;
      }
    }

    if (chardefBeginIdx > -1 && chardefEndIdx > -1) {
      let newBlock = [];
      for (let [c, chars] of charDefCodeMap) {
        chars.forEach(ch => newBlock.push(`${c}\t${ch}`));
      }
      // Overwrite the currently modified block with the initialized word block
      linesFile.splice(chardefBeginIdx + 1, chardefEndIdx - chardefBeginIdx - 1, ...newBlock);
      filecontent = linesFile.join('\n');

      // Comprehensive refresh of the system state
      processContent();
    }

  } else {
    // === [Original Reset Logic for Phrase Mode (Keep Unchanged)] ===
    let originalOrderMap = new Map();
    for (let [key, words] of originalSnapshotState) {
      words.forEach(word => {
        if (!originalOrderMap.has(word)) {
          originalOrderMap.set(word, true);
        }
      });
    }

    let currentLines = mcc0string.split('\n').filter(l => l.trim() !== '');
    let newlyAddedWords = currentLines.filter(word => !originalOrderMap.has(word));
    let restoredOriginalLines = [];

    let rawFileLines = filecontent.split('\n').map(line => line.replace(/\r$/, ''));
    let xLine = -1;
    for (let i = rawFileLines.length - 1; i >= 0; i--) {
      if (rawFileLines[i].startsWith('x')) {
        xLine = i;
        break;
      }
    }
    let charDefEndLine = rawFileLines.indexOf('%chardef end');

    if (xLine !== -1 && charDefEndLine !== -1) {
      let rawMcc0Lines = rawFileLines.slice(xLine + 1, charDefEndLine);
      rawMcc0Lines = rawMcc0Lines
        .filter(line => {
          const parts = line.split('\t');
          if (parts.length > 1 && parts[0].startsWith('z')) {
            if (parts[0].slice(1).includes('z')) return false;
          }
          return true;
        })
        .map(line => {
          const parts = line.split('\t');
          return parts.length > 1 ? parts[1] : line;
        })
        .filter((line, index, arr) => arr.indexOf(line) == index);

      let activeOriginalWords = new Set(currentLines.filter(word => originalOrderMap.has(word)));
      restoredOriginalLines = rawMcc0Lines.filter(word => activeOriginalWords.has(word));
    }

    let mergedRevertedLines = [...restoredOriginalLines, ...newlyAddedWords];
    processContent(JSON.stringify(mergedRevertedLines));
  }

  // === [Common UI Clear and Re-render] ===
  changedWords.clear();
  selectedEditItem = null;
  justMovedItem = null;
  pendingAction = null;
  updateActionButtonsUI();
  renderEditArea();
}

```