SavvyBot Phase 6
UX改善とインタラクティブTODO管理
ユーザー名表示、期限表示改善、LINE Quick Reply Buttons統合、インタラクティブTODO管理(完了/未完了/削除/編集)を実装。メニューシステム、日本語エイリアス対応で操作性を大幅に向上。
Technologies Used
SavvyBot Phase 6 ― UX改善とインタラクティブTODO管理
← Phase 5へ | Phase 7計画へ → | プロジェクト概要
概要
Phase 6では、ユーザー体験を大幅に改善し、インタラクティブなTODO管理機能を実装しました。LINE Quick Reply Buttonsを活用した3ステップ対話型UIにより、複雑な操作を直感的に行えるようになりました。
Phase 6完了状況(2025年11月2日時点):
- ✅ Phase 6.1: ユーザー名表示と暗黙的自己割り当て
- ✅ Phase 6.2: 期限表示改善とキーワード検出
- ✅ Phase 6.3.1: インタラクティブTODO管理(テキストコマンド)
- ✅ Phase 6.3.1 追加: メニューシステム + 日本語エイリアス + データ品質改善
- ✅ Phase 6.3.2: LINE Quick Reply Buttons
- ✅ Phase 6.4: TODO Done/Undone/Delete Commands
- ✅ Phase 6.5: TODO Edit Feature
Phase 6.1: ユーザー名表示と暗黙的自己割り当て(完了 - 2025年10月28日)
達成事項
- ✅ LINEユーザープロファイルキャッシュシステムの実装(24時間TTL)
- ✅ TODOの暗黙的自己割り当て機能("私がやる" → 発言者に自動割り当て)
- ✅ 全コマンドでuserIdの代わりに表示名を表示(@林田夏樹、@田中さん など)
- ✅ 実機テストで動作確認完了
技術的実装
LINEユーザープロファイルキャッシュ
データベーススキーマ:
CREATE TABLE user_profiles (
user_id TEXT PRIMARY KEY,
display_name TEXT NOT NULL,
picture_url TEXT,
status_message TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- TTLチェック用インデックス
CREATE INDEX idx_user_profiles_updated_at ON user_profiles(updated_at);
プロフィール取得ロジック (lib/db.ts):
export async function getUserProfile(
userId: string,
lineAccessToken: string
): Promise<Result<UserProfile, string>> {
// キャッシュ確認(24時間以内)
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000)
const { data: cached } = await supabase
.from('user_profiles')
.select('*')
.eq('user_id', userId)
.gte('updated_at', twentyFourHoursAgo.toISOString())
.single()
if (cached) {
return { ok: true, value: cached }
}
// LINE Profile API呼び出し
const response = await fetch(
`https://api.line.me/v2/bot/profile/${userId}`,
{ headers: { Authorization: `Bearer ${lineAccessToken}` } }
)
const profile = await response.json()
// キャッシュ更新
await supabase.from('user_profiles').upsert({
user_id: userId,
display_name: profile.displayName,
picture_url: profile.pictureUrl,
status_message: profile.statusMessage,
updated_at: new Date().toISOString(),
})
return { ok: true, value: profile }
}
暗黙的自己割り当て
実装内容 (index.ts):
// OpenAIがTODOを抽出
const extractionResult = await extractInformation(messages, userId, groupId)
for (const todo of extractionResult.value.todos) {
// assigneeが認識できない場合は発言者に割り当て
const assignee = todo.assignee || userId
await db.insertTodo({
group_id: groupId,
task: todo.task,
assignee, // 暗黙的自己割り当て
deadline: todo.deadline,
})
}
効果
修正前:
⏳ タスク1 (担当: U1234567890)
⏳ タスク2 (担当: U9876543210)
修正後:
⏳ タスク1 (担当: @林田夏樹)
⏳ タスク2 (担当: @田中さん)
Phase 6.2: 期限表示改善とキーワード検出(完了 - 2025年10月28日)
達成事項
- ✅ 期限なしTODOに「期日未定」と明示表示
- ✅ 緊急度キーワードの自動認識(「今日」「急ぎ」「至急」→今日の日付を自動設定)
- ✅ OpenAIへの現在日時コンテキスト提供(相対表現の正確な解釈)
- ✅ デプロイ完了
実装内容
期限表示ロジック (lib/commands.ts):
let deadline = ''
if (t.deadline) {
const jstDate = new Date(t.deadline)
jstDate.setHours(jstDate.getHours() + 9) // UTC → JST
deadline = ` (期限: ${jstDate.toLocaleDateString('ja-JP')})`
} else {
deadline = ' (期日未定)'
}
現在日時コンテキスト (lib/openai.ts):
const currentDate = new Date().toLocaleString('ja-JP', {
timeZone: 'Asia/Tokyo',
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long',
hour: '2-digit',
minute: '2-digit',
})
const systemPrompt = `あなたはSavvyBotです。
現在の日時: ${currentDate}
TODOの期限を抽出する際のルール:
1. 明示的な日付(「10月30日まで」)→ その日付の23:59
2. 緊急度キーワード(「今日」「急ぎ」「至急」)→ 今日の23:59
3. 相対表現(「明日」「今週」「来週」)→ 適切に計算
4. 期限の言及なし → null`
効果
修正前:
⏳ パンを買う
修正後:
⏳ パンを買う (期日未定)
⏳ 今日牛乳を買う (期限: 10/28)
Phase 6.3: インタラクティブTODO管理
Phase 6.3.1: テキストコマンド + メニューシステム(完了 - 2025年10月28日)
新しいコマンド
TODO操作コマンド:
/sv:done <番号>- TODO完了/sv:undone <番号>- TODO未完了に戻す/sv:delete todo <番号>- TODO削除/sv:edit todo <番号> [オプション]- TODO編集
メニューシステム:
/sv:menuまたは/sv:メニュー- コマンドメニュー表示/sv:1〜/sv:9- 番号で直接実行/sv:1- 議事録生成/sv:2- 決定事項確認/sv:3- タスク一覧/sv:4- 質問リスト/sv:5- 会話検索/sv:6- タスク完了/sv:7- タスク削除/sv:8- タスク編集/sv:9- ヘルプ表示
日本語エイリアス:
/sv:議事録,/sv:決定,/sv:タスク,/sv:質問,/sv:検索/sv:完了,/sv:削除,/sv:編集,/sv:ヘルプ
データ品質改善
非実行可能TODO のフィルタリング (lib/openai.ts):
function isValidTodo(task: string): boolean {
const nonActionablePatterns = [
/特に.*ない/,
/今のところ.*ない/,
/特になし/,
/特に決まっていない/,
]
for (const pattern of nonActionablePatterns) {
if (pattern.test(task)) {
logger.debug({
event_type: 'extraction.todo.filtered',
task,
reason: 'non_actionable',
})
return false
}
}
return true
}
Phase 6.3.2: LINE Quick Reply Buttons(完了 - 2025年10月28日)
達成事項
- ✅ LINE Messaging API Quick Reply統合
- ✅ メニューコマンドに6つのQuick Replyボタン追加
- ✅ TODOリストに表示モード切り替えボタン追加
- ✅ 型安全なCommandResponse実装(Union Type)
- ✅ デプロイ完了(実機テスト済み)
ユーザーフィードバック: 「最高です!」
技術的実装
LINE Quick Reply型定義 (lib/line.ts):
export interface QuickReplyButton {
label: string // ボタンラベル(最大20文字)
text: string // タップ時に送信されるテキスト
}
interface LineQuickReply {
items: Array<{
type: 'action'
action: {
type: 'message'
label: string
text: string
}
}>
}
CommandResponse Union Type:
export type CommandResponse =
| string
| {
text: string
quickReply?: QuickReplyButton[]
}
replyToLine()拡張 (lib/line.ts):
export async function replyToLine(
replyToken: string,
text: string,
config: Config,
logger: Logger,
requestId: string,
isError: boolean = false,
quickReply?: QuickReplyButton[]
): Promise<Result<void, string>> {
const message: any = {
type: 'text',
text,
}
if (quickReply && quickReply.length > 0) {
message.quickReply = {
items: quickReply.slice(0, 13).map(btn => ({
type: 'action',
action: {
type: 'message',
label: btn.label,
text: btn.text,
},
})),
}
}
const response = await fetch('https://api.line.me/v2/bot/message/reply', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${config.lineChannelAccessToken}`,
},
body: JSON.stringify({
replyToken,
messages: [message],
}),
})
return { ok: true, value: undefined }
}
UI例
メニューコマンド(/sv:menu):
📋 【コマンドメニュー】
以下の番号でコマンドを実行できます:
1️⃣ /sv:1 - 議事録を生成
2️⃣ /sv:2 - 決定事項を確認
3️⃣ /sv:3 - タスク一覧(期限別)
...
[📝 議事録] [✅ 決定事項] [📋 タスク] [❓ 質問] [🔍 検索] [ℹ️ ヘルプ]
TODOリスト(期限別表示中):
📋 【タスク一覧】
1. ⏳ 資料準備 (期限: 11/5)
2. ✅ 会議設定 (期日未定)
[📅 日別表示] [👤 担当者別] [📋 メニュー]
Phase 6.4: TODO Done/Undone/Delete Commands(完了 - 2025年10月30日)
達成事項
- ✅
/sv:done,/sv:undone,/sv:deleteコマンド実装 - ✅ 対話型UI(Quick Reply選択)
- ✅ 一貫したソート順(sortTodos関数)
- ✅ インデックスベース操作(getTodoByIndex関数)
- ✅ 実機テスト成功
3ステップフロー
Step 1: コマンド実行 → TODO一覧 + Quick Replyボタン
📋 【完了するTODOを選択】
1. ⏳ 資料を準備する (期限: 11/5)
2. ⏳ 会議を設定する (期日未定)
━━━━━━━━━━━━━
👇 完了するTODOをタップ
[1] [2] [3] ... (Quick Replyボタン)
Step 2: 番号選択 → 確認メッセージ
✅ TODO 1 を完了にしました!
「資料を準備する」
確認: /sv:todo
Step 3: 結果確認 → 更新されたTODOリスト
📋 【タスク一覧】
1. ⏳ 会議を設定する (期日未定)
✅ 完了済み:
- 資料を準備する
実装内容
handleDoneCommand (lib/commands.ts):
export async function handleDoneCommand(
cmd: ParsedCommand,
ctx: CommandContext
): Promise<Result<CommandResponse, string>> {
const { args } = cmd
const { groupId, db } = ctx
// Step 1: No args → Show all TODOs with Quick Reply
if (args.length === 0) {
const allTodos = await db.getTodosByGroupId(groupId)
const sortedTodos = sortTodos(allTodos.value)
const quickReply: QuickReplyButton[] = sortedTodos
.filter(t => t.status !== 'completed')
.slice(0, 13)
.map((t, i) => ({
label: `${i + 1}`,
text: `/sv:done ${i + 1}`,
}))
return {
ok: true,
value: {
text: todoList,
quickReply,
},
}
}
// Step 2: Parse index and execute
const index = parseInt(args[0], 10) - 1
const todo = getTodoByIndex(allTodos.value, index)
if (!todo) {
return { ok: false, error: 'TODOが見つかりません' }
}
await db.completeTodo(todo.id)
return {
ok: true,
value: `✅ TODO ${index + 1} を完了にしました!\n「${todo.task}」\n\n確認: /sv:todo`,
}
}
Phase 6.5: TODO Edit Feature(完了 - 2025年11月2日)
達成事項
- ✅
/sv:editコマンドの対話型UI実装 - ✅ 3ステップフロー(一覧 → 選択 → 編集)
- ✅ 複数フィールド編集対応(task/deadline/assignee)
- ✅ Quick Reply選択UI(最大13個)
- ✅ 実機テスト成功(タスク内容編集を確認)
編集可能フィールド
task: タスク内容(複数単語対応、args.slice(i + 1).join(' '))deadline: 期限日付(YYYY-MM-DD形式、または 'none'/'null'/'削除' でクリア)assignee: 担当者名(@記号を自動削除)
実装内容 (lib/commands.ts):
export async function handleEditCommand(
cmd: ParsedCommand,
ctx: CommandContext
): Promise<Result<CommandResponse, string>> {
const { args } = cmd
const { groupId, db } = ctx
// Step 1: No args → Show all TODOs with Quick Reply
if (args.length === 0) {
const sortedTodos = sortTodos(allTodos)
const quickReply: QuickReplyButton[] = sortedTodos.slice(0, 13).map((t, i) => ({
label: `${i + 1}`,
text: `/sv:edit ${i + 1}`,
}))
return { ok: true, value: { text: todoList, quickReply } }
}
// Step 2: Only index provided → Show edit instructions
if (args.length === 1) {
return {
ok: true,
value: `✏️ TODO ${index + 1} を編集します
「${todo.task}」
編集方法:
━━━━━━━━━━━━━
📝 内容を変更:
/sv:edit ${index + 1} task 新しいタスク内容
📅 期限を変更:
/sv:edit ${index + 1} deadline 2025-11-05
👤 担当者を変更:
/sv:edit ${index + 1} assignee 田中`,
}
}
// Step 3: Parse field and value, execute update
const updates: { task?: string; assignee?: string; deadline?: Date | null } = {}
for (let i = 1; i < args.length; i++) {
const field = args[i].toLowerCase()
if (field === 'task' && i + 1 < args.length) {
updates.task = args.slice(i + 1).join(' ') // Multi-word support
break
} else if (field === 'deadline' && i + 1 < args.length) {
const dateValue = args[i + 1]
if (['none', 'null', '削除'].includes(dateValue)) {
updates.deadline = null
} else {
const deadlineDate = new Date(dateValue)
const jstDate = new Date(deadlineDate.getTime() + 9 * 60 * 60 * 1000)
jstDate.setHours(23, 59, 59, 999)
updates.deadline = jstDate
}
i++
} else if (field === 'assignee' && i + 1 < args.length) {
updates.assignee = args[i + 1].replace('@', '')
i++
}
}
const result = await db.updateTodo(todoId, updates)
return {
ok: true,
value: `✏️ TODO ${index + 1} を更新しました!
「${todo.task}」
${Object.entries(updates).map(([k, v]) => `${k}: ${v}`).join('\n')}
確認: /sv:todo`,
}
}
実機テスト結果
テスト1: タスク内容編集
入力: /sv:edit 1 task 新しいタスク内容
出力: ✅ 成功「資料を準備する必要がある」→「新しいタスク内容」
Phase 6の成果
UX改善の効果
修正前:
- ユーザーIDで表示(U1234567890)
- 期限なしTODOの曖昧な表示
- 複雑なコマンド構文の暗記が必要
- テキスト入力のみの操作
修正後:
- ユーザー名で表示(@林田夏樹)
- 期限の明確な表示(期日未定、期限: 11/5)
- メニューシステム + 番号ショートカット
- Quick Replyボタンでタップ操作
- 日本語コマンド対応
- 3ステップ対話型UI
技術的成果
- LINE Profile API統合(24時間TTLキャッシュ)
- バッチプロフィール取得による効率化
- ノンブロッキングなキャッシュ更新
- Union型(CommandResponse)による型安全性
- 3ステップフロー実装(一覧 → 選択 → 実行)
- 複数フィールド編集対応
- 柔軟な期限クリア('none'/'null'/'削除')
- JST期限設定(23:59:59.999に自動設定)
操作性向上
修正前の操作:
ユーザー: /sv:edit todo 1 --deadline 2025-11-05 --task 新しいタスク
修正後の操作:
ユーザー: /sv:編集
Bot: [1] [2] [3] ...(Quick Replyボタン)
ユーザー: [タップで1を選択]
Bot: 編集方法を表示
ユーザー: /sv:edit 1 task 新しいタスク
Bot: ✅ 更新完了
Phase 6完了日: 2025年11月2日 主な技術的成果: LINE Quick Reply Buttons、3ステップ対話型UI、複数フィールド編集、日本語エイリアス対応