在 GASO 專案的開發過程中,我們不斷地在功能完整性和使用者體驗之間尋找平衡點。經過二十六天的開發,我們已經建立了一個功能豐富的學習地圖平台,但隨著功能的增加,介面也變得越來越複雜。
今天,我們將專注於一個重要的設計原則:簡化。有時候,移除不必要的功能比添加新功能更能提升使用者體驗。讓我們一起探索如何通過簡化來優化 GASO 的使用者體驗。
今天最重要的改進其實不在程式碼層面,而是在內容層面。我完成了 65個節點的完整提示詞撰寫,這是 GASO 專案的核心價值所在。
每個提示詞都經過精心設計,包含以下要素:
這 65 個提示詞代表了:
在之前的版本中,當使用者點擊節點時,彈出視窗會顯示大量的技術資訊:
<!-- 之前的複雜顯示 -->
<div class="info-row">
<span class="info-label">ID:</span>
<span class="info-value">${nodeDetail.id}</span>
</div>
<div class="info-row">
<span class="info-label">屬性:</span>
<span class="info-value">${nodeDetail.attribute}</span>
</div>
<div class="info-row">
<span class="info-label">座標:</span>
<span class="info-value">X: ${nodeCoordinates.x.toFixed(2)}, Y: ${nodeCoordinates.y.toFixed(2)}</span>
</div>
<div class="info-row">
<span class="info-label">個人化:</span>
<span class="info-value">已套用您的個人資訊</span>
</div>
這些資訊對一般使用者來說:
我們將節點資訊簡化為只顯示最重要的內容:
<!-- 簡化後的顯示 -->
<div class="node-info">
<div class="info-row">
<span class="info-label">提示:</span>
<div class="info-prompt">
${fullPrompt.replace(/\n/g, '<br>')}
<button class="copy-button" data-prompt='${fullPrompt.replace(/'/g, "'")}'>複製</button>
</div>
</div>
</div>
在之前的版本中,複製按鈕使用 onclick
屬性直接綁定事件:
<!-- 之前的實現方式 -->
<button class="copy-button" onclick="copyPromptToClipboard('${fullPrompt.replace(/'/g, "\\'")}')">複製</button>
這種方式存在以下問題:
我們改用 data
屬性和事件委託的方式:
<!-- 改善後的實現方式 -->
<button class="copy-button" data-prompt='${fullPrompt.replace(/'/g, "'")}'>複製</button>
// 在 showCustomModal 函數中添加事件監聽器
const copyButton = content.querySelector('.copy-button');
if (copyButton) {
copyButton.addEventListener('click', function() {
const promptText = this.getAttribute('data-prompt');
copyPromptToClipboard(promptText);
});
}
我們將 showCustomModal
函數的職責更加明確:
function showCustomModal(nodeDetail) {
console.log("顯示節點詳情:", nodeDetail);
const modal = document.getElementById('nodeModal');
const title = document.getElementById('modalTitle');
const content = document.getElementById('modalContent');
// 生成客製化的 prompt
const customPrefix = generateCustomPromptPrefix();
const fullPrompt = customPrefix + nodeDetail.prompt;
// 設定標題
title.textContent = `${nodeDetail.label}`;
// 設定內容(簡化版)
content.innerHTML = `
<div class="node-info">
<div class="info-row">
<span class="info-label">提示:</span>
<div class="info-prompt">
${fullPrompt.replace(/\n/g, '<br>')}
<button class="copy-button" data-prompt='${fullPrompt.replace(/'/g, "'")}'>複製</button>
</div>
</div>
</div>
`;
// 顯示彈出視窗
modal.style.display = 'block';
// 綁定事件監聽器
bindCopyButtonEvent(content);
}
我們將事件綁定邏輯提取為獨立的函數:
function bindCopyButtonEvent(content) {
const copyButton = content.querySelector('.copy-button');
if (copyButton) {
copyButton.addEventListener('click', function() {
const promptText = this.getAttribute('data-prompt');
copyPromptToClipboard(promptText);
});
}
}
在 GASO 專案中,提示詞不僅僅是技術實現的一部分,更是整個學習體驗的核心。沒有高品質的提示詞,再好的介面設計也沒有意義。
// 提示詞的價值鏈
const valueChain = {
content: "高品質的提示詞", // 核心價值
interface: "簡潔的介面設計", // 價值傳遞
experience: "優質的學習體驗" // 最終目標
};
撰寫 65 個節點的提示詞面臨以下挑戰:
每個提示詞都遵循以下結構:
## 提示詞結構模板
### 1. 明確的學習目標
"請教導如何使用 Google Apps Script 自動化 Google Calendar 操作"
### 2. 結構化的學習內容
1. Calendar API 的基本概念
2. 如何建立、修改和刪除事件
3. 行事曆的查詢和篩選
4. 重複事件的處理
5. 提醒和通知設定
6. 實際的應用場景範例
7. 權限管理和安全性考量
### 3. 具體的學習要求
"請提供完整的程式碼範例和實用案例"
每個提示詞都必須符合以下標準:
// 提示詞的資料結構
const promptStructure = {
nodeId: "Ca", // 節點唯一識別碼
nodeLabel: "Google Calendar 自動化", // 節點顯示名稱
prompt: `請教導如何使用 Google Apps Script 自動化 Google Calendar 操作:
1. Calendar API 的基本概念
2. 如何建立、修改和刪除事件
3. 行事曆的查詢和篩選
4. 重複事件的處理
5. 提醒和通知設定
6. 實際的應用場景範例
7. 權限管理和安全性考量
請提供完整的程式碼範例和實用案例。`
};
// 提示詞與個人化資訊的整合
function generateCustomPromptPrefix() {
const userInfo = state.userInfo;
if (userInfo.role && userInfo.title && userInfo.gasLevel) {
return `我是${userInfo.role}的${userInfo.title},我對 Google Apps Script 熟悉的程度是${userInfo.gasLevel}。`;
}
return "";
}
// 完整的提示詞生成
function generateFullPrompt(nodeDetail) {
const customPrefix = generateCustomPromptPrefix();
return customPrefix + nodeDetail.prompt;
}
// 提示詞的持續改進
const promptImprovement = {
collectFeedback() {
// 收集使用者對提示詞的回饋
},
analyzeEffectiveness() {
// 分析提示詞的學習效果
},
updatePrompts() {
// 根據回饋更新提示詞
}
};
我們移除了以下不必要的資訊:
對於必要的功能,我們進行了簡化:
在簡化的基礎上,我們優化了使用者體驗:
// 問自己:使用者真的需要這個資訊嗎?
if (userNeedsThisInfo) {
// 保留
} else {
// 移除
}
// 問自己:這個功能對核心目標有幫助嗎?
if (helpsCoreGoal) {
// 保留並優化
} else {
// 移除或簡化
}
// 問自己:這個功能容易維護嗎?
if (easyToMaintain) {
// 保留
} else {
// 重構或移除
}
// 之前的複雜轉義
onclick="copyPromptToClipboard('${fullPrompt.replace(/'/g, "\\'")}')"
// 使用 HTML 實體,更安全且更清晰
data-prompt='${fullPrompt.replace(/'/g, "'")}'
<!-- 內聯事件處理的問題 -->
<button onclick="someFunction()">按鈕</button>
// 事件委託的優點
element.addEventListener('click', function() {
// 事件處理邏輯
});
<!-- 過多的 DOM 元素 -->
<div class="info-row">
<span class="info-label">ID:</span>
<span class="info-value">...</span>
</div>
<div class="info-row">
<span class="info-label">屬性:</span>
<span class="info-value">...</span>
</div>
<!-- 更多不必要的元素 -->
<!-- 精簡的 DOM 結構 -->
<div class="node-info">
<div class="info-row">
<span class="info-label">提示:</span>
<div class="info-prompt">
<!-- 只保留必要的內容 -->
</div>
</div>
</div>
使用者點擊節點後看到:
使用者點擊節點後只看到:
┌─────────────────────────┐
│ 節點名稱 │
├─────────────────────────┤
│ ID: node_123 │
│ 屬性: learning │
│ 座標: X: 123.45, Y: 67.89│
│ 個人化: 已套用 │
│ 提示: 學習內容... │
│ [複製] │
└─────────────────────────┘
┌─────────────────────────┐
│ 節點名稱 │
├─────────────────────────┤
│ 提示: 學習內容... │
│ [複製] │
└─────────────────────────┘
<div class="node-info">
<div class="info-row">...</div> <!-- ID -->
<div class="info-row">...</div> <!-- 屬性 -->
<div class="info-row">...</div> <!-- 座標 -->
<div class="info-row">...</div> <!-- 個人化 -->
<div class="info-row">...</div> <!-- 提示 -->
</div>
<div class="node-info">
<div class="info-row">...</div> <!-- 提示 -->
</div>
功能數量 ↑ = 產品價值 ↑
功能精準度 ↑ = 使用者滿意度 ↑
function calculateUserValue(feature) {
const userNeed = feature.solvesUserProblem;
const frequency = feature.usageFrequency;
const satisfaction = feature.userSatisfaction;
return userNeed * frequency * satisfaction;
}
function calculateMaintenanceCost(feature) {
const complexity = feature.codeComplexity;
const bugs = feature.bugFrequency;
const updates = feature.updateFrequency;
return complexity * bugs * updates;
}
function alignsWithCoreGoal(feature) {
const coreGoal = "幫助使用者學習 Google Apps Script";
return feature.contributesTo(coreGoal);
}
// 不是這樣:
removeAllFeatures();
// 而是這樣:
focusOnCoreFeatures();
// 不是這樣:
// 懶得實作,所以移除
// 而是這樣:
// 經過深思熟慮,決定移除
## 功能清單
- [ ] 節點 ID 顯示
- [ ] 節點屬性顯示
- [ ] 座標資訊顯示
- [ ] 個人化狀態顯示
- [ ] 提示內容顯示
- [ ] 複製功能
## 功能評估
- [ ] 節點 ID 顯示 - 使用者不需要 ❌
- [ ] 節點屬性顯示 - 內部資訊 ❌
- [ ] 座標資訊顯示 - 開發者資訊 ❌
- [ ] 個人化狀態顯示 - 系統狀態 ❌
- [ ] 提示內容顯示 - 核心功能 ✅
- [ ] 複製功能 - 核心功能 ✅
## 最終決定
- [x] 提示內容顯示 - 保留並優化
- [x] 複製功能 - 保留並改善
- [ ] 其他功能 - 移除
// 測試簡化前後的版本
const versionA = "複雜版本";
const versionB = "簡化版本";
// 比較使用者行為
compareUserBehavior(versionA, versionB);
// 詢問使用者對簡化的看法
const questions = [
"你覺得這個介面如何?",
"你覺得哪些資訊是必要的?",
"你覺得哪些資訊是多餘的?"
];
interviewUsers(questions);
// 移除技術細節
removeTechnicalDetails();
// 簡化事件處理
simplifyEventHandling();
// 優化互動流程
optimizeUserInteraction();
function showCustomModal(nodeDetail) {
// 大量複雜的邏輯
const customPrefix = generateCustomPromptPrefix();
const fullPrompt = customPrefix + nodeDetail.prompt;
const nodeCoordinates = getNodeCoordinates(nodeDetail.id);
// 複雜的 HTML 生成
content.innerHTML = `
<div class="node-info">
<div class="info-row">
<span class="info-label">ID:</span>
<span class="info-value">${nodeDetail.id}</span>
</div>
${nodeDetail.attribute ? `
<div class="info-row">
<span class="info-label">屬性:</span>
<span class="info-value">${nodeDetail.attribute}</span>
</div>
` : ''}
${nodeCoordinates ? `
<div class="info-row">
<span class="info-label">座標:</span>
<span class="info-value">X: ${nodeCoordinates.x.toFixed(2)}, Y: ${nodeCoordinates.y.toFixed(2)}</span>
</div>
` : ''}
<div class="info-row">
<span class="info-label">提示:</span>
<div class="info-prompt">
${fullPrompt.replace(/\n/g, '<br>')}
<button class="copy-button" onclick="copyPromptToClipboard('${fullPrompt.replace(/'/g, "\\'")}')">複製</button>
</div>
</div>
${state.userInfo.role && state.userInfo.title && state.userInfo.gasLevel ? `
<div class="info-row">
<span class="info-label">個人化:</span>
<span class="info-value">已套用您的個人資訊</span>
</div>
` : ''}
</div>
`;
}
function showCustomModal(nodeDetail) {
const modal = document.getElementById('nodeModal');
const title = document.getElementById('modalTitle');
const content = document.getElementById('modalContent');
// 生成客製化的 prompt
const customPrefix = generateCustomPromptPrefix();
const fullPrompt = customPrefix + nodeDetail.prompt;
// 設定標題和內容
title.textContent = `${nodeDetail.label}`;
content.innerHTML = generateModalContent(fullPrompt);
// 顯示彈出視窗並綁定事件
modal.style.display = 'block';
bindCopyButtonEvent(content);
}
function generateModalContent(fullPrompt) {
return `
<div class="node-info">
<div class="info-row">
<span class="info-label">提示:</span>
<div class="info-prompt">
${fullPrompt.replace(/\n/g, '<br>')}
<button class="copy-button" data-prompt='${fullPrompt.replace(/'/g, "'")}'>複製</button>
</div>
</div>
</div>
`;
}
function bindCopyButtonEvent(content) {
const copyButton = content.querySelector('.copy-button');
if (copyButton) {
copyButton.addEventListener('click', function() {
const promptText = this.getAttribute('data-prompt');
copyPromptToClipboard(promptText);
});
}
}
// 將功能分離到不同的模組
const ModalModule = {
show(nodeDetail) {
// 顯示邏輯
},
generateContent(fullPrompt) {
// 內容生成邏輯
},
bindEvents(content) {
// 事件綁定邏輯
}
};
const CopyModule = {
bindButton(content) {
// 複製按鈕綁定邏輯
},
copyToClipboard(text) {
// 複製到剪貼簿邏輯
}
};
// 測試內容生成
describe('generateModalContent', function() {
it('should generate correct HTML structure', function() {
const prompt = 'Test prompt';
const result = generateModalContent(prompt);
expect(result).toContain('node-info');
expect(result).toContain('copy-button');
expect(result).toContain('Test prompt');
});
});
// 測試事件綁定
describe('bindCopyButtonEvent', function() {
it('should bind click event to copy button', function() {
const mockContent = {
querySelector: jest.fn().mockReturnValue({
addEventListener: jest.fn()
})
};
bindCopyButtonEvent(mockContent);
expect(mockContent.querySelector).toHaveBeenCalledWith('.copy-button');
});
});
今天的開發讓我深刻體會到兩個重要的智慧:內容為王和簡化之美。
首先,內容為王:完成了 65 個節點的完整提示詞撰寫,這才是 GASO 專案真正的核心價值。沒有高品質的內容,再好的介面設計也沒有意義。
其次,簡化之美:在軟體開發中,我們常常陷入「功能越多越好」的迷思,但實際上,真正的價值在於解決使用者的核心問題,而不是提供所有可能的功能。
在 GASO 的開發旅程中,我們學會了在功能完整性和使用者體驗之間找到平衡點。有時候,移除功能比添加功能更能提升產品的價值。
簡化的智慧,不在於做得少,而在於做得對。
如果想要看一些我鐵人賽之外的 Google Apps Script 分享,
也歡迎追蹤我的 Threads 和 Facebook