Quellcode durchsuchen

Merge branch 'v1.0.0' of git.atmob.com:Atmob-Flutter/ElectronicAssistant into v1.0.0

# Conflicts:
#	lib/module/home/view.dart
#	lib/utils/expand.dart
Destiny vor 1 Jahr
Ursprung
Commit
26b3e92624
36 geänderte Dateien mit 1191 neuen und 146 gelöschten Zeilen
  1. 14 11
      android/app/src/main/AndroidManifest.xml
  2. 48 0
      android/app/src/main/res/xml/network_security_config.xml
  3. BIN
      assets/anim/anim_chat_response_loading.zip
  4. BIN
      assets/images/icon_rename_clear_txt.webp
  5. BIN
      assets/images/icon_rename_close.webp
  6. BIN
      assets/images/icon_talk_delete.webp
  7. 10 0
      assets/string/base/string.xml
  8. 1 1
      lib/base/base_request.dart
  9. 18 0
      lib/data/api/atmob_api.dart
  10. 18 0
      lib/data/api/request/agenda_status_request.dart
  11. 21 0
      lib/data/api/request/chat_history_request.dart
  12. 14 0
      lib/data/api/request/talk_delete_request.dart
  13. 18 0
      lib/data/api/request/talk_rename_request.dart
  14. 16 0
      lib/data/api/response/chat_history_response.dart
  15. 31 0
      lib/data/bean/chat_item.dart
  16. 23 0
      lib/data/bean/progressing_chat_item.dart
  17. 52 0
      lib/data/bean/stream_chat_origin_data.dart
  18. 15 0
      lib/data/repositories/agenda_repository.dart
  19. 10 0
      lib/data/repositories/chat_repository.dart
  20. 14 0
      lib/data/repositories/task_repository.dart
  21. 163 0
      lib/dialog/rename_dialog.dart
  22. 93 0
      lib/dialog/talk_delete_dialog.dart
  23. 107 11
      lib/module/chat/controller.dart
  24. 144 17
      lib/module/chat/view.dart
  25. 38 0
      lib/module/home/controller.dart
  26. 108 87
      lib/module/home/view.dart
  27. 8 0
      lib/module/record/controller.dart
  28. 12 0
      lib/module/record/view.dart
  29. 104 0
      lib/popup/talk_popup.dart
  30. 6 0
      lib/router/app_pages.dart
  31. 20 7
      lib/utils/animated_list_controller.dart
  32. 14 0
      lib/utils/expand.dart
  33. 13 1
      lib/utils/sse_parse_util.dart
  34. 1 1
      lib/utils/toast_util.dart
  35. 34 10
      pubspec.lock
  36. 3 0
      pubspec.yaml

+ 14 - 11
android/app/src/main/AndroidManifest.xml

@@ -1,29 +1,32 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.atmob.elec_asst">
+
+    <uses-permission android:name="android.permission.INTERNET" />
+
     <application
-        android:label="electronic_assistant"
         android:name="${applicationName}"
-        android:icon="@mipmap/ic_launcher">
+        android:icon="@mipmap/ic_launcher"
+        android:label="electronic_assistant"
+        android:networkSecurityConfig="@xml/network_security_config">
         <activity
             android:name=".MainActivity"
+            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
             android:exported="true"
+            android:hardwareAccelerated="true"
             android:launchMode="singleTop"
             android:taskAffinity=""
             android:theme="@style/LaunchTheme"
-            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
-            android:hardwareAccelerated="true"
             android:windowSoftInputMode="adjustResize">
             <!-- Specifies an Android theme to apply to this Activity as soon as
                  the Android process has started. This theme is visible to the user
                  while the Flutter UI initializes. After that, this theme continues
                  to determine the Window background behind the Flutter UI. -->
             <meta-data
-              android:name="io.flutter.embedding.android.NormalTheme"
-              android:resource="@style/NormalTheme"
-              />
+                android:name="io.flutter.embedding.android.NormalTheme"
+                android:resource="@style/NormalTheme" />
             <intent-filter>
-                <action android:name="android.intent.action.MAIN"/>
-                <category android:name="android.intent.category.LAUNCHER"/>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
         <!-- Don't delete the meta-data below.
@@ -39,8 +42,8 @@
          In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
     <queries>
         <intent>
-            <action android:name="android.intent.action.PROCESS_TEXT"/>
-            <data android:mimeType="text/plain"/>
+            <action android:name="android.intent.action.PROCESS_TEXT" />
+            <data android:mimeType="text/plain" />
         </intent>
     </queries>
 </manifest>

+ 48 - 0
android/app/src/main/res/xml/network_security_config.xml

@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config xmlns:tools="http://schemas.android.com/tools"
+    tools:ignore="MissingDefaultResource">
+
+    <!-- For AdColony, this permits all cleartext traffic: -->
+    <base-config cleartextTrafficPermitted="true">
+        <trust-anchors>
+            <certificates src="system" />
+        </trust-anchors>
+    </base-config>
+    <!-- End AdColony section -->
+
+    <domain-config cleartextTrafficPermitted="true">
+
+        <!-- For Meta Audience Network, this permits cleartext traffic to localhost: -->
+        <domain includeSubdomains="true">127.0.0.1</domain>
+        <!-- End Meta Audience Network section -->
+
+    </domain-config>
+
+    <domain-config cleartextTrafficPermitted="true">
+        <domain includeSubdomains="true">192.168.10.230</domain>
+    </domain-config>
+
+    <domain-config cleartextTrafficPermitted="true">
+        <domain includeSubdomains="true">cdn.atmob.com</domain>
+    </domain-config>
+
+    <domain-config cleartextTrafficPermitted="true">
+        <domain includeSubdomains="true">test-micro.atmob.com</domain>
+    </domain-config>
+
+    <domain-config cleartextTrafficPermitted="true">
+        <domain includeSubdomains="true">api-img-sh.fengkongcloud.com</domain>
+    </domain-config>
+
+    <domain-config cleartextTrafficPermitted="true">
+        <domain includeSubdomains="true">api-text-gz.fengkongcloud.com</domain>
+    </domain-config>
+
+    <domain-config cleartextTrafficPermitted="true">
+        <domain includeSubdomains="true">global-chatbotai-cdn.atmob.com</domain>
+    </domain-config>
+
+    <domain-config cleartextTrafficPermitted="true">
+        <domain includeSubdomains="true">cdn.supercleaner.club</domain>
+    </domain-config>
+</network-security-config>

BIN
assets/anim/anim_chat_response_loading.zip


BIN
assets/images/icon_rename_clear_txt.webp


BIN
assets/images/icon_rename_close.webp


BIN
assets/images/icon_talk_delete.webp


+ 10 - 0
assets/string/base/string.xml

@@ -31,4 +31,14 @@
     <string name="task_item_todo">进行中</string>
     <string name="task_item_done">已完成</string>
     <string name="cancel">取消</string>
+    <string name="sure">确认</string>
+    <string name="talk_rename">重命名</string>
+    <string name="talk_delete">删除</string>
+    <string name="talk_rename_title">重命名标题</string>
+    <string name="talk_rename_title_hint">请输入谈话记录标题</string>
+    <string name="talk_rename_success">修改成功</string>
+    <string name="talk_rename_fail">修改失败</string>
+    <string name="talk_rename_delete_prompt">是否删除“%s”模板?该谈话包含您的待办事项,将一并删除。</string>
+    <string name="talk_delete_success">删除成功</string>
+    <string name="talk_delete_fail">删除失败</string>
 </resources>

+ 1 - 1
lib/base/base_request.dart

@@ -114,6 +114,6 @@ class BaseRequest {
   }
 
   void initDeviceInfo() {
-    androidId = "testtesttest";
+    androidId = "1123123qweqw1";
   }
 }

+ 18 - 0
lib/data/api/atmob_api.dart

@@ -3,10 +3,15 @@ import 'package:electronic_assistant/base/app_base_request.dart';
 import 'package:electronic_assistant/base/base_response.dart';
 import 'package:electronic_assistant/data/api/network_module.dart';
 import 'package:electronic_assistant/data/api/request/agenda_request.dart';
+import 'package:electronic_assistant/data/api/request/agenda_status_request.dart';
+import 'package:electronic_assistant/data/api/request/chat_history_request.dart';
 import 'package:electronic_assistant/data/api/request/login_request.dart';
+import 'package:electronic_assistant/data/api/request/talk_delete_request.dart';
+import 'package:electronic_assistant/data/api/request/talk_rename_request.dart';
 import 'package:electronic_assistant/data/api/request/user_info_update_request.dart';
 import 'package:electronic_assistant/data/api/request/verification_code_request.dart';
 import 'package:electronic_assistant/data/api/response/agenda_response.dart';
+import 'package:electronic_assistant/data/api/response/chat_history_response.dart';
 import 'package:electronic_assistant/data/api/response/home_info_response.dart';
 import 'package:electronic_assistant/data/api/response/login_response.dart';
 import 'package:electronic_assistant/data/consts/constants.dart';
@@ -35,6 +40,19 @@ abstract class AtmobApi {
   @POST("/project/secretary/v1/agenda/page")
   Future<BaseResponse<AgendaResponse>> agendaPage(
       @Body() AgendaRequest request);
+
+  @POST("/project/secretary/v1/talk/update")
+  Future<BaseResponse> talkRename(@Body() TalkRenameRequest request);
+
+  @POST("/project/secretary/v1/talk/delete")
+  Future<BaseResponse> talkDelete(@Body() TalkDeleteRequest request);
+
+  @POST("/project/secretary/v1/agenda/complete")
+  Future<BaseResponse> agendaFinish(@Body() AgendaStatusRequest request);
+
+  @POST("/project/secretary/v1/chat/page")
+  Future<BaseResponse<ChatHistoryResponse>> chatHistory(
+      @Body() ChatHistoryRequest request);
 }
 
 final atmobApi = AtmobApi(defaultDio, baseUrl: Constants.baseUrl);

+ 18 - 0
lib/data/api/request/agenda_status_request.dart

@@ -0,0 +1,18 @@
+import 'package:electronic_assistant/base/app_base_request.dart';
+import 'package:json_annotation/json_annotation.dart';
+
+part 'agenda_status_request.g.dart';
+
+@JsonSerializable()
+class AgendaStatusRequest extends AppBaseRequest {
+  @JsonKey(name: 'id')
+  int? id;
+
+  @JsonKey(name: 'complete')
+  late bool complete;
+
+  AgendaStatusRequest(this.id, this.complete);
+
+  @override
+  Map<String, dynamic> toJson() => _$AgendaStatusRequestToJson(this);
+}

+ 21 - 0
lib/data/api/request/chat_history_request.dart

@@ -0,0 +1,21 @@
+import 'package:electronic_assistant/base/app_base_request.dart';
+import 'package:json_annotation/json_annotation.dart';
+
+part 'chat_history_request.g.dart';
+
+@JsonSerializable()
+class ChatHistoryRequest extends AppBaseRequest {
+  @JsonKey(name: 'page')
+  int pageCount;
+
+  @JsonKey(name: 'pageSize')
+  int pageSize;
+
+  @JsonKey(name: 'id')
+  int? lastId;
+
+  ChatHistoryRequest(this.pageCount, this.pageSize, this.lastId);
+
+  @override
+  Map<String, dynamic> toJson() => _$ChatHistoryRequestToJson(this);
+}

+ 14 - 0
lib/data/api/request/talk_delete_request.dart

@@ -0,0 +1,14 @@
+import 'package:electronic_assistant/base/app_base_request.dart';
+
+class TalkDeleteRequest extends AppBaseRequest {
+  String? id;
+
+  TalkDeleteRequest(this.id);
+
+  @override
+  Map<String, dynamic> toJson() {
+    final Map<String, dynamic> data = super.toJson();
+    data['id'] = id;
+    return data;
+  }
+}

+ 18 - 0
lib/data/api/request/talk_rename_request.dart

@@ -0,0 +1,18 @@
+import 'package:electronic_assistant/base/app_base_request.dart';
+import 'package:json_annotation/json_annotation.dart';
+
+part 'talk_rename_request.g.dart';
+
+@JsonSerializable()
+class TalkRenameRequest extends AppBaseRequest {
+  @JsonKey(name: "id")
+  String? id;
+
+  @JsonKey(name: "title")
+  String? title;
+
+  TalkRenameRequest(this.id, this.title);
+
+  @override
+  Map<String, dynamic> toJson() => _$TalkRenameRequestToJson(this);
+}

+ 16 - 0
lib/data/api/response/chat_history_response.dart

@@ -0,0 +1,16 @@
+import 'package:json_annotation/json_annotation.dart';
+
+import '../../bean/chat_item.dart';
+
+part 'chat_history_response.g.dart';
+
+@JsonSerializable()
+class ChatHistoryResponse {
+  @JsonKey(name: 'list')
+  List<ChatItem> chatItems;
+
+  ChatHistoryResponse({this.chatItems = const []});
+
+  factory ChatHistoryResponse.fromJson(Map<String, dynamic> json) =>
+      _$ChatHistoryResponseFromJson(json);
+}

+ 31 - 0
lib/data/bean/chat_item.dart

@@ -0,0 +1,31 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'chat_item.g.dart';
+
+@JsonSerializable()
+class ChatItem {
+  @JsonKey(name: "id")
+  final int id;
+
+  @JsonKey(name: "chatId")
+  final String conversationId;
+
+  @JsonKey(name: "role")
+  final String role;
+
+  @JsonKey(name: "content")
+  String content;
+
+  @JsonKey(name: "createTime")
+  final String createTime;
+
+  ChatItem({
+    required this.id,
+    required this.conversationId,
+    required this.role,
+    required this.content,
+    required this.createTime,
+  });
+
+  factory ChatItem.fromJson(Map<String, dynamic> json) => _$ChatItemFromJson(json);
+}

+ 23 - 0
lib/data/bean/progressing_chat_item.dart

@@ -0,0 +1,23 @@
+import 'package:electronic_assistant/data/bean/chat_item.dart';
+import 'package:get/get.dart';
+
+class ProgressingChatItem extends ChatItem {
+  final RxString streamContent = "".obs;
+
+  final RxBool isFinished = false.obs;
+
+  final RxBool isFailed = false.obs;
+
+  final RxString error = "".obs;
+
+  ProgressingChatItem(
+      {required super.id,
+      required super.conversationId,
+      required super.role,
+      required super.content,
+      required super.createTime});
+
+  void append(String content) {
+    streamContent.value += content;
+  }
+}

+ 52 - 0
lib/data/bean/stream_chat_origin_data.dart

@@ -0,0 +1,52 @@
+/*
+ * {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-4o-mini", "system_fingerprint": "fp_44709d6fcb", "choices":[{"index":0,"delta":{"role":"assistant","content":""},"logprobs":null,"finish_reason":null}]}
+ */
+
+import 'package:json_annotation/json_annotation.dart';
+
+part 'stream_chat_origin_data.g.dart';
+
+@JsonSerializable()
+class StreamChatOriginData {
+  @JsonKey(name: "choices")
+  final List<Choices>? choices;
+
+  StreamChatOriginData({
+    required this.choices,
+  });
+
+  factory StreamChatOriginData.fromJson(Map<String, dynamic> json) => _$StreamChatOriginDataFromJson(json);
+}
+
+@JsonSerializable()
+class Choices {
+  @JsonKey(name: "index")
+  final int? index;
+  @JsonKey(name: "delta")
+  final Delta? delta;
+  @JsonKey(name: "finishReason")
+  final String? finishReason;
+
+  Choices({
+    required this.index,
+    required this.delta,
+    required this.finishReason,
+  });
+
+  factory Choices.fromJson(Map<String, dynamic> json) => _$ChoicesFromJson(json);
+}
+
+@JsonSerializable()
+class Delta {
+  @JsonKey(name: "role")
+  final String? role;
+  @JsonKey(name: "content")
+  final String? content;
+
+  Delta({
+    required this.role,
+    required this.content,
+  });
+
+  factory Delta.fromJson(Map<String, dynamic> json) => _$DeltaFromJson(json);
+}

+ 15 - 0
lib/data/repositories/agenda_repository.dart

@@ -0,0 +1,15 @@
+import '../../utils/http_handler.dart';
+import '../api/atmob_api.dart';
+import '../api/request/agenda_status_request.dart';
+
+class AgendaRepository {
+  AgendaRepository._();
+
+  Future<void> agendaFinish(int? id, bool complete) {
+    return atmobApi
+        .agendaFinish(AgendaStatusRequest(id, complete))
+        .then(HttpHandler.handle(true));
+  }
+}
+
+final agendaRepository = AgendaRepository._();

+ 10 - 0
lib/data/repositories/chat_repository.dart

@@ -1,7 +1,10 @@
 import 'dart:convert';
 
 import 'package:electronic_assistant/base/base_response.dart';
+import 'package:electronic_assistant/data/api/atmob_api.dart';
+import 'package:electronic_assistant/data/api/request/chat_history_request.dart';
 import 'package:electronic_assistant/data/api/request/chat_request.dart';
+import 'package:electronic_assistant/data/bean/chat_item.dart';
 import 'package:electronic_assistant/utils/http_handler.dart';
 import 'package:electronic_assistant/utils/sse_parse_util.dart';
 
@@ -31,6 +34,13 @@ class ChatRepository {
       throw Exception('Invalid content type');
     }).then((stream) => SSEParseUtil.parse(stream));
   }
+
+  Future<List<ChatItem>> chatHistory(int? lastId) {
+    return atmobApi
+        .chatHistory(ChatHistoryRequest(1, 20, lastId))
+        .then(HttpHandler.handle(true))
+        .then((response) => response.chatItems);
+  }
 }
 
 final chatRepository = ChatRepository._();

+ 14 - 0
lib/data/repositories/task_repository.dart

@@ -1,7 +1,9 @@
 import 'package:electronic_assistant/data/api/atmob_api.dart';
+import 'package:electronic_assistant/data/api/request/talk_delete_request.dart';
 
 import '../../utils/http_handler.dart';
 import '../api/request/agenda_request.dart';
+import '../api/request/talk_rename_request.dart';
 import '../api/response/agenda_response.dart';
 
 class TaskRepository {
@@ -16,6 +18,18 @@ class TaskRepository {
             completeStatus: completeStatus?.value))
         .then(HttpHandler.handle(true));
   }
+
+  Future<void> talkRename(String? id, String? title) {
+    return atmobApi
+        .talkRename(TalkRenameRequest(id, title))
+        .then(HttpHandler.handle(true));
+  }
+
+  Future<void> talkDelete(String? id) {
+    return atmobApi
+        .talkDelete(TalkDeleteRequest(id))
+        .then(HttpHandler.handle(true));
+  }
 }
 
 final taskRepository = TaskRepository._();

+ 163 - 0
lib/dialog/rename_dialog.dart

@@ -0,0 +1,163 @@
+import 'package:electronic_assistant/resource/assets.gen.dart';
+import 'package:electronic_assistant/resource/colors.gen.dart';
+import 'package:electronic_assistant/resource/string.gen.dart';
+import 'package:electronic_assistant/utils/expand.dart';
+import 'package:electronic_assistant/utils/toast_util.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
+import 'package:get/get.dart';
+
+typedef RenameBuilder = void Function(String? content);
+
+void reNameDialog(String title, String? content,
+    {String? hintTxt, int? maxLength, RenameBuilder? returnBuilder}) {
+  final controller = TextEditingController();
+  controller.text = content ?? "";
+  final contentObs = content.obs;
+  controller.addListener(() {
+    contentObs.value = controller.text;
+  });
+  SmartDialog.show(
+      builder: (_) {
+        return IntrinsicHeight(
+          child: Container(
+            padding: EdgeInsets.all(16.w),
+            width: 336.w,
+            decoration: BoxDecoration(
+              color: Colors.white,
+              borderRadius: BorderRadius.circular(12),
+            ),
+            child: Column(
+              children: [
+                Row(
+                  crossAxisAlignment: CrossAxisAlignment.center,
+                  children: [
+                    Text(
+                      title,
+                      style: TextStyle(
+                          fontWeight: FontWeight.bold,
+                          fontSize: 15.sp,
+                          color: ColorName.primaryTextColor),
+                    ),
+                    const Spacer(),
+                    GestureDetector(
+                      onTap: () {
+                        SmartDialog.dismiss();
+                      },
+                      child: SizedBox(
+                        width: 28.w,
+                        height: 28.w,
+                        child: Assets.images.iconRenameClose.image(),
+                      ),
+                    )
+                  ],
+                ),
+                SizedBox(height: 24.h),
+                Container(
+                  decoration: BoxDecoration(
+                    color: "#F0F0F0".toColor(),
+                    borderRadius: BorderRadius.circular(8),
+                  ),
+                  padding: EdgeInsets.symmetric(horizontal: 14.w),
+                  height: 51.h,
+                  child: Row(
+                    children: [
+                      Expanded(
+                        child: TextField(
+                          maxLength: maxLength,
+                          style: TextStyle(
+                              fontSize: 16.sp,
+                              color: ColorName.primaryTextColor),
+                          controller: controller,
+                          maxLines: 1,
+                          decoration: InputDecoration(
+                            counterText: '',
+                            hintText: hintTxt,
+                            hintStyle: TextStyle(
+                                fontSize: 16.sp,
+                                color: ColorName.tertiaryTextColor),
+                            border: InputBorder.none,
+                            fillColor: Colors.transparent,
+                          ),
+                        ),
+                      ),
+                      Obx(() {
+                        return Visibility(
+                          visible: contentObs.value!.isNotEmpty,
+                          child: GestureDetector(
+                            onTap: () {
+                              controller.clear();
+                            },
+                            child: SizedBox(
+                              width: 20.w,
+                              height: 20.w,
+                              child: Assets.images.iconRenameClearTxt.image(),
+                            ),
+                          ),
+                        );
+                      })
+                    ],
+                  ),
+                ),
+                SizedBox(height: 28.h),
+                Row(
+                  children: [
+                    Expanded(
+                        child: GestureDetector(
+                      onTap: () {
+                        SmartDialog.dismiss();
+                      },
+                      child: Container(
+                        padding: EdgeInsets.symmetric(vertical: 12.h),
+                        decoration: BoxDecoration(
+                          color: '#F0F0F0'.toColor(),
+                          borderRadius: BorderRadius.circular(8),
+                        ),
+                        child: Center(
+                          child: Text(
+                            StringName.cancel.tr,
+                            style: TextStyle(
+                                fontSize: 16.sp,
+                                color: ColorName.secondaryTextColor),
+                          ),
+                        ),
+                      ),
+                    )),
+                    SizedBox(width: 12.w),
+                    Expanded(
+                        child: GestureDetector(
+                      onTap: () {
+                        if (contentObs.value!.isEmpty) {
+                          ToastUtil.showToast(hintTxt);
+                          return;
+                        }
+                        returnBuilder?.call(contentObs.value);
+                        SmartDialog.dismiss();
+                      },
+                      child: Container(
+                        padding: EdgeInsets.symmetric(vertical: 12.h),
+                        decoration: BoxDecoration(
+                          color: ColorName.colorPrimary,
+                          borderRadius: BorderRadius.circular(8),
+                        ),
+                        child: Center(
+                          child: Text(
+                            StringName.sure.tr,
+                            style: TextStyle(
+                                fontSize: 16.sp, color: ColorName.white),
+                          ),
+                        ),
+                      ),
+                    )),
+                  ],
+                ),
+                SizedBox(height: 8.h),
+              ],
+            ),
+          ),
+        );
+      },
+      clickMaskDismiss: false);
+}

+ 93 - 0
lib/dialog/talk_delete_dialog.dart

@@ -0,0 +1,93 @@
+import 'package:electronic_assistant/utils/expand.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
+import 'package:get/get.dart';
+
+import '../resource/colors.gen.dart';
+import '../resource/string.gen.dart';
+
+typedef TalkDeleteBuilder = void Function();
+
+void talkDeleteDialog(String? talkId, String? talkTitle,
+    {TalkDeleteBuilder? returnBuilder}) {
+  SmartDialog.show(
+      builder: (_) {
+        return Container(
+          padding: EdgeInsets.all(16.w),
+          width: 280.w,
+          decoration: BoxDecoration(
+            color: Colors.white,
+            borderRadius: BorderRadius.circular(12),
+          ),
+          child: IntrinsicHeight(
+            child: Column(
+              children: [
+                SizedBox(height: 24.h),
+                Text(
+                  StringName.talkRenameDeletePrompt.tr
+                      .replacePlaceholders([talkTitle]),
+                  style: TextStyle(
+                      fontSize: 15.sp,
+                      color: ColorName.primaryTextColor,
+                      fontWeight: FontWeight.bold),
+                ),
+                SizedBox(height: 35.h),
+                Row(
+                  children: [
+                    Expanded(
+                        child: GestureDetector(
+                      onTap: () {
+                        SmartDialog.dismiss();
+                      },
+                      child: Container(
+                        padding: EdgeInsets.symmetric(vertical: 8.h),
+                        decoration: BoxDecoration(
+                          color: '#F0F0F0'.toColor(),
+                          borderRadius: BorderRadius.circular(8),
+                        ),
+                        child: Center(
+                          child: Text(
+                            StringName.cancel.tr,
+                            style: TextStyle(
+                                fontSize: 16.sp,
+                                color: ColorName.secondaryTextColor),
+                          ),
+                        ),
+                      ),
+                    )),
+                    SizedBox(width: 12.w),
+                    Expanded(
+                        child: GestureDetector(
+                      onTap: () {
+                        SmartDialog.dismiss();
+                        if (returnBuilder != null) {
+                          returnBuilder.call();
+                        }
+                      },
+                      child: Container(
+                        padding: EdgeInsets.symmetric(vertical: 8.h),
+                        decoration: BoxDecoration(
+                          color: ColorName.colorPrimary,
+                          borderRadius: BorderRadius.circular(8),
+                        ),
+                        child: Center(
+                          child: Text(
+                            StringName.sure.tr,
+                            style: TextStyle(
+                                fontSize: 16.sp, color: ColorName.white),
+                          ),
+                        ),
+                      ),
+                    )),
+                  ],
+                ),
+                SizedBox(height: 4.h),
+              ],
+            ),
+          ),
+        );
+      },
+      clickMaskDismiss: false);
+}

+ 107 - 11
lib/module/chat/controller.dart

@@ -1,24 +1,44 @@
+import 'dart:convert';
+
 import 'package:electronic_assistant/base/base_controller.dart';
+import 'package:electronic_assistant/data/bean/chat_item.dart';
+import 'package:electronic_assistant/data/bean/progressing_chat_item.dart';
+import 'package:electronic_assistant/data/bean/stream_chat_origin_data.dart';
 import 'package:electronic_assistant/data/repositories/chat_repository.dart';
 import 'package:electronic_assistant/module/chat/start/view.dart';
 import 'package:electronic_assistant/resource/colors.gen.dart';
-import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import 'package:pull_to_refresh/pull_to_refresh.dart';
+
+import '../../utils/http_handler.dart';
 
 class ChatController extends BaseController {
+  final RefreshController refreshController = RefreshController();
+  final ScrollController listScrollController = ScrollController();
+  final TextEditingController inputController = TextEditingController();
+
+  final RxList chatItems = [].obs;
+
   var isOpenStart = false;
 
   @override
-  void onInit() {
-    super.onInit();
+  void onReady() {
+    super.onReady();
+    refreshController.requestLoading();
+  }
 
-    chatRepository.streamChat("以《黑神话:悟空》为题,写一篇小说").then((stream) {
-      stream.listen((event) {
-        debugPrint(
-            "id: ${event.id}, event: ${event.event}, data: ${event.data}");
-      });
-    }).catchError((e) {
-      debugPrint("error: $e");
+  void loadMoreHistory() {
+    bool isEmpty = chatItems.isEmpty;
+
+    chatRepository
+        .chatHistory(chatItems.isEmpty ? null : chatItems.last.id)
+        .then((value) => chatItems.addAll(value))
+        .whenComplete(() => refreshController.loadComplete())
+        .whenComplete(() {
+      if (isEmpty) {
+        scrollToBottom();
+      }
     });
   }
 
@@ -31,11 +51,87 @@ class ChatController extends BaseController {
           barrierColor: ColorName.black55,
           backgroundColor: ColorName.transparent,
           builder: (BuildContext context) {
-            return ChatStartPage();
+            return const ChatStartPage();
           },
         );
       });
       isOpenStart = true;
     }
   }
+
+  sendMessage() {
+    if (inputController.text.isEmpty) {
+      return;
+    }
+    String chatContent = inputController.text;
+    inputController.clear();
+
+    chatItems.insert(
+        0,
+        ChatItem(
+            id: chatItems.isEmpty ? 0 : chatItems.last.id + 1,
+            conversationId:
+                chatItems.isEmpty ? "" : chatItems.last.conversationId,
+            role: "user",
+            content: chatContent,
+            createTime: DateTime.now().toString()));
+
+    ProgressingChatItem progressingChatItem = ProgressingChatItem(
+      id: chatItems.last.id + 1,
+      conversationId: chatItems.last.conversationId,
+      role: "assistant",
+      content: "",
+      createTime: DateTime.now().toString(),
+    );
+    chatItems.insert(0, progressingChatItem);
+
+    scrollToBottom();
+
+    chatRepository.streamChat(chatContent).then((stream) {
+      stream.listen((event) {
+        try {
+          Map<String, dynamic> json = jsonDecode(event.data);
+          if (json.isEmpty) {
+            return;
+          }
+          StreamChatOriginData data = StreamChatOriginData.fromJson(json);
+          if (data.choices == null || data.choices!.isEmpty) {
+            return;
+          }
+          Delta? delta = data.choices![0].delta;
+          if (delta == null) {
+            return;
+          }
+          progressingChatItem.append(delta.content ?? "");
+        } catch (ignore) {}
+      }, onDone: () {
+        progressingChatItem.content = progressingChatItem.streamContent.value;
+        progressingChatItem.isFinished.value = true;
+      }, onError: (error) {
+        progressingChatItem.isFailed.value = true;
+        progressingChatItem.error.value = "网络错误,请检查网络连接";
+        debugPrint("error: $error");
+        debugPrintStack();
+      });
+    }).catchError((error) {
+      progressingChatItem.isFailed.value = true;
+      if (error is ServerErrorException) {
+        progressingChatItem.error.value = error.message ?? "服务出错,请稍后再试";
+      } else {
+        progressingChatItem.error.value = "网络错误,请检查网络连接";
+        debugPrint("error: $error");
+        debugPrintStack();
+      }
+    });
+  }
+
+  void scrollToBottom() {
+    Future.delayed(const Duration(milliseconds: 300), () {
+      listScrollController.animateTo(
+        0,
+        duration: const Duration(milliseconds: 300),
+        curve: Curves.easeOut,
+      );
+    });
+  }
 }

+ 144 - 17
lib/module/chat/view.dart

@@ -1,13 +1,17 @@
 import 'package:electronic_assistant/base/base_page.dart';
+import 'package:electronic_assistant/data/bean/chat_item.dart';
 import 'package:electronic_assistant/module/chat/controller.dart';
-import 'package:electronic_assistant/module/chat/start/view.dart';
 import 'package:electronic_assistant/resource/colors.gen.dart';
 import 'package:electronic_assistant/utils/expand.dart';
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:get/get.dart';
+import 'package:lottie/lottie.dart';
+import 'package:pull_to_refresh/pull_to_refresh.dart';
 
+import '../../data/bean/progressing_chat_item.dart';
 import '../../resource/assets.gen.dart';
 
 class ChatPage extends BasePage<ChatController> {
@@ -20,8 +24,6 @@ class ChatPage extends BasePage<ChatController> {
 
   @override
   Widget buildBody(BuildContext context) {
-    var controller = this.controller;
-
     // 第一次启动时弹出定制窗口
     controller.showStartSheet(context);
 
@@ -61,22 +63,58 @@ class ChatPage extends BasePage<ChatController> {
               ),
             ),
           ),
-          body: buildBodyContent(),
+          body: buildBodyContent(context),
         )
       ],
     );
   }
 
-  Widget buildBodyContent() {
+  Widget buildBodyContent(BuildContext context) {
     return Column(
       children: [
         Expanded(
-            child: Padding(
+            child: Container(
           padding: EdgeInsets.symmetric(horizontal: 12.w),
-          child: AnimatedList(
-            itemBuilder: _chatItemBuilder,
-            initialItemCount: 20,
-          ),
+          child: Obx(() {
+            return NotificationListener<ScrollNotification>(
+              onNotification: (scrollNotification) {
+                if (scrollNotification is ScrollStartNotification) {
+                  FocusScope.of(context).unfocus();
+                }
+                return false;
+              },
+              child: SmartRefresher(
+                controller: controller.refreshController,
+                footer: CustomFooter(
+                  loadStyle: LoadStyle.ShowWhenLoading,
+                  builder: (context, mode) {
+                    if (mode == LoadStatus.loading ||
+                        mode == LoadStatus.canLoading) {
+                      return const SizedBox(
+                        height: 60.0,
+                        child: SizedBox(
+                          height: 20.0,
+                          width: 20.0,
+                          child: CupertinoActivityIndicator(),
+                        ),
+                      );
+                    } else {
+                      return Container();
+                    }
+                  },
+                ),
+                enablePullDown: false,
+                enablePullUp: true,
+                onLoading: controller.loadMoreHistory,
+                onRefresh: controller.loadMoreHistory,
+                child: ListView.builder(
+                    reverse: true,
+                    controller: controller.listScrollController,
+                    itemBuilder: _chatItemBuilder,
+                    itemCount: controller.chatItems.length),
+              ),
+            );
+          }),
         )),
         Container(
           margin: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h),
@@ -103,6 +141,7 @@ class ChatPage extends BasePage<ChatController> {
                         child: Container(
                       margin: EdgeInsets.only(right: 6.w),
                       child: CupertinoTextField(
+                        controller: controller.inputController,
                         padding: EdgeInsets.symmetric(vertical: 3.w),
                         style: TextStyle(
                             fontSize: 14.w, color: ColorName.primaryTextColor),
@@ -124,10 +163,15 @@ class ChatPage extends BasePage<ChatController> {
                         height: 26.w),
                     Container(
                       margin: EdgeInsets.only(left: 16.w),
-                      child: Image(
-                          image: Assets.images.iconChatSend.provider(),
-                          width: 26.w,
-                          height: 26.w),
+                      child: GestureDetector(
+                        onTap: () {
+                          controller.sendMessage();
+                        },
+                        child: Image(
+                            image: Assets.images.iconChatSend.provider(),
+                            width: 26.w,
+                            height: 26.w),
+                      ),
                     )
                   ],
                 )
@@ -139,9 +183,92 @@ class ChatPage extends BasePage<ChatController> {
     );
   }
 
-  Widget _chatItemBuilder(
-      BuildContext context, int index, Animation<double> animation) {
-    return Text('聊天内容 $index');
+  Widget _chatItemBuilder(BuildContext context, int index) {
+    ChatItem chatItem = controller.chatItems[index];
+    if (chatItem.role == 'user') {
+      return _buildUserChatItem(context, chatItem);
+    } else if (chatItem.role == 'assistant') {
+      return _buildAssistantChatItem(context, chatItem);
+    } else {
+      return Container();
+    }
+  }
+
+  Widget _buildAssistantChatItem(BuildContext context, ChatItem chatItem) {
+    ProgressingChatItem? progressingChatItem;
+    if (chatItem is ProgressingChatItem) {
+      progressingChatItem = chatItem;
+    }
+    return Align(
+      alignment: Alignment.centerLeft,
+      child: IntrinsicWidth(
+        child: progressingChatItem == null
+            ? _buildAssistantChatItemContent(null, chatItem.content)
+            : Obx(() {
+                bool? isStreamStarted = progressingChatItem == null
+                    ? null
+                    : progressingChatItem.streamContent.isNotEmpty ||
+                        progressingChatItem.isFinished.value ||
+                        progressingChatItem.isFailed.value;
+                return _buildAssistantChatItemContent(
+                    isStreamStarted,
+                    progressingChatItem!.isFailed.value
+                        ? progressingChatItem.error.value
+                        : progressingChatItem.streamContent.value);
+              }),
+      ),
+    );
+  }
+
+  Container _buildAssistantChatItemContent(
+      bool? isStreamStarted, String content) {
+    return Container(
+      padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
+      margin: EdgeInsets.symmetric(vertical: 10.h),
+      alignment: Alignment.centerLeft,
+      constraints: BoxConstraints(
+        maxWidth: 0.78.sw, // 65% of screen width
+      ),
+      decoration: BoxDecoration(
+          border: isStreamStarted == null || isStreamStarted == true
+              ? null
+              : Border.all(color: ColorName.colorPrimary, width: 1.w),
+          color: ColorName.white,
+          borderRadius: BorderRadius.only(
+              topRight: Radius.circular(20.w),
+              bottomRight: Radius.circular(20.w),
+              bottomLeft: Radius.circular(20.w))),
+      child: isStreamStarted != null && isStreamStarted == false
+          ? Lottie.asset("assets/anim/anim_chat_response_loading.zip",
+              width: 46.w, height: 20.w)
+          : SelectableText(content,
+              style:
+                  TextStyle(fontSize: 14.w, color: ColorName.primaryTextColor)),
+    );
+  }
+
+  Widget _buildUserChatItem(BuildContext context, ChatItem chatItem) {
+    return Align(
+      alignment: Alignment.centerRight,
+      child: IntrinsicWidth(
+        child: Container(
+          padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
+          margin: EdgeInsets.symmetric(vertical: 10.h),
+          alignment: Alignment.centerRight,
+          constraints: BoxConstraints(
+            maxWidth: 0.78.sw, // 65% of screen width
+          ),
+          decoration: BoxDecoration(
+              color: ColorName.colorPrimary,
+              borderRadius: BorderRadius.only(
+                  topLeft: Radius.circular(16.w),
+                  bottomRight: Radius.circular(16.w),
+                  bottomLeft: Radius.circular(16.w))),
+          child: Text(chatItem.content,
+              style: TextStyle(fontSize: 14.w, color: ColorName.white)),
+        ),
+      ),
+    );
   }
 
   Widget buildTopGradient() {

+ 38 - 0
lib/module/home/controller.dart

@@ -1,13 +1,18 @@
 import 'package:electronic_assistant/base/base_controller.dart';
 import 'package:electronic_assistant/data/bean/talks.dart';
+import 'package:electronic_assistant/data/repositories/agenda_repository.dart';
+import 'package:electronic_assistant/data/repositories/task_repository.dart';
+import 'package:electronic_assistant/module/home/view.dart';
 import 'package:electronic_assistant/module/main/controller.dart';
 import 'package:electronic_assistant/resource/string.gen.dart';
+import 'package:electronic_assistant/utils/toast_util.dart';
 import 'package:electronic_assistant/widget/pull_to_refresh.dart';
 import 'package:get/get.dart';
 import '../../data/bean/agenda.dart';
 import '../../data/repositories/account_repository.dart';
 import '../../data/repositories/home_repository.dart';
 import '../../utils/animated_list_controller.dart';
+import '../../utils/error_handler.dart';
 
 class HomePageController extends BaseController {
   get isLogin => accountRepository.isLogin.value;
@@ -41,6 +46,19 @@ class HomePageController extends BaseController {
     });
   }
 
+  void requestName(String? newName, TalkBean bean) {
+    taskRepository.talkRename(bean.id, newName).then((data) {
+      bean.title = newName;
+      int index = taskList.indexOf(bean);
+      if (index != -1) {
+        taskList.update(index, bean);
+        ToastUtil.showToast(StringName.talkRenameSuccess.tr);
+      }
+    }).catchError((error) {
+      ErrorHandler.toastError(error, message: StringName.talkRenameFail.tr);
+    });
+  }
+
   void goTalkRecordPage() {
     Get.find<MainController>().updateIndexByPageName(StringName.mainTabFile);
   }
@@ -48,4 +66,24 @@ class HomePageController extends BaseController {
   void showLoginDrawer() {
     Get.find<MainController>().openDrawer();
   }
+
+  void requestDelete(TalkBean item) {
+    taskRepository.talkDelete(item.id).then((data) {
+      requestHomeData();
+    }).catchError((error) {
+      ErrorHandler.toastError(error, message: StringName.talkDeleteFail.tr);
+    });
+  }
+
+  void agendaComplete(Agenda item) {
+    agendaRepository.agendaFinish(item.id, true).then((data) {
+      item.isDone = true;
+      agendaList.removeItem(
+          item,
+          (context, animation, item) =>
+              buildRemoveTodoItem(context, animation, item));
+    }).catchError((error) {
+      ErrorHandler.toastError(error);
+    });
+  }
 }

+ 108 - 87
lib/module/home/view.dart

@@ -1,6 +1,8 @@
 import 'package:electronic_assistant/base/base_page.dart';
 import 'package:electronic_assistant/data/bean/talks.dart';
-import 'package:electronic_assistant/data/repositories/account_repository.dart';
+import 'package:electronic_assistant/dialog/rename_dialog.dart';
+import 'package:electronic_assistant/dialog/talk_delete_dialog.dart';
+import 'package:electronic_assistant/popup/talk_popup.dart';
 import 'package:electronic_assistant/resource/assets.gen.dart';
 import 'package:electronic_assistant/resource/colors.gen.dart';
 import 'package:electronic_assistant/resource/string.gen.dart';
@@ -42,7 +44,6 @@ class HomePage extends BasePage<HomePageController> {
                 controller: controller.refreshController,
                 onRefresh: () {
                   controller.requestHomeData();
-                  // controller.refreshController.refreshCompleted();
                 },
                 child: CustomScrollView(
                   slivers: [
@@ -164,7 +165,6 @@ class HomePage extends BasePage<HomePageController> {
               ),
             )),
         onTap: () {
-          accountRepository.logout();
           ToastUtil.showToast('GoStore');
         });
   }
@@ -310,77 +310,88 @@ class HomePage extends BasePage<HomePageController> {
       Animation<double> animation, TalkBean item) {
     return FadeTransition(
       opacity: animation,
-      child: _buildTalkView(item),
+      child: _buildTalkView(item, onLongPressStart: (details) {
+        showTalkPopup(details.globalPosition, Alignment.bottomRight,
+            onRename: () {
+          showRenameTalkDialog(item);
+        }, onDelete: () {
+          showDeleteTalkDialog(item);
+        });
+      }),
     );
   }
 
-  Widget _buildTalkView(TalkBean item) {
-    return Container(
-        width: 258.w,
-        margin: EdgeInsets.only(right: 8.w),
-        decoration: BoxDecoration(
-          color: Colors.white,
-          border: Border.all(color: '#F0F0F0'.toColor(), width: 1),
-          borderRadius: BorderRadius.circular(8),
-        ),
-        height: double.infinity,
-        padding: EdgeInsets.symmetric(horizontal: 12.w),
-        child: Column(
-          mainAxisAlignment: MainAxisAlignment.center,
-          crossAxisAlignment: CrossAxisAlignment.start,
-          children: [
-            Row(
-              children: [
-                Visibility(
-                  visible: item.isExample.isTrue,
-                  child: Container(
-                      padding: const EdgeInsets.symmetric(horizontal: 6).w,
-                      decoration: BoxDecoration(
-                        color: '#DFE4FC'.toColor(),
-                        borderRadius: BorderRadius.circular(4),
-                      ),
-                      child: Text(
-                        StringName.homeTalkExample.tr,
-                        style: TextStyle(
-                            fontSize: 12.sp, color: ColorName.colorPrimary),
-                      )),
-                ),
-                SizedBox(width: 6.w),
-                Text(item.title.orEmpty,
-                    maxLines: 1,
-                    overflow: TextOverflow.ellipsis,
-                    style: TextStyle(
-                        fontSize: 15.sp,
-                        color: ColorName.colorPrimary,
-                        fontWeight: FontWeight.bold))
-              ],
-            ),
-            SizedBox(height: 5.h),
-            Text(
-              item.summary.orEmpty,
-              style: TextStyle(
-                  fontSize: 12.sp, color: ColorName.secondaryTextColor),
-              overflow: TextOverflow.ellipsis,
-              maxLines: 2,
-            ),
-            SizedBox(height: 8.h),
-            Row(
-              crossAxisAlignment: CrossAxisAlignment.center,
-              children: [
-                Text(item.duration.toFormattedDuration(),
-                    style: TextStyle(
-                        fontSize: 12.sp, color: ColorName.tertiaryTextColor)),
-                SizedBox(width: 6.w),
-                Container(
-                    width: 1, height: 9, color: ColorName.tertiaryTextColor),
-                SizedBox(width: 6.w),
-                Text(item.createTime.orEmpty,
-                    style: TextStyle(
-                        fontSize: 12.sp, color: ColorName.tertiaryTextColor))
-              ],
-            )
-          ],
-        ));
+  Widget _buildTalkView(TalkBean item,
+      {GestureLongPressStartCallback? onLongPressStart}) {
+    return GestureDetector(
+      onLongPressStart: onLongPressStart,
+      child: Container(
+          width: 258.w,
+          margin: EdgeInsets.only(right: 8.w),
+          decoration: BoxDecoration(
+            color: Colors.white,
+            border: Border.all(color: '#F0F0F0'.toColor(), width: 1),
+            borderRadius: BorderRadius.circular(8),
+          ),
+          height: double.infinity,
+          padding: EdgeInsets.symmetric(horizontal: 12.w),
+          child: Column(
+            mainAxisAlignment: MainAxisAlignment.center,
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              Row(
+                children: [
+                  Visibility(
+                    visible: item.isExample.isTrue,
+                    child: Container(
+                        padding: const EdgeInsets.symmetric(horizontal: 6).w,
+                        decoration: BoxDecoration(
+                          color: '#DFE4FC'.toColor(),
+                          borderRadius: BorderRadius.circular(4),
+                        ),
+                        child: Text(
+                          StringName.homeTalkExample.tr,
+                          style: TextStyle(
+                              fontSize: 12.sp, color: ColorName.colorPrimary),
+                        )),
+                  ),
+                  SizedBox(width: 6.w),
+                  Text(item.title.orEmpty,
+                      maxLines: 1,
+                      overflow: TextOverflow.ellipsis,
+                      style: TextStyle(
+                          fontSize: 15.sp,
+                          color: ColorName.colorPrimary,
+                          fontWeight: FontWeight.bold))
+                ],
+              ),
+              SizedBox(height: 5.h),
+              Text(
+                item.summary.orEmpty,
+                style: TextStyle(
+                    fontSize: 12.sp, color: ColorName.secondaryTextColor),
+                overflow: TextOverflow.ellipsis,
+                maxLines: 2,
+              ),
+              SizedBox(height: 8.h),
+              Row(
+                crossAxisAlignment: CrossAxisAlignment.center,
+                children: [
+                  Text(item.duration.toFormattedDuration(),
+                      style: TextStyle(
+                          fontSize: 12.sp, color: ColorName.tertiaryTextColor)),
+                  SizedBox(width: 6.w),
+                  Container(
+                      width: 1, height: 9, color: ColorName.tertiaryTextColor),
+                  SizedBox(width: 6.w),
+                  Text(item.createTime.orEmpty,
+                      style: TextStyle(
+                          fontSize: 12.sp, color: ColorName.tertiaryTextColor))
+                ],
+              )
+            ],
+          )),
+    );
   }
 
   Widget _buildInsertTodoItem(BuildContext context, int index,
@@ -388,20 +399,15 @@ class HomePage extends BasePage<HomePageController> {
     HomePageController controller = Get.find();
     return FadeTransition(
       opacity: animation,
-      child: taskItemView(item, onCheckClick: () {
-        controller.agendaList.remove(
-            index,
-            (context, animation, item) =>
-                _buildRemoveTodoItem(context, index, animation, item));
-      }),
+      child: taskItemView(
+        item,
+        onCheckClick: () {
+          controller.agendaComplete(item);
+        },
+      ),
     );
   }
 
-  Widget _buildRemoveTodoItem(BuildContext context, int index,
-      Animation<double> animation, Agenda item) {
-    return SizeTransition(sizeFactor: animation, child: taskItemView(item));
-  }
-
   Widget buildTitle(String titleName, VoidCallback? onTap) {
     return Padding(
       padding: const EdgeInsets.symmetric(horizontal: 12).w,
@@ -442,10 +448,10 @@ class HomePage extends BasePage<HomePageController> {
     );
   }
 
-  attachDialog(BuildContext? context, Alignment alignment) {
+  void showUnfinishedRecordPopup() {
     SmartDialog.showAttach(
-      targetContext: context,
-      alignment: alignment,
+      targetContext: todoTargetContext,
+      alignment: Alignment.bottomRight,
       animationType: SmartAnimationType.fade,
       clickMaskDismiss: true,
       maskColor: Colors.transparent,
@@ -471,7 +477,22 @@ class HomePage extends BasePage<HomePageController> {
     );
   }
 
-  void showUnfinishedRecordPopup() {
-    attachDialog(todoTargetContext, Alignment.bottomRight);
+  void showRenameTalkDialog(TalkBean item) {
+    reNameDialog(StringName.talkRenameTitle.tr, item.title,
+        hintTxt: StringName.talkRenameTitleHint.tr,
+        maxLength: 15, returnBuilder: (newName) {
+      controller.requestName(newName, item);
+    });
+  }
+
+  void showDeleteTalkDialog(TalkBean item) {
+    talkDeleteDialog(item.id, item.title, returnBuilder: () {
+      controller.requestDelete(item);
+    });
   }
 }
+
+Widget buildRemoveTodoItem(
+    BuildContext context, Animation<double> animation, Agenda item) {
+  return SizeTransition(sizeFactor: animation, child: taskItemView(item));
+}

+ 8 - 0
lib/module/record/controller.dart

@@ -0,0 +1,8 @@
+import 'package:electronic_assistant/base/base_controller.dart';
+
+class RecordController extends BaseController {
+  @override
+  void onInit() {
+    super.onInit();
+  }
+}

+ 12 - 0
lib/module/record/view.dart

@@ -0,0 +1,12 @@
+import 'package:electronic_assistant/base/base_page.dart';
+import 'package:electronic_assistant/module/record/controller.dart';
+import 'package:flutter/material.dart';
+
+class RecordPage extends BasePage<RecordController> {
+  const RecordPage({super.key});
+
+  @override
+  Widget buildBody(BuildContext context) {
+    throw UnimplementedError();
+  }
+}

+ 104 - 0
lib/popup/talk_popup.dart

@@ -0,0 +1,104 @@
+import 'dart:ui';
+
+import 'package:electronic_assistant/resource/colors.gen.dart';
+import 'package:electronic_assistant/resource/string.gen.dart';
+import 'package:electronic_assistant/utils/expand.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
+import 'package:get/get.dart';
+
+import '../resource/assets.gen.dart';
+
+void showTalkPopup(Offset offset, Alignment alignment,
+    {VoidCallback? onRename, VoidCallback? onDelete}) {
+  SmartDialog.showAttach(
+    targetContext: null,
+    targetBuilder: (_, __) => offset,
+    animationType: SmartAnimationType.fade,
+    clickMaskDismiss: true,
+    alignment: alignment,
+    maskColor: Colors.transparent,
+    builder: (_) {
+      return Container(
+        width: 128.w,
+        decoration: BoxDecoration(
+          color: Colors.white,
+          border: Border.all(color: '#D8D8D8'.toColor(), width: 1), // 边框
+          borderRadius: BorderRadius.circular(8), // 圆角
+          boxShadow: [
+            BoxShadow(
+              color: Colors.black.withOpacity(0.1), // 阴影颜色
+              spreadRadius: 2, // 阴影扩散半径
+              blurRadius: 6, // 阴影模糊半径
+              offset: const Offset(0, 3), // 阴影偏移量
+            ),
+          ],
+        ),
+        child: Column(
+          children: [
+            _createNormalItem(StringName.talkRename.tr, onItemClick: () {
+              SmartDialog.dismiss();
+              onRename?.call();
+            }),
+            Divider(color: "#F6F6F6".toColor(), height: 1),
+            _buildDeleteItem(onDelete),
+          ],
+        ),
+      );
+    },
+  );
+}
+
+GestureDetector _buildDeleteItem(VoidCallback? onDelete) {
+  return GestureDetector(
+    onTap: () {
+      SmartDialog.dismiss();
+      onDelete?.call();
+    },
+    child: Container(
+      color: Colors.transparent,
+      padding: EdgeInsets.symmetric(horizontal: _itemPadding),
+      height: _itemHeight,
+      child: Row(
+        crossAxisAlignment: CrossAxisAlignment.center,
+        children: [
+          Text(
+            StringName.talkDelete.tr,
+            style: TextStyle(color: '#F5574E'.toColor(), fontSize: 14.sp),
+          ),
+          const Spacer(),
+          SizedBox(
+              width: 20.w,
+              height: 20.w,
+              child: Assets.images.iconTalkDelete.image())
+        ],
+      ),
+    ),
+  );
+}
+
+Widget _createNormalItem(String title, {VoidCallback? onItemClick}) {
+  return GestureDetector(
+    onTap: onItemClick,
+    child: Container(
+      color: Colors.transparent,
+      padding: EdgeInsets.symmetric(horizontal: _itemPadding),
+      height: _itemHeight,
+      child: Align(
+        alignment: Alignment.centerLeft,
+        child: Text(
+          StringName.talkRename.tr,
+          style: TextStyle(
+            fontSize: 14.sp,
+            color: ColorName.primaryTextColor,
+          ),
+        ),
+      ),
+    ),
+  );
+}
+
+final _itemHeight = 52.h;
+final _itemPadding = 14.w;

+ 6 - 0
lib/router/app_pages.dart

@@ -1,4 +1,5 @@
 import 'package:electronic_assistant/module/main/controller.dart';
+import 'package:electronic_assistant/module/record/controller.dart';
 import 'package:electronic_assistant/module/task/search/task_search.dart';
 import 'package:electronic_assistant/module/task/view.dart';
 import 'package:get/get.dart';
@@ -11,6 +12,7 @@ import '../module/home/controller.dart';
 import '../module/login/controller.dart';
 import '../module/login/view.dart';
 import '../module/main/view.dart';
+import '../module/record/view.dart';
 import '../module/splash/view.dart';
 import '../module/task/controller.dart';
 
@@ -36,6 +38,8 @@ abstract class RoutePath {
   static const taskSearch = '/taskSearch';
 
   static const chat = '/chat';
+
+  static const record = '/record';
 }
 
 class AppBinding extends Bindings {
@@ -46,6 +50,7 @@ class AppBinding extends Bindings {
     lazyPut(() => LoginController());
     lazyPut(() => TaskController());
     lazyPut(() => ChatController());
+    lazyPut(() => RecordController());
   }
 
   void lazyPut<S>(InstanceBuilderCallback<S> builder) {
@@ -62,4 +67,5 @@ final generalPages = [
   GetPage(name: RoutePath.chat, page: () => const ChatPage()),
   GetPage(name: RoutePath.task, page: () => const TaskPage()),
   GetPage(name: RoutePath.taskSearch, page: () => const TaskSearchPage()),
+  GetPage(name: RoutePath.record, page: () => const RecordPage()),
 ];

+ 20 - 7
lib/utils/animated_list_controller.dart

@@ -37,8 +37,22 @@ class AnimatedListController<T> {
   }
 
   void update(int index, T item) {
-    items[index] = item;
-    listKey.currentState?.insertItem(index);
+    clearAt(index);
+    add(item, index: index);
+  }
+
+  void removeItem(
+      T item, Widget Function(BuildContext, Animation<double>, T) buildItem,
+      {Duration duration = const Duration(milliseconds: 300)}) {
+    final index = items.indexOf(item);
+    if (index != -1) {
+      items.removeAt(index);
+      listKey.currentState?.removeItem(
+        index,
+        (context, animation) => buildItem(context, animation, item),
+        duration: duration,
+      );
+    }
   }
 
   void remove(
@@ -84,11 +98,6 @@ class AnimatedListController<T> {
         ?.removeItem(index, (context, animation) => Container());
   }
 
-  void clearItem(int index) {
-    items.removeAt(index);
-    listKey.currentState
-        ?.removeItem(index, (context, animation) => Container());
-  }
 
   void clearAll() {
     items.clear();
@@ -104,4 +113,8 @@ class AnimatedListController<T> {
   T get(int index) {
     return items[index];
   }
+
+  int indexOf(T item) {
+    return items.indexOf(item);
+  }
 }

+ 14 - 0
lib/utils/expand.dart

@@ -35,3 +35,17 @@ extension DurationExtension on double? {
     }
   }
 }
+
+extension StringExtensions on String {
+  String replacePlaceholders(List<dynamic> replacements) {
+    var result = this;
+    for (var replacement in replacements) {
+      if (replacement is String) {
+        result = result.replaceFirst('%s', replacement);
+      } else if (replacement is int) {
+        result = result.replaceFirst('%d', replacement.toString());
+      }
+    }
+    return result;
+  }
+}

+ 13 - 1
lib/utils/sse_parse_util.dart

@@ -21,13 +21,25 @@ class Message {
       required this.event,
       required this.data,
       required this.retry});
+
+  @override
+  String toString() {
+    return 'Message{id: $id, event: $event, data: $data, retry: $retry}';
+  }
 }
 
 class SSETransformer extends StreamTransformerBase<Uint8List, Message> {
   @override
   Stream<Message> bind(Stream<Uint8List> stream) {
     return Stream.eventTransformed(
-      stream.map((bytes) => utf8.decoder.convert(bytes)),
+      stream.map((bytes) {
+        try {
+          return utf8.decoder.convert(bytes);
+        } catch (e) {
+          debugPrint("$e: $bytes");
+        }
+        return "";
+      }),
       (sink) => SseEventSink(sink),
     );
   }

+ 1 - 1
lib/utils/toast_util.dart

@@ -5,7 +5,7 @@ class ToastUtil {
 
   static void showToast(String? msg,
       {Duration? displayTime,
-      SmartToastType? displayType = SmartToastType.onlyRefresh}) {
+      SmartToastType? displayType = SmartToastType.normal}) {
     if (msg != null) {
       SmartDialog.showToast(msg,
           displayType: displayType, displayTime: displayTime);

+ 34 - 10
pubspec.lock

@@ -181,10 +181,10 @@ packages:
     dependency: transitive
     description:
       name: crypto
-      sha256: "1dceb0cf05cb63a7852c11560060e53ec2f182079a16ced6f4395c5b0875baf8"
+      sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27
       url: "https://pub.dev"
     source: hosted
-    version: "3.0.4"
+    version: "3.0.5"
   cupertino_icons:
     dependency: "direct main"
     description:
@@ -266,18 +266,18 @@ packages:
     dependency: transitive
     description:
       name: flutter_gen_core
-      sha256: d8e828ad015a8511624491b78ad8e3f86edb7993528b1613aefbb4ad95947795
+      sha256: "638d518897f1aefc55a24278968027591d50223a6943b6ae9aa576fe1494d99d"
       url: "https://pub.dev"
     source: hosted
-    version: "5.6.0"
+    version: "5.7.0"
   flutter_gen_runner:
     dependency: "direct dev"
     description:
       name: flutter_gen_runner
-      sha256: "931b03f77c164df0a4815aac0efc619a6ac8ec4cada55025119fca4894dada90"
+      sha256: "7f2f02d95e3ec96cf70a1c515700c0dd3ea905af003303a55d6fb081240e6b8a"
       url: "https://pub.dev"
     source: hosted
-    version: "5.6.0"
+    version: "5.7.0"
   flutter_launcher_icons:
     dependency: "direct dev"
     description:
@@ -306,10 +306,10 @@ packages:
     dependency: "direct main"
     description:
       name: flutter_smart_dialog
-      sha256: "3d376ba47f64391cc657f706815b32dd6ff2e0f80553a5c9f33b812ba7c59462"
+      sha256: "6b5fd32cd2900745df30c1d95ef597ea0ee1ee8cfa557eab62010e3db1d3d717"
       url: "https://pub.dev"
     source: hosted
-    version: "4.9.8"
+    version: "4.9.8+1"
   flutter_test:
     dependency: "direct dev"
     description: flutter
@@ -355,6 +355,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.0.0"
+  http:
+    dependency: transitive
+    description:
+      name: http
+      sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.2.2"
   http_multi_server:
     dependency: transitive
     description:
@@ -459,6 +467,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.2.0"
+  lottie:
+    dependency: "direct main"
+    description:
+      name: lottie
+      sha256: "6a24ade5d3d918c306bb1c21a6b9a04aab0489d51a2582522eea820b4093b62b"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.1.2"
   matcher:
     dependency: transitive
     description:
@@ -487,10 +503,10 @@ packages:
     dependency: transitive
     description:
       name: mime
-      sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2"
+      sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a"
       url: "https://pub.dev"
     source: hosted
-    version: "1.0.5"
+    version: "1.0.6"
   mmkv:
     dependency: "direct main"
     description:
@@ -667,6 +683,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.3.0"
+  pull_to_refresh:
+    dependency: "direct main"
+    description:
+      name: pull_to_refresh
+      sha256: bbadd5a931837b57739cf08736bea63167e284e71fb23b218c8c9a6e042aad12
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.0"
   retrofit:
     dependency: "direct main"
     description:

+ 3 - 0
pubspec.yaml

@@ -57,6 +57,8 @@ dependencies:
   #上、下拉刷新
   pull_to_refresh: ^2.0.0
 
+  #lottie
+  lottie: ^3.1.2
 
 dev_dependencies:
   flutter_test:
@@ -101,6 +103,7 @@ flutter:
 
   assets:
     - assets/images/
+    - assets/anim/
 
   # An image asset can refer to one or more resolution-specific "variants", see
   # https://flutter.dev/assets-and-images/#resolution-aware