diff --git a/.gitignore b/.gitignore
index 546f686..d797cd2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@ API/bin
API/obj
.idea
+/rust-backend/.env.example
diff --git a/API/API.csproj b/API/API.csproj
index 99e95d1..1cb680c 100644
--- a/API/API.csproj
+++ b/API/API.csproj
@@ -10,6 +10,10 @@
.
+
+ <_ContentIncludedByDefault Remove="appsettings.json" />
+
+
diff --git a/Mobile/.env.example b/Mobile/.env.example
new file mode 100644
index 0000000..9847a1d
--- /dev/null
+++ b/Mobile/.env.example
@@ -0,0 +1 @@
+OPENAI_API_KEY=
\ No newline at end of file
diff --git a/Mobile/.gitignore b/Mobile/.gitignore
index d90f52b..6b4559a 100644
--- a/Mobile/.gitignore
+++ b/Mobile/.gitignore
@@ -43,4 +43,5 @@ app.*.map.json
/android/app/release
environment.json
-node_modules
\ No newline at end of file
+node_modules
+.env
\ No newline at end of file
diff --git a/Mobile/lib/base/sidemenu.dart b/Mobile/lib/base/sidemenu.dart
index 0208d7d..c24c80e 100644
--- a/Mobile/lib/base/sidemenu.dart
+++ b/Mobile/lib/base/sidemenu.dart
@@ -1,5 +1,5 @@
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:google_fonts/google_fonts.dart';
import 'variables.dart';
@@ -96,7 +96,7 @@ class _SideMenuState extends State {
Navigator.pushReplacementNamed(context, '/favorites');
},
),
- ListTile(
+ ListTile(
title: const Text('Profile'),
leading: const Icon(Icons.person),
selected: _selectedIndex == 2,
@@ -104,6 +104,14 @@ class _SideMenuState extends State {
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(
color: Colors.grey,
thickness: 2,
@@ -118,6 +126,19 @@ class _SideMenuState extends State {
),
]
: [
+ 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(
title: const Text('Register'),
leading: const Icon(Icons.add_box_outlined),
diff --git a/Mobile/lib/createreview.dart b/Mobile/lib/createreview.dart
index 4b7060b..86d9d7f 100644
--- a/Mobile/lib/createreview.dart
+++ b/Mobile/lib/createreview.dart
@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:mobile/base/sidemenu.dart';
import 'models.dart' as models;
-import 'api.dart' as api;
+import 'services/api.dart' as api;
import 'package:path/path.dart' as path;
class CreateReviewPage extends StatefulWidget {
diff --git a/Mobile/lib/editprofile.dart b/Mobile/lib/editprofile.dart
index bc230b7..ff4b456 100644
--- a/Mobile/lib/editprofile.dart
+++ b/Mobile/lib/editprofile.dart
@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:mobile/models.dart' as models;
import 'package:shared_preferences/shared_preferences.dart';
-import 'api.dart' as api;
+import 'services/api.dart' as api;
import 'base/variables.dart';
class EditProfilePage extends StatefulWidget {
diff --git a/Mobile/lib/favorites.dart b/Mobile/lib/favorites.dart
index 8ac10bf..667abb5 100644
--- a/Mobile/lib/favorites.dart
+++ b/Mobile/lib/favorites.dart
@@ -1,5 +1,5 @@
import 'dart:convert';
-import 'api.dart' as api;
+import 'services/api.dart' as api;
import 'package:flutter/material.dart';
import 'base/sidemenu.dart'; // Import the base layout widget
import 'models.dart';
diff --git a/Mobile/lib/login.dart b/Mobile/lib/login.dart
index 6c21619..084e553 100644
--- a/Mobile/lib/login.dart
+++ b/Mobile/lib/login.dart
@@ -4,7 +4,7 @@ import 'package:mobile/models.dart' as models;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:google_fonts/google_fonts.dart';
import 'dart:convert';
-import 'api.dart' as api;
+import 'services/api.dart' as api;
import 'base/variables.dart';
class LoginPage extends StatefulWidget {
diff --git a/Mobile/lib/main.dart b/Mobile/lib/main.dart
index f035a73..1732e43 100644
--- a/Mobile/lib/main.dart
+++ b/Mobile/lib/main.dart
@@ -8,18 +8,22 @@ import 'package:mobile/createreview.dart';
import 'package:mobile/favorites.dart';
import 'package:mobile/register.dart';
import 'package:mobile/reviewlist.dart';
+import 'package:mobile/touristguidebook.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'login.dart';
import 'base/sidemenu.dart';
import 'profile.dart';
import 'models.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;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
+ await dotenv.load(fileName: ".env");
+
// Refresh JWT on startup
final prefs = await SharedPreferences.getInstance();
if (prefs.getString("token") != null && prefs.getString("refresh-token") != null) {
@@ -50,6 +54,7 @@ class MyApp extends StatelessWidget {
'/register': (context) => const RegisterPage(),
'/reviews': (context) => const ReviewListPage(),
'/create-review': (context) => const CreateReviewPage(),
+ '/tourist-guide-book': (context) => const TouristGuideBookPage(),
},
);
}
@@ -293,7 +298,7 @@ class _MyHomePageState extends State {
LatLngBounds bounds = _mapController.camera.visibleBounds;
_getOpenStreetMapData(LatLng(bounds.southWest.latitude, bounds.southWest.longitude),LatLng(bounds.northEast.latitude, bounds.northEast.longitude));
- }
+ }
Future _getOpenStreetMapData(LatLng southWest, LatLng northEast) async {
diff --git a/Mobile/lib/profile.dart b/Mobile/lib/profile.dart
index 6fa34fc..121176c 100644
--- a/Mobile/lib/profile.dart
+++ b/Mobile/lib/profile.dart
@@ -4,7 +4,7 @@ import 'package:mobile/base/variables.dart';
import 'package:mobile/models.dart' as models;
import 'package:shared_preferences/shared_preferences.dart';
import 'base/sidemenu.dart';
-import 'api.dart' as api;
+import 'services/api.dart' as api;
import 'editprofile.dart';
class ProfilePage extends StatefulWidget {
diff --git a/Mobile/lib/register.dart b/Mobile/lib/register.dart
index b5d9503..0e5df1b 100644
--- a/Mobile/lib/register.dart
+++ b/Mobile/lib/register.dart
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:mobile/base/sidemenu.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:mobile/base/variables.dart';
-import 'api.dart' as api;
+import 'services/api.dart' as api;
class RegisterPage extends StatefulWidget {
const RegisterPage({super.key});
diff --git a/Mobile/lib/reviewlist.dart b/Mobile/lib/reviewlist.dart
index fdc69a0..ec8a010 100644
--- a/Mobile/lib/reviewlist.dart
+++ b/Mobile/lib/reviewlist.dart
@@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
import 'package:mobile/base/sidemenu.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'models.dart' as models;
-import 'api.dart' as api;
+import 'services/api.dart' as api;
class ReviewListPage extends StatefulWidget {
const ReviewListPage({super.key});
diff --git a/Mobile/lib/api.dart b/Mobile/lib/services/api.dart
similarity index 98%
rename from Mobile/lib/api.dart
rename to Mobile/lib/services/api.dart
index 3ee3e61..63b0bfa 100644
--- a/Mobile/lib/api.dart
+++ b/Mobile/lib/services/api.dart
@@ -199,5 +199,3 @@ void logout() async {
prefs.remove('refresh-token');
prefs.remove('id');
}
-
-//------------------------------OPENAI API------------------------------//
diff --git a/Mobile/lib/services/openaiservice.dart b/Mobile/lib/services/openaiservice.dart
new file mode 100644
index 0000000..a9ed368
--- /dev/null
+++ b/Mobile/lib/services/openaiservice.dart
@@ -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 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();
+ }
+}
\ No newline at end of file
diff --git a/Mobile/lib/touristguidebook.dart b/Mobile/lib/touristguidebook.dart
new file mode 100644
index 0000000..75d9346
--- /dev/null
+++ b/Mobile/lib/touristguidebook.dart
@@ -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 createState() => _TouristGuideBookPageState();
+}
+
+class _TouristGuideBookPageState extends State {
+ String? _selectedCountry;
+ bool _isLoading = false;
+ Map _touristbook = {};
+ final List 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 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(
+ 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',
+ ),
+ ],
+ ],
+ ),
+ ),
+ ),
+ );
+}
+}
diff --git a/Mobile/pubspec.lock b/Mobile/pubspec.lock
index c537257..ce7561a 100644
--- a/Mobile/pubspec.lock
+++ b/Mobile/pubspec.lock
@@ -73,6 +73,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@@ -142,6 +150,14 @@ packages:
description: flutter
source: sdk
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:
dependency: "direct main"
description:
diff --git a/Mobile/pubspec.yaml b/Mobile/pubspec.yaml
index 3f66b54..6d22f5d 100644
--- a/Mobile/pubspec.yaml
+++ b/Mobile/pubspec.yaml
@@ -43,6 +43,8 @@ dependencies:
geolocator: ^13.0.1
image_picker: ^1.1.2
flutter_image_compress: ^2.3.0
+ flutter_dotenv: ^5.1.0
+ dropdown_search: ^5.0.6
dev_dependencies:
flutter_test:
@@ -62,6 +64,7 @@ dev_dependencies:
flutter:
assets:
- assets/
+ - .env
uses-material-design: true
# To add assets to your application, add an assets section, like this:
diff --git a/rust-backend/.env.example b/rust-backend/.env.example
deleted file mode 100644
index f574e23..0000000
--- a/rust-backend/.env.example
+++ /dev/null
@@ -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=