Enhance project setup and configuration for RolePlay application
- Update .gitignore to include additional build outputs and IDE files. - Modify pom.xml to add new plugin versions and configurations for frontend and OpenAPI TypeScript client generation. - Introduce project guidelines in roleplay-project.mdc, detailing architecture, coding standards, and testing practices. - Add initial documentation for the RolePlay concept and specifications. - Implement catch-all JAX-RS resource for serving the React application and establish API base path in application.yml. - Create foundational web components and TypeScript configuration for the frontend application.
This commit is contained in:
parent
e8bb6a64b7
commit
f91604aea6
69
.cursor/rules/roleplay-project.mdc
Normal file
69
.cursor/rules/roleplay-project.mdc
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
description: RolePlay project context, architecture, and coding standards (Java/Quarkus, Maven, frontend)
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# RolePlay – Project Instructions
|
||||||
|
|
||||||
|
## Project context
|
||||||
|
|
||||||
|
- **Project**: RolePlay
|
||||||
|
- **Java**: 21
|
||||||
|
- **Build**: Maven
|
||||||
|
- **Framework**: Quarkus (latest stable)
|
||||||
|
- **GroupId**: de.neitzel | **ArtifactId**: roleplay
|
||||||
|
- **Base package**: `de.neitzel.roleplay`
|
||||||
|
- **Sub-packages**: `business`, `common`, `data`, `fascade`
|
||||||
|
- Startup code lives in the base package.
|
||||||
|
- **DB migrations**: Liquibase; scripts in `src/main/resources/db/migration`.
|
||||||
|
|
||||||
|
## Architecture and package rules
|
||||||
|
|
||||||
|
- **Startup/bootstrap**: `de.neitzel.roleplay`
|
||||||
|
- **Business logic**: `de.neitzel.roleplay.business`
|
||||||
|
- **Shared utilities, cross-cutting types**: `de.neitzel.roleplay.common`
|
||||||
|
- **Persistence / data access**: `de.neitzel.roleplay.data`
|
||||||
|
- **External-facing facades (REST, API)**: `de.neitzel.roleplay.fascade`
|
||||||
|
- Keep clear package boundaries; avoid circular dependencies.
|
||||||
|
|
||||||
|
## Coding standards (Java)
|
||||||
|
|
||||||
|
- Use Lombok to reduce boilerplate where it helps.
|
||||||
|
- Prefer immutability: `final` fields, constructor injection.
|
||||||
|
- Use Quarkus idioms; avoid heavyweight frameworks.
|
||||||
|
- Keep methods small and focused; one responsibility per class.
|
||||||
|
- Use Java 21 features when they improve clarity.
|
||||||
|
- Add concise comments only for non-obvious logic.
|
||||||
|
- All classes, fields, and methods (including private) must have JavaDoc describing purpose and usage.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- Add or update unit tests for new or changed logic.
|
||||||
|
- Use JUnit 5 and Mockito; Arrange–Act–Assert; keep tests deterministic.
|
||||||
|
- Mock external dependencies; add integration tests only when requested.
|
||||||
|
|
||||||
|
## Maven and dependencies
|
||||||
|
|
||||||
|
- Target Java 21 in the Maven toolchain.
|
||||||
|
- Use the latest stable Quarkus platform BOM.
|
||||||
|
- Keep dependencies minimal; no new libraries without need.
|
||||||
|
- Keep versions up to date (especially for security).
|
||||||
|
- Put plugin and dependency versions in the `properties` section of `pom.xml`.
|
||||||
|
- Use `quarkus-maven-plugin` for build/run; `maven-surefire-plugin` for tests (JUnit 5); `maven-compiler-plugin` with Java 21.
|
||||||
|
|
||||||
|
## Frontend (React / Vite)
|
||||||
|
|
||||||
|
- Frontend lives under `src/main/web` (React, TypeScript, Vite, MUI).
|
||||||
|
- OpenAPI TypeScript client is generated into `src/main/web/src/api/generated/` by Maven (`mvn generate-sources`); do not edit by hand.
|
||||||
|
- Production build output: `target/web-dist`; copied to `META-INF/resources` by `maven-resources-plugin` for the Quarkus SPA.
|
||||||
|
|
||||||
|
## Do and don’t
|
||||||
|
|
||||||
|
- **Do**: keep code in the correct package; add tests for business logic.
|
||||||
|
- **Don’t**: put startup code outside the base package; add unnecessary abstraction layers.
|
||||||
|
|
||||||
|
## Output expectations
|
||||||
|
|
||||||
|
- Provide complete, compilable code with package declarations and imports.
|
||||||
|
- Prefer Quarkus-friendly APIs and annotations.
|
||||||
|
- When adding or changing logic, update or add unit tests.
|
||||||
19
.gitignore
vendored
19
.gitignore
vendored
@ -1,2 +1,21 @@
|
|||||||
|
# Maven build output
|
||||||
/target/
|
/target/
|
||||||
|
|
||||||
|
# Vite build output (written into resources so it gets into the JAR)
|
||||||
|
src/main/resources/META-INF/resources/
|
||||||
|
|
||||||
|
# IDE
|
||||||
/.idea/
|
/.idea/
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
# Node dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# OpenAPI-generated TypeScript client (regenerated by mvn generate-sources)
|
||||||
|
src/main/web/src/api/generated/
|
||||||
|
|
||||||
|
# TypeScript incremental build cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# OS metadata
|
||||||
|
.DS_Store
|
||||||
|
|||||||
66
pom.xml
66
pom.xml
@ -11,6 +11,7 @@
|
|||||||
<properties>
|
<properties>
|
||||||
<java.version>21</java.version>
|
<java.version>21</java.version>
|
||||||
<maven.compiler.plugin.version>3.12.1</maven.compiler.plugin.version>
|
<maven.compiler.plugin.version>3.12.1</maven.compiler.plugin.version>
|
||||||
|
<maven.resources.plugin.version>3.3.1</maven.resources.plugin.version>
|
||||||
<maven.surefire.plugin.version>3.2.5</maven.surefire.plugin.version>
|
<maven.surefire.plugin.version>3.2.5</maven.surefire.plugin.version>
|
||||||
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
|
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
|
||||||
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
|
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
|
||||||
@ -20,6 +21,9 @@
|
|||||||
<junit.jupiter.version>5.10.3</junit.jupiter.version>
|
<junit.jupiter.version>5.10.3</junit.jupiter.version>
|
||||||
<mockito.version>5.12.0</mockito.version>
|
<mockito.version>5.12.0</mockito.version>
|
||||||
<openapi.generator.version>7.11.0</openapi.generator.version>
|
<openapi.generator.version>7.11.0</openapi.generator.version>
|
||||||
|
<frontend.plugin.version>1.15.1</frontend.plugin.version>
|
||||||
|
<node.version>v22.13.1</node.version>
|
||||||
|
<npm.version>10.9.2</npm.version>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
@ -139,6 +143,26 @@
|
|||||||
<generateSupportingFiles>false</generateSupportingFiles>
|
<generateSupportingFiles>false</generateSupportingFiles>
|
||||||
</configuration>
|
</configuration>
|
||||||
</execution>
|
</execution>
|
||||||
|
<execution>
|
||||||
|
<id>generate-typescript-client</id>
|
||||||
|
<goals>
|
||||||
|
<goal>generate</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<inputSpec>${project.basedir}/src/main/resources/openapi-roleplay-public-v1.yml</inputSpec>
|
||||||
|
<generatorName>typescript-fetch</generatorName>
|
||||||
|
<output>${project.basedir}/src/main/web/src/api/generated</output>
|
||||||
|
<configOptions>
|
||||||
|
<supportsES6>true</supportsES6>
|
||||||
|
<useSingleRequestParameter>true</useSingleRequestParameter>
|
||||||
|
<enumPropertyNaming>original</enumPropertyNaming>
|
||||||
|
<modelPropertyNaming>camelCase</modelPropertyNaming>
|
||||||
|
<stringEnums>true</stringEnums>
|
||||||
|
<withInterfaces>false</withInterfaces>
|
||||||
|
</configOptions>
|
||||||
|
<generateSupportingFiles>true</generateSupportingFiles>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
</executions>
|
</executions>
|
||||||
</plugin>
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
@ -164,6 +188,48 @@
|
|||||||
<argLine>-Dnet.bytebuddy.experimental=true</argLine>
|
<argLine>-Dnet.bytebuddy.experimental=true</argLine>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>com.github.eirslett</groupId>
|
||||||
|
<artifactId>frontend-maven-plugin</artifactId>
|
||||||
|
<version>${frontend.plugin.version}</version>
|
||||||
|
<configuration>
|
||||||
|
<workingDirectory>src/main/web</workingDirectory>
|
||||||
|
<installDirectory>${project.build.directory}/node</installDirectory>
|
||||||
|
</configuration>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>install-node-and-npm</id>
|
||||||
|
<phase>initialize</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>install-node-and-npm</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<nodeVersion>${node.version}</nodeVersion>
|
||||||
|
<npmVersion>${npm.version}</npmVersion>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
<execution>
|
||||||
|
<id>npm-install</id>
|
||||||
|
<phase>generate-resources</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>npm</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<arguments>install</arguments>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
<execution>
|
||||||
|
<id>npm-build</id>
|
||||||
|
<phase>generate-resources</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>npm</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<arguments>run build</arguments>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@ -0,0 +1,69 @@
|
|||||||
|
package de.neitzel.roleplay.fascade;
|
||||||
|
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.PathParam;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import jakarta.ws.rs.core.Response;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Catch-all JAX-RS resource that serves {@code index.html} for every path
|
||||||
|
* that is not handled by a more specific resource. This enables React Router's
|
||||||
|
* client-side routing: when the user navigates directly to
|
||||||
|
* {@code /session/abc123} the browser receives {@code index.html} and React
|
||||||
|
* Router takes over.
|
||||||
|
*
|
||||||
|
* <p>JAX-RS selects the most specific matching path first, so all
|
||||||
|
* {@code /api/v1/...} routes defined by the generated API interfaces always
|
||||||
|
* take priority over this catch-all.
|
||||||
|
*/
|
||||||
|
@Path("/")
|
||||||
|
public class SpaFallbackResource {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code index.html} for the application root.
|
||||||
|
*
|
||||||
|
* @return 200 response with the React application shell
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Produces(MediaType.TEXT_HTML)
|
||||||
|
public Response index() {
|
||||||
|
return serveIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads {@code index.html} from {@code META-INF/resources} on the
|
||||||
|
* classpath and streams it as the response body.
|
||||||
|
*
|
||||||
|
* @return 200 with index.html, or 404 if the file is not on the classpath
|
||||||
|
*/
|
||||||
|
private Response serveIndex() {
|
||||||
|
InputStream stream = getClass()
|
||||||
|
.getClassLoader()
|
||||||
|
.getResourceAsStream("META-INF/resources/index.html");
|
||||||
|
if (stream == null) {
|
||||||
|
return Response.status(Response.Status.NOT_FOUND)
|
||||||
|
.entity("index.html not found – run 'mvn process-resources' first.")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
return Response.ok(stream, MediaType.TEXT_HTML).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code index.html} for any sub-path not matched by a more
|
||||||
|
* specific JAX-RS resource.
|
||||||
|
*
|
||||||
|
* @param path the unmatched path segment (ignored)
|
||||||
|
* @return 200 response with the React application shell
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Path("{path:(?!api/).*}")
|
||||||
|
@Produces(MediaType.TEXT_HTML)
|
||||||
|
public Response fallback(@PathParam("path") String path) {
|
||||||
|
return serveIndex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -9,5 +9,5 @@ quarkus:
|
|||||||
url: http://debian:11434
|
url: http://debian:11434
|
||||||
connect-timeout: 5000
|
connect-timeout: 5000
|
||||||
read-timeout: 120000
|
read-timeout: 120000
|
||||||
http:
|
rest:
|
||||||
root-path: /api/v1
|
path: /api/v1
|
||||||
|
|||||||
12
src/main/web/index.html
Normal file
12
src/main/web/index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>RolePlay</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2624
src/main/web/package-lock.json
generated
Normal file
2624
src/main/web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "roleplay-web",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.13.5",
|
||||||
|
"@emotion/styled": "^11.13.5",
|
||||||
|
"@mui/icons-material": "^6.4.4",
|
||||||
|
"@mui/material": "^6.4.4",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-router-dom": "^7.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^19.0.8",
|
||||||
|
"@types/react-dom": "^19.0.3",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"typescript": "~5.7.2",
|
||||||
|
"vite": "^6.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
import {BrowserRouter, Route, Routes} from 'react-router-dom'
|
||||||
|
import StartPage from './pages/StartPage'
|
||||||
|
import SessionPage from './pages/SessionPage'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Root application component. Sets up client-side routing between the model
|
||||||
|
* selection page and the active session page.
|
||||||
|
*/
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<BrowserRouter>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<StartPage/>}/>
|
||||||
|
<Route path="/session/:sessionId" element={<SessionPage/>}/>
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* Central re-export of the generated OpenAPI client and its models.
|
||||||
|
*
|
||||||
|
* The actual sources under ./generated/ are produced by
|
||||||
|
* openapi-generator-maven-plugin (typescript-fetch generator) during
|
||||||
|
* `mvn generate-sources`. Run `mvn generate-sources` once before opening
|
||||||
|
* the project in an IDE to make these imports resolve.
|
||||||
|
*/
|
||||||
|
export * from './generated/index'
|
||||||
|
|
||||||
|
/** Base URL used by the generated fetch client. Always points to /api/v1. */
|
||||||
|
export const API_BASE = '/api/v1'
|
||||||
@ -0,0 +1,156 @@
|
|||||||
|
import {useState} from 'react'
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionDetails,
|
||||||
|
AccordionSummary,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
CircularProgress,
|
||||||
|
TextField,
|
||||||
|
ToggleButton,
|
||||||
|
ToggleButtonGroup,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/material'
|
||||||
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
||||||
|
import RecordVoiceOverIcon from '@mui/icons-material/RecordVoiceOver'
|
||||||
|
import DirectionsRunIcon from '@mui/icons-material/DirectionsRun'
|
||||||
|
import SendIcon from '@mui/icons-material/Send'
|
||||||
|
import type {RecommendationRequest, TurnRequest} from '../api/generated/index'
|
||||||
|
import {UserActionRequestTypeEnum,} from '../api/generated/index'
|
||||||
|
|
||||||
|
/***** Props for the ActionInput component. */
|
||||||
|
interface ActionInputProps {
|
||||||
|
/** Called when the user submits an action. */
|
||||||
|
onSubmit: (request: TurnRequest) => void
|
||||||
|
/** Whether the submit button should be disabled (e.g. waiting for response). */
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionType = 'speech' | 'action'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides the user interface for composing and submitting a turn action.
|
||||||
|
* Includes a type toggle (speech / action), a free-text input, and an
|
||||||
|
* optional collapsible section for narrative recommendations.
|
||||||
|
*/
|
||||||
|
export default function ActionInput({onSubmit, disabled}: ActionInputProps) {
|
||||||
|
const [actionType, setActionType] = useState<ActionType>('speech')
|
||||||
|
const [content, setContent] = useState<string>('')
|
||||||
|
const [desiredTone, setDesiredTone] = useState<string>('')
|
||||||
|
const [preferredDirection, setPreferredDirection] = useState<string>('')
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (!content.trim()) return
|
||||||
|
|
||||||
|
const recommendation: RecommendationRequest | undefined =
|
||||||
|
desiredTone.trim() || preferredDirection.trim()
|
||||||
|
? {
|
||||||
|
desiredTone: desiredTone.trim() || undefined,
|
||||||
|
preferredDirection: preferredDirection.trim() || undefined,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
const typeEnum =
|
||||||
|
actionType === 'speech'
|
||||||
|
? UserActionRequestTypeEnum.speech
|
||||||
|
: UserActionRequestTypeEnum.action
|
||||||
|
|
||||||
|
const request: TurnRequest = {
|
||||||
|
userAction: {
|
||||||
|
type: typeEnum,
|
||||||
|
content: content.trim(),
|
||||||
|
},
|
||||||
|
recommendation,
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit(request)
|
||||||
|
setContent('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
||||||
|
handleSubmit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box display="flex" flexDirection="column" gap={2}>
|
||||||
|
<ToggleButtonGroup
|
||||||
|
value={actionType}
|
||||||
|
exclusive
|
||||||
|
onChange={(_, v: ActionType | null) => v && setActionType(v)}
|
||||||
|
size="small"
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<ToggleButton value="speech" aria-label="speech">
|
||||||
|
<RecordVoiceOverIcon sx={{mr: 0.5, fontSize: 18}}/>
|
||||||
|
Speech
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton value="action" aria-label="action">
|
||||||
|
<DirectionsRunIcon sx={{mr: 0.5, fontSize: 18}}/>
|
||||||
|
Action
|
||||||
|
</ToggleButton>
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
multiline
|
||||||
|
minRows={2}
|
||||||
|
maxRows={6}
|
||||||
|
fullWidth
|
||||||
|
value={content}
|
||||||
|
onChange={(e) => setContent(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder={
|
||||||
|
actionType === 'speech'
|
||||||
|
? 'What does your character say?'
|
||||||
|
: 'What does your character do?'
|
||||||
|
}
|
||||||
|
disabled={disabled}
|
||||||
|
helperText="Ctrl+Enter to submit"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Accordion disableGutters elevation={0} sx={{border: '1px solid', borderColor: 'divider', borderRadius: 1}}>
|
||||||
|
<AccordionSummary expandIcon={<ExpandMoreIcon/>}>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Narrative recommendation (optional)
|
||||||
|
</Typography>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<Box display="flex" flexDirection="column" gap={2}>
|
||||||
|
<TextField
|
||||||
|
label="Desired tone"
|
||||||
|
value={desiredTone}
|
||||||
|
onChange={(e) => setDesiredTone(e.target.value)}
|
||||||
|
helperText='e.g. "tense", "humorous", "mysterious"'
|
||||||
|
disabled={disabled}
|
||||||
|
fullWidth
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Preferred direction"
|
||||||
|
value={preferredDirection}
|
||||||
|
onChange={(e) => setPreferredDirection(e.target.value)}
|
||||||
|
helperText="Free-text hint for the narrator"
|
||||||
|
disabled={disabled}
|
||||||
|
fullWidth
|
||||||
|
size="small"
|
||||||
|
multiline
|
||||||
|
maxRows={3}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Box display="flex" justifyContent="flex-end">
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
endIcon={disabled ? <CircularProgress size={16} color="inherit"/> : <SendIcon/>}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={!content.trim() || disabled}
|
||||||
|
>
|
||||||
|
{disabled ? 'Processing…' : 'Submit'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
import {Divider, Paper, Typography} from '@mui/material'
|
||||||
|
|
||||||
|
/** Props for the NarrativeView component. */
|
||||||
|
interface NarrativeViewProps {
|
||||||
|
/** The narrative text to display. */
|
||||||
|
narrative: string
|
||||||
|
/** Current turn number, displayed as subtitle. */
|
||||||
|
turnNumber: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the AI-generated narrative text for the current turn inside a
|
||||||
|
* styled MUI Paper card with atmospheric typography.
|
||||||
|
*/
|
||||||
|
export default function NarrativeView({narrative, turnNumber}: NarrativeViewProps) {
|
||||||
|
return (
|
||||||
|
<Paper elevation={3} sx={{p: 3, borderRadius: 2}}>
|
||||||
|
<Typography variant="overline" color="text.secondary">
|
||||||
|
{turnNumber === 0 ? 'Opening Scene' : `Turn ${turnNumber}`}
|
||||||
|
</Typography>
|
||||||
|
<Divider sx={{my: 1}}/>
|
||||||
|
{narrative.split('\n').filter(Boolean).map((paragraph, i) => (
|
||||||
|
<Typography
|
||||||
|
key={i}
|
||||||
|
variant="body1"
|
||||||
|
paragraph
|
||||||
|
sx={{lineHeight: 1.8, fontStyle: 'italic', mb: 1.5}}
|
||||||
|
>
|
||||||
|
{paragraph}
|
||||||
|
</Typography>
|
||||||
|
))}
|
||||||
|
</Paper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,85 @@
|
|||||||
|
import {Box, Chip, Paper, Typography} from '@mui/material'
|
||||||
|
import type {Suggestion} from '../api/generated/index'
|
||||||
|
|
||||||
|
/** Props for the SuggestionList component. */
|
||||||
|
interface SuggestionListProps {
|
||||||
|
/** Suggestions to display. */
|
||||||
|
suggestions: Suggestion[]
|
||||||
|
/** Called when the user selects a suggestion. */
|
||||||
|
onSelect: (suggestion: Suggestion) => void
|
||||||
|
/** Whether interaction is currently disabled (e.g. while submitting). */
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Colour map from risk level to MUI chip colour. */
|
||||||
|
const riskColour: Record<string, 'success' | 'warning' | 'error'> = {
|
||||||
|
low: 'success',
|
||||||
|
medium: 'warning',
|
||||||
|
high: 'error',
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Icon map from suggestion type to emoji label. */
|
||||||
|
const typeIcon: Record<string, string> = {
|
||||||
|
player_action: '🗡️',
|
||||||
|
world_event: '🌍',
|
||||||
|
npc_action: '🧑',
|
||||||
|
twist: '🌀',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a grid of suggestion cards. Each card can be clicked to select
|
||||||
|
* that suggestion as the user's next action (type = "choice").
|
||||||
|
*/
|
||||||
|
export default function SuggestionList({suggestions, onSelect, disabled}: SuggestionListProps) {
|
||||||
|
if (suggestions.length === 0) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="overline" color="text.secondary" display="block" mb={1}>
|
||||||
|
What could happen next?
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
display="grid"
|
||||||
|
gridTemplateColumns="repeat(auto-fill, minmax(220px, 1fr))"
|
||||||
|
gap={2}
|
||||||
|
>
|
||||||
|
{suggestions.map((s) => (
|
||||||
|
<Paper
|
||||||
|
key={s.id}
|
||||||
|
elevation={2}
|
||||||
|
sx={{
|
||||||
|
p: 2,
|
||||||
|
cursor: disabled ? 'default' : 'pointer',
|
||||||
|
opacity: disabled ? 0.6 : 1,
|
||||||
|
border: '1px solid transparent',
|
||||||
|
transition: 'border-color 0.2s, transform 0.15s',
|
||||||
|
'&:hover': disabled ? {} : {
|
||||||
|
borderColor: 'primary.main',
|
||||||
|
transform: 'translateY(-2px)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClick={() => !disabled && onSelect(s)}
|
||||||
|
>
|
||||||
|
<Box display="flex" justifyContent="space-between" alignItems="flex-start" mb={1}>
|
||||||
|
<Typography variant="body2" fontWeight={700} sx={{flex: 1, pr: 1}}>
|
||||||
|
{typeIcon[s.type] ?? '•'} {s.title}
|
||||||
|
</Typography>
|
||||||
|
{s.riskLevel && (
|
||||||
|
<Chip
|
||||||
|
label={s.riskLevel}
|
||||||
|
color={riskColour[s.riskLevel] ?? 'default'}
|
||||||
|
size="small"
|
||||||
|
sx={{fontSize: '0.65rem', height: 20}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{s.description}
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
import {StrictMode} from 'react'
|
||||||
|
import {createRoot} from 'react-dom/client'
|
||||||
|
import {CssBaseline, ThemeProvider, createTheme} from '@mui/material'
|
||||||
|
import App from './App'
|
||||||
|
|
||||||
|
/** Dark-toned MUI theme that fits the atmospheric role-play genre. */
|
||||||
|
const theme = createTheme({
|
||||||
|
palette: {
|
||||||
|
mode: 'dark',
|
||||||
|
primary: {
|
||||||
|
main: '#b39ddb',
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
main: '#80cbc4',
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
default: '#1a1a2e',
|
||||||
|
paper: '#16213e',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
typography: {
|
||||||
|
fontFamily: '"Georgia", "Palatino", serif',
|
||||||
|
h4: {fontWeight: 700},
|
||||||
|
h5: {fontWeight: 600},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<CssBaseline/>
|
||||||
|
<App/>
|
||||||
|
</ThemeProvider>
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
|
|
||||||
@ -0,0 +1,145 @@
|
|||||||
|
import {useEffect, useState} from 'react'
|
||||||
|
import {useNavigate, useParams} from 'react-router-dom'
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
AppBar,
|
||||||
|
Box,
|
||||||
|
CircularProgress,
|
||||||
|
Container,
|
||||||
|
Divider,
|
||||||
|
IconButton,
|
||||||
|
Toolbar,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/material'
|
||||||
|
import ArrowBackIcon from '@mui/icons-material/ArrowBack'
|
||||||
|
import AutoStoriesIcon from '@mui/icons-material/AutoStories'
|
||||||
|
import type {SessionResponse, Suggestion, TurnRequest} from '../api/generated/index'
|
||||||
|
import {Configuration, SessionsApi, TurnsApi, UserActionRequestTypeEnum,} from '../api/generated/index'
|
||||||
|
import NarrativeView from '../components/NarrativeView'
|
||||||
|
import SuggestionList from '../components/SuggestionList'
|
||||||
|
import ActionInput from '../components/ActionInput'
|
||||||
|
|
||||||
|
const API_BASE = '/api/v1'
|
||||||
|
const sessionsApi = new SessionsApi(new Configuration({basePath: API_BASE}))
|
||||||
|
const turnsApi = new TurnsApi(new Configuration({basePath: API_BASE}))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Active role-play session page. Loads the session state, displays the
|
||||||
|
* current narrative, shows suggestions, and accepts user input for the next
|
||||||
|
* turn.
|
||||||
|
*/
|
||||||
|
export default function SessionPage() {
|
||||||
|
const {sessionId} = useParams<{ sessionId: string }>()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const [session, setSession] = useState<SessionResponse | null>(null)
|
||||||
|
const [narrative, setNarrative] = useState<string>('')
|
||||||
|
const [suggestions, setSuggestions] = useState<Suggestion[]>([])
|
||||||
|
const [turnNumber, setTurnNumber] = useState<number>(0)
|
||||||
|
const [loading, setLoading] = useState<boolean>(true)
|
||||||
|
const [submitting, setSubmitting] = useState<boolean>(false)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
/** Load existing session on mount. */
|
||||||
|
useEffect(() => {
|
||||||
|
if (!sessionId) return
|
||||||
|
sessionsApi.getSession({sessionId})
|
||||||
|
.then((s) => {
|
||||||
|
setSession(s)
|
||||||
|
setNarrative(s.narrative ?? '')
|
||||||
|
setSuggestions(s.suggestions ?? [])
|
||||||
|
setTurnNumber(s.turnNumber)
|
||||||
|
})
|
||||||
|
.catch(() => setError('Session not found or server unavailable.'))
|
||||||
|
.finally(() => setLoading(false))
|
||||||
|
}, [sessionId])
|
||||||
|
|
||||||
|
/** Submit a user action turn. */
|
||||||
|
const handleTurnSubmit = async (request: TurnRequest) => {
|
||||||
|
if (!sessionId) return
|
||||||
|
setSubmitting(true)
|
||||||
|
setError(null)
|
||||||
|
try {
|
||||||
|
const turn = await turnsApi.submitTurn({sessionId, turnRequest: request})
|
||||||
|
setNarrative(turn.narrative)
|
||||||
|
setSuggestions(turn.suggestions ?? [])
|
||||||
|
setTurnNumber(turn.turnNumber)
|
||||||
|
} catch {
|
||||||
|
setError('Failed to submit turn. Please try again.')
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Handle a suggestion click – submit it as a "choice" action. */
|
||||||
|
const handleSuggestionSelect = (suggestion: Suggestion) => {
|
||||||
|
const request: TurnRequest = {
|
||||||
|
userAction: {
|
||||||
|
type: UserActionRequestTypeEnum.choice,
|
||||||
|
content: suggestion.title,
|
||||||
|
selectedSuggestionId: suggestion.id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
void handleTurnSubmit(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box display="flex" flexDirection="column" minHeight="100vh">
|
||||||
|
<AppBar position="static" elevation={0} color="transparent"
|
||||||
|
sx={{borderBottom: '1px solid', borderColor: 'divider'}}>
|
||||||
|
<Toolbar>
|
||||||
|
<Tooltip title="Back to model selection">
|
||||||
|
<IconButton edge="start" onClick={() => navigate('/')} sx={{mr: 1}}>
|
||||||
|
<ArrowBackIcon/>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<AutoStoriesIcon sx={{mr: 1, color: 'primary.main'}}/>
|
||||||
|
<Typography variant="h6" sx={{flex: 1}}>
|
||||||
|
RolePlay
|
||||||
|
</Typography>
|
||||||
|
{session && (
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{session.model} · {session.language}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
|
||||||
|
<Container maxWidth="md" sx={{flex: 1, py: 4}}>
|
||||||
|
{error && (
|
||||||
|
<Alert severity="error" sx={{mb: 3}}>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<Box display="flex" justifyContent="center" py={8}>
|
||||||
|
<CircularProgress/>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Box display="flex" flexDirection="column" gap={3}>
|
||||||
|
{narrative && (
|
||||||
|
<NarrativeView narrative={narrative} turnNumber={turnNumber}/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{suggestions.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Divider/>
|
||||||
|
<SuggestionList
|
||||||
|
suggestions={suggestions}
|
||||||
|
onSelect={handleSuggestionSelect}
|
||||||
|
disabled={submitting}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Divider/>
|
||||||
|
|
||||||
|
<ActionInput onSubmit={handleTurnSubmit} disabled={submitting}/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,145 @@
|
|||||||
|
import {useEffect, useState} from 'react'
|
||||||
|
import {useNavigate} from 'react-router-dom'
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
CircularProgress,
|
||||||
|
Container,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
MenuItem,
|
||||||
|
Paper,
|
||||||
|
Select,
|
||||||
|
SelectChangeEvent,
|
||||||
|
TextField,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/material'
|
||||||
|
import AutoStoriesIcon from '@mui/icons-material/AutoStories'
|
||||||
|
import type {CreateSessionRequest, ModelInfo} from '../api/generated/index'
|
||||||
|
import {Configuration, CreateSessionRequestSafetyLevelEnum, ModelsApi, SessionsApi,} from '../api/generated/index'
|
||||||
|
|
||||||
|
/***** API base path – must match quarkus.rest.path in application.yml */
|
||||||
|
const API_BASE = '/api/v1'
|
||||||
|
|
||||||
|
const modelsApi = new ModelsApi(new Configuration({basePath: API_BASE}))
|
||||||
|
const sessionsApi = new SessionsApi(new Configuration({basePath: API_BASE}))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Landing page where the user selects an Ollama model and optional settings
|
||||||
|
* before starting a new role-play session.
|
||||||
|
*/
|
||||||
|
export default function StartPage() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const [models, setModels] = useState<ModelInfo[]>([])
|
||||||
|
const [selectedModel, setSelectedModel] = useState<string>('')
|
||||||
|
const [language, setLanguage] = useState<string>('en')
|
||||||
|
const [loading, setLoading] = useState<boolean>(true)
|
||||||
|
const [starting, setStarting] = useState<boolean>(false)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
/** Load available models on mount. */
|
||||||
|
useEffect(() => {
|
||||||
|
modelsApi.listModels()
|
||||||
|
.then((resp) => {
|
||||||
|
setModels(resp.models ?? [])
|
||||||
|
if (resp.models && resp.models.length > 0) {
|
||||||
|
setSelectedModel(resp.models[0].name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => setError('Could not load models. Is the Quarkus server running?'))
|
||||||
|
.finally(() => setLoading(false))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleModelChange = (event: SelectChangeEvent) => {
|
||||||
|
setSelectedModel(event.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a new session and navigate to the session page. */
|
||||||
|
const handleStart = async () => {
|
||||||
|
if (!selectedModel) return
|
||||||
|
setStarting(true)
|
||||||
|
setError(null)
|
||||||
|
try {
|
||||||
|
const request: CreateSessionRequest = {
|
||||||
|
model: selectedModel,
|
||||||
|
language,
|
||||||
|
safetyLevel: CreateSessionRequestSafetyLevelEnum.standard,
|
||||||
|
}
|
||||||
|
const session = await sessionsApi.createSession({createSessionRequest: request})
|
||||||
|
navigate(`/session/${session.sessionId}`)
|
||||||
|
} catch {
|
||||||
|
setError('Failed to create session. Please try again.')
|
||||||
|
} finally {
|
||||||
|
setStarting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container maxWidth="sm" sx={{mt: 8}}>
|
||||||
|
<Paper elevation={4} sx={{p: 5, borderRadius: 3}}>
|
||||||
|
<Box display="flex" alignItems="center" gap={2} mb={4}>
|
||||||
|
<AutoStoriesIcon sx={{fontSize: 48, color: 'primary.main'}}/>
|
||||||
|
<Typography variant="h4" component="h1">
|
||||||
|
RolePlay
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Typography variant="body1" color="text.secondary" mb={4}>
|
||||||
|
Choose an AI model and start your story.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Alert severity="error" sx={{mb: 3}}>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<Box display="flex" justifyContent="center" py={4}>
|
||||||
|
<CircularProgress/>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Box display="flex" flexDirection="column" gap={3}>
|
||||||
|
<FormControl fullWidth disabled={starting}>
|
||||||
|
<InputLabel id="model-label">AI Model</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="model-label"
|
||||||
|
value={selectedModel}
|
||||||
|
label="AI Model"
|
||||||
|
onChange={handleModelChange}
|
||||||
|
>
|
||||||
|
{models.map((m) => (
|
||||||
|
<MenuItem key={m.name} value={m.name}>
|
||||||
|
{m.displayName ?? m.name}
|
||||||
|
{m.family ? ` · ${m.family}` : ''}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Language"
|
||||||
|
value={language}
|
||||||
|
onChange={(e) => setLanguage(e.target.value)}
|
||||||
|
helperText="BCP-47 language tag, e.g. en, de, fr"
|
||||||
|
disabled={starting}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
size="large"
|
||||||
|
onClick={handleStart}
|
||||||
|
disabled={!selectedModel || starting || loading}
|
||||||
|
startIcon={starting ? <CircularProgress size={18} color="inherit"/> : <AutoStoriesIcon/>}
|
||||||
|
>
|
||||||
|
{starting ? 'Starting…' : 'Begin Story'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
30
src/main/web/tsconfig.app.json
Normal file
30
src/main/web/tsconfig.app.json
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": [
|
||||||
|
"ES2020",
|
||||||
|
"DOM",
|
||||||
|
"DOM.Iterable"
|
||||||
|
],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
]
|
||||||
|
}
|
||||||
7
src/main/web/tsconfig.json
Normal file
7
src/main/web/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
24
src/main/web/tsconfig.node.json
Normal file
24
src/main/web/tsconfig.node.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": [
|
||||||
|
"ES2023",
|
||||||
|
"DOM"
|
||||||
|
],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"strict": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"vite.config.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
base: '/',
|
||||||
|
build: {
|
||||||
|
outDir: '../resources/META-INF/resources',
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:8080',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
217
src/target/web-dist/assets/index-PUayaP4F.js
Normal file
217
src/target/web-dist/assets/index-PUayaP4F.js
Normal file
File diff suppressed because one or more lines are too long
12
src/target/web-dist/index.html
Normal file
12
src/target/web-dist/index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>RolePlay</title>
|
||||||
|
<script type="module" crossorigin src="/assets/index-PUayaP4F.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
x
Reference in New Issue
Block a user