Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ out/
### Firebase Service Account ###
timetamer-smarcalendar-firebase-adminsdk-fbsvc-8be370036a.json

### Secrets ###
/src/main/resources/application-test-real.properties
### Secrets ###
.env

### Internal usage ###
Expand Down
1 change: 0 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ COPY src ./src

RUN --mount=type=cache,target=/root/.gradle \
./gradlew bootJar --no-daemon

FROM eclipse-temurin:21-jdk-alpine

RUN apk add --no-cache ffmpeg curl
Expand Down
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,15 @@ Spring Boot backend for the "TimeTamer" SmartCalendar application. Provides REST

---

## Docker

### Run with Docker Compose
```bash
docker compose up -d --build
```
or use sh/ps1 scripts
---

## Configuration

### Essential Environment Variables
Expand Down Expand Up @@ -151,9 +160,14 @@ Run tests with:
```bash
./gradlew test
```
- Uses separate in-memory H2 database
- External services (OpenAI) are mocked
- Test coverage reports: `build/reports/tests`
### Notes
- Uses separate in-memory H2 database via test profiles
- Real OpenAI tests are tagged `openAI-api` and excluded from default `test`
- Run real-API tests explicitly:
```bash
./gradlew testOpenAI
```
- Real-API tests use `application-test-real.properties` and require valid `CHATGPT_API_KEY`

### Postman Collection

Expand All @@ -176,3 +190,4 @@ MIT License - see [LICENSE](LICENSE.txt) file
## Contributors
- [Dmitry Rusanov](https://github.com/DimaRus05)
- [Mikhail Minaev](https://github.com/minmise)
- [Usatov Pavel](https://github.com/UsatovPavel)
39 changes: 39 additions & 0 deletions src/main/java/com/smartcalendar/controller/ChatGPTController.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package com.smartcalendar.controller;

import com.smartcalendar.model.Event;
import com.smartcalendar.service.AudioProcessingService;
import com.smartcalendar.service.ChatGPTService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

Expand All @@ -14,6 +18,7 @@
public class ChatGPTController {

private final ChatGPTService chatGPTService;
private final AudioProcessingService audioProcessingService;

@PostMapping("/ask")
public ResponseEntity<String> askChatGPT(@RequestBody Map<String, String> requestBody) {
Expand Down Expand Up @@ -56,4 +61,38 @@ public ResponseEntity<?> generateEntities(@RequestBody Map<String, String> reque
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
@PostMapping("{userId}/generate/suggestions")
public ResponseEntity<Map<String, ?>> generateSuggestions(@PathVariable Long userId, @RequestBody Map<String, String> body) {
try {
String query = body.get("query");

List<Event> result = chatGPTService.generateSuggestions(userId, query);
return ResponseEntity.ok(Map.of("events", result));

} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}

@PostMapping("{userId}/generate/suggestions/audio")
public ResponseEntity<Map<String, ?>> generateSuggestionsFromAudio(
@PathVariable Long userId,
@RequestParam("file") MultipartFile audioFile) {
String query;
try {
query = audioProcessingService.transcribeAudio(audioFile);
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of("error", "Failed to transcribe: "+e.getMessage()));
}
try{
List<Event> result = chatGPTService.generateSuggestions(userId, query);

return ResponseEntity.ok(Map.of(
"query", query,
"events", result
));
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
}
36 changes: 24 additions & 12 deletions src/main/java/com/smartcalendar/model/Event.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@
@NoArgsConstructor
@AllArgsConstructor
public class Event {
static public class Interval {
public final LocalDateTime start;
public final LocalDateTime end;

public Interval(LocalDateTime start, LocalDateTime end) {
this.start = start;
this.end = end;
}
}
@Id
//@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
Expand All @@ -39,7 +48,7 @@ public class Event {
private LocalDateTime end;

@Column(name = "event_location")
private String location;
private String location = "";

@Enumerated(EnumType.STRING)
private EventType type;
Expand Down Expand Up @@ -80,16 +89,19 @@ public class Event {
)
@JsonIgnore
private List<User> participants = new ArrayList<>();

public LocalDateTime getEnd() {
return end;
}

public LocalDateTime getStart() {
return start;
}

public List<Tag> getTags() {
return tags;
public Event(Event other) {
this.id = other.id != null ? other.id : UUID.randomUUID();
this.title = other.title;
this.description = other.description;
this.start = other.start;
this.end = other.end;
this.location = other.location != null ? other.location : "";
this.type = other.type != null ? other.type : EventType.COMMON;
this.organizer = other.organizer;
this.creationTime = other.creationTime != null ? other.creationTime : LocalDateTime.now();
this.completed = other.completed;
this.isShared = other.isShared;
this.invitees = other.invitees != null ? new ArrayList<>(other.invitees) : new ArrayList<>();
this.participants = other.participants != null ? new ArrayList<>(other.participants) : new ArrayList<>();
}
}
57 changes: 57 additions & 0 deletions src/main/java/com/smartcalendar/model/FormatterUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.smartcalendar.model;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import static java.util.Map.entry;

@Component
public class FormatterUtils {
private final ObjectMapper objectMapper;
private static final Logger logger = LoggerFactory.getLogger(FormatterUtils.class);

public FormatterUtils(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}

public String convertEventsToJson(List<Event> events) {
try {
List<Map<String, String>> simplifiedEvents = events.stream()
.map(e -> Map.ofEntries(
entry("title", Objects.toString(e.getTitle(), "")),
entry("description", Objects.toString(e.getDescription(), "")),
entry("start", e.getStart() != null ? e.getStart().toString() : ""),
entry("end", e.getEnd() != null ? e.getEnd().toString() : ""),
entry("location", Objects.toString(e.getLocation(), "")),
entry("type", e.getType() != null ? e.getType().toString() : "COMMON")
))
.toList();
return objectMapper.writeValueAsString(simplifiedEvents);
} catch (Exception ex) {
logger.error("Failed to convert events to JSON", ex);
return "[]";
}
}
public String convertDayIntervalsToJson(LocalDate date, List<Event.Interval> intervals){
try {
List<Map<String, String>> simplifiedIntervals = intervals.stream()
.map(e -> Map.ofEntries(
entry("date", date.toString()),
entry("start", Objects.toString(e.start, "")),
entry("end", Objects.toString(e.end, ""))
))
.toList();
return objectMapper.writeValueAsString(simplifiedIntervals);
} catch (Exception ex) {
logger.error("Failed to convert events to JSON", ex);
return "[]";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,20 @@
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;

@Repository
public interface EventRepository extends JpaRepository<Event, UUID> {
List<Event> findByOrganizerId(Long organizerId);
@Query("SELECT e FROM Event e " +
"LEFT JOIN e.participants p " +
"WHERE (e.organizer.id = :userId OR p.id = :userId) " +
"AND e.start BETWEEN :startOfDay AND :endOfDay")
List<Event> findByUserIdAndDate(Long userId,
LocalDateTime startOfDay,
LocalDateTime endOfDay);
@Query("SELECT e FROM Event e WHERE LOWER(e.location) = LOWER(:location) ORDER BY e.end ASC")
List<Event> findByLocationIgnoreCase(String location);
@Query("SELECT e FROM Event e WHERE LOWER(e.location) = LOWER(:location) AND :userId IS NOT NULL " +
Expand Down
Loading