Merge remote-tracking branch 'origin/openai-implementation'

This commit is contained in:
Reimar 2024-09-17 09:26:00 +02:00
commit bdb5647e88
Signed by: Reimar
GPG Key ID: 93549FA07F0AE268
19 changed files with 304 additions and 21 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@ API/bin
API/obj API/obj
.idea .idea
/rust-backend/.env.example

View File

@ -10,6 +10,10 @@
<DockerfileContext>.</DockerfileContext> <DockerfileContext>.</DockerfileContext>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<_ContentIncludedByDefault Remove="appsettings.json" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AWSSDK.S3" Version="3.7.402.4" /> <PackageReference Include="AWSSDK.S3" Version="3.7.402.4" />
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" /> <PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />

1
Mobile/.env.example Normal file
View File

@ -0,0 +1 @@
OPENAI_API_KEY=

1
Mobile/.gitignore vendored
View File

@ -44,3 +44,4 @@ app.*.map.json
environment.json environment.json
node_modules node_modules
.env

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:mobile/api.dart' as api; import 'package:mobile/services/api.dart' as api;
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'variables.dart'; import 'variables.dart';
@ -104,6 +104,14 @@ class _SideMenuState extends State<SideMenu> {
Navigator.pushReplacementNamed(context, '/profile'); Navigator.pushReplacementNamed(context, '/profile');
}, },
), ),
ListTile(
title: const Text('GuideBook'),
leading: const Icon(Icons.menu_book_sharp),
selected: _selectedIndex == 5,
onTap: () {
Navigator.pushReplacementNamed(context, '/tourist-guide-book');
},
),
const Divider( const Divider(
color: Colors.grey, color: Colors.grey,
thickness: 2, thickness: 2,
@ -118,6 +126,19 @@ class _SideMenuState extends State<SideMenu> {
), ),
] ]
: [ : [
ListTile(
title: const Text('GuideBook'),
leading: const Icon(Icons.menu_book_sharp),
selected: _selectedIndex == 5,
onTap: () {
Navigator.pushReplacementNamed(context, '/tourist-guide-book');
},
),
const Divider(
color: Colors.grey,
thickness: 2,
indent: 40,
),
ListTile( ListTile(
title: const Text('Register'), title: const Text('Register'),
leading: const Icon(Icons.add_box_outlined), leading: const Icon(Icons.add_box_outlined),

View File

@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:mobile/base/sidemenu.dart'; import 'package:mobile/base/sidemenu.dart';
import 'models.dart' as models; import 'models.dart' as models;
import 'api.dart' as api; import 'services/api.dart' as api;
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
class CreateReviewPage extends StatefulWidget { class CreateReviewPage extends StatefulWidget {

View File

@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:mobile/models.dart' as models; import 'package:mobile/models.dart' as models;
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'api.dart' as api; import 'services/api.dart' as api;
import 'base/variables.dart'; import 'base/variables.dart';
class EditProfilePage extends StatefulWidget { class EditProfilePage extends StatefulWidget {

View File

@ -1,5 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'api.dart' as api; import 'services/api.dart' as api;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'base/sidemenu.dart'; // Import the base layout widget import 'base/sidemenu.dart'; // Import the base layout widget
import 'models.dart'; import 'models.dart';

View File

@ -4,7 +4,7 @@ import 'package:mobile/models.dart' as models;
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'dart:convert'; import 'dart:convert';
import 'api.dart' as api; import 'services/api.dart' as api;
import 'base/variables.dart'; import 'base/variables.dart';
class LoginPage extends StatefulWidget { class LoginPage extends StatefulWidget {

View File

@ -8,18 +8,22 @@ import 'package:mobile/createreview.dart';
import 'package:mobile/favorites.dart'; import 'package:mobile/favorites.dart';
import 'package:mobile/register.dart'; import 'package:mobile/register.dart';
import 'package:mobile/reviewlist.dart'; import 'package:mobile/reviewlist.dart';
import 'package:mobile/touristguidebook.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'login.dart'; import 'login.dart';
import 'base/sidemenu.dart'; import 'base/sidemenu.dart';
import 'profile.dart'; import 'profile.dart';
import 'models.dart'; import 'models.dart';
import 'package:geolocator/geolocator.dart'; import 'package:geolocator/geolocator.dart';
import 'api.dart' as api; import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'services/api.dart' as api;
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await dotenv.load(fileName: ".env");
// Refresh JWT on startup // Refresh JWT on startup
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
if (prefs.getString("token") != null && prefs.getString("refresh-token") != null) { if (prefs.getString("token") != null && prefs.getString("refresh-token") != null) {
@ -50,6 +54,7 @@ class MyApp extends StatelessWidget {
'/register': (context) => const RegisterPage(), '/register': (context) => const RegisterPage(),
'/reviews': (context) => const ReviewListPage(), '/reviews': (context) => const ReviewListPage(),
'/create-review': (context) => const CreateReviewPage(), '/create-review': (context) => const CreateReviewPage(),
'/tourist-guide-book': (context) => const TouristGuideBookPage(),
}, },
); );
} }

View File

@ -4,7 +4,7 @@ import 'package:mobile/base/variables.dart';
import 'package:mobile/models.dart' as models; import 'package:mobile/models.dart' as models;
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'base/sidemenu.dart'; import 'base/sidemenu.dart';
import 'api.dart' as api; import 'services/api.dart' as api;
import 'editprofile.dart'; import 'editprofile.dart';
class ProfilePage extends StatefulWidget { class ProfilePage extends StatefulWidget {

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:mobile/base/sidemenu.dart'; import 'package:mobile/base/sidemenu.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:mobile/base/variables.dart'; import 'package:mobile/base/variables.dart';
import 'api.dart' as api; import 'services/api.dart' as api;
class RegisterPage extends StatefulWidget { class RegisterPage extends StatefulWidget {
const RegisterPage({super.key}); const RegisterPage({super.key});

View File

@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
import 'package:mobile/base/sidemenu.dart'; import 'package:mobile/base/sidemenu.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'models.dart' as models; import 'models.dart' as models;
import 'api.dart' as api; import 'services/api.dart' as api;
class ReviewListPage extends StatefulWidget { class ReviewListPage extends StatefulWidget {
const ReviewListPage({super.key}); const ReviewListPage({super.key});

View File

@ -199,5 +199,3 @@ void logout() async {
prefs.remove('refresh-token'); prefs.remove('refresh-token');
prefs.remove('id'); prefs.remove('id');
} }
//------------------------------OPENAI API------------------------------//

View File

@ -0,0 +1,55 @@
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:flutter_dotenv/flutter_dotenv.dart';
class OpenAIService {
static Future<String> getGuideBook(String country) async {
final apiKey = dotenv.env['OPENAI_API_KEY'];
final uri = Uri.parse('https://api.openai.com/v1/chat/completions');
final requestBody = jsonEncode({
"model": "gpt-4o-mini",
"messages": [
{
"role": "system",
"content": "You are a Tourist-guide book that makes a guidebook over a given country and further instructions in the message."
},
{
"role": "user",
"content": "Make a tourist guide book over $country. The topics should be: A little introduction to how the people of the country is like and what to expect from the environment, famous tourist attractions in the country(amusement park always included), Basic words that would be nice to learn in the country's language(should always be from English to the country's language) and transportation. These topics should be in a Json-Object format in this form: {Country: {introduction: {people: , environment: }, tourist_attractions: {famous_sites: [{name: , description: }]}, basic_words: {}, transportation: {overview: , public_transport: {buses: , trains: , taxis: }}}}"
}
],
"max_tokens": 1500
});
final response = await http.post(
uri,
headers: {
'Authorization': 'Bearer $apiKey',
'Content-Type': 'application/json',
},
body: requestBody,
);
if (response.statusCode == 200) {
final decodedBody = utf8.decode(response.bodyBytes);
final data = jsonDecode(decodedBody);
var content = data['choices'][0]['message']['content'];
content = content.replaceAll('```json', '').replaceAll('```', '').trim();
return _extractDescription(content);
} else {
throw Exception('Fejl ved billedanalyse: ${response.statusCode}');
}
}
static String _extractDescription(String content) {
final lines = content.split('\n');
final startIndex = lines.indexWhere((line) => line.startsWith('**Beskrivelse:**'));
if (startIndex == -1) return content;
return lines.skip(startIndex).join('\n').trim();
}
}

View File

@ -0,0 +1,185 @@
import 'dart:convert';
import 'package:dropdown_search/dropdown_search.dart';
import 'package:flutter/material.dart';
import 'package:mobile/base/sidemenu.dart';
import 'services/openaiservice.dart' as api;
class TouristGuideBookPage extends StatefulWidget {
const TouristGuideBookPage({super.key});
@override
State<TouristGuideBookPage> createState() => _TouristGuideBookPageState();
}
class _TouristGuideBookPageState extends State<TouristGuideBookPage> {
String? _selectedCountry;
bool _isLoading = false;
Map<String, dynamic> _touristbook = {};
final List<String> countries = [
"Afghanistan", "Albania", "Algeria", "Andorra", "Angola", "Argentina",
"Armenia", "Australia", "Austria", "Azerbaijan", "Bahamas", "Bahrain",
"Bangladesh", "Barbados", "Belarus", "Belgium", "Belize", "Benin",
"Bhutan", "Bolivia", "Bosnia and Herzegovina", "Botswana", "Brazil",
"Brunei", "Bulgaria", "Burkina Faso", "Burundi", "Cabo Verde", "Cambodia",
"Cameroon", "Canada", "Central African Republic", "Chad", "Chile", "China",
"Colombia", "Comoros", "Congo (Congo-Brazzaville)", "Congo (Democratic Republic)",
"Costa Rica", "Croatia", "Cuba", "Cyprus", "Czech Republic", "Denmark",
"Djibouti", "Dominica", "Dominican Republic", "Ecuador", "Egypt", "El Salvador",
"Equatorial Guinea", "Eritrea", "Estonia", "Eswatini", "Ethiopia", "Fiji",
"Finland", "France", "Gabon", "Gambia", "Georgia", "Germany", "Ghana",
"Greece", "Grenada", "Guatemala", "Guinea", "Guinea-Bissau", "Guyana",
"Haiti", "Honduras", "Hungary", "Iceland", "India", "Indonesia", "Iran",
"Iraq", "Ireland", "Israel", "Italy", "Jamaica", "Japan", "Jordan", "Kazakhstan",
"Kenya", "Kiribati", "Kuwait", "Kyrgyzstan", "Laos", "Latvia", "Lebanon",
"Lesotho", "Liberia", "Libya", "Liechtenstein", "Lithuania", "Luxembourg",
"Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands",
"Mauritania", "Mauritius", "Mexico", "Micronesia", "Moldova", "Monaco",
"Mongolia", "Montenegro", "Morocco", "Mozambique", "Myanmar", "Namibia",
"Nauru", "Nepal", "Netherlands", "New Zealand", "Nicaragua", "Niger",
"Nigeria", "North Korea", "North Macedonia", "Norway", "Oman", "Pakistan",
"Palau", "Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines",
"Poland", "Portugal", "Qatar", "Romania", "Russia", "Rwanda", "Saint Kitts and Nevis",
"Saint Lucia", "Saint Vincent and the Grenadines", "Samoa", "San Marino",
"Sao Tome and Principe", "Saudi Arabia", "Senegal", "Serbia", "Seychelles",
"Sierra Leone", "Singapore", "Slovakia", "Slovenia", "Solomon Islands",
"Somalia", "South Africa", "South Korea", "South Sudan", "Spain", "Sri Lanka",
"Sudan", "Suriname", "Sweden", "Switzerland", "Syria", "Taiwan", "Tajikistan",
"Tanzania", "Thailand", "Timor-Leste", "Togo", "Tonga", "Trinidad and Tobago",
"Tunisia", "Turkey", "Turkmenistan", "Tuvalu", "Uganda", "Ukraine",
"United Arab Emirates", "United Kingdom", "United States", "Uruguay", "Uzbekistan",
"Vanuatu", "Vatican City", "Venezuela", "Vietnam", "Yemen", "Zambia", "Zimbabwe"
];
Future<void> getBook() async {
if (_selectedCountry != null) {
setState(() {
_isLoading = true;
});
try {
final response = await api.OpenAIService.getGuideBook(_selectedCountry!);
print(response);
final jsonData = jsonDecode(response);
setState(() {
_touristbook = jsonData[_selectedCountry!];
});
} catch (e) {
print('Error fetching guidebook: $e');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to load guidebook')),
);
} finally {
setState(() {
_isLoading = false;
});
}
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Choose a country')),
);
}
}
Widget _buildTouristBookSection(String title, String content) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(title, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
SizedBox(height: 5),
Text(content, style: TextStyle(fontSize: 16)),
],
),
);
}
@override
Widget build(BuildContext context) {
return SideMenu(
selectedIndex: 5,
body: Scaffold(
appBar: AppBar(
title: Text('Select Country'),
),
body: _isLoading
? Center(child: CircularProgressIndicator())
: SingleChildScrollView( // Added SingleChildScrollView here
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
DropdownSearch<String>(
items: countries,
popupProps: const PopupProps.menu(
showSearchBox: true,
searchFieldProps: TextFieldProps(
decoration: InputDecoration(
labelText: "Search a country",
border: OutlineInputBorder(),
),
),
),
dropdownDecoratorProps: const DropDownDecoratorProps(
dropdownSearchDecoration: InputDecoration(
labelText: "Select a country",
hintText: "Search a country",
border: OutlineInputBorder(),
),
),
onChanged: (String? country) {
setState(() {
_selectedCountry = country;
});
},
selectedItem: "Select a country you want a touristbook for",
),
SizedBox(height: 20),
ElevatedButton(
onPressed: getBook,
child: Text('Get Guidebook'),
),
if (_touristbook.isNotEmpty) ...[
_buildTouristBookSection(
_selectedCountry!,""
),
_buildTouristBookSection(
"Introduction - People",
_touristbook['introduction']?['people'] ?? 'No data',
),
_buildTouristBookSection(
"Introduction - Environment",
_touristbook['introduction']?['environment'] ?? 'No data',
),
_buildTouristBookSection(
"Famous Sites",
_touristbook['tourist_attractions']?['famous_sites']?.map((site) => site['name'] + ':\n' + site['description']).join('\n') ?? 'No data',
),
_buildTouristBookSection(
"Basic Language - Greetings",
_touristbook['basic_words']?.entries.map((e) => '${e.key}: ${e.value}').join('\n') ?? 'No data',
),
_buildTouristBookSection(
"Transportation Overview",
_touristbook['transportation']?['overview'] ?? 'No data',
),
_buildTouristBookSection(
"Public Transport - Buses",
_touristbook['transportation']?['public_transport']?['buses'] ?? 'No data',
),
_buildTouristBookSection(
"Public Transport - Trains",
_touristbook['transportation']?['public_transport']?['trains'] ?? 'No data',
),
_buildTouristBookSection(
"Public Transport - Taxis",
_touristbook['transportation']?['public_transport']?['taxis'] ?? 'No data',
),
],
],
),
),
),
);
}
}

View File

@ -73,6 +73,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
dropdown_search:
dependency: "direct main"
description:
name: dropdown_search
sha256: "55106e8290acaa97ed15bea1fdad82c3cf0c248dd410e651f5a8ac6870f783ab"
url: "https://pub.dev"
source: hosted
version: "5.0.6"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@ -142,6 +150,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_dotenv:
dependency: "direct main"
description:
name: flutter_dotenv
sha256: "9357883bdd153ab78cbf9ffa07656e336b8bbb2b5a3ca596b0b27e119f7c7d77"
url: "https://pub.dev"
source: hosted
version: "5.1.0"
flutter_image_compress: flutter_image_compress:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -43,6 +43,8 @@ dependencies:
geolocator: ^13.0.1 geolocator: ^13.0.1
image_picker: ^1.1.2 image_picker: ^1.1.2
flutter_image_compress: ^2.3.0 flutter_image_compress: ^2.3.0
flutter_dotenv: ^5.1.0
dropdown_search: ^5.0.6
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@ -62,6 +64,7 @@ dev_dependencies:
flutter: flutter:
assets: assets:
- assets/ - assets/
- .env
uses-material-design: true uses-material-design: true
# To add assets to your application, add an assets section, like this: # To add assets to your application, add an assets section, like this:

View File

@ -1,7 +0,0 @@
JWT_SECRET=DenHerMåAldrigVæreOffentligKunIDetteDemoProjekt
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_ENDPOINT_URL=
AWS_REGION=
R2_BUCKET_NAME=
R2_BUCKET_URL=