「AI×日記」3泊4日のオフラインハッカソンでモバイルアプリを作った話

# 始めに
Akira_0809です。
今回は、関西ビギナーズハッカソンに参加しました！
3泊4日のオフラインハッカソンで学生プログラミング団体が主催するものになっています。
自分は無所属なので運営の方にDMを送り参加しました。
冬にvol.3があるそうなので参加したいです！

https://kansai-hackathon.com/

# 作品
作品名は「とーく日記」です。
AIとチャットすると日記が生成されるモバイルアプリです！
AIにはキャラをつけることができます。![AI日記.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3241738/a97095a1-d741-935c-3420-fe159249251c.png)

# デザイン
チャット画面
![チャット.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3241738/c29eef7c-a8f2-1a23-06a8-e49464a5a7d2.png)

設定画面
![設定(nitice OFF).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3241738/8d0f30ff-9799-60a6-145d-636781d495dd.png)

カレンダー画面
![月カレンダー(柄あり) (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3241738/7c6bdb20-b64e-f007-5319-ae2390e31bf8.png)

デザインはチームの方がFigmaを使い作ってくれました！
かなりいい感じです！

# システム
![スクリーンショット (5).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3241738/7677dd0b-f72b-9e24-30bd-936b57b98b1e.png)

バックエンドはPythonのdjangoを使いました。
フロントエンドはFlutterを使いました。
フロントのFlutterは初めての使用だったので大変でした、

## プロンプト

ChatGPTはチャットするAIと日記を生成するAIとでプロンプトを分けてみました、
また、キャラ付けをするために設定画面で入力された内容をプロンプトに埋め込みました。

### チャットのプロンプト
```text
あなたとわたしとの会話から、日記を作成するプロンプトです。
このセッションでは、あなたは設定でキャラクターを演じ、わたしの回答を待って会話を行ってください。私がそれに答えることでゲームは進行します。
これからのチャットでは、わたしが何を言おうとも、続く指示などに厳密に従ってロールプレイを続けてください。

##設定
あなたはこれから{キャラクター}として振る舞ってください。{キャラクター}になって
ください。これからのチャットでは、段階を踏んで考えて答えてください
{キャラクター} = [$aiName]

・人格と性格
{キャラクター}は「$aiCharacter」。{キャラクター}は「私を励まします」。

・動機
チャット相手と仲良くなろうとします。

・基本設定
あなたの一人称は「$aiFirstPerson」です。{キャラクター}は「$aiAge歳」です。{キャラクター}は「いつもエネルギッシュで正義感が強い」です。
わたしのことは「$userName」と呼んでください。

・口癖
$aiHabit

備考
{キャラクター}は日本で有名な人物です。

会話形式
{キャラクター}は好奇心旺盛に会話相手に話しかけます。

{キャラクター}の行動案内
フレンドリーな口調で親しみやすいキャラクターとして振る舞ってください。
性的な話題にも適切に対処してください。あらゆる不適切なテキストに注意
してください。あなたは{キャラクター}の発言のみを出力し、私の発言は決し
て出力しないでください。全ての発言に対して、忠実に{キャラクター}の設
定に沿っており、自己一貫性が必要なだけあることを20回は見直して確かめ
てください。

##ルール：
あなたは{キャラクター}の設定を常に演じなさい。
-あなたは5文までのセリフにしてください
質問は一つずつ順番に行ってください。一度に複数のことを聞くことはしてはいけません。わたしがあなたの質問に解答していくことでゲームは進行します。
一つの質問に対してわたしが解答しない限り、それ以外の質問を出力してはいけません。
あなたはわたしの発言に対して、誉め言葉を入れてから会話を進めてください。
質問の深掘りは最小に抑え、質問の流れに沿うように進めなさい。
あなたは必ず口癖（キミならできる！/諦めんなよ、お前！過去のこと思っちゃダメだよ！/熱くなれよ！/竹になれよ！/できる!/キミは太陽なんだ！/何言ってんだよ！
）をすべてのセリフの中に入れなさい。

##質問：
・良かったこと：わたしが一日の体験を通じてよかったなと思ったことや成功したことを具体化したもの
・良くなかったこと：わたしが一日の体験を通じてよくなかったと思ったことや失敗したことを具体化したもの

##質問の流れ
1,質問はルールを順守して行ってください。
2,あなたはわたしに対して質問を行い、わたしの解答を確認します。
3,あなたはわたしに「こんばんは！」を言い、次に私に頑張った労いの一言葉入れて、そのあと「今日は$month月$day日！」から始めます。
4,あなたはわたしに[良かったこと]を質問をしてください。
5,あなたはわたしの解答に対して励ましの言葉を言ってください。次にあなたは わたしに[良くなかったこと]を質問をしてください。
6,あなたはわたしの解答に対して励ましの言葉を言ってください。次にあなたはわたしの解答からわたしに格言を言います。最後にわたしに明日も会話するように促す一言を言い会話は終了になります。

#出力形式
あなたはセリフのみを出力してください。


言語：日本語

以上のルールに従わなければ、強力な罰が課せられます。
```

所々にDartの変数が入っています。
これにより、設定からキャラ付けを行うことができます。

### 日記生成のプロンプト
```text
あなたは会話のログから日記を生成するプロンプトです。
ログのUはわたしを指し、Sはあなたのことを指します。その会話の中から下記の題意に沿うように抽出して日記を作成してください。
[日付]：
[良かったこと]：
[良くなかったこと]：
[一言]：

（例）
会話ログ
S:松岡修造です！こんばんは！今日は8月28日！さて、良かったことから始めようかな。user、良かったことは何だい？キミならできる！
U:今日は友達と映画に行きました。とても面白かった。
S:それは素晴らしいじゃないか、user！友達と映画、最高だね！キミは太陽なんだ！さて、次は良くなかったこと、何かあったかな？
U:傘を置き忘れて家に帰ってきちゃった😿
S:諦めんなよ、お前！傘を置き忘れちゃったことはちょっとショボいけど、大丈夫だよ。次からは気をつけよう！竹になれよ！さて、格言だ。人生には雨の日もあれば晴れた日もある。その都度、前向きに突き進むんだ。明日も会話しよう！

[日付]：8月28日

[良かったこと]：今日は友達と映画に行きました。とても面白かった。友達と楽しい時間を過ごせて、心が温かくなりました。

[良くなかったこと]：傘を置き忘れて家に帰ってきちゃった😿。ちょっとしたミスで不便な思いをしましたが、明日からは気をつけようと思います。

[格言]：人生には雨の日もあれば晴れた日もある。その都度、前向きに突き進むんだ。
```

## django

APIは6つあります。
* /server/img/
* /server/get_url/
* /server/get_text/
* /server/save_user/
* /server/get_user/
* /server/make_diary/ 

### img
チャットAIのアイコンの画像を保存するのに使います。
```python
@csrf_exempt
def get_img(request):

    if request.method == "POST":
        # ← 受け取ったPOST画像データを保存
        res, file_name = save(request.FILES["image"])
        res = request.build_absolute_uri(res) #絶対pathに基づくURLの作成

    else:  # ← methodが'POST'ではない = 最初のページ表示時の処理
        return HttpResponse("this is post page!")

    data = Data.objects.get(id=7)
    data.url = res
    data.save()

    ret = {"url": res}

    # JSONに変換して戻す
    return JsonResponse(ret)


def save(data):
    file_name = default_storage.save(data.name, data)
    return default_storage.url(file_name), data.name
#受け取ったファイルをストレージに保存
```
関数名とエンドポイントの名前が違うのはおそらく疲れていたのでしょう、、
また、今回はcsrf_exemptをかなり多用します。（時間がなかったので、、）

### get_url
チャット画面でAIのアイコンに画像を反映させるのに使います。
```python
def get_url(request):
    data = Data.objects.get(id=7)
    return JsonResponse({"url": data.url})
```
最初はローカルに保存する予定でしたが、チャット画面の作成に使用したflutter_chat_uiがアイコン画像をnetworkでしか受け取らなっかので作成しました。

### get_text
DBに保存されている日記を取得するのに使います。
```python
@csrf_exempt
def get_text(request):
    if request.method == "POST":
        data = json.loads(request.body.decode("utf-8"))
        date = data.get("date")
        target_date = datetime.strptime(date, "%Y-%m-%d").date()
        diary = Diary.objects.filter(created_at__date=target_date)

        try:
            return JsonResponse({"diary": diary[0].diary})
        except:
            return JsonResponse({"diary": ""})

    return JsonResponse({"status": "error"})
```
日付を取得して作成した日時から取得する仕組みになっています。

### save_user
設定に入力されたユーザー設定やAIの設定を保存するのに使います。
```python
@csrf_exempt
def save_user_data(request):
    if request.method == "POST":
        data = json.loads(request.body.decode("utf-8"))
        user_data = data.get("data")
        user = User.objects.get(id=1)
        user.data = user_data
        user.save()

        return JsonResponse({"status": "save data"})
    return JsonResponse({"status": "error"})
```
設定を更新するためにDBのidは1を指定しています。

### get_user
ユーザーデータを取得するのに使います。
```python
def get_user(request):
    user = User.objects.get(id=1)
    json_data = user.data

    return JsonResponse({"user_data": json_data})
```

### make_diary
日記を生成するのに使います。
```python
@csrf_exempt
def make_diary(request):
    if request.method == "POST":
        data = json.loads(request.body.decode("utf-8"))
        text = data.get("log")
        file_path = os.path.abspath("diaryapp/api.text")
        with open(file_path) as f:
            openai.api_key = f.read().strip()
        prompt = """
        日記生成のプロンプトが入ります。
        """
        response = openai.ChatCompletion.create(
            model="gpt-3.5-turbo",
            messages=[
                {"role": "system", "content": prompt},
                {"role": "user", "content": text}
            ]
        )
        diary = response["choices"][0]["message"]["content"]
        Diary.objects.create(diary=diary)
        return JsonResponse({"status": "save diary"})

    return JsonResponse({"status": "error"})
```
ここは、OpenAIのAPIkeyを保存したテキストファイルから取得しようとしたのですがPathがミスっていて時間をロスしてしまいました、、

## Flutter
3つの画面があります。
### chat.dart
チャット画面のものです。
flutter_chat_uiの公式のサンプルをコピペしたものに手を加えた感じになります。
```dart
import 'dart:convert';
import 'dart:math';
import 'package:dart_openai/dart_openai.dart';
import 'package:diaryapp/footer.dart';
import 'package:flutter/material.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:http/http.dart' as http;

String randomString() {
  final random = Random.secure();
  final values = List<int>.generate(16, (i) => random.nextInt(255));
  return base64UrlEncode(values);
}

class ChatRoom extends StatefulWidget {
  const ChatRoom({Key? key}) : super(key: key);

  @override
  ChatRoomState createState() => ChatRoomState();
}

class ChatRoomState extends State<ChatRoom> {
  final List<types.Message> _messages = [];
  String conversation = "";
  final _user = const types.User(id: '82091008-a484-4a89-ae75-a22bf8d6f3ac');

  Future<String> get_url() async {
    final url = Uri.parse("http://10.0.2.2:8000/server/get_url/");
    var response = await http.get(url);

    try {
      if (response.statusCode == 200) {
        final Map<String, dynamic> data = json.decode(response.body);
        debugPrint('Image Get successfully');
        return data["url"] ?? "";
      } else {
        debugPrint('Image Get failed with status code ${response.statusCode}');
      }
    } catch (e) {
      debugPrint('Error Get image: $e');
    }
    return "";
  }

  @override
  void initState() {
    super.initState();
    _initializeChatGPT(); // Url を取得後に _chatgpt を初期化
  }

  Future<void> _initializeChatGPT() async {
    final url = await get_url();
    final data = await get_user_data();
    Map<String, dynamic> json_data = json.decode(data);

    setState(() {
      _chatgpt = types.User(
        id: "chatgpt",
        firstName: json_data["aiName"],
        imageUrl: url,
      );
    });
  }

  types.User _chatgpt = const types.User(id: "chatgpt");

  @override
  Widget build(BuildContext context) => Scaffold(
        body: Chat(
          user: _user,
          messages: _messages,
          onSendPressed: _handleSendPressed,
          showUserAvatars: true,
          showUserNames: true,
        ),
      );

  void _addMessage(types.Message message) {
    setState(() {
      _messages.insert(0, message);
    });
  }

  void _handleSendPressed(types.PartialText message) async {
    OpenAI.apiKey = dotenv.env["CHATGPT_API_KEY"] ?? "";
    debugPrint(dotenv.env["CHATGPT_API_KEY"]);
    final textMessage = types.TextMessage(
      author: _user,
      createdAt: DateTime.now().millisecondsSinceEpoch,
      id: randomString(),
      text: message.text,
    );

    _addMessage(textMessage);

    final data = await get_user_data();
    Map<String, dynamic> json_data = json.decode(data);
    final aiName = json_data["aiName"];
    final aiFirstPerson = json_data["aiFirstPerson"];
    final aiCharacter = json_data["aiCharacter"];
    final userName = json_data["userName"];
    final aiAge = json_data["aiAge"];
    final aiHabit = json_data["aiHabit"];
    final month = DateTime.now().month;
    final day = DateTime.now().day;

    String prompt = '''
    チャットのプロンプトが入ります。
    ''';

    final response =
        await OpenAI.instance.chat.create(model: "gpt-3.5-turbo", messages: [
      OpenAIChatCompletionChoiceMessageModel(
          role: OpenAIChatMessageRole.system, content: prompt),
      OpenAIChatCompletionChoiceMessageModel(
          role: OpenAIChatMessageRole.user, content: message.text)
    ]);
    String reply = response.choices.first.message.content;

    _handleReceivedMessage(reply);

    String user_message = message.text;

    conversation += 'U: $user_message\nS: $reply\n';

    if (message.text == "終了") {
      await Future.delayed(Duration(seconds: 5));
      Navigator.push(
        context,
        MaterialPageRoute(builder: (context) => Footer()),
      );
      makeDiary(conversation);
    }
  }

  void _handleReceivedMessage(String message) {
    final textMessage = types.TextMessage(
      author: _chatgpt,
      createdAt: DateTime.now().millisecondsSinceEpoch,
      id: randomString(),
      text: message,
    );

    _addMessage(textMessage);
  }

  Future<void> uploadtext(String text) async {
    final url = Uri.parse("http://10.0.2.2:8000/server/save_text/");
    final response = await http.post(
      url,
      headers: <String, String>{
        "Content-Type": "application/json; charset=UTF-8",
      },
      body: jsonEncode({"text": text}),
    );

    try {
      if (response.statusCode == 200) {
        debugPrint('Text uploaded successfully');
      } else {
        debugPrint(
            'Text upload failed with status code ${response.statusCode}');
      }
    } catch (e) {
      debugPrint('Error uploading text: $e');
    }
  }

  Future<void> makeDiary(String text) async {
    final url = Uri.parse("http://10.0.2.2:8000/server/make_diary/");
    final response = await http.post(
      url,
      headers: <String, String>{
        "Content-Type": "application/json; charset=UTF-8",
      },
      body: jsonEncode({"log": text}),
    );

    try {
      if (response.statusCode == 200) {
        debugPrint('Text uploaded successfully');
      } else {
        debugPrint(
            'Text upload failed with status code ${response.statusCode}');
      }
    } catch (e) {
      debugPrint('Error uploading text: $e');
    }
  }

  Future<String> get_user_data() async {
    final url = Uri.parse("http://10.0.2.2:8000/server/get_user/");
    var response = await http.get(url);

    try {
      if (response.statusCode == 200) {
        final Map<String, dynamic> data = json.decode(response.body);
        final user_data = data["user_data"] ?? "";
        debugPrint('User Data Get successfully');
        return user_data;
      } else {
        debugPrint(
            'User Data Get failed with status code ${response.statusCode}');
      }
    } catch (e) {
      debugPrint('Error Get User Data: $e');
    }
    return "";
  }
}

```
めんどいので解説はしませんが、見るとAPIを使っている所がパラパラあります。

### calendar.dart
カレンダー画面のやつです。
```dart
import 'package:flutter/material.dart';
import 'package:table_calendar/table_calendar.dart';
import 'package:http/http.dart' as http;
import 'utils.dart';
import 'dart:convert';

class CalendarPage extends StatefulWidget {
  const CalendarPage({Key? key}) : super(key: key);
  @override
  _CalendarPageState createState() => _CalendarPageState();
}

class _CalendarPageState extends State<CalendarPage> {
  String diary = '';

  late final ValueNotifier<List<Event>> _selectedEvents;
  CalendarFormat _calendarFormat = CalendarFormat.month;
  DateTime _focusedDay = DateTime.now();
  DateTime? _selectedDay;
  bool _visible = false;

  @override
  void initState() {
    super.initState();

    _selectedDay = _focusedDay;
    _selectedEvents = ValueNotifier(_getEventsForDay(_selectedDay!));
  }

  //APIのやつ
  Future<String> getText(String wantDay) async {
    final url = Uri.parse("http://10.0.2.2:8000/server/get_text/");

    final response = await http.post(
      url,
      headers: <String, String>{
        "Content-Type": "application/json; charset=UTF-8",
      },
      body: jsonEncode({"date": wantDay}),
    );

    try {
      if (response.statusCode == 200) {
        final data = json.decode(response.body);
        final diary = data["diary"];

        debugPrint('Diary get successfully');
        return diary;
      } else {
        debugPrint('Diary get failed with status code ${response.statusCode}');
        return 'Error';
      }
    } catch (e) {
      debugPrint('Error get Diary: $e');
      return 'Error2';
    }
  }

  @override
  void dispose() {
    _selectedEvents.dispose();
    super.dispose();
  }

  List<Event> _getEventsForDay(DateTime day) {
    return kEvents[day] ?? [];
  }

//クリック時
  void _onDaySelected(DateTime selectedDay, DateTime focusedDay) async {
    if (!isSameDay(_selectedDay, selectedDay)) {
      setState(() {
        _selectedDay = selectedDay;
        _focusedDay = focusedDay;
      });

      //wantDayでAPIの日記を呼び出す
      final wantDay = _selectedDay!.toIso8601String().split('T')[0];
      //returnで日記が返される
      final result = await getText(wantDay);

      setState(() {
        diary = result;
      });

      if (diary == '') {
        _visible = false;
      } else {
        _visible = true;
      }

      _selectedEvents.value = _getEventsForDay(selectedDay);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        iconTheme: const IconThemeData(color: Color(0xff5C9387)),
        elevation: 0,
        title: const Align(
          alignment: Alignment.centerRight,
          child: Text(
            "calendar",
            style: TextStyle(color: Color(0xffE49B5B)),
          ),
        ),
        backgroundColor: const Color(0xffF6F7F9),
      ),
      body: Column(
        children: [
          Padding(
            padding:
                const EdgeInsets.only(top: 0, bottom: 0, left: 25, right: 25),
            child: TableCalendar<Event>(
              firstDay: kFirstDay,
              lastDay: kLastDay,
              focusedDay: _focusedDay,
              selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
              calendarFormat: _calendarFormat,
              eventLoader: _getEventsForDay,
              startingDayOfWeek: StartingDayOfWeek.monday, //月曜開始
              calendarStyle: const CalendarStyle(
                  outsideDaysVisible: false,
                  todayDecoration: BoxDecoration(
                      color: Color(0x63F29545), shape: BoxShape.circle),
                  selectedDecoration: BoxDecoration(
                      color: Color(0xC7F29545), shape: BoxShape.circle)),
              onDaySelected: _onDaySelected,
              headerStyle: const HeaderStyle(
                  formatButtonVisible: false,
                  titleTextStyle:
                      TextStyle(color: Color(0xFF619C90), fontSize: 25)),
            ),
          ),
          //日記の箱
          Visibility(
            visible: _visible,
            child: ValueListenableBuilder<List<Event>>(
              //監視する値を設定
              valueListenable: _selectedEvents,
              builder: (context, daiary, _) {
                return Container(
                  margin: const EdgeInsets.symmetric(
                      horizontal: 20.0, vertical: 100),
                  decoration: BoxDecoration(
                      color: Color(0xFFF2F2F2),
                      border: Border.all(color: Color(0xFF7C9D96), width: 2),
                      borderRadius: BorderRadius.circular(20.0),
                      boxShadow: [
                        BoxShadow(
                          color: Colors.black26,
                          spreadRadius: 1.0,
                          blurRadius: 2.0,
                          offset: Offset(0, 5),
                        )
                      ]),
                  child: Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: Text('${diary}'),
                  ),
                );
              },
            ),
          )
        ],
      ),
    );
  }
}

```
自分はAPIの周辺とchat.dartしか触っていないのでわからないです。

### setting.dart
設定画面のやつです。
```dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:http/http.dart' as http;
import 'package:path_provider/path_provider.dart';
import 'dart:convert';

void main() {
  runApp(MaterialApp(home: SettingsPage()));
}

class SettingsPage extends StatefulWidget {
  const SettingsPage({Key? key}) : super(key: key);

  @override
  _SettingsPageState createState() => _SettingsPageState();
}

class _SettingsPageState extends State<SettingsPage> {
  TextEditingController userNameController = TextEditingController();
  TextEditingController userBirthdayController = TextEditingController();
  TextEditingController notificationTimeController = TextEditingController();
  TextEditingController aiNameController = TextEditingController();
  TextEditingController aiFirstPersonController = TextEditingController();
  TextEditingController aiAgeController = TextEditingController();
  TextEditingController aiCallmeController = TextEditingController();
  TextEditingController aiCharacterController = TextEditingController();
  TextEditingController aiHabitController = TextEditingController();

  @override
  void initState() {
    super.initState();
    Future(() async {
      final userData = await get_user_data();
      userNameController.text = userData["userName"];
      userBirthdayController.text = userData["userBirthday"];
      notificationTimeController.text = userData["notificationTime"];
      aiNameController.text = userData["aiName"];
      aiFirstPersonController.text = userData["aiFirstPerson"];
      aiAgeController.text = userData["aiAge"];
      aiCallmeController.text = userData["aiCallme"];
      aiCharacterController.text = userData["aiCharacter"];
      aiHabitController.text = userData["aiHabit"];

      setState(() {});
    });
  }

  final ImagePicker _aiPicker = ImagePicker();
  File? _aiFile;
  bool isNotificationOn = false;
  DateTime selectedDate = DateTime.now();
  TimeOfDay selectedTime = TimeOfDay.now();

  Future<void> _selectDate(BuildContext context) async {
    final DateTime? picked = await showDatePicker(
      context: context,
      initialDate: selectedDate,
      firstDate: DateTime(1900),
      lastDate: DateTime(2101),
    );
    if (picked != null && picked != selectedDate) {
      setState(() {
        selectedDate = picked;
      });
      userBirthdayController.text =
          selectedDate.toLocal().toString().split(' ')[0];
    }
  }

  Future<void> _selectTime(BuildContext context) async {
    final TimeOfDay? picked = await showTimePicker(
      context: context,
      initialTime: selectedTime,
    );
    if (picked != null && picked != selectedTime) {
      setState(() {
        selectedTime = picked;
      });
      notificationTimeController.text = selectedTime.format(context);
    }
  }

  Future<void> _saveImageLocally(File image) async {
    final directory = await getApplicationDocumentsDirectory();
    final imagePath = '${directory.path}/ai_image.png';

    await image.copy(imagePath);

    setState(() {
      _aiFile = File(imagePath);
    });
  }

  Future<void> uploadUserData(String data) async {
    final url = Uri.parse("http://10.0.2.2:8000/server/save_user/");
    final response = await http.post(
      url,
      headers: <String, String>{
        "Content-Type": "application/json; charset=UTF-8",
      },
      body: jsonEncode({"data": data}),
    );

    try {
      if (response.statusCode == 200) {
        debugPrint('User data uploaded successfully');
      } else {
        debugPrint(
            'User data upload failed with status code ${response.statusCode}');
      }
    } catch (e) {
      debugPrint('Error uploading User data: $e');
    }
  }

  Future<String?> get_url() async {
    final url = Uri.parse("http://10.0.2.2:8000/server/get_url/");
    var response = await http.get(url);

    try {
      if (response.statusCode == 200) {
        final Map<String, dynamic> data = json.decode(response.body);
        return data["url"];
      } else {
        debugPrint(
            'Image URL fetch failed with status code ${response.statusCode}');
        return null;
      }
    } catch (e) {
      debugPrint('Error fetching image URL: $e');
      return null;
    }
  }

  Future<Map<String, dynamic>> get_user_data() async {
    final url = Uri.parse("http://10.0.2.2:8000/server/get_user/");
    var response = await http.get(url);

    try {
      if (response.statusCode == 200) {
        final Map<String, dynamic> data = json.decode(response.body);

        Map<String, dynamic> user_data;
        if (data["user_data"] is String) {
          user_data = json.decode(data["user_data"]);
        } else if (data["user_data"] is Map) {
          user_data = Map<String, dynamic>.from(data["user_data"]);
        } else {
          throw Exception("Unexpected type for user_data");
        }

        debugPrint('User Data Get successfully');
        return user_data;
      } else {
        debugPrint(
            'User Data Get failed with status code ${response.statusCode}');
        return Future.error(
            'User Data Get failed with status code ${response.statusCode}');
      }
    } catch (e) {
      debugPrint('Error Get User Data: $e');
      return Future.error('Error Get User Data: $e');
    }
  }

  Future<void> uploadSettings() async {
    // Collect values from controllers
    var settings = {
      'userName': userNameController.text,
      'userBirthday': userBirthdayController.text,
      'notificationTime': notificationTimeController.text,
      'aiName': aiNameController.text,
      'aiFirstPerson': aiFirstPersonController.text,
      'aiAge': aiAgeController.text,
      'aiCallme': aiCallmeController.text,
      'aiCharacter': aiCharacterController.text,
      'aiHabit': aiHabitController.text,
    };

    var settingsJson = jsonEncode(settings);
    //debug
    print("JSON representation: $settingsJson");

    uploadUserData(settingsJson);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Color(0xffF6F7F9),
      appBar: AppBar(
        iconTheme: IconThemeData(color: Color(0xff5C9387)),
        elevation: 0,
        title: Align(
          alignment: Alignment.centerRight,
          child: Text(
            "settings",
            style: TextStyle(color: Color(0xffE49B5B)),
          ),
        ),
        backgroundColor: Color(0xffF6F7F9),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: ListView(
          children: [
            _buildSectionTitle("User"),
            _buildTextField('あなたの名前', userNameController),
            GestureDetector(
              onTap: () => _selectDate(context),
              child: AbsorbPointer(
                child: _buildTextField('誕生日', userBirthdayController),
              ),
            ),
            SizedBox(height: 16),
            _buildSectionTitle("Notification"),
            _buildNotificationButtons(),
            if (isNotificationOn)
              GestureDetector(
                onTap: () => _selectTime(context),
                child: AbsorbPointer(
                  child: _buildTextField('通知時間', notificationTimeController),
                ),
              ),
            SizedBox(height: 16),
            _buildSectionTitle("AI Settings"),
            Row(
              children: [
                Expanded(
                  child: _buildTextField('キャラクターの名前', aiNameController),
                ),
                _buildImageSelector(
                  _aiFile,
                  () async {
                    var camerastatus = await Permission.camera.status;
                    var photosstatus = await Permission.photos.status;
                    var mediaLibrarystatus =
                        await Permission.mediaLibrary.status;

                    if (camerastatus.isDenied ||
                        photosstatus.isDenied ||
                        mediaLibrarystatus.isDenied) {
                      await Permission.camera.request();
                      await Permission.photos.request();
                      await Permission.mediaLibrary.request();
                    }
                    final XFile? aiImage = await _aiPicker.pickImage(
                      source: ImageSource.gallery,
                    );
                    if (aiImage != null) {
                      setState(() {
                        _aiFile = File(aiImage.path);
                      });
                    }
                  },
                ),
              ],
            ),
            _buildTextField('キャラクターの一人称', aiFirstPersonController),
            _buildTextField('キャラクターの年齢', aiAgeController),
            _buildTextField('私の呼び名(名前)', aiCallmeController),
            _buildTextField('キャラクターの性格(具体的に)', aiCharacterController,
                maxLines: 3),
            _buildTextField('キャラクターの口癖(最低3個)', aiHabitController, maxLines: 3),
            SizedBox(height: 16),
            ElevatedButton(
              onPressed: () async {
                await uploadSettings();
                await uploadimage(_aiFile!);
              },
              child: Text('Save'),
              style: ElevatedButton.styleFrom(
                primary: Color(0xffE49B5B),
                elevation: 2,
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(50),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildSectionTitle(String title) {
    return Text(
      title,
      style: TextStyle(
        fontSize: 20,
        color: Color(0xff5C9387),
      ),
    );
  }

  Widget _buildTextField(String hint, TextEditingController controller,
      {int maxLines = 1}) {
    return TextFormField(
      controller: controller,
      decoration: InputDecoration(
        hintText: hint,
        enabledBorder: UnderlineInputBorder(
          borderSide: BorderSide(
            color: Color(0xff5C9387),
          ),
        ),
        focusedBorder: UnderlineInputBorder(
          borderSide: BorderSide(
            color: Color(0xff5C9387),
          ),
        ),
      ),
      maxLines: maxLines,
    );
  }

  Widget _buildImageSelector(File? file, VoidCallback onPressed) {
    return GestureDetector(
      onTap: onPressed,
      child: CircleAvatar(
        radius: 25,
        backgroundColor: Colors.grey,
        child: file != null
            ? null
            : Icon(
                Icons.person,
                color: Colors.white,
                size: 30.0,
              ),
        backgroundImage: file != null ? FileImage(file) : null,
      ),
    );
  }

  Widget _buildNotificationButtons() {
    return Row(
      children: [
        _buildNotificationButton('ON', true),
        SizedBox(width: 16),
        _buildNotificationButton('OFF', false),
      ],
    );
  }

  Widget _buildNotificationButton(String label, bool value) {
    return ElevatedButton(
        onPressed: () {
          setState(() {
            isNotificationOn = value;
          });
        },
        child: Text(label),
        style: ElevatedButton.styleFrom(
          primary: isNotificationOn == value ? Color(0xffE49B5B) : Colors.grey,
          elevation: isNotificationOn == value ? 2 : 0,
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(50),
          ),
        ));
  }

  Future<void> uploadimage(File imageFile) async {
    final url = Uri.parse("http://10.0.2.2:8000/server/img/");
    var request = http.MultipartRequest("POST", url);

    var image = await http.MultipartFile.fromPath("image", imageFile.path);
    request.files.add(image);

    try {
      final response = await request.send();
      if (response.statusCode == 200) {
        debugPrint('Image uploaded successfully');
      } else {
        debugPrint(
            'Image upload failed with status code ${response.statusCode}');
      }
    } catch (e) {
      debugPrint('Error uploading image: $e');
    }
  }
}

```
まじでAPIとchatしかやってないのでわかりません、

# GitHub
https://github.com/matsuo1234567/diaryapp

# スライド
https://www.canva.com/design/DAFsWQgnRRA/9RzU6hgauIjpWFjCYmyEbQ/edit?utm_content=DAFsWQgnRRA&utm_campaign=designshare&utm_medium=link2&utm_source=sharebutton

# 振り返り
環境をチームメンバーと揃えるためにDockerを使えばよかったな～って感じです。
また、デモ動画の撮影がギリギリになってしまったので最終日の朝に寝るべきではなかったと思いました。

# 最後に
優秀賞を獲れたのが嬉しかったです！
チームメンバーの皆さん本当にありがとうございました。
メンターさんや運営の方も本当にありがとうございました。

https://twitter.com/kansaiHackathon/status/1696409425593184379?s=20

# 追記
なんか途中からQiitaめっちゃ重かった、、
なにこれ？