iT邦幫忙

2025 iThome 鐵人賽

DAY 27
0
Mobile Development

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

Day 27 - 讓 AI 行程編輯器動起來:畫面實作與假資料串接

  • 分享至 

  • xImage
  •  

昨天針對「AI 對話式行程編輯器」進行了完整的設計與規劃,從使用者痛點、功能目標到 API 資料流和程式碼架構,都已經有了清晰的藍圖。今天的目標很單純就是一直寫程式、一直寫程式、一直寫程式把畫面做完串完假資料~


畫面實作:新增與調整

首先,按照昨天的規劃,將現有的畫面進行調整。

1. 行程總覽頁(trip_detail_view.dart

這個頁面是使用者瀏覽單一行程的起點。為了讓使用者能隨時啟動 AI 編輯功能,將在畫面的右下角新增一個 Floating Action Button (FAB)

當使用者點擊這個按鈕時,應用程式會導航至專為 AI 聊天設計的新介面。這個實作很直覺,只需在 Scaffold 中加入 floatingActionButton 屬性即可。

另外,由於頁面上已有一個手動新增活動的 FAB,為了避免兩個按鈕的 Hero 動畫發生衝突,必須為新加入的 FAB 指定一個獨特的 heroTag。這個標籤能幫助 Flutter 區分它們,確保動畫流暢且不會出錯。

// trip_detail_view.dart

class TripDetailView extends ConsumerWidget {
  final Trip trip;
  // ... 其他程式碼

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(title: Text(trip.title)),
      // ... 其他頁面內容
      floatingActionButton: Column(
            crossAxisAlignment: CrossAxisAlignment.end,
            mainAxisSize: MainAxisSize.min,
            spacing: AppSpacing.medium,
            children: [
              AppFloatingActionButton(
                heroTag: 'ai_chat_fab',
                onPressed: () => Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (_) => AIChatView(tripId: tripId), // AI 助理聊天頁面
                  ),
                ),
                label: 'AI',
                icon: 'chat-bubble',
              ),
              AppFloatingActionButton(
                heroTag: 'add_activity_fab',
                onPressed: () => Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (_) =>
                        ActivityEditorView(tripId: tripId, activity: null),
                  ),
                ),
                label: 'Add Activity',
                icon: 'add',
              ),
            ],
          ),
    );
  }
}

2. AI 聊天介面(ai_chat_view.dart

這是今天開發的核心。這個畫面將會是一個獨立的聊天室,用來展示使用者與 AI 的對話。

整個畫面由以下幾個主要部分組成:

  • AppBar:顯示頁面標題。
  • ListView.builder:用來動態產生聊天訊息列表。將根據 chatProvider 的狀態來渲染訊息。
  • 訊息輸入框:包含一個 TextField 和一個「送出」按鈕,讓使用者可以輸入文字。

以下是 ai_chat_view.dart 的骨架程式碼:

// ai/chat/ai_chat_view.dart

class _ChatPageState extends ConsumerState<AIChatView> {
  final _scrollController = ScrollController();

  void _scrollToBottom() {
    if (!_scrollController.hasClients) return;
    _scrollController.animateTo(
      _scrollController.position.maxScrollExtent,
      duration: const Duration(milliseconds: 300),
      curve: Curves.easeOut,
    );
  }

  @override
  Widget build(BuildContext context) {
    final colors = Theme.of(context).extension<AppColorExtension>()!;
    final messages = ref.watch(chatControllerProvider(widget.tripId));

    // 只要 messages 長度有變化,就自動捲到底
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _scrollToBottom();
    });

    return Scaffold(
      // ... 其他頁面內容
      body: Column(
        children: [
          Expanded(
            child: _ChatList(
              tripId: widget.tripId,
              messages: messages,
              scrollController: _scrollController,
            ),
          ),
          SafeArea(top: false, child: ChatInputBar(tripId: widget.tripId)),
        ],
      ),
    );
  }
}

3. 行程預覽頁與訊息泡泡

昨天規劃中提到,當 AI 回傳 itinerary 參數時,我們會顯示一個 「預覽行程」按鈕。今天我們可以將這個邏輯實作在我們的 ChatMessageBubble 元件中。

當 AI 訊息的 itinerary 欄位不為空時,就在訊息泡泡下方顯示一個按鈕。點擊後,導航至新的 AiTripDetailView。這個預覽畫面可以重用大部分的 TripDetailView 邏輯,只是資料來源變成了 AI 提供的 JSON。

// widgets/chat_message_bubble.dart

Column(
crossAxisAlignment: CrossAxisAlignment.start,
    children: [
          Text(message.text, style: textStyle),
          const SizedBox(height: AppSpacing.small),
          PrimaryButton(
            text: '預覽行程',
            size: AppButtonSize.medium,
            onPressed: () {
              ref
                  .read(aiTripListProvider.notifier)
                  .previewAITrip(trip: message.itinerary!);

              Navigator.push(
                context,
                MaterialPageRoute(builder: (_) => AITripDetailView()),
              );
            },
          ),
        ],
    );
}

4. 假資料串接

FakeChatRepository 中準備好了假資料,並將它與 ChatController 串接起來,讓聊天功能能夠模擬運作。ChatControllersendText 方法將會是處理使用者輸入的關鍵。這個方法會執行以下步驟:

  1. 將使用者輸入的訊息加入列表。
  2. 呼叫 _repo.streamReply() 方法,模擬 AI 逐字回覆的行為。
  3. 將 AI 的回覆逐一更新到訊息列表中,形成流暢的打字效果。
Future<void> sendText(String text) async {
// 串流回覆
_streamSub?.cancel();
_streamSub = _repo
    .streamReply(sessionId: _convId, text: text, itinerary: itinerary)
    .listen(
      (chunk) {
        // 逐段更新最後一則 AI 訊息
        final lastIndex = state.lastIndexWhere((m) => m.id == aiPending.id);
        if (lastIndex == -1) return;

        final updated = state[lastIndex].copyWith(
          pending: false,
          text: state[lastIndex].text + chunk.text,
          itinerary: chunk.itinerary ?? state[lastIndex].itinerary,
        );

        final newList = [...state];
        newList[lastIndex] = updated;
        state = newList;
        _convId = chunk.sessionId;
      },
      onError: (e, st) {
        // 錯誤處理
      },
    );
}

成果展示

經過上述的實作,現在已經有一個能夠運作的 AI 聊天編輯器雛形了。雖然後端還未正式串接,但透過 FakeChatRepository 的模擬,我們可以清楚地看到整個流程。

  • 點擊 AI 編輯按鈕:從行程總覽頁進入聊天室。
  • 輸入指令:例如:「水社碼頭離其他點很遠,請幫我調整」。
  • AI 回覆:畫面會即時顯示 AI 的逐字回覆,並在回覆中加入「預覽行程」按鈕。
  • 預覽行程:點擊按鈕後,可以立即看到 AI 修改後的行程,確認是否符合預期。


上一篇
Day 26 - 擴充行程規劃 APP:加入 AI 對話式編輯器
下一篇
Day 28:從雛形到產品,讓 AI 編輯器不再只是展示品
系列文
《30 天 Flutter:跨平台 AI 行程規劃 App》29
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言