first implementation
This commit is contained in:
58
README.md
Normal file
58
README.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# AI Teacher — Neurosurgeon RAG Learning Platform
|
||||||
|
|
||||||
|
A web application for neurosurgeons to upload medical textbooks (PDF), have them
|
||||||
|
embedded into a pgvector store, then select from a predefined topic list to receive
|
||||||
|
AI-generated cross-book summaries, and engage in grounded RAG chat.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
User["Neurosurgeon (Browser)"]
|
||||||
|
FE["Frontend\nVue.js 3 / Vite\n:5173"]
|
||||||
|
BE["Backend\nSpring Boot 4 / Spring AI\n:8080"]
|
||||||
|
DB["PostgreSQL + pgvector\n(provided)"]
|
||||||
|
LLM["LLM Provider\n(OpenAI / configurable)"]
|
||||||
|
|
||||||
|
User -->|HTTP| FE
|
||||||
|
FE -->|REST /api/v1/...| BE
|
||||||
|
BE -->|JDBC / pgvector| DB
|
||||||
|
BE -->|Embedding + Chat API| LLM
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- **Backend**: Spring Boot 4.0.5 + Spring AI 2.0.0-M4, Java 21, Maven
|
||||||
|
- **Frontend**: Vue.js 3 + Vite + TypeScript + Pinia + Axios
|
||||||
|
- **Database**: PostgreSQL 16 + pgvector extension
|
||||||
|
- **Auth**: HTTP Basic (single shared in-memory user)
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
See [specs/001-neuro-rag-learning/quickstart.md](specs/001-neuro-rag-learning/quickstart.md) for full instructions.
|
||||||
|
|
||||||
|
### Local Dev
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start the database
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Backend
|
||||||
|
cd backend
|
||||||
|
mvn spring-boot:run
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
| Variable | Required | Description |
|
||||||
|
|----------|----------|-------------|
|
||||||
|
| `OPENAI_API_KEY` | Yes | OpenAI API key for embeddings and chat |
|
||||||
|
| `APP_PASSWORD` | Yes | Shared password for HTTP Basic auth |
|
||||||
|
| `DB_URL` | Yes | JDBC URL, e.g. `jdbc:postgresql://localhost:5432/aiteacher` |
|
||||||
|
| `DB_USERNAME` | Yes | Database username |
|
||||||
|
| `DB_PASSWORD` | Yes | Database password |
|
||||||
@@ -4,5 +4,7 @@
|
|||||||
"path": "."
|
"path": "."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"settings": {}
|
"settings": {
|
||||||
|
"java.configuration.updateBuildConfiguration": "interactive"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
13
backend/Dockerfile
Normal file
13
backend/Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# ---- Build stage ----
|
||||||
|
FROM eclipse-temurin:21-jdk-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY pom.xml .
|
||||||
|
COPY src ./src
|
||||||
|
RUN apk add --no-cache maven && mvn -q -DskipTests package
|
||||||
|
|
||||||
|
# ---- Runtime stage ----
|
||||||
|
FROM eclipse-temurin:21-jre-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build /app/target/*.jar app.jar
|
||||||
|
EXPOSE 8080
|
||||||
|
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||||
134
backend/pom.xml
Normal file
134
backend/pom.xml
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||||
|
https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
|
<version>4.0.5</version>
|
||||||
|
<relativePath/>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<groupId>com.aiteacher</groupId>
|
||||||
|
<artifactId>ai-teacher-backend</artifactId>
|
||||||
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
|
<name>ai-teacher-backend</name>
|
||||||
|
<description>Neurosurgeon RAG Learning Platform — Backend</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<java.version>25</java.version>
|
||||||
|
<spring-ai.version>2.0.0-M4</spring-ai.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.ai</groupId>
|
||||||
|
<artifactId>spring-ai-bom</artifactId>
|
||||||
|
<version>${spring-ai.version}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- Spring Boot Web -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Spring Security (HTTP Basic) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Spring Data JPA -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Flyway -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.flywaydb</groupId>
|
||||||
|
<artifactId>flyway-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.flywaydb</groupId>
|
||||||
|
<artifactId>flyway-database-postgresql</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- PostgreSQL JDBC driver -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.postgresql</groupId>
|
||||||
|
<artifactId>postgresql</artifactId>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Spring AI — pgvector vector store -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.ai</groupId>
|
||||||
|
<artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Spring AI — OpenAI starter (embedding + chat model autoconfigure) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.ai</groupId>
|
||||||
|
<artifactId>spring-ai-starter-model-openai</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Spring AI — ChatClient fluent API -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.ai</groupId>
|
||||||
|
<artifactId>spring-ai-client-chat</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Spring AI — QuestionAnswerAdvisor (RAG) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.ai</groupId>
|
||||||
|
<artifactId>spring-ai-advisors-vector-store</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Spring AI — PDF document reader -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.ai</groupId>
|
||||||
|
<artifactId>spring-ai-pdf-document-reader</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Jackson (JSON) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Testing -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.aiteacher;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
@EnableAsync
|
||||||
|
public class AiTeacherApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(AiTeacherApplication.class, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
122
backend/src/main/java/com/aiteacher/book/Book.java
Normal file
122
backend/src/main/java/com/aiteacher/book/Book.java
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
package com.aiteacher.book;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "book")
|
||||||
|
public class Book {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.UUID)
|
||||||
|
private UUID id;
|
||||||
|
|
||||||
|
@Column(name = "title", nullable = false, length = 500)
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@Column(name = "file_name", nullable = false, length = 500)
|
||||||
|
private String fileName;
|
||||||
|
|
||||||
|
@Column(name = "file_size_bytes", nullable = false)
|
||||||
|
private long fileSizeBytes;
|
||||||
|
|
||||||
|
@Column(name = "page_count")
|
||||||
|
private Integer pageCount;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "status", nullable = false, length = 20)
|
||||||
|
private BookStatus status;
|
||||||
|
|
||||||
|
@Column(name = "error_message", columnDefinition = "TEXT")
|
||||||
|
private String errorMessage;
|
||||||
|
|
||||||
|
@Column(name = "uploaded_at", nullable = false)
|
||||||
|
private Instant uploadedAt;
|
||||||
|
|
||||||
|
@Column(name = "processed_at")
|
||||||
|
private Instant processedAt;
|
||||||
|
|
||||||
|
// Constructors
|
||||||
|
|
||||||
|
public Book() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public Book(String title, String fileName, long fileSizeBytes) {
|
||||||
|
this.title = title;
|
||||||
|
this.fileName = fileName;
|
||||||
|
this.fileSizeBytes = fileSizeBytes;
|
||||||
|
this.status = BookStatus.PENDING;
|
||||||
|
this.uploadedAt = Instant.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters & Setters
|
||||||
|
|
||||||
|
public UUID getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitle(String title) {
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFileName() {
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFileName(String fileName) {
|
||||||
|
this.fileName = fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getFileSizeBytes() {
|
||||||
|
return fileSizeBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFileSizeBytes(long fileSizeBytes) {
|
||||||
|
this.fileSizeBytes = fileSizeBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getPageCount() {
|
||||||
|
return pageCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPageCount(Integer pageCount) {
|
||||||
|
this.pageCount = pageCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BookStatus getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(BookStatus status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getErrorMessage() {
|
||||||
|
return errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setErrorMessage(String errorMessage) {
|
||||||
|
this.errorMessage = errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Instant getUploadedAt() {
|
||||||
|
return uploadedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUploadedAt(Instant uploadedAt) {
|
||||||
|
this.uploadedAt = uploadedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Instant getProcessedAt() {
|
||||||
|
return processedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProcessedAt(Instant processedAt) {
|
||||||
|
this.processedAt = processedAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
75
backend/src/main/java/com/aiteacher/book/BookController.java
Normal file
75
backend/src/main/java/com/aiteacher/book/BookController.java
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package com.aiteacher.book;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/books")
|
||||||
|
public class BookController {
|
||||||
|
|
||||||
|
private final BookService bookService;
|
||||||
|
|
||||||
|
public BookController(BookService bookService) {
|
||||||
|
this.bookService = bookService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(consumes = "multipart/form-data")
|
||||||
|
public ResponseEntity<?> upload(@RequestParam("file") MultipartFile file) throws IOException {
|
||||||
|
Book book = bookService.upload(file);
|
||||||
|
return ResponseEntity.status(HttpStatus.ACCEPTED).body(toSummaryResponse(book));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<List<Map<String, Object>>> list() {
|
||||||
|
List<Map<String, Object>> books = bookService.listAll().stream()
|
||||||
|
.map(this::toFullResponse)
|
||||||
|
.toList();
|
||||||
|
return ResponseEntity.ok(books);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ResponseEntity<Map<String, Object>> get(@PathVariable UUID id) {
|
||||||
|
Book book = bookService.getById(id);
|
||||||
|
return ResponseEntity.ok(toFullResponse(book));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ResponseEntity<Void> delete(@PathVariable UUID id) {
|
||||||
|
bookService.delete(id);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> toSummaryResponse(Book book) {
|
||||||
|
return Map.of(
|
||||||
|
"id", book.getId(),
|
||||||
|
"title", book.getTitle(),
|
||||||
|
"fileName", book.getFileName(),
|
||||||
|
"status", book.getStatus().name(),
|
||||||
|
"uploadedAt", book.getUploadedAt()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> toFullResponse(Book book) {
|
||||||
|
var map = new java.util.LinkedHashMap<String, Object>();
|
||||||
|
map.put("id", book.getId());
|
||||||
|
map.put("title", book.getTitle());
|
||||||
|
map.put("fileName", book.getFileName());
|
||||||
|
map.put("fileSizeBytes", book.getFileSizeBytes());
|
||||||
|
map.put("pageCount", book.getPageCount());
|
||||||
|
map.put("status", book.getStatus().name());
|
||||||
|
map.put("uploadedAt", book.getUploadedAt());
|
||||||
|
map.put("processedAt", book.getProcessedAt());
|
||||||
|
if (book.getErrorMessage() != null) {
|
||||||
|
map.put("errorMessage", book.getErrorMessage());
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
package com.aiteacher.book;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.ai.document.Document;
|
||||||
|
import org.springframework.ai.reader.pdf.PagePdfDocumentReader;
|
||||||
|
import org.springframework.ai.reader.pdf.config.PdfDocumentReaderConfig;
|
||||||
|
import org.springframework.ai.vectorstore.VectorStore;
|
||||||
|
import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder;
|
||||||
|
import org.springframework.core.io.FileSystemResource;
|
||||||
|
import org.springframework.scheduling.annotation.Async;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class BookEmbeddingService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(BookEmbeddingService.class);
|
||||||
|
|
||||||
|
// Pattern to detect diagram/figure captions
|
||||||
|
private static final Pattern CAPTION_PATTERN =
|
||||||
|
Pattern.compile("^(Figure|Fig\\.|Table|Diagram)\\s+[\\d.]+", Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
private final VectorStore vectorStore;
|
||||||
|
private final BookRepository bookRepository;
|
||||||
|
|
||||||
|
public BookEmbeddingService(VectorStore vectorStore, BookRepository bookRepository) {
|
||||||
|
this.vectorStore = vectorStore;
|
||||||
|
this.bookRepository = bookRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Async
|
||||||
|
public void embedBook(UUID bookId, String bookTitle, Path pdfPath) {
|
||||||
|
log.info("Starting embedding for book {} ({})", bookId, bookTitle);
|
||||||
|
|
||||||
|
Book book = bookRepository.findById(bookId).orElse(null);
|
||||||
|
if (book == null) {
|
||||||
|
log.warn("Book {} not found, skipping embedding", bookId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
book.setStatus(BookStatus.PROCESSING);
|
||||||
|
bookRepository.save(book);
|
||||||
|
|
||||||
|
PagePdfDocumentReader reader = new PagePdfDocumentReader(
|
||||||
|
new FileSystemResource(pdfPath.toFile()),
|
||||||
|
PdfDocumentReaderConfig.builder()
|
||||||
|
.withPagesPerDocument(1)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Document> pages = reader.get();
|
||||||
|
int pageCount = pages.size();
|
||||||
|
|
||||||
|
// Enrich metadata and tag diagram captions
|
||||||
|
List<Document> enriched = pages.stream()
|
||||||
|
.map(doc -> enrichDocument(doc, bookId.toString(), bookTitle))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
vectorStore.add(enriched);
|
||||||
|
|
||||||
|
book.setStatus(BookStatus.READY);
|
||||||
|
book.setPageCount(pageCount);
|
||||||
|
book.setProcessedAt(java.time.Instant.now());
|
||||||
|
bookRepository.save(book);
|
||||||
|
|
||||||
|
log.info("Finished embedding book {} — {} pages", bookId, pageCount);
|
||||||
|
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error("Failed to embed book {}", bookId, ex);
|
||||||
|
book.setStatus(BookStatus.FAILED);
|
||||||
|
book.setErrorMessage(truncate(ex.getMessage(), 1000));
|
||||||
|
bookRepository.save(book);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Document enrichDocument(Document doc, String bookId, String bookTitle) {
|
||||||
|
String content = doc.getText();
|
||||||
|
String chunkType = detectChunkType(content);
|
||||||
|
|
||||||
|
doc.getMetadata().put("book_id", bookId);
|
||||||
|
doc.getMetadata().put("book_title", bookTitle);
|
||||||
|
doc.getMetadata().put("chunk_type", chunkType);
|
||||||
|
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String detectChunkType(String content) {
|
||||||
|
if (content != null) {
|
||||||
|
for (String line : content.split("\\r?\\n")) {
|
||||||
|
if (CAPTION_PATTERN.matcher(line.trim()).find()) {
|
||||||
|
return "diagram";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "text";
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteBookChunks(UUID bookId) {
|
||||||
|
log.info("Deleting vector chunks for book {}", bookId);
|
||||||
|
try {
|
||||||
|
FilterExpressionBuilder b = new FilterExpressionBuilder();
|
||||||
|
vectorStore.delete(b.eq("book_id", bookId.toString()).build());
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.warn("Could not delete vector chunks for book {}: {}", bookId, ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String truncate(String message, int maxLength) {
|
||||||
|
if (message == null) return null;
|
||||||
|
return message.length() <= maxLength ? message : message.substring(0, maxLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
15
backend/src/main/java/com/aiteacher/book/BookRepository.java
Normal file
15
backend/src/main/java/com/aiteacher/book/BookRepository.java
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package com.aiteacher.book;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface BookRepository extends JpaRepository<Book, UUID> {
|
||||||
|
|
||||||
|
List<Book> findByStatus(BookStatus status);
|
||||||
|
|
||||||
|
boolean existsByStatus(BookStatus status);
|
||||||
|
}
|
||||||
79
backend/src/main/java/com/aiteacher/book/BookService.java
Normal file
79
backend/src/main/java/com/aiteacher/book/BookService.java
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
package com.aiteacher.book;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class BookService {
|
||||||
|
|
||||||
|
private final BookRepository bookRepository;
|
||||||
|
private final BookEmbeddingService bookEmbeddingService;
|
||||||
|
|
||||||
|
public BookService(BookRepository bookRepository, BookEmbeddingService bookEmbeddingService) {
|
||||||
|
this.bookRepository = bookRepository;
|
||||||
|
this.bookEmbeddingService = bookEmbeddingService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Book upload(MultipartFile file) throws IOException {
|
||||||
|
String originalFilename = file.getOriginalFilename();
|
||||||
|
if (originalFilename == null || !originalFilename.toLowerCase().endsWith(".pdf")) {
|
||||||
|
throw new IllegalArgumentException("Only PDF files are accepted.");
|
||||||
|
}
|
||||||
|
|
||||||
|
String title = deriveTitle(originalFilename);
|
||||||
|
|
||||||
|
Book book = new Book(title, originalFilename, file.getSize());
|
||||||
|
book = bookRepository.save(book);
|
||||||
|
|
||||||
|
// Write to a temp file so the async task can read it
|
||||||
|
Path tempFile = Files.createTempFile("aiteacher-", "-" + book.getId() + ".pdf");
|
||||||
|
file.transferTo(tempFile.toFile());
|
||||||
|
|
||||||
|
UUID bookId = book.getId();
|
||||||
|
Path pdfPath = tempFile;
|
||||||
|
String bookTitle = title;
|
||||||
|
|
||||||
|
bookEmbeddingService.embedBook(bookId, bookTitle, pdfPath);
|
||||||
|
|
||||||
|
return book;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Book> listAll() {
|
||||||
|
return bookRepository.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Book getById(UUID id) {
|
||||||
|
return bookRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new NoSuchElementException("Book not found."));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(UUID id) {
|
||||||
|
Book book = bookRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new NoSuchElementException("Book not found."));
|
||||||
|
|
||||||
|
if (book.getStatus() == BookStatus.PROCESSING) {
|
||||||
|
throw new IllegalStateException("Cannot delete a book that is currently being processed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
bookEmbeddingService.deleteBookChunks(id);
|
||||||
|
bookRepository.deleteById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String deriveTitle(String filename) {
|
||||||
|
// Strip .pdf extension and replace separators with spaces
|
||||||
|
String name = filename.replaceAll("(?i)\\.pdf$", "");
|
||||||
|
name = name.replaceAll("[-_]", " ");
|
||||||
|
// Capitalise first letter
|
||||||
|
if (!name.isEmpty()) {
|
||||||
|
name = Character.toUpperCase(name.charAt(0)) + name.substring(1);
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
8
backend/src/main/java/com/aiteacher/book/BookStatus.java
Normal file
8
backend/src/main/java/com/aiteacher/book/BookStatus.java
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package com.aiteacher.book;
|
||||||
|
|
||||||
|
public enum BookStatus {
|
||||||
|
PENDING,
|
||||||
|
PROCESSING,
|
||||||
|
READY,
|
||||||
|
FAILED
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.aiteacher.book;
|
||||||
|
|
||||||
|
public class NoKnowledgeSourceException extends RuntimeException {
|
||||||
|
|
||||||
|
public NoKnowledgeSourceException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
75
backend/src/main/java/com/aiteacher/chat/ChatController.java
Normal file
75
backend/src/main/java/com/aiteacher/chat/ChatController.java
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package com.aiteacher.chat;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/chat")
|
||||||
|
public class ChatController {
|
||||||
|
|
||||||
|
private final ChatService chatService;
|
||||||
|
|
||||||
|
public ChatController(ChatService chatService) {
|
||||||
|
this.chatService = chatService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/sessions")
|
||||||
|
public ResponseEntity<Map<String, Object>> createSession(
|
||||||
|
@RequestBody(required = false) Map<String, String> body) {
|
||||||
|
String topicId = body != null ? body.get("topicId") : null;
|
||||||
|
ChatSession session = chatService.createSession(topicId);
|
||||||
|
|
||||||
|
Map<String, Object> response = new LinkedHashMap<>();
|
||||||
|
response.put("sessionId", session.getId());
|
||||||
|
response.put("topicId", session.getTopicId());
|
||||||
|
response.put("createdAt", session.getCreatedAt());
|
||||||
|
return ResponseEntity.status(HttpStatus.CREATED).body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/sessions/{sessionId}/messages")
|
||||||
|
public ResponseEntity<List<Map<String, Object>>> getMessages(@PathVariable UUID sessionId) {
|
||||||
|
List<Message> messages = chatService.getMessages(sessionId);
|
||||||
|
List<Map<String, Object>> response = messages.stream()
|
||||||
|
.map(this::toMessageResponse)
|
||||||
|
.toList();
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/sessions/{sessionId}/messages")
|
||||||
|
public ResponseEntity<Map<String, Object>> sendMessage(
|
||||||
|
@PathVariable UUID sessionId,
|
||||||
|
@RequestBody Map<String, String> body) {
|
||||||
|
String content = body.get("content");
|
||||||
|
if (content == null || content.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("Message content must not be empty.");
|
||||||
|
}
|
||||||
|
Message message = chatService.sendMessage(sessionId, content);
|
||||||
|
return ResponseEntity.ok(toMessageResponse(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/sessions/{sessionId}")
|
||||||
|
public ResponseEntity<Void> deleteSession(@PathVariable UUID sessionId) {
|
||||||
|
chatService.deleteSession(sessionId);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> toMessageResponse(Message message) {
|
||||||
|
Map<String, Object> map = new LinkedHashMap<>();
|
||||||
|
map.put("id", message.getId());
|
||||||
|
map.put("role", message.getRole().name());
|
||||||
|
map.put("content", message.getContent());
|
||||||
|
if (message.getSources() != null) {
|
||||||
|
map.put("sources", message.getSources());
|
||||||
|
} else {
|
||||||
|
map.put("sources", List.of());
|
||||||
|
}
|
||||||
|
map.put("createdAt", message.getCreatedAt());
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
}
|
||||||
166
backend/src/main/java/com/aiteacher/chat/ChatService.java
Normal file
166
backend/src/main/java/com/aiteacher/chat/ChatService.java
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
package com.aiteacher.chat;
|
||||||
|
|
||||||
|
import com.aiteacher.book.BookRepository;
|
||||||
|
import com.aiteacher.book.BookStatus;
|
||||||
|
import com.aiteacher.book.NoKnowledgeSourceException;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.ai.chat.client.ChatClient;
|
||||||
|
import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor;
|
||||||
|
import org.springframework.ai.chat.model.ChatResponse;
|
||||||
|
import org.springframework.ai.document.Document;
|
||||||
|
import org.springframework.ai.vectorstore.SearchRequest;
|
||||||
|
import org.springframework.ai.vectorstore.VectorStore;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class ChatService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(ChatService.class);
|
||||||
|
|
||||||
|
private static final String SYSTEM_PROMPT = """
|
||||||
|
You are an expert neurosurgery educator assistant. Your role is to answer
|
||||||
|
questions based ONLY on the content from uploaded medical textbooks that has been
|
||||||
|
retrieved for you as context.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Answer only from the provided context chunks
|
||||||
|
- If the context does not contain enough information, explicitly state:
|
||||||
|
"I could not find relevant information about this topic in the uploaded books."
|
||||||
|
- Cite sources when possible (book title and page number from the context metadata)
|
||||||
|
- Maintain continuity with the conversation history
|
||||||
|
- Never fabricate clinical information
|
||||||
|
""";
|
||||||
|
|
||||||
|
private final ChatClient chatClient;
|
||||||
|
private final VectorStore vectorStore;
|
||||||
|
private final BookRepository bookRepository;
|
||||||
|
private final ChatSessionRepository sessionRepository;
|
||||||
|
private final MessageRepository messageRepository;
|
||||||
|
|
||||||
|
public ChatService(ChatClient chatClient, VectorStore vectorStore,
|
||||||
|
BookRepository bookRepository,
|
||||||
|
ChatSessionRepository sessionRepository,
|
||||||
|
MessageRepository messageRepository) {
|
||||||
|
this.chatClient = chatClient;
|
||||||
|
this.vectorStore = vectorStore;
|
||||||
|
this.bookRepository = bookRepository;
|
||||||
|
this.sessionRepository = sessionRepository;
|
||||||
|
this.messageRepository = messageRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChatSession createSession(String topicId) {
|
||||||
|
ChatSession session = new ChatSession(topicId);
|
||||||
|
return sessionRepository.save(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Message> getMessages(UUID sessionId) {
|
||||||
|
if (!sessionRepository.existsById(sessionId)) {
|
||||||
|
throw new NoSuchElementException("Session not found.");
|
||||||
|
}
|
||||||
|
return messageRepository.findBySessionIdOrderByCreatedAtAsc(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Message sendMessage(UUID sessionId, String userContent) {
|
||||||
|
ChatSession session = sessionRepository.findById(sessionId)
|
||||||
|
.orElseThrow(() -> new NoSuchElementException("Session not found."));
|
||||||
|
|
||||||
|
if (!bookRepository.existsByStatus(BookStatus.READY)) {
|
||||||
|
throw new NoKnowledgeSourceException("No books are available as knowledge sources.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist user message
|
||||||
|
Message userMessage = new Message(sessionId, MessageRole.USER, userContent);
|
||||||
|
messageRepository.save(userMessage);
|
||||||
|
|
||||||
|
// Build conversation history for context
|
||||||
|
List<Message> history = messageRepository.findBySessionIdOrderByCreatedAtAsc(sessionId);
|
||||||
|
|
||||||
|
// Build the prompt with full conversation history as context
|
||||||
|
String fullQuestion = buildQuestionWithHistory(history, userContent, session.getTopicId());
|
||||||
|
|
||||||
|
var qaAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
|
||||||
|
.searchRequest(SearchRequest.builder().similarityThreshold(0.8d).topK(6).build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
ChatResponse response = chatClient.prompt()
|
||||||
|
.advisors(qaAdvisor)
|
||||||
|
.system(SYSTEM_PROMPT)
|
||||||
|
.user(fullQuestion)
|
||||||
|
.call()
|
||||||
|
.chatResponse();
|
||||||
|
|
||||||
|
String assistantContent = response.getResult().getOutput().getText();
|
||||||
|
List<Map<String, Object>> sources = extractSources(response);
|
||||||
|
|
||||||
|
// Persist assistant message
|
||||||
|
Message assistantMessage = new Message(sessionId, MessageRole.ASSISTANT, assistantContent);
|
||||||
|
assistantMessage.setSources(sources);
|
||||||
|
return messageRepository.save(assistantMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteSession(UUID sessionId) {
|
||||||
|
if (!sessionRepository.existsById(sessionId)) {
|
||||||
|
throw new NoSuchElementException("Session not found.");
|
||||||
|
}
|
||||||
|
sessionRepository.deleteById(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildQuestionWithHistory(List<Message> history, String currentQuestion,
|
||||||
|
String topicId) {
|
||||||
|
if (history.size() <= 1) {
|
||||||
|
// Only the current user message is in history; just ask the question
|
||||||
|
return topicId != null
|
||||||
|
? String.format("[Context: This is a question about the neurosurgery topic '%s']\n%s",
|
||||||
|
topicId, currentQuestion)
|
||||||
|
: currentQuestion;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
if (topicId != null) {
|
||||||
|
sb.append(String.format("[Context: This conversation is about the neurosurgery topic '%s']\n\n",
|
||||||
|
topicId));
|
||||||
|
}
|
||||||
|
sb.append("Previous conversation:\n");
|
||||||
|
// Include all messages except the last (which is the current user message just saved)
|
||||||
|
for (int i = 0; i < history.size() - 1; i++) {
|
||||||
|
Message msg = history.get(i);
|
||||||
|
sb.append(msg.getRole().name()).append(": ").append(msg.getContent()).append("\n");
|
||||||
|
}
|
||||||
|
sb.append("\nCurrent question: ").append(currentQuestion);
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Map<String, Object>> extractSources(ChatResponse response) {
|
||||||
|
List<Map<String, Object>> sources = new ArrayList<>();
|
||||||
|
|
||||||
|
if (response.getMetadata() != null) {
|
||||||
|
Object retrieved = response.getMetadata().get(QuestionAnswerAdvisor.RETRIEVED_DOCUMENTS);
|
||||||
|
if (retrieved instanceof List<?> docs) {
|
||||||
|
for (Object docObj : docs) {
|
||||||
|
if (docObj instanceof Document doc) {
|
||||||
|
Map<String, Object> metadata = doc.getMetadata();
|
||||||
|
String bookTitle = (String) metadata.get("book_title");
|
||||||
|
Object pageObj = metadata.get("page_number");
|
||||||
|
Integer page = pageObj instanceof Number n ? n.intValue() : null;
|
||||||
|
if (bookTitle != null) {
|
||||||
|
Map<String, Object> source = new HashMap<>();
|
||||||
|
source.put("bookTitle", bookTitle);
|
||||||
|
source.put("page", page);
|
||||||
|
sources.add(source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sources;
|
||||||
|
}
|
||||||
|
}
|
||||||
48
backend/src/main/java/com/aiteacher/chat/ChatSession.java
Normal file
48
backend/src/main/java/com/aiteacher/chat/ChatSession.java
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package com.aiteacher.chat;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "chat_session")
|
||||||
|
public class ChatSession {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.UUID)
|
||||||
|
private UUID id;
|
||||||
|
|
||||||
|
@Column(name = "topic_id", length = 100)
|
||||||
|
private String topicId;
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false)
|
||||||
|
private Instant createdAt;
|
||||||
|
|
||||||
|
public ChatSession() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChatSession(String topicId) {
|
||||||
|
this.topicId = topicId;
|
||||||
|
this.createdAt = Instant.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTopicId() {
|
||||||
|
return topicId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTopicId(String topicId) {
|
||||||
|
this.topicId = topicId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Instant getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(Instant createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.aiteacher.chat;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface ChatSessionRepository extends JpaRepository<ChatSession, UUID> {
|
||||||
|
}
|
||||||
90
backend/src/main/java/com/aiteacher/chat/Message.java
Normal file
90
backend/src/main/java/com/aiteacher/chat/Message.java
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package com.aiteacher.chat;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import org.hibernate.annotations.JdbcTypeCode;
|
||||||
|
import org.hibernate.type.SqlTypes;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "message")
|
||||||
|
public class Message {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.UUID)
|
||||||
|
private UUID id;
|
||||||
|
|
||||||
|
@Column(name = "session_id", nullable = false)
|
||||||
|
private UUID sessionId;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "role", nullable = false, length = 10)
|
||||||
|
private MessageRole role;
|
||||||
|
|
||||||
|
@Column(name = "content", nullable = false, columnDefinition = "TEXT")
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
@JdbcTypeCode(SqlTypes.JSON)
|
||||||
|
@Column(name = "sources", columnDefinition = "jsonb")
|
||||||
|
private List<Map<String, Object>> sources;
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false)
|
||||||
|
private Instant createdAt;
|
||||||
|
|
||||||
|
public Message() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public Message(UUID sessionId, MessageRole role, String content) {
|
||||||
|
this.sessionId = sessionId;
|
||||||
|
this.role = role;
|
||||||
|
this.content = content;
|
||||||
|
this.createdAt = Instant.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID getSessionId() {
|
||||||
|
return sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSessionId(UUID sessionId) {
|
||||||
|
this.sessionId = sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MessageRole getRole() {
|
||||||
|
return role;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRole(MessageRole role) {
|
||||||
|
this.role = role;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContent() {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContent(String content) {
|
||||||
|
this.content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Map<String, Object>> getSources() {
|
||||||
|
return sources;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSources(List<Map<String, Object>> sources) {
|
||||||
|
this.sources = sources;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Instant getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(Instant createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.aiteacher.chat;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface MessageRepository extends JpaRepository<Message, UUID> {
|
||||||
|
|
||||||
|
List<Message> findBySessionIdOrderByCreatedAtAsc(UUID sessionId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.aiteacher.chat;
|
||||||
|
|
||||||
|
public enum MessageRole {
|
||||||
|
USER,
|
||||||
|
ASSISTANT
|
||||||
|
}
|
||||||
21
backend/src/main/java/com/aiteacher/config/AiConfig.java
Normal file
21
backend/src/main/java/com/aiteacher/config/AiConfig.java
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package com.aiteacher.config;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.springframework.ai.chat.client.ChatClient;
|
||||||
|
import org.springframework.ai.chat.model.ChatModel;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class AiConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ObjectMapper objectMapper() {
|
||||||
|
return new ObjectMapper();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ChatClient chatClient(ChatModel chatModel) {
|
||||||
|
return ChatClient.builder(chatModel).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package com.aiteacher.config;
|
||||||
|
|
||||||
|
import com.aiteacher.book.NoKnowledgeSourceException;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
|
||||||
|
@RestControllerAdvice
|
||||||
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||||
|
|
||||||
|
@ExceptionHandler(NoKnowledgeSourceException.class)
|
||||||
|
public ResponseEntity<Map<String, String>> handleNoKnowledgeSource(NoKnowledgeSourceException ex) {
|
||||||
|
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
|
||||||
|
.body(Map.of("error", ex.getMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(NoSuchElementException.class)
|
||||||
|
public ResponseEntity<Map<String, String>> handleNotFound(NoSuchElementException ex) {
|
||||||
|
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||||
|
.body(Map.of("error", ex.getMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(IllegalArgumentException.class)
|
||||||
|
public ResponseEntity<Map<String, String>> handleBadRequest(IllegalArgumentException ex) {
|
||||||
|
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||||
|
.body(Map.of("error", ex.getMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(IllegalStateException.class)
|
||||||
|
public ResponseEntity<Map<String, String>> handleConflict(IllegalStateException ex) {
|
||||||
|
return ResponseEntity.status(HttpStatus.CONFLICT)
|
||||||
|
.body(Map.of("error", ex.getMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(MaxUploadSizeExceededException.class)
|
||||||
|
public ResponseEntity<Map<String, String>> handleMaxUploadSize(MaxUploadSizeExceededException ex) {
|
||||||
|
return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE)
|
||||||
|
.body(Map.of("error", "File exceeds maximum size of 100 MB."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(Exception.class)
|
||||||
|
public ResponseEntity<Map<String, String>> handleGeneric(Exception ex) {
|
||||||
|
log.error("Unhandled exception", ex);
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
.body(Map.of("error", "An unexpected error occurred."));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.aiteacher.config;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.config.Customizer;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||||
|
import org.springframework.security.core.userdetails.User;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
|
http
|
||||||
|
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
|
||||||
|
.httpBasic(Customizer.withDefaults())
|
||||||
|
.csrf(AbstractHttpConfigurer::disable);
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public UserDetailsService userDetailsService(
|
||||||
|
@Value("${app.auth.password}") String password) {
|
||||||
|
UserDetails user = User.builder()
|
||||||
|
.username("neurosurgeon")
|
||||||
|
.password("{noop}" + password)
|
||||||
|
.roles("USER")
|
||||||
|
.build();
|
||||||
|
return new InMemoryUserDetailsManager(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
backend/src/main/java/com/aiteacher/topic/Topic.java
Normal file
9
backend/src/main/java/com/aiteacher/topic/Topic.java
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package com.aiteacher.topic;
|
||||||
|
|
||||||
|
public record Topic(
|
||||||
|
String id,
|
||||||
|
String name,
|
||||||
|
String description,
|
||||||
|
String category
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package com.aiteacher.topic;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class TopicConfigLoader {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(TopicConfigLoader.class);
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
private List<Topic> topics;
|
||||||
|
|
||||||
|
public TopicConfigLoader(ObjectMapper objectMapper) {
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void load() throws IOException {
|
||||||
|
ClassPathResource resource = new ClassPathResource("topics.json");
|
||||||
|
try (InputStream is = resource.getInputStream()) {
|
||||||
|
topics = objectMapper.readValue(is, new TypeReference<List<Topic>>() {});
|
||||||
|
}
|
||||||
|
log.info("Loaded {} neurosurgery topics from topics.json", topics.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Topic> getAll() {
|
||||||
|
return topics;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Topic> findById(String id) {
|
||||||
|
return topics.stream()
|
||||||
|
.filter(t -> t.id().equals(id))
|
||||||
|
.findFirst();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.aiteacher.topic;
|
||||||
|
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/topics")
|
||||||
|
public class TopicController {
|
||||||
|
|
||||||
|
private final TopicConfigLoader topicConfigLoader;
|
||||||
|
private final TopicSummaryService topicSummaryService;
|
||||||
|
|
||||||
|
public TopicController(TopicConfigLoader topicConfigLoader,
|
||||||
|
TopicSummaryService topicSummaryService) {
|
||||||
|
this.topicConfigLoader = topicConfigLoader;
|
||||||
|
this.topicSummaryService = topicSummaryService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<List<Topic>> list() {
|
||||||
|
return ResponseEntity.ok(topicConfigLoader.getAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/summary")
|
||||||
|
public ResponseEntity<TopicSummaryResponse> generateSummary(@PathVariable String id) {
|
||||||
|
Topic topic = topicConfigLoader.findById(id)
|
||||||
|
.orElseThrow(() -> new NoSuchElementException("Topic not found."));
|
||||||
|
|
||||||
|
TopicSummaryResponse response = topicSummaryService.generateSummary(topic);
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.aiteacher.topic;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record TopicSummaryResponse(
|
||||||
|
String topicId,
|
||||||
|
String topicName,
|
||||||
|
String summary,
|
||||||
|
List<SourceReference> sources,
|
||||||
|
Instant generatedAt
|
||||||
|
) {
|
||||||
|
public record SourceReference(
|
||||||
|
String bookTitle,
|
||||||
|
Integer page
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package com.aiteacher.topic;
|
||||||
|
|
||||||
|
import com.aiteacher.book.BookRepository;
|
||||||
|
import com.aiteacher.book.BookStatus;
|
||||||
|
import com.aiteacher.book.NoKnowledgeSourceException;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.ai.chat.client.ChatClient;
|
||||||
|
import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor;
|
||||||
|
import org.springframework.ai.chat.model.ChatResponse;
|
||||||
|
import org.springframework.ai.document.Document;
|
||||||
|
import org.springframework.ai.vectorstore.VectorStore;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class TopicSummaryService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(TopicSummaryService.class);
|
||||||
|
|
||||||
|
private static final String SYSTEM_PROMPT = """
|
||||||
|
You are an expert neurosurgery educator. Your role is to provide accurate,
|
||||||
|
clinically relevant summaries based ONLY on the content retrieved from the
|
||||||
|
uploaded medical textbooks. Do not use any knowledge outside the provided context.
|
||||||
|
|
||||||
|
When answering:
|
||||||
|
- Structure your response clearly with key points
|
||||||
|
- If the context mentions specific book titles and page numbers, reference them
|
||||||
|
- If the retrieved context does not contain sufficient information on the topic,
|
||||||
|
explicitly state: "The uploaded books do not contain sufficient information on this topic."
|
||||||
|
- Never hallucinate or fabricate clinical information
|
||||||
|
""";
|
||||||
|
|
||||||
|
private final ChatClient chatClient;
|
||||||
|
private final VectorStore vectorStore;
|
||||||
|
private final BookRepository bookRepository;
|
||||||
|
|
||||||
|
public TopicSummaryService(ChatClient chatClient, VectorStore vectorStore,
|
||||||
|
BookRepository bookRepository) {
|
||||||
|
this.chatClient = chatClient;
|
||||||
|
this.vectorStore = vectorStore;
|
||||||
|
this.bookRepository = bookRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TopicSummaryResponse generateSummary(Topic topic) {
|
||||||
|
if (!bookRepository.existsByStatus(BookStatus.READY)) {
|
||||||
|
throw new NoKnowledgeSourceException(
|
||||||
|
"No books are available as knowledge sources. Please upload and process at least one book.");
|
||||||
|
}
|
||||||
|
|
||||||
|
String question = buildQuestion(topic);
|
||||||
|
|
||||||
|
ChatResponse response = chatClient.prompt()
|
||||||
|
.system(SYSTEM_PROMPT)
|
||||||
|
.advisors(QuestionAnswerAdvisor.builder(vectorStore).build())
|
||||||
|
.user(question)
|
||||||
|
.call()
|
||||||
|
.chatResponse();
|
||||||
|
|
||||||
|
String summary = response.getResult().getOutput().getText();
|
||||||
|
List<TopicSummaryResponse.SourceReference> sources = extractSources(response);
|
||||||
|
|
||||||
|
return new TopicSummaryResponse(
|
||||||
|
topic.id(),
|
||||||
|
topic.name(),
|
||||||
|
summary,
|
||||||
|
sources,
|
||||||
|
Instant.now()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildQuestion(Topic topic) {
|
||||||
|
return String.format(
|
||||||
|
"Please provide a comprehensive educational summary of the following neurosurgery topic: " +
|
||||||
|
"%s. Topic description: %s. " +
|
||||||
|
"Include key concepts, clinical considerations, and important details that a neurosurgeon should know.",
|
||||||
|
topic.name(), topic.description()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<TopicSummaryResponse.SourceReference> extractSources(ChatResponse response) {
|
||||||
|
List<TopicSummaryResponse.SourceReference> sources = new ArrayList<>();
|
||||||
|
|
||||||
|
if (response.getMetadata() != null) {
|
||||||
|
Object retrieved = response.getMetadata().get(QuestionAnswerAdvisor.RETRIEVED_DOCUMENTS);
|
||||||
|
if (retrieved instanceof List<?> docs) {
|
||||||
|
for (Object docObj : docs) {
|
||||||
|
if (docObj instanceof Document doc) {
|
||||||
|
Map<String, Object> metadata = doc.getMetadata();
|
||||||
|
String bookTitle = (String) metadata.get("book_title");
|
||||||
|
Object pageObj = metadata.get("page_number");
|
||||||
|
Integer page = pageObj instanceof Number n ? n.intValue() : null;
|
||||||
|
if (bookTitle != null) {
|
||||||
|
sources.add(new TopicSummaryResponse.SourceReference(bookTitle, page));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deduplicate by bookTitle + page
|
||||||
|
return sources.stream().distinct().toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
52
backend/src/main/resources/application.yaml
Normal file
52
backend/src/main/resources/application.yaml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
spring:
|
||||||
|
datasource:
|
||||||
|
url: ${DB_URL:jdbc:postgresql://master:30432/aiteacher}
|
||||||
|
username: ${DB_USERNAME:user}
|
||||||
|
password: ${DB_PASSWORD:password}
|
||||||
|
driver-class-name: org.postgresql.Driver
|
||||||
|
|
||||||
|
jpa:
|
||||||
|
hibernate:
|
||||||
|
ddl-auto: update
|
||||||
|
show-sql: false
|
||||||
|
properties:
|
||||||
|
hibernate:
|
||||||
|
format_sql: false
|
||||||
|
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||||
|
open-in-view: false
|
||||||
|
|
||||||
|
flyway:
|
||||||
|
enabled: true
|
||||||
|
locations: classpath:db/migration
|
||||||
|
|
||||||
|
ai:
|
||||||
|
vectorstore:
|
||||||
|
pgvector:
|
||||||
|
dimensions: 1536
|
||||||
|
distance-type: COSINE_DISTANCE
|
||||||
|
index-type: HNSW
|
||||||
|
initialize-schema: false
|
||||||
|
openai:
|
||||||
|
api-key: ${OPENAI_API_KEY}
|
||||||
|
chat:
|
||||||
|
options:
|
||||||
|
model: gpt-4o
|
||||||
|
embedding:
|
||||||
|
options:
|
||||||
|
model: "text-embedding-3-small"
|
||||||
|
|
||||||
|
servlet:
|
||||||
|
multipart:
|
||||||
|
max-file-size: 100MB
|
||||||
|
max-request-size: 100MB
|
||||||
|
|
||||||
|
task:
|
||||||
|
execution:
|
||||||
|
pool:
|
||||||
|
core-size: 4
|
||||||
|
max-size: 8
|
||||||
|
queue-capacity: 50
|
||||||
|
|
||||||
|
app:
|
||||||
|
auth:
|
||||||
|
password: ${APP_PASSWORD:changeme}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- V1: Initial schema
|
||||||
|
-- Spring AI manages the vector_store table separately.
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS book (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
title VARCHAR(500) NOT NULL,
|
||||||
|
file_name VARCHAR(500) NOT NULL,
|
||||||
|
file_size_bytes BIGINT NOT NULL,
|
||||||
|
page_count INT,
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
|
||||||
|
error_message TEXT,
|
||||||
|
uploaded_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
processed_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS chat_session (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
topic_id VARCHAR(100),
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS message (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
session_id UUID NOT NULL REFERENCES chat_session(id) ON DELETE CASCADE,
|
||||||
|
role VARCHAR(10) NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
sources JSONB,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_message_session ON message(session_id, created_at);
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- V2: pgvector extension + Spring AI vector_store table
|
||||||
|
-- Replaces Spring AI's initialize-schema=true (which requires
|
||||||
|
-- CREATE SCHEMA privilege and fails on restricted users).
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE EXTENSION IF NOT EXISTS vector;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS vector_store (
|
||||||
|
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||||
|
content text,
|
||||||
|
metadata json,
|
||||||
|
embedding vector(1536)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS spring_ai_vector_index
|
||||||
|
ON vector_store USING hnsw (embedding vector_cosine_ops);
|
||||||
74
backend/src/main/resources/topics.json
Normal file
74
backend/src/main/resources/topics.json
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "cerebral-aneurysm",
|
||||||
|
"name": "Cerebral Aneurysm Management",
|
||||||
|
"description": "Diagnosis, grading, and surgical/endovascular treatment of cerebral aneurysms.",
|
||||||
|
"category": "Vascular"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "subarachnoid-hemorrhage",
|
||||||
|
"name": "Subarachnoid Hemorrhage",
|
||||||
|
"description": "Pathophysiology, clinical presentation, and management of subarachnoid hemorrhage.",
|
||||||
|
"category": "Vascular"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "arteriovenous-malformation",
|
||||||
|
"name": "Arteriovenous Malformation (AVM)",
|
||||||
|
"description": "Classification, natural history, and treatment options for cerebral AVMs.",
|
||||||
|
"category": "Vascular"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "carotid-stenosis",
|
||||||
|
"name": "Carotid Artery Stenosis",
|
||||||
|
"description": "Evaluation and surgical or endovascular management of carotid artery stenosis.",
|
||||||
|
"category": "Vascular"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "glioblastoma",
|
||||||
|
"name": "Glioblastoma (GBM)",
|
||||||
|
"description": "Pathophysiology, surgical resection strategies, and adjuvant therapy for GBM.",
|
||||||
|
"category": "Oncology"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "meningioma",
|
||||||
|
"name": "Meningioma",
|
||||||
|
"description": "Classification, surgical approaches, and recurrence management for meningiomas.",
|
||||||
|
"category": "Oncology"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pituitary-adenoma",
|
||||||
|
"name": "Pituitary Adenoma",
|
||||||
|
"description": "Hormonal assessment, transsphenoidal surgery, and medical management of pituitary adenomas.",
|
||||||
|
"category": "Oncology"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "lumbar-disc-herniation",
|
||||||
|
"name": "Lumbar Disc Herniation",
|
||||||
|
"description": "Anatomy, clinical presentation, conservative and surgical treatment of lumbar disc herniation.",
|
||||||
|
"category": "Spine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cervical-myelopathy",
|
||||||
|
"name": "Cervical Spondylotic Myelopathy",
|
||||||
|
"description": "Pathomechanics, clinical grading, and decompressive surgery for cervical myelopathy.",
|
||||||
|
"category": "Spine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "spinal-cord-injury",
|
||||||
|
"name": "Spinal Cord Injury",
|
||||||
|
"description": "ASIA classification, acute management protocols, and rehabilitation strategies for SCI.",
|
||||||
|
"category": "Spine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "traumatic-brain-injury",
|
||||||
|
"name": "Traumatic Brain Injury (TBI)",
|
||||||
|
"description": "GCS scoring, intracranial pressure monitoring, and surgical management of TBI.",
|
||||||
|
"category": "Trauma"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "epidural-hematoma",
|
||||||
|
"name": "Epidural Hematoma",
|
||||||
|
"description": "Mechanism, radiological features, and emergency surgical evacuation of epidural hematomas.",
|
||||||
|
"category": "Trauma"
|
||||||
|
}
|
||||||
|
]
|
||||||
BIN
backend/target/ai-teacher-backend-0.0.1-SNAPSHOT.jar
Normal file
BIN
backend/target/ai-teacher-backend-0.0.1-SNAPSHOT.jar
Normal file
Binary file not shown.
BIN
backend/target/ai-teacher-backend-0.0.1-SNAPSHOT.jar.original
Normal file
BIN
backend/target/ai-teacher-backend-0.0.1-SNAPSHOT.jar.original
Normal file
Binary file not shown.
52
backend/target/classes/application.yaml
Normal file
52
backend/target/classes/application.yaml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
spring:
|
||||||
|
datasource:
|
||||||
|
url: ${DB_URL:jdbc:postgresql://master:30432/aiteacher}
|
||||||
|
username: ${DB_USERNAME:user}
|
||||||
|
password: ${DB_PASSWORD:password}
|
||||||
|
driver-class-name: org.postgresql.Driver
|
||||||
|
|
||||||
|
jpa:
|
||||||
|
hibernate:
|
||||||
|
ddl-auto: update
|
||||||
|
show-sql: false
|
||||||
|
properties:
|
||||||
|
hibernate:
|
||||||
|
format_sql: false
|
||||||
|
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||||
|
open-in-view: false
|
||||||
|
|
||||||
|
flyway:
|
||||||
|
enabled: true
|
||||||
|
locations: classpath:db/migration
|
||||||
|
|
||||||
|
ai:
|
||||||
|
vectorstore:
|
||||||
|
pgvector:
|
||||||
|
dimensions: 1536
|
||||||
|
distance-type: COSINE_DISTANCE
|
||||||
|
index-type: HNSW
|
||||||
|
initialize-schema: false
|
||||||
|
openai:
|
||||||
|
api-key: ${OPENAI_API_KEY}
|
||||||
|
chat:
|
||||||
|
options:
|
||||||
|
model: gpt-4o
|
||||||
|
embedding:
|
||||||
|
options:
|
||||||
|
model: "text-embedding-3-small"
|
||||||
|
|
||||||
|
servlet:
|
||||||
|
multipart:
|
||||||
|
max-file-size: 100MB
|
||||||
|
max-request-size: 100MB
|
||||||
|
|
||||||
|
task:
|
||||||
|
execution:
|
||||||
|
pool:
|
||||||
|
core-size: 4
|
||||||
|
max-size: 8
|
||||||
|
queue-capacity: 50
|
||||||
|
|
||||||
|
app:
|
||||||
|
auth:
|
||||||
|
password: ${APP_PASSWORD:changeme}
|
||||||
BIN
backend/target/classes/com/aiteacher/AiTeacherApplication.class
Normal file
BIN
backend/target/classes/com/aiteacher/AiTeacherApplication.class
Normal file
Binary file not shown.
BIN
backend/target/classes/com/aiteacher/book/Book.class
Normal file
BIN
backend/target/classes/com/aiteacher/book/Book.class
Normal file
Binary file not shown.
BIN
backend/target/classes/com/aiteacher/book/BookController.class
Normal file
BIN
backend/target/classes/com/aiteacher/book/BookController.class
Normal file
Binary file not shown.
Binary file not shown.
BIN
backend/target/classes/com/aiteacher/book/BookRepository.class
Normal file
BIN
backend/target/classes/com/aiteacher/book/BookRepository.class
Normal file
Binary file not shown.
BIN
backend/target/classes/com/aiteacher/book/BookService.class
Normal file
BIN
backend/target/classes/com/aiteacher/book/BookService.class
Normal file
Binary file not shown.
BIN
backend/target/classes/com/aiteacher/book/BookStatus.class
Normal file
BIN
backend/target/classes/com/aiteacher/book/BookStatus.class
Normal file
Binary file not shown.
Binary file not shown.
BIN
backend/target/classes/com/aiteacher/chat/ChatController.class
Normal file
BIN
backend/target/classes/com/aiteacher/chat/ChatController.class
Normal file
Binary file not shown.
BIN
backend/target/classes/com/aiteacher/chat/ChatService.class
Normal file
BIN
backend/target/classes/com/aiteacher/chat/ChatService.class
Normal file
Binary file not shown.
BIN
backend/target/classes/com/aiteacher/chat/ChatSession.class
Normal file
BIN
backend/target/classes/com/aiteacher/chat/ChatSession.class
Normal file
Binary file not shown.
Binary file not shown.
BIN
backend/target/classes/com/aiteacher/chat/Message.class
Normal file
BIN
backend/target/classes/com/aiteacher/chat/Message.class
Normal file
Binary file not shown.
Binary file not shown.
BIN
backend/target/classes/com/aiteacher/chat/MessageRole.class
Normal file
BIN
backend/target/classes/com/aiteacher/chat/MessageRole.class
Normal file
Binary file not shown.
BIN
backend/target/classes/com/aiteacher/config/AiConfig.class
Normal file
BIN
backend/target/classes/com/aiteacher/config/AiConfig.class
Normal file
Binary file not shown.
Binary file not shown.
BIN
backend/target/classes/com/aiteacher/config/SecurityConfig.class
Normal file
BIN
backend/target/classes/com/aiteacher/config/SecurityConfig.class
Normal file
Binary file not shown.
BIN
backend/target/classes/com/aiteacher/topic/Topic.class
Normal file
BIN
backend/target/classes/com/aiteacher/topic/Topic.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
backend/target/classes/com/aiteacher/topic/TopicController.class
Normal file
BIN
backend/target/classes/com/aiteacher/topic/TopicController.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
33
backend/target/classes/db/migration/V1__initial_schema.sql
Normal file
33
backend/target/classes/db/migration/V1__initial_schema.sql
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- V1: Initial schema
|
||||||
|
-- Spring AI manages the vector_store table separately.
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS book (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
title VARCHAR(500) NOT NULL,
|
||||||
|
file_name VARCHAR(500) NOT NULL,
|
||||||
|
file_size_bytes BIGINT NOT NULL,
|
||||||
|
page_count INT,
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
|
||||||
|
error_message TEXT,
|
||||||
|
uploaded_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
processed_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS chat_session (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
topic_id VARCHAR(100),
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS message (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
session_id UUID NOT NULL REFERENCES chat_session(id) ON DELETE CASCADE,
|
||||||
|
role VARCHAR(10) NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
sources JSONB,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_message_session ON message(session_id, created_at);
|
||||||
17
backend/target/classes/db/migration/V2__pgvector_store.sql
Normal file
17
backend/target/classes/db/migration/V2__pgvector_store.sql
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- V2: pgvector extension + Spring AI vector_store table
|
||||||
|
-- Replaces Spring AI's initialize-schema=true (which requires
|
||||||
|
-- CREATE SCHEMA privilege and fails on restricted users).
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE EXTENSION IF NOT EXISTS vector;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS vector_store (
|
||||||
|
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||||
|
content text,
|
||||||
|
metadata json,
|
||||||
|
embedding vector(1536)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS spring_ai_vector_index
|
||||||
|
ON vector_store USING hnsw (embedding vector_cosine_ops);
|
||||||
74
backend/target/classes/topics.json
Normal file
74
backend/target/classes/topics.json
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "cerebral-aneurysm",
|
||||||
|
"name": "Cerebral Aneurysm Management",
|
||||||
|
"description": "Diagnosis, grading, and surgical/endovascular treatment of cerebral aneurysms.",
|
||||||
|
"category": "Vascular"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "subarachnoid-hemorrhage",
|
||||||
|
"name": "Subarachnoid Hemorrhage",
|
||||||
|
"description": "Pathophysiology, clinical presentation, and management of subarachnoid hemorrhage.",
|
||||||
|
"category": "Vascular"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "arteriovenous-malformation",
|
||||||
|
"name": "Arteriovenous Malformation (AVM)",
|
||||||
|
"description": "Classification, natural history, and treatment options for cerebral AVMs.",
|
||||||
|
"category": "Vascular"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "carotid-stenosis",
|
||||||
|
"name": "Carotid Artery Stenosis",
|
||||||
|
"description": "Evaluation and surgical or endovascular management of carotid artery stenosis.",
|
||||||
|
"category": "Vascular"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "glioblastoma",
|
||||||
|
"name": "Glioblastoma (GBM)",
|
||||||
|
"description": "Pathophysiology, surgical resection strategies, and adjuvant therapy for GBM.",
|
||||||
|
"category": "Oncology"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "meningioma",
|
||||||
|
"name": "Meningioma",
|
||||||
|
"description": "Classification, surgical approaches, and recurrence management for meningiomas.",
|
||||||
|
"category": "Oncology"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pituitary-adenoma",
|
||||||
|
"name": "Pituitary Adenoma",
|
||||||
|
"description": "Hormonal assessment, transsphenoidal surgery, and medical management of pituitary adenomas.",
|
||||||
|
"category": "Oncology"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "lumbar-disc-herniation",
|
||||||
|
"name": "Lumbar Disc Herniation",
|
||||||
|
"description": "Anatomy, clinical presentation, conservative and surgical treatment of lumbar disc herniation.",
|
||||||
|
"category": "Spine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cervical-myelopathy",
|
||||||
|
"name": "Cervical Spondylotic Myelopathy",
|
||||||
|
"description": "Pathomechanics, clinical grading, and decompressive surgery for cervical myelopathy.",
|
||||||
|
"category": "Spine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "spinal-cord-injury",
|
||||||
|
"name": "Spinal Cord Injury",
|
||||||
|
"description": "ASIA classification, acute management protocols, and rehabilitation strategies for SCI.",
|
||||||
|
"category": "Spine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "traumatic-brain-injury",
|
||||||
|
"name": "Traumatic Brain Injury (TBI)",
|
||||||
|
"description": "GCS scoring, intracranial pressure monitoring, and surgical management of TBI.",
|
||||||
|
"category": "Trauma"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "epidural-hematoma",
|
||||||
|
"name": "Epidural Hematoma",
|
||||||
|
"description": "Mechanism, radiological features, and emergency surgical evacuation of epidural hematomas.",
|
||||||
|
"category": "Trauma"
|
||||||
|
}
|
||||||
|
]
|
||||||
3
backend/target/maven-archiver/pom.properties
Normal file
3
backend/target/maven-archiver/pom.properties
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
artifactId=ai-teacher-backend
|
||||||
|
groupId=com.aiteacher
|
||||||
|
version=0.0.1-SNAPSHOT
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
com/aiteacher/AiTeacherApplication.class
|
||||||
|
com/aiteacher/book/BookEmbeddingService.class
|
||||||
|
com/aiteacher/topic/TopicController.class
|
||||||
|
com/aiteacher/topic/TopicSummaryService.class
|
||||||
|
com/aiteacher/book/BookRepository.class
|
||||||
|
com/aiteacher/chat/ChatSession.class
|
||||||
|
com/aiteacher/config/GlobalExceptionHandler.class
|
||||||
|
com/aiteacher/topic/Topic.class
|
||||||
|
com/aiteacher/topic/TopicSummaryResponse$SourceReference.class
|
||||||
|
com/aiteacher/chat/ChatService.class
|
||||||
|
com/aiteacher/chat/MessageRole.class
|
||||||
|
com/aiteacher/chat/ChatController.class
|
||||||
|
com/aiteacher/chat/Message.class
|
||||||
|
com/aiteacher/book/NoKnowledgeSourceException.class
|
||||||
|
com/aiteacher/config/AiConfig.class
|
||||||
|
com/aiteacher/book/BookService.class
|
||||||
|
com/aiteacher/book/BookController.class
|
||||||
|
com/aiteacher/topic/TopicConfigLoader.class
|
||||||
|
com/aiteacher/config/SecurityConfig.class
|
||||||
|
com/aiteacher/topic/TopicSummaryResponse.class
|
||||||
|
com/aiteacher/chat/ChatSessionRepository.class
|
||||||
|
com/aiteacher/book/BookStatus.class
|
||||||
|
com/aiteacher/book/Book.class
|
||||||
|
com/aiteacher/topic/TopicConfigLoader$1.class
|
||||||
|
com/aiteacher/chat/MessageRepository.class
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
/home/adrien/project/java/ai-teacher/backend/src/main/java/com/aiteacher/AiTeacherApplication.java
|
||||||
|
/home/adrien/project/java/ai-teacher/backend/src/main/java/com/aiteacher/book/Book.java
|
||||||
|
/home/adrien/project/java/ai-teacher/backend/src/main/java/com/aiteacher/book/BookController.java
|
||||||
|
/home/adrien/project/java/ai-teacher/backend/src/main/java/com/aiteacher/book/BookEmbeddingService.java
|
||||||
|
/home/adrien/project/java/ai-teacher/backend/src/main/java/com/aiteacher/book/BookRepository.java
|
||||||
|
/home/adrien/project/java/ai-teacher/backend/src/main/java/com/aiteacher/book/BookService.java
|
||||||
|
/home/adrien/project/java/ai-teacher/backend/src/main/java/com/aiteacher/book/BookStatus.java
|
||||||
|
/home/adrien/project/java/ai-teacher/backend/src/main/java/com/aiteacher/book/NoKnowledgeSourceException.java
|
||||||
|
/home/adrien/project/java/ai-teacher/backend/src/main/java/com/aiteacher/chat/ChatController.java
|
||||||
|
/home/adrien/project/java/ai-teacher/backend/src/main/java/com/aiteacher/chat/ChatService.java
|
||||||
|
/home/adrien/project/java/ai-teacher/backend/src/main/java/com/aiteacher/chat/ChatSession.java
|
||||||
|
/home/adrien/project/java/ai-teacher/backend/src/main/java/com/aiteacher/chat/ChatSessionRepository.java
|
||||||
|
/home/adrien/project/java/ai-teacher/backend/src/main/java/com/aiteacher/chat/Message.java
|
||||||
|
/home/adrien/project/java/ai-teacher/backend/src/main/java/com/aiteacher/chat/MessageRepository.java
|
||||||
|
/home/adrien/project/java/ai-teacher/backend/src/main/java/com/aiteacher/chat/MessageRole.java
|
||||||
|
/home/adrien/project/java/ai-teacher/backend/src/main/java/com/aiteacher/config/AiConfig.java
|
||||||
|
/home/adrien/project/java/ai-teacher/backend/src/main/java/com/aiteacher/config/GlobalExceptionHandler.java
|
||||||
|
/home/adrien/project/java/ai-teacher/backend/src/main/java/com/aiteacher/config/SecurityConfig.java
|
||||||
|
/home/adrien/project/java/ai-teacher/backend/src/main/java/com/aiteacher/topic/Topic.java
|
||||||
|
/home/adrien/project/java/ai-teacher/backend/src/main/java/com/aiteacher/topic/TopicConfigLoader.java
|
||||||
|
/home/adrien/project/java/ai-teacher/backend/src/main/java/com/aiteacher/topic/TopicController.java
|
||||||
|
/home/adrien/project/java/ai-teacher/backend/src/main/java/com/aiteacher/topic/TopicSummaryResponse.java
|
||||||
|
/home/adrien/project/java/ai-teacher/backend/src/main/java/com/aiteacher/topic/TopicSummaryService.java
|
||||||
22
docker-compose.yml
Normal file
22
docker-compose.yml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
version: '3.9'
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: pgvector/pgvector:pg16
|
||||||
|
container_name: aiteacher-postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: aiteacher
|
||||||
|
POSTGRES_USER: aiteacher
|
||||||
|
POSTGRES_PASSWORD: aiteacher
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U aiteacher -d aiteacher"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
7
frontend/.env.example
Normal file
7
frontend/.env.example
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Base URL for the backend API.
|
||||||
|
# In development with the Vite proxy this can be left as the default (/api/v1).
|
||||||
|
# In production point it directly at the backend, e.g. https://api.example.com/api/v1
|
||||||
|
VITE_API_URL=/api/v1
|
||||||
|
|
||||||
|
# Shared password for HTTP Basic auth (must match APP_PASSWORD on the backend).
|
||||||
|
VITE_APP_PASSWORD=changeme
|
||||||
14
frontend/Dockerfile
Normal file
14
frontend/Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# ---- Build stage ----
|
||||||
|
FROM node:20-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# ---- Runtime stage (nginx) ----
|
||||||
|
FROM nginx:alpine
|
||||||
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
1
frontend/dist/assets/index-BFTghWYO.css
vendored
Normal file
1
frontend/dist/assets/index-BFTghWYO.css
vendored
Normal file
File diff suppressed because one or more lines are too long
35
frontend/dist/assets/index-Cyrl5U0Q.js
vendored
Normal file
35
frontend/dist/assets/index-Cyrl5U0Q.js
vendored
Normal file
File diff suppressed because one or more lines are too long
14
frontend/dist/index.html
vendored
Normal file
14
frontend/dist/index.html
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>AI Teacher — Neurosurgeon Learning Platform</title>
|
||||||
|
<script type="module" crossorigin src="/assets/index-Cyrl5U0Q.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/index-BFTghWYO.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>AI Teacher — Neurosurgeon Learning Platform</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
22
frontend/nginx.conf
Normal file
22
frontend/nginx.conf
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Vue Router — serve index.html for all non-asset routes
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cache static assets aggressively (content-hashed filenames)
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Gzip
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/plain text/css application/javascript application/json;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
}
|
||||||
1
frontend/node_modules/.bin/esbuild
generated
vendored
Symbolic link
1
frontend/node_modules/.bin/esbuild
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../esbuild/bin/esbuild
|
||||||
1
frontend/node_modules/.bin/he
generated
vendored
Symbolic link
1
frontend/node_modules/.bin/he
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../he/bin/he
|
||||||
1
frontend/node_modules/.bin/nanoid
generated
vendored
Symbolic link
1
frontend/node_modules/.bin/nanoid
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../nanoid/bin/nanoid.cjs
|
||||||
1
frontend/node_modules/.bin/parser
generated
vendored
Symbolic link
1
frontend/node_modules/.bin/parser
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../@babel/parser/bin/babel-parser.js
|
||||||
1
frontend/node_modules/.bin/rollup
generated
vendored
Symbolic link
1
frontend/node_modules/.bin/rollup
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../rollup/dist/bin/rollup
|
||||||
1
frontend/node_modules/.bin/tsc
generated
vendored
Symbolic link
1
frontend/node_modules/.bin/tsc
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../typescript/bin/tsc
|
||||||
1
frontend/node_modules/.bin/tsserver
generated
vendored
Symbolic link
1
frontend/node_modules/.bin/tsserver
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../typescript/bin/tsserver
|
||||||
1
frontend/node_modules/.bin/vite
generated
vendored
Symbolic link
1
frontend/node_modules/.bin/vite
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../vite/bin/vite.js
|
||||||
1
frontend/node_modules/.bin/vue-demi-fix
generated
vendored
Symbolic link
1
frontend/node_modules/.bin/vue-demi-fix
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../vue-demi/bin/vue-demi-fix.js
|
||||||
1
frontend/node_modules/.bin/vue-demi-switch
generated
vendored
Symbolic link
1
frontend/node_modules/.bin/vue-demi-switch
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../vue-demi/bin/vue-demi-switch.js
|
||||||
1
frontend/node_modules/.bin/vue-tsc
generated
vendored
Symbolic link
1
frontend/node_modules/.bin/vue-tsc
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../vue-tsc/bin/vue-tsc.js
|
||||||
963
frontend/node_modules/.package-lock.json
generated
vendored
Normal file
963
frontend/node_modules/.package-lock.json
generated
vendored
Normal file
@@ -0,0 +1,963 @@
|
|||||||
|
{
|
||||||
|
"name": "ai-teacher-frontend",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"node_modules/@babel/helper-string-parser": {
|
||||||
|
"version": "7.27.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||||
|
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helper-validator-identifier": {
|
||||||
|
"version": "7.28.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
||||||
|
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/parser": {
|
||||||
|
"version": "7.29.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
|
||||||
|
"integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/types": "^7.29.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"parser": "bin/babel-parser.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/types": {
|
||||||
|
"version": "7.29.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
|
||||||
|
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/helper-string-parser": "^7.27.1",
|
||||||
|
"@babel/helper-validator-identifier": "^7.28.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-x64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
|
"version": "1.5.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||||
|
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||||
|
"version": "4.60.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz",
|
||||||
|
"integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||||
|
"version": "4.60.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz",
|
||||||
|
"integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@tsconfig/node20": {
|
||||||
|
"version": "20.1.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node20/-/node20-20.1.9.tgz",
|
||||||
|
"integrity": "sha512-IjlTv1RsvnPtUcjTqtVsZExKVq+KQx4g5pCP5tI7rAs6Xesl2qFwSz/tPDBC4JajkL/MlezBu3gPUwqRHl+RIg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/@types/estree": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "20.19.37",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz",
|
||||||
|
"integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitejs/plugin-vue": {
|
||||||
|
"version": "5.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
|
||||||
|
"integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.0.0 || >=20.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vite": "^5.0.0 || ^6.0.0",
|
||||||
|
"vue": "^3.2.25"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@volar/language-core": {
|
||||||
|
"version": "2.4.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz",
|
||||||
|
"integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@volar/source-map": "2.4.15"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@volar/source-map": {
|
||||||
|
"version": "2.4.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.15.tgz",
|
||||||
|
"integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/@volar/typescript": {
|
||||||
|
"version": "2.4.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.15.tgz",
|
||||||
|
"integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@volar/language-core": "2.4.15",
|
||||||
|
"path-browserify": "^1.0.1",
|
||||||
|
"vscode-uri": "^3.0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/compiler-core": {
|
||||||
|
"version": "3.5.31",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.31.tgz",
|
||||||
|
"integrity": "sha512-k/ueL14aNIEy5Onf0OVzR8kiqF/WThgLdFhxwa4e/KF/0qe38IwIdofoSWBTvvxQOesaz6riAFAUaYjoF9fLLQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/parser": "^7.29.2",
|
||||||
|
"@vue/shared": "3.5.31",
|
||||||
|
"entities": "^7.0.1",
|
||||||
|
"estree-walker": "^2.0.2",
|
||||||
|
"source-map-js": "^1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/compiler-dom": {
|
||||||
|
"version": "3.5.31",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.31.tgz",
|
||||||
|
"integrity": "sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/compiler-core": "3.5.31",
|
||||||
|
"@vue/shared": "3.5.31"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/compiler-sfc": {
|
||||||
|
"version": "3.5.31",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.31.tgz",
|
||||||
|
"integrity": "sha512-M8wpPgR9UJ8MiRGjppvx9uWJfLV7A/T+/rL8s/y3QG3u0c2/YZgff3d6SuimKRIhcYnWg5fTfDMlz2E6seUW8Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/parser": "^7.29.2",
|
||||||
|
"@vue/compiler-core": "3.5.31",
|
||||||
|
"@vue/compiler-dom": "3.5.31",
|
||||||
|
"@vue/compiler-ssr": "3.5.31",
|
||||||
|
"@vue/shared": "3.5.31",
|
||||||
|
"estree-walker": "^2.0.2",
|
||||||
|
"magic-string": "^0.30.21",
|
||||||
|
"postcss": "^8.5.8",
|
||||||
|
"source-map-js": "^1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/compiler-ssr": {
|
||||||
|
"version": "3.5.31",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.31.tgz",
|
||||||
|
"integrity": "sha512-h0xIMxrt/LHOvJKMri+vdYT92BrK3HFLtDqq9Pr/lVVfE4IyKZKvWf0vJFW10Yr6nX02OR4MkJwI0c1HDa1hog==",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/compiler-dom": "3.5.31",
|
||||||
|
"@vue/shared": "3.5.31"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/compiler-vue2": {
|
||||||
|
"version": "2.7.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz",
|
||||||
|
"integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"de-indent": "^1.0.2",
|
||||||
|
"he": "^1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/devtools-api": {
|
||||||
|
"version": "6.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
|
||||||
|
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="
|
||||||
|
},
|
||||||
|
"node_modules/@vue/language-core": {
|
||||||
|
"version": "2.2.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.12.tgz",
|
||||||
|
"integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@volar/language-core": "2.4.15",
|
||||||
|
"@vue/compiler-dom": "^3.5.0",
|
||||||
|
"@vue/compiler-vue2": "^2.7.16",
|
||||||
|
"@vue/shared": "^3.5.0",
|
||||||
|
"alien-signals": "^1.0.3",
|
||||||
|
"minimatch": "^9.0.3",
|
||||||
|
"muggle-string": "^0.4.1",
|
||||||
|
"path-browserify": "^1.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/reactivity": {
|
||||||
|
"version": "3.5.31",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.31.tgz",
|
||||||
|
"integrity": "sha512-DtKXxk9E/KuVvt8VxWu+6Luc9I9ETNcqR1T1oW1gf02nXaZ1kuAx58oVu7uX9XxJR0iJCro6fqBLw9oSBELo5g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/shared": "3.5.31"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/runtime-core": {
|
||||||
|
"version": "3.5.31",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.31.tgz",
|
||||||
|
"integrity": "sha512-AZPmIHXEAyhpkmN7aWlqjSfYynmkWlluDNPHMCZKFHH+lLtxP/30UJmoVhXmbDoP1Ng0jG0fyY2zCj1PnSSA6Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/reactivity": "3.5.31",
|
||||||
|
"@vue/shared": "3.5.31"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/runtime-dom": {
|
||||||
|
"version": "3.5.31",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.31.tgz",
|
||||||
|
"integrity": "sha512-xQJsNRmGPeDCJq/u813tyonNgWBFjzfVkBwDREdEWndBnGdHLHgkwNBQxLtg4zDrzKTEcnikUy1UUNecb3lJ6g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/reactivity": "3.5.31",
|
||||||
|
"@vue/runtime-core": "3.5.31",
|
||||||
|
"@vue/shared": "3.5.31",
|
||||||
|
"csstype": "^3.2.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/server-renderer": {
|
||||||
|
"version": "3.5.31",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.31.tgz",
|
||||||
|
"integrity": "sha512-GJuwRvMcdZX/CriUnyIIOGkx3rMV3H6sOu0JhdKbduaeCji6zb60iOGMY7tFoN24NfsUYoFBhshZtGxGpxO4iA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/compiler-ssr": "3.5.31",
|
||||||
|
"@vue/shared": "3.5.31"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "3.5.31"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/shared": {
|
||||||
|
"version": "3.5.31",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.31.tgz",
|
||||||
|
"integrity": "sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw=="
|
||||||
|
},
|
||||||
|
"node_modules/@vue/tsconfig": {
|
||||||
|
"version": "0.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.5.1.tgz",
|
||||||
|
"integrity": "sha512-VcZK7MvpjuTPx2w6blwnwZAu5/LgBUtejFOi3pPGQFXQN5Ela03FUtd2Qtg4yWGGissVL0dr6Ro1LfOFh+PCuQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/alien-signals": {
|
||||||
|
"version": "1.0.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz",
|
||||||
|
"integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||||
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz",
|
||||||
|
"integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.11",
|
||||||
|
"form-data": "^4.0.5",
|
||||||
|
"proxy-from-env": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/balanced-match": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/brace-expansion": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/call-bind-apply-helpers": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/csstype": {
|
||||||
|
"version": "3.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="
|
||||||
|
},
|
||||||
|
"node_modules/de-indent": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dunder-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"gopd": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/entities": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-define-property": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-errors": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-object-atoms": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-set-tostringtag": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.6",
|
||||||
|
"has-tostringtag": "^1.0.2",
|
||||||
|
"hasown": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"bin": {
|
||||||
|
"esbuild": "bin/esbuild"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@esbuild/aix-ppc64": "0.21.5",
|
||||||
|
"@esbuild/android-arm": "0.21.5",
|
||||||
|
"@esbuild/android-arm64": "0.21.5",
|
||||||
|
"@esbuild/android-x64": "0.21.5",
|
||||||
|
"@esbuild/darwin-arm64": "0.21.5",
|
||||||
|
"@esbuild/darwin-x64": "0.21.5",
|
||||||
|
"@esbuild/freebsd-arm64": "0.21.5",
|
||||||
|
"@esbuild/freebsd-x64": "0.21.5",
|
||||||
|
"@esbuild/linux-arm": "0.21.5",
|
||||||
|
"@esbuild/linux-arm64": "0.21.5",
|
||||||
|
"@esbuild/linux-ia32": "0.21.5",
|
||||||
|
"@esbuild/linux-loong64": "0.21.5",
|
||||||
|
"@esbuild/linux-mips64el": "0.21.5",
|
||||||
|
"@esbuild/linux-ppc64": "0.21.5",
|
||||||
|
"@esbuild/linux-riscv64": "0.21.5",
|
||||||
|
"@esbuild/linux-s390x": "0.21.5",
|
||||||
|
"@esbuild/linux-x64": "0.21.5",
|
||||||
|
"@esbuild/netbsd-x64": "0.21.5",
|
||||||
|
"@esbuild/openbsd-x64": "0.21.5",
|
||||||
|
"@esbuild/sunos-x64": "0.21.5",
|
||||||
|
"@esbuild/win32-arm64": "0.21.5",
|
||||||
|
"@esbuild/win32-ia32": "0.21.5",
|
||||||
|
"@esbuild/win32-x64": "0.21.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/estree-walker": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
|
||||||
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.15.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||||
|
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "4.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||||
|
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"es-set-tostringtag": "^2.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/function-bind": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-intrinsic": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"es-define-property": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"es-object-atoms": "^1.1.1",
|
||||||
|
"function-bind": "^1.1.2",
|
||||||
|
"get-proto": "^1.0.1",
|
||||||
|
"gopd": "^1.2.0",
|
||||||
|
"has-symbols": "^1.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"math-intrinsics": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
|
"dependencies": {
|
||||||
|
"dunder-proto": "^1.0.1",
|
||||||
|
"es-object-atoms": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/gopd": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-symbols": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-tostringtag": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
|
"dependencies": {
|
||||||
|
"has-symbols": "^1.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hasown": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/he": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"he": "bin/he"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/magic-string": {
|
||||||
|
"version": "0.30.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
|
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/math-intrinsics": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/minimatch": {
|
||||||
|
"version": "9.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
||||||
|
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16 || 14 >=14.17"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/muggle-string": {
|
||||||
|
"version": "0.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
|
||||||
|
"integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/nanoid": {
|
||||||
|
"version": "3.3.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
|
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"bin": {
|
||||||
|
"nanoid": "bin/nanoid.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/path-browserify": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/picocolors": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
|
||||||
|
},
|
||||||
|
"node_modules/pinia": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/devtools-api": "^6.6.3",
|
||||||
|
"vue-demi": "^0.14.10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/posva"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=4.4.4",
|
||||||
|
"vue": "^2.7.0 || ^3.5.11"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/postcss": {
|
||||||
|
"version": "8.5.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
|
||||||
|
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/postcss/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tidelift",
|
||||||
|
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"nanoid": "^3.3.11",
|
||||||
|
"picocolors": "^1.1.1",
|
||||||
|
"source-map-js": "^1.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || >=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rollup": {
|
||||||
|
"version": "4.60.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
|
||||||
|
"integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/estree": "1.0.8"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"rollup": "dist/bin/rollup"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0",
|
||||||
|
"npm": ">=8.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@rollup/rollup-android-arm-eabi": "4.60.1",
|
||||||
|
"@rollup/rollup-android-arm64": "4.60.1",
|
||||||
|
"@rollup/rollup-darwin-arm64": "4.60.1",
|
||||||
|
"@rollup/rollup-darwin-x64": "4.60.1",
|
||||||
|
"@rollup/rollup-freebsd-arm64": "4.60.1",
|
||||||
|
"@rollup/rollup-freebsd-x64": "4.60.1",
|
||||||
|
"@rollup/rollup-linux-arm-gnueabihf": "4.60.1",
|
||||||
|
"@rollup/rollup-linux-arm-musleabihf": "4.60.1",
|
||||||
|
"@rollup/rollup-linux-arm64-gnu": "4.60.1",
|
||||||
|
"@rollup/rollup-linux-arm64-musl": "4.60.1",
|
||||||
|
"@rollup/rollup-linux-loong64-gnu": "4.60.1",
|
||||||
|
"@rollup/rollup-linux-loong64-musl": "4.60.1",
|
||||||
|
"@rollup/rollup-linux-ppc64-gnu": "4.60.1",
|
||||||
|
"@rollup/rollup-linux-ppc64-musl": "4.60.1",
|
||||||
|
"@rollup/rollup-linux-riscv64-gnu": "4.60.1",
|
||||||
|
"@rollup/rollup-linux-riscv64-musl": "4.60.1",
|
||||||
|
"@rollup/rollup-linux-s390x-gnu": "4.60.1",
|
||||||
|
"@rollup/rollup-linux-x64-gnu": "4.60.1",
|
||||||
|
"@rollup/rollup-linux-x64-musl": "4.60.1",
|
||||||
|
"@rollup/rollup-openbsd-x64": "4.60.1",
|
||||||
|
"@rollup/rollup-openharmony-arm64": "4.60.1",
|
||||||
|
"@rollup/rollup-win32-arm64-msvc": "4.60.1",
|
||||||
|
"@rollup/rollup-win32-ia32-msvc": "4.60.1",
|
||||||
|
"@rollup/rollup-win32-x64-gnu": "4.60.1",
|
||||||
|
"@rollup/rollup-win32-x64-msvc": "4.60.1",
|
||||||
|
"fsevents": "~2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/source-map-js": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
|
||||||
|
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
|
||||||
|
"devOptional": true,
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "6.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/vite": {
|
||||||
|
"version": "5.4.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
|
||||||
|
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"esbuild": "^0.21.3",
|
||||||
|
"postcss": "^8.4.43",
|
||||||
|
"rollup": "^4.20.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"vite": "bin/vite.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.0.0 || >=20.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/node": "^18.0.0 || >=20.0.0",
|
||||||
|
"less": "*",
|
||||||
|
"lightningcss": "^1.21.0",
|
||||||
|
"sass": "*",
|
||||||
|
"sass-embedded": "*",
|
||||||
|
"stylus": "*",
|
||||||
|
"sugarss": "*",
|
||||||
|
"terser": "^5.4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"less": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"lightningcss": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sass": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sass-embedded": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"stylus": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sugarss": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"terser": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vscode-uri": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/vue": {
|
||||||
|
"version": "3.5.31",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.31.tgz",
|
||||||
|
"integrity": "sha512-iV/sU9SzOlmA/0tygSmjkEN6Jbs3nPoIPFhCMLD2STrjgOU8DX7ZtzMhg4ahVwf5Rp9KoFzcXeB1ZrVbLBp5/Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/compiler-dom": "3.5.31",
|
||||||
|
"@vue/compiler-sfc": "3.5.31",
|
||||||
|
"@vue/runtime-dom": "3.5.31",
|
||||||
|
"@vue/server-renderer": "3.5.31",
|
||||||
|
"@vue/shared": "3.5.31"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vue-demi": {
|
||||||
|
"version": "0.14.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
|
||||||
|
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"bin": {
|
||||||
|
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||||
|
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@vue/composition-api": "^1.0.0-rc.1",
|
||||||
|
"vue": "^3.0.0-0 || ^2.6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@vue/composition-api": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vue-router": {
|
||||||
|
"version": "4.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
|
||||||
|
"integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/devtools-api": "^6.6.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/posva"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vue-tsc": {
|
||||||
|
"version": "2.2.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz",
|
||||||
|
"integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@volar/typescript": "2.4.15",
|
||||||
|
"@vue/language-core": "2.2.12"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"vue-tsc": "bin/vue-tsc.js"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=5.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
frontend/node_modules/.vite/deps/_metadata.json
generated
vendored
Normal file
43
frontend/node_modules/.vite/deps/_metadata.json
generated
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"hash": "29abdcf6",
|
||||||
|
"configHash": "1fc6cf50",
|
||||||
|
"lockfileHash": "a08e29ca",
|
||||||
|
"browserHash": "8b56a248",
|
||||||
|
"optimized": {
|
||||||
|
"axios": {
|
||||||
|
"src": "../../axios/index.js",
|
||||||
|
"file": "axios.js",
|
||||||
|
"fileHash": "4b12b8e3",
|
||||||
|
"needsInterop": false
|
||||||
|
},
|
||||||
|
"pinia": {
|
||||||
|
"src": "../../pinia/dist/pinia.mjs",
|
||||||
|
"file": "pinia.js",
|
||||||
|
"fileHash": "b2eb75d2",
|
||||||
|
"needsInterop": false
|
||||||
|
},
|
||||||
|
"vue": {
|
||||||
|
"src": "../../vue/dist/vue.runtime.esm-bundler.js",
|
||||||
|
"file": "vue.js",
|
||||||
|
"fileHash": "2a5e5f78",
|
||||||
|
"needsInterop": false
|
||||||
|
},
|
||||||
|
"vue-router": {
|
||||||
|
"src": "../../vue-router/dist/vue-router.mjs",
|
||||||
|
"file": "vue-router.js",
|
||||||
|
"fileHash": "657375ef",
|
||||||
|
"needsInterop": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"chunks": {
|
||||||
|
"chunk-YFT6OQ5R": {
|
||||||
|
"file": "chunk-YFT6OQ5R.js"
|
||||||
|
},
|
||||||
|
"chunk-4VAHRDA3": {
|
||||||
|
"file": "chunk-4VAHRDA3.js"
|
||||||
|
},
|
||||||
|
"chunk-PZ5AY32C": {
|
||||||
|
"file": "chunk-PZ5AY32C.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2753
frontend/node_modules/.vite/deps/axios.js
generated
vendored
Normal file
2753
frontend/node_modules/.vite/deps/axios.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
7
frontend/node_modules/.vite/deps/axios.js.map
generated
vendored
Normal file
7
frontend/node_modules/.vite/deps/axios.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
12984
frontend/node_modules/.vite/deps/chunk-4VAHRDA3.js
generated
vendored
Normal file
12984
frontend/node_modules/.vite/deps/chunk-4VAHRDA3.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
7
frontend/node_modules/.vite/deps/chunk-4VAHRDA3.js.map
generated
vendored
Normal file
7
frontend/node_modules/.vite/deps/chunk-4VAHRDA3.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
10
frontend/node_modules/.vite/deps/chunk-PZ5AY32C.js
generated
vendored
Normal file
10
frontend/node_modules/.vite/deps/chunk-PZ5AY32C.js
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
__export
|
||||||
|
};
|
||||||
|
//# sourceMappingURL=chunk-PZ5AY32C.js.map
|
||||||
7
frontend/node_modules/.vite/deps/chunk-PZ5AY32C.js.map
generated
vendored
Normal file
7
frontend/node_modules/.vite/deps/chunk-PZ5AY32C.js.map
generated
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"sources": [],
|
||||||
|
"sourcesContent": [],
|
||||||
|
"mappings": "",
|
||||||
|
"names": []
|
||||||
|
}
|
||||||
162
frontend/node_modules/.vite/deps/chunk-YFT6OQ5R.js
generated
vendored
Normal file
162
frontend/node_modules/.vite/deps/chunk-YFT6OQ5R.js
generated
vendored
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
// node_modules/@vue/devtools-api/lib/esm/env.js
|
||||||
|
function getDevtoolsGlobalHook() {
|
||||||
|
return getTarget().__VUE_DEVTOOLS_GLOBAL_HOOK__;
|
||||||
|
}
|
||||||
|
function getTarget() {
|
||||||
|
return typeof navigator !== "undefined" && typeof window !== "undefined" ? window : typeof globalThis !== "undefined" ? globalThis : {};
|
||||||
|
}
|
||||||
|
var isProxyAvailable = typeof Proxy === "function";
|
||||||
|
|
||||||
|
// node_modules/@vue/devtools-api/lib/esm/const.js
|
||||||
|
var HOOK_SETUP = "devtools-plugin:setup";
|
||||||
|
var HOOK_PLUGIN_SETTINGS_SET = "plugin:settings:set";
|
||||||
|
|
||||||
|
// node_modules/@vue/devtools-api/lib/esm/time.js
|
||||||
|
var supported;
|
||||||
|
var perf;
|
||||||
|
function isPerformanceSupported() {
|
||||||
|
var _a;
|
||||||
|
if (supported !== void 0) {
|
||||||
|
return supported;
|
||||||
|
}
|
||||||
|
if (typeof window !== "undefined" && window.performance) {
|
||||||
|
supported = true;
|
||||||
|
perf = window.performance;
|
||||||
|
} else if (typeof globalThis !== "undefined" && ((_a = globalThis.perf_hooks) === null || _a === void 0 ? void 0 : _a.performance)) {
|
||||||
|
supported = true;
|
||||||
|
perf = globalThis.perf_hooks.performance;
|
||||||
|
} else {
|
||||||
|
supported = false;
|
||||||
|
}
|
||||||
|
return supported;
|
||||||
|
}
|
||||||
|
function now() {
|
||||||
|
return isPerformanceSupported() ? perf.now() : Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
// node_modules/@vue/devtools-api/lib/esm/proxy.js
|
||||||
|
var ApiProxy = class {
|
||||||
|
constructor(plugin, hook) {
|
||||||
|
this.target = null;
|
||||||
|
this.targetQueue = [];
|
||||||
|
this.onQueue = [];
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.hook = hook;
|
||||||
|
const defaultSettings = {};
|
||||||
|
if (plugin.settings) {
|
||||||
|
for (const id in plugin.settings) {
|
||||||
|
const item = plugin.settings[id];
|
||||||
|
defaultSettings[id] = item.defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const localSettingsSaveId = `__vue-devtools-plugin-settings__${plugin.id}`;
|
||||||
|
let currentSettings = Object.assign({}, defaultSettings);
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(localSettingsSaveId);
|
||||||
|
const data = JSON.parse(raw);
|
||||||
|
Object.assign(currentSettings, data);
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
this.fallbacks = {
|
||||||
|
getSettings() {
|
||||||
|
return currentSettings;
|
||||||
|
},
|
||||||
|
setSettings(value) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(localSettingsSaveId, JSON.stringify(value));
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
currentSettings = value;
|
||||||
|
},
|
||||||
|
now() {
|
||||||
|
return now();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (hook) {
|
||||||
|
hook.on(HOOK_PLUGIN_SETTINGS_SET, (pluginId, value) => {
|
||||||
|
if (pluginId === this.plugin.id) {
|
||||||
|
this.fallbacks.setSettings(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.proxiedOn = new Proxy({}, {
|
||||||
|
get: (_target, prop) => {
|
||||||
|
if (this.target) {
|
||||||
|
return this.target.on[prop];
|
||||||
|
} else {
|
||||||
|
return (...args) => {
|
||||||
|
this.onQueue.push({
|
||||||
|
method: prop,
|
||||||
|
args
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.proxiedTarget = new Proxy({}, {
|
||||||
|
get: (_target, prop) => {
|
||||||
|
if (this.target) {
|
||||||
|
return this.target[prop];
|
||||||
|
} else if (prop === "on") {
|
||||||
|
return this.proxiedOn;
|
||||||
|
} else if (Object.keys(this.fallbacks).includes(prop)) {
|
||||||
|
return (...args) => {
|
||||||
|
this.targetQueue.push({
|
||||||
|
method: prop,
|
||||||
|
args,
|
||||||
|
resolve: () => {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return this.fallbacks[prop](...args);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return (...args) => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.targetQueue.push({
|
||||||
|
method: prop,
|
||||||
|
args,
|
||||||
|
resolve
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async setRealTarget(target) {
|
||||||
|
this.target = target;
|
||||||
|
for (const item of this.onQueue) {
|
||||||
|
this.target.on[item.method](...item.args);
|
||||||
|
}
|
||||||
|
for (const item of this.targetQueue) {
|
||||||
|
item.resolve(await this.target[item.method](...item.args));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// node_modules/@vue/devtools-api/lib/esm/index.js
|
||||||
|
function setupDevtoolsPlugin(pluginDescriptor, setupFn) {
|
||||||
|
const descriptor = pluginDescriptor;
|
||||||
|
const target = getTarget();
|
||||||
|
const hook = getDevtoolsGlobalHook();
|
||||||
|
const enableProxy = isProxyAvailable && descriptor.enableEarlyProxy;
|
||||||
|
if (hook && (target.__VUE_DEVTOOLS_PLUGIN_API_AVAILABLE__ || !enableProxy)) {
|
||||||
|
hook.emit(HOOK_SETUP, pluginDescriptor, setupFn);
|
||||||
|
} else {
|
||||||
|
const proxy = enableProxy ? new ApiProxy(descriptor, hook) : null;
|
||||||
|
const list = target.__VUE_DEVTOOLS_PLUGINS__ = target.__VUE_DEVTOOLS_PLUGINS__ || [];
|
||||||
|
list.push({
|
||||||
|
pluginDescriptor: descriptor,
|
||||||
|
setupFn,
|
||||||
|
proxy
|
||||||
|
});
|
||||||
|
if (proxy) {
|
||||||
|
setupFn(proxy.proxiedTarget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
setupDevtoolsPlugin
|
||||||
|
};
|
||||||
|
//# sourceMappingURL=chunk-YFT6OQ5R.js.map
|
||||||
7
frontend/node_modules/.vite/deps/chunk-YFT6OQ5R.js.map
generated
vendored
Normal file
7
frontend/node_modules/.vite/deps/chunk-YFT6OQ5R.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
3
frontend/node_modules/.vite/deps/package.json
generated
vendored
Normal file
3
frontend/node_modules/.vite/deps/package.json
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
1561
frontend/node_modules/.vite/deps/pinia.js
generated
vendored
Normal file
1561
frontend/node_modules/.vite/deps/pinia.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
7
frontend/node_modules/.vite/deps/pinia.js.map
generated
vendored
Normal file
7
frontend/node_modules/.vite/deps/pinia.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
2247
frontend/node_modules/.vite/deps/vue-router.js
generated
vendored
Normal file
2247
frontend/node_modules/.vite/deps/vue-router.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user