iT邦幫忙

2025 iThome 鐵人賽

DAY 26
0
Mobile Development

《30 天 Flutter:跨平台 AI 行程規劃 App》系列 第 26

Day 26 - 擴充行程規劃 APP:加入 AI 對話式編輯器

  • 分享至 

  • xImage
  •  

背景與痛點

在近期實際規劃行程的過程中,我試用了這款 AI 行程規劃 APP。不得不說,AI 自動產生行程的功能確實大幅降低了使用門檻,使用者只要輸入需求就能立即獲得完整的旅行計畫。
不過在實際使用時,仍然遇到了一些明顯的痛點:

  1. 時間微調不便:AI 生成的時間安排往往不完全符合需求,手動調整起來相當繁瑣。
  2. 插入行程麻煩:若臨時想加入一間餐廳或景點,需要手動新增並重新調整順序。

這些問題在一定程度上削弱了 AI 行程規劃的流暢度。
在既有版本的基礎上,我開始思考如何進一步優化體驗,因此規劃了一個新功能 —— AI 對話式行程編輯器,讓使用者能透過自然語言直接修改行程。

以下內容將從「功能目標」一路延伸到「架構設計」,分享我的完整思考過程。


功能目標

  • 讓使用者能透過「自然語言對話」直接修改行程,而非繁瑣的手動操作。
  • 提供 即時預覽,避免誤解或 AI 出錯。
  • 保留 人工確認,確保最終行程符合需求後再進行儲存。

使用流程設計

  1. 初始化對話
    前端會將「當前行程 JSON」與「使用者訊息」一併傳給後端。
    後端除了回傳更新後的行程 JSON,還會針對每個行程(trip)建立一個對話 ID,未來可透過此 ID 持續進行對話與修改。

  2. AI 對話編輯
    使用者可能輸入:「幫我把早餐改成 10:30」或「在下午加一個西門町散步」。
    AI 會依照指令回傳修改後的行程 JSON。若使用者的需求過於模糊,系統會先回問並收集足夠資訊,再開始生成行程 JSON。

  3. 行程預覽
    當 AI 回應中帶有行程 JSON 參數時,對話框會出現一顆「預覽行程」按鈕。
    點擊後即可進入預覽畫面,使用者能立即看到修改後的行程效果。

  4. 來回確認
    如果使用者對行程不滿意,可以從預覽畫面返回,繼續輸入需求。
    AI 會持續更新行程,直到使用者滿意為止。

  5. 儲存行程
    當使用者最終確認沒問題後,按下「儲存」按鈕,行程 JSON 會被寫回資料庫。


畫面規劃

由於專案已經進入倒數五天,為了控制開發成本與複雜度,畫面規劃會盡量沿用現有架構與元件。
主要調整如下:

  • 行程總覽頁面:新增一顆可啟動 AI 對話的 Floating Action Button。
  • AI 對話畫面:新增專屬對話介面,用來與 AI 互動。
  • 其他畫面(如行程預覽、詳細資訊)則維持既有設計,不額外修改。

https://ithelp.ithome.com.tw/upload/images/20250909/20178195Dtv8TGDgFv.png

以上圖片用 Stitch Designer 產出,產出方式可參考:Day 2 - 把藍圖化為實際:用 Stitch Designer 產出 UI 初稿


資料流規劃:Update API

在 AI 對話式行程編輯器中,主要透過 Update API 進行資料交換。流程大致如下:

1. 首次與 AI 溝通

前端會帶上以下格式的請求:

  • itinerary:當前行程 JSON,讓 AI 在調整時有參照依據
  • text:使用者的第一句對話
// ===== request =====
{
  "itinerary": {
    "title": "日月潭兩日遊",
    "activities": []
  },
  "text": "幫我加入一個中餐行程"
}

AI 回應的內容會包含:

  • sessionId:後端為此 trip 生成的唯一 ID,用於之後的多輪對話
  • text:AI 回覆的訊息
// ===== response =====
{
  "itinerary": null,
  "sessionId": "481758166535634944",
  "text": "請提供以下資訊,以便我為您加入午餐行程:\n\n1. 您希望午餐的時間是幾點?\n2. 您對午餐的餐廳類型或菜系有偏好嗎?\n3. 您希望午餐地點在哪個區域?(例如:日月潭水社碼頭附近、伊達邵碼頭附近等)"
}

2. 多輪溝通過程

之後的對話中,前端會帶著 sessionId 與最新的 text 傳給後端,AI 會持續追問或提供調整,直到資訊足夠可以生成行程。


3. AI 生成行程

當 AI 判斷資訊完整,可以開始產生行程時,回應會包含:

  • itinerary:AI 規劃完成的行程 JSON
  • sessionId:此次對話的唯一 ID
  • text:AI 的回覆訊息(通常是生成完成的提示或補充說明)
// ===== response =====
{
  "itinerary": {
    "title": "日月潭兩日遊",
    "activities": []
  },
  "sessionId": "481758166535634944",
  "text": "我已為您加入午餐行程,請查看更新後的規劃。"
}

4. 前端處理

當前端收到回應中包含 itinerary 參數時,會在對話框中顯示 「預覽行程」按鈕
使用者點擊後即可進入預覽畫面,查看最新的行程內容,並決定是否進一步修改或儲存。


程式碼架構概覽

專案以 Riverpod 為核心做狀態管理,前端以「訊息列表」驅動 Chat UI,並透過 Repository 介面封裝與後端(或假資料)的互動。Model(資料結構)→ Provider(邏輯/狀態)→ Repository(資料來源)→ Views(畫面)。


models/chat_message.dart

class ChatMessage {
  final String? sessionId;     // 後端對話/行程 session(對應單一 trip)
  final ChatSender sender;     // user / ai
  final DateTime timestamp;    // 建立時間
  final Trip? itinerary;       // 可選:AI 回傳的行程(用於預覽)
  final String text;           // 顯示用訊息(含串流累積後的內容)

  const ChatMessage({
    this.sessionId,
    required this.sender,
    required this.timestamp,
    required this.text,
    this.itinerary,
  });
}

provider/chat_provider.dart

  • conversationIdProvider:提供當前對話/會話識別(例如同一個 trip 的聊天室)。

  • chatRepositoryProvider:注入資料來源(此處先用 FakeChatRepository)。

  • ChatController

    • State = 訊息列表(由舊到新)
    • 管理「送出訊息、建立 AI pending 氣泡、啟動串流、錯誤/重試、釋放訂閱」。
    • ref.onDispose 內部取消串流訂閱,避免 memory leak。
class ChatController extends AutoDisposeNotifier<List<ChatMessage>> {
  ChatRepository get _repo => ref.read(chatRepositoryProvider);
  String get _convId => ref.read(conversationIdProvider);

  StreamSubscription<String>? _streamSub;

  @override
  List<ChatMessage> build() {
    ref.onDispose(() => _streamSub?.cancel());
    return <ChatMessage>[];
  }

Future<void> sendText(String text) async {
    // 1) 添加使用者訊息
    // 2) 添加 AI 佔位泡泡
    // 3) 開始串流
    // 4) 完成或出錯
  }

  Future<void> retryLastFailed() async {
    // 根據最後一則失敗的 AI 訊息重送
  }
}

repositories/chat_repository.dart

  • ChatRepository 抽象了「送出使用者文字 → 取得 AI 逐字回覆」行為。
  • FakeChatRepository 先以切段字串模擬 token-by-token 串流。
abstract class ChatRepository {
  Stream<String> streamReply({
    String? sessionId,
    Trip? itinerary,
    required String text,
  });
}

class FakeChatRepository implements ChatRepository {
  @override
  Stream<String> streamReply({
     String? sessionId,
     Trip? itinerary,
     required String text,
  }) async* {
    final tokens = ("你剛剛說:「$text」;我可以如何協助?")
        .split(RegExp(r'(?<=,|。|!|?|\s)'));
    for (final t in tokens) {
      await Future.delayed(const Duration(milliseconds: 180));
      yield t;
    }
  }
}

views

views/
  trip_list_view.dart          // 旅遊列表
  trip_detail_view.dart        // 行程總覽頁
  ai/ai_trip_detail_view.dart  // AI 行程預覽頁(建議名稱說明用途:預覽)
  ai/chat/ai_chat_view.dart    // AI 聊天畫面(對話編輯)

今天已經完成了 AI 對話式行程編輯器 的整體規劃,從使用者痛點、功能目標,到 API 資料流與程式碼架構都已經梳理清楚。
明天將正式進入實作階段,開始把這些設計逐步落地,讓功能真正運行在既有的專案之中。


上一篇
Day 25 - 地圖實戰:告別制式地圖,自己的地圖自己定義
下一篇
Day 27 - 讓 AI 行程編輯器動起來:畫面實作與假資料串接
系列文
《30 天 Flutter:跨平台 AI 行程規劃 App》29
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言