characters = new ArrayList<>();
+ if (scenario.getUserCharacter() != null) {
+ characters.add(toCharacterState(scenario.getUserCharacter(), true));
+ }
+ if (scenario.getAiCharacters() != null) {
+ for (CharacterDefinition def : scenario.getAiCharacters()) {
+ characters.add(toCharacterState(def, false));
+ }
+ }
+ return characters;
+ }
+
+ /**
+ * Maps a character definition to initial character state.
+ *
+ * @param def the definition
+ * @param isUserCharacter whether this is the user-controlled character
+ * @return character state with id, name, role and isUserCharacter set
+ */
+ private static CharacterState toCharacterState(final CharacterDefinition def,
+ final boolean isUserCharacter) {
+ CharacterState state = new CharacterState(def.getId(), def.getName(), isUserCharacter);
+ state.setRole(def.getRole());
+ return state;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Updates situation and/or characters when provided; omitted fields are unchanged.
+ */
+ @Override
+ public Optional updateSession(final String sessionId,
+ final UpdateSessionRequest request) {
+ SessionResponse session = sessions.get(sessionId);
+ if (session == null) {
+ return Optional.empty();
+ }
+ if (request != null) {
+ if (request.getSituation() != null) {
+ session.setSituation(request.getSituation());
+ }
+ if (request.getCharacters() != null) {
+ session.setCharacters(new ArrayList<>(request.getCharacters()));
+ }
+ }
+ sessions.put(sessionId, session);
+ return Optional.of(session);
+ }
+
/**
* {@inheritDoc}
*/
diff --git a/src/main/java/de/neitzel/roleplay/business/SessionService.java b/src/main/java/de/neitzel/roleplay/business/SessionService.java
index 8ab5463..a70cfa4 100644
--- a/src/main/java/de/neitzel/roleplay/business/SessionService.java
+++ b/src/main/java/de/neitzel/roleplay/business/SessionService.java
@@ -4,6 +4,7 @@ import de.neitzel.roleplay.fascade.model.CreateSessionRequest;
import de.neitzel.roleplay.fascade.model.SessionResponse;
import de.neitzel.roleplay.fascade.model.TurnRequest;
import de.neitzel.roleplay.fascade.model.TurnResponse;
+import de.neitzel.roleplay.fascade.model.UpdateSessionRequest;
import java.util.Optional;
@@ -31,6 +32,16 @@ public interface SessionService {
*/
Optional getSession(String sessionId);
+ /**
+ * Partially updates an existing session (situation and/or characters).
+ * Omitted fields in the request are left unchanged.
+ *
+ * @param sessionId the unique session identifier
+ * @param request the update payload; may be null or have null fields
+ * @return an {@link Optional} containing the updated session, or empty if not found
+ */
+ Optional updateSession(String sessionId, UpdateSessionRequest request);
+
/**
* Processes a user's turn within an existing session. Runs the two-call
* Ollama pattern and returns the resulting narrative with updated state.
diff --git a/src/main/java/de/neitzel/roleplay/fascade/OllamaApi.java b/src/main/java/de/neitzel/roleplay/fascade/OllamaApi.java
index f269849..304ebec 100644
--- a/src/main/java/de/neitzel/roleplay/fascade/OllamaApi.java
+++ b/src/main/java/de/neitzel/roleplay/fascade/OllamaApi.java
@@ -3,14 +3,17 @@ package de.neitzel.roleplay.fascade;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
+import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
/**
* Quarkus declarative REST client for the Ollama HTTP API.
* Configuration (base URL, timeouts) is read from
* {@code quarkus.rest-client.ollama-api.*} in {@code application.yml}.
+ * All outgoing requests and responses are logged via {@link OllamaClientLoggingFilter}.
*/
@RegisterRestClient(configKey = "ollama-api")
+@RegisterProvider(OllamaClientLoggingFilter.class)
@Path("/api")
public interface OllamaApi {
diff --git a/src/main/java/de/neitzel/roleplay/fascade/OllamaClient.java b/src/main/java/de/neitzel/roleplay/fascade/OllamaClient.java
index 90fa5e5..4ff2686 100644
--- a/src/main/java/de/neitzel/roleplay/fascade/OllamaClient.java
+++ b/src/main/java/de/neitzel/roleplay/fascade/OllamaClient.java
@@ -6,6 +6,8 @@ import de.neitzel.roleplay.common.StateUpdateResponse;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.eclipse.microprofile.rest.client.inject.RestClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.List;
@@ -18,6 +20,11 @@ import java.util.List;
@ApplicationScoped
public class OllamaClient {
+ private static final Logger LOG = LoggerFactory.getLogger(OllamaClient.class);
+
+ /** Maximum characters of narrative/JSON to log at TRACE (avoid huge logs). */
+ private static final int TRACE_CONTENT_LIMIT = 200;
+
/** Low-level REST client for the Ollama HTTP API. */
private final OllamaApi ollamaApi;
@@ -43,10 +50,14 @@ public class OllamaClient {
* @return model metadata, or an empty list if none are installed
*/
public List listModels() {
+ LOG.debug("Fetching Ollama models (GET /api/tags)");
OllamaTagsResponse response = ollamaApi.getTags();
if (response == null || response.getModels() == null) {
+ LOG.debug("Received null or empty models list from Ollama");
return Collections.emptyList();
}
+ int count = response.getModels().size();
+ LOG.debug("Received {} model(s) from Ollama", count);
return response.getModels();
}
@@ -62,6 +73,11 @@ public class OllamaClient {
public String generateNarrative(final String model,
final String systemPrompt,
final String userContent) {
+ int systemLen = systemPrompt != null ? systemPrompt.length() : 0;
+ int userLen = userContent != null ? userContent.length() : 0;
+ LOG.debug("Calling Ollama for narrative: model={}, systemPromptLength={}, userContentLength={}",
+ model, systemLen, userLen);
+
OllamaChatRequest request = OllamaChatRequest.builder()
.model(model)
.messages(List.of(
@@ -77,7 +93,16 @@ public class OllamaClient {
.build();
OllamaChatResponse response = ollamaApi.chat(request);
- return response.getMessage().getContent();
+ String content = response.getMessage().getContent();
+ int len = content != null ? content.length() : 0;
+ LOG.debug("Received narrative from Ollama, length={}", len);
+ if (LOG.isTraceEnabled() && content != null && !content.isEmpty()) {
+ String snippet = content.length() <= TRACE_CONTENT_LIMIT
+ ? content
+ : content.substring(0, TRACE_CONTENT_LIMIT) + "...";
+ LOG.trace("Narrative snippet: {}", snippet);
+ }
+ return content;
}
/**
@@ -94,6 +119,11 @@ public class OllamaClient {
public StateUpdateResponse generateStateUpdate(final String model,
final String systemPrompt,
final String userContent) {
+ int systemLen = systemPrompt != null ? systemPrompt.length() : 0;
+ int userLen = userContent != null ? userContent.length() : 0;
+ LOG.debug("Calling Ollama for state update: model={}, systemPromptLength={}, userContentLength={}",
+ model, systemLen, userLen);
+
OllamaChatRequest request = OllamaChatRequest.builder()
.model(model)
.format("json")
@@ -111,10 +141,21 @@ public class OllamaClient {
OllamaChatResponse response = ollamaApi.chat(request);
String json = response.getMessage().getContent();
+ int jsonLen = json != null ? json.length() : 0;
+ LOG.debug("Received state update from Ollama, JSON length={}", jsonLen);
+ if (LOG.isTraceEnabled() && json != null && !json.isEmpty()) {
+ String snippet = json.length() <= TRACE_CONTENT_LIMIT
+ ? json
+ : json.substring(0, TRACE_CONTENT_LIMIT) + "...";
+ LOG.trace("State update JSON snippet: {}", snippet);
+ }
try {
- return objectMapper.readValue(json, StateUpdateResponse.class);
+ StateUpdateResponse result = objectMapper.readValue(json, StateUpdateResponse.class);
+ LOG.debug("Parsed state update successfully");
+ return result;
} catch (JsonProcessingException e) {
+ LOG.debug("Failed to parse state update JSON from Ollama response: {}", e.getMessage());
throw new OllamaParseException(
"Failed to parse state update JSON from Ollama response", e);
}
diff --git a/src/main/java/de/neitzel/roleplay/fascade/OllamaClientLoggingFilter.java b/src/main/java/de/neitzel/roleplay/fascade/OllamaClientLoggingFilter.java
new file mode 100644
index 0000000..f843cfe
--- /dev/null
+++ b/src/main/java/de/neitzel/roleplay/fascade/OllamaClientLoggingFilter.java
@@ -0,0 +1,53 @@
+package de.neitzel.roleplay.fascade;
+
+import jakarta.ws.rs.client.ClientRequestContext;
+import jakarta.ws.rs.client.ClientRequestFilter;
+import jakarta.ws.rs.client.ClientResponseContext;
+import jakarta.ws.rs.client.ClientResponseFilter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+/**
+ * JAX-RS client filter that logs every outgoing request and response for the
+ * Ollama REST client. Registered only on {@link OllamaApi} via
+ * {@link org.eclipse.microprofile.rest.client.annotation.RegisterProvider}.
+ * Logs method, URI and (for responses) status and content-type so that
+ * Ollama communication can be inspected without logging full bodies here
+ * (body logging is configured via Quarkus REST client logging).
+ */
+public class OllamaClientLoggingFilter implements ClientRequestFilter, ClientResponseFilter {
+
+ private static final Logger LOG = LoggerFactory.getLogger(OllamaClientLoggingFilter.class);
+
+ /**
+ * Logs the outgoing request method and URI before the request is sent.
+ *
+ * @param requestContext the request context
+ */
+ @Override
+ public void filter(final ClientRequestContext requestContext) throws IOException {
+ String method = requestContext.getMethod();
+ String uri = requestContext.getUri().toString();
+ LOG.debug("Ollama request: {} {}", method, uri);
+ }
+
+ /**
+ * Logs the response status and content-type after the response is received.
+ *
+ * @param requestContext the request context (unused)
+ * @param responseContext the response context
+ */
+ @Override
+ public void filter(final ClientRequestContext requestContext,
+ final ClientResponseContext responseContext) throws IOException {
+ int status = responseContext.getStatus();
+ String statusInfo = responseContext.getStatusInfo() != null
+ ? responseContext.getStatusInfo().getReasonPhrase()
+ : "";
+ String contentType = responseContext.getHeaderString("Content-Type");
+ LOG.debug("Ollama response: {} {} Content-Type: {}",
+ status, statusInfo, contentType != null ? contentType : "(none)");
+ }
+}
diff --git a/src/main/java/de/neitzel/roleplay/fascade/SessionResource.java b/src/main/java/de/neitzel/roleplay/fascade/SessionResource.java
index fa5a0dd..e3d4fb4 100644
--- a/src/main/java/de/neitzel/roleplay/fascade/SessionResource.java
+++ b/src/main/java/de/neitzel/roleplay/fascade/SessionResource.java
@@ -3,6 +3,7 @@ package de.neitzel.roleplay.fascade;
import de.neitzel.roleplay.business.SessionService;
import de.neitzel.roleplay.fascade.model.CreateSessionRequest;
import de.neitzel.roleplay.fascade.model.SessionResponse;
+import de.neitzel.roleplay.fascade.model.UpdateSessionRequest;
import de.neitzel.roleplay.generated.api.SessionsApi;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@@ -54,5 +55,20 @@ public class SessionResource implements SessionsApi {
.orElseThrow(() -> new NotFoundException(
"No session found with id: " + sessionId));
}
+
+ /**
+ * {@inheritDoc}
+ *
+ * Delegates to {@link SessionService#updateSession(String, UpdateSessionRequest)}.
+ * Returns 404 if the session is not found.
+ */
+ @Override
+ public SessionResponse updateSession(final String sessionId,
+ final UpdateSessionRequest updateSessionRequest) {
+ return sessionService.updateSession(sessionId,
+ updateSessionRequest != null ? updateSessionRequest : new UpdateSessionRequest())
+ .orElseThrow(() -> new NotFoundException(
+ "No session found with id: " + sessionId));
+ }
}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 63e784e..5b5ef23 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -20,5 +20,14 @@ quarkus:
url: http://debian:11434
connect-timeout: 5000
read-timeout: 120000
+ logging:
+ scope: request-response
+ body-limit: 2048
rest:
path: /api/v1
+ log:
+ category:
+ "de.neitzel.roleplay.fascade.OllamaClient":
+ level: DEBUG
+ "org.jboss.resteasy.reactive.client.logging":
+ level: DEBUG
diff --git a/src/main/resources/openapi-roleplay-public-v1.yml b/src/main/resources/openapi-roleplay-public-v1.yml
index c2f8158..865e5f3 100644
--- a/src/main/resources/openapi-roleplay-public-v1.yml
+++ b/src/main/resources/openapi-roleplay-public-v1.yml
@@ -101,6 +101,40 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
+ patch:
+ operationId: updateSession
+ summary: Update session state
+ description: |
+ Partially updates an existing session. Provide situation and/or characters
+ to replace the current values. Omitted fields are left unchanged.
+ tags:
+ - sessions
+ parameters:
+ - $ref: '#/components/parameters/SessionId'
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/UpdateSessionRequest'
+ responses:
+ "200":
+ description: Session updated; full state returned.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SessionResponse'
+ "404":
+ description: Session not found.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ "400":
+ description: Invalid request body.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
/sessions/{sessionId}/turns:
post:
@@ -245,6 +279,19 @@ components:
items:
$ref: '#/components/schemas/CharacterDefinition'
+ UpdateSessionRequest:
+ type: object
+ description: Request body for partially updating a session.
+ properties:
+ situation:
+ $ref: '#/components/schemas/SituationState'
+ description: Replace session situation when provided.
+ characters:
+ type: array
+ items:
+ $ref: '#/components/schemas/CharacterState'
+ description: Replace session character list when provided.
+
CharacterDefinition:
type: object
description: Definition of a character for session initialisation.
diff --git a/src/main/web/src/pages/SessionPage.tsx b/src/main/web/src/pages/SessionPage.tsx
index 4eb64f0..cfb311f 100644
--- a/src/main/web/src/pages/SessionPage.tsx
+++ b/src/main/web/src/pages/SessionPage.tsx
@@ -1,20 +1,43 @@
+import type {ChangeEvent} from 'react'
import {useEffect, useState} from 'react'
import {useNavigate, useParams} from 'react-router-dom'
import {
Alert,
AppBar,
Box,
+ Button,
+ Card,
+ CardContent,
CircularProgress,
Container,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
Divider,
IconButton,
+ List,
+ ListItem,
+ ListItemText,
+ TextField,
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 DeleteIcon from '@mui/icons-material/Delete'
+import EditIcon from '@mui/icons-material/Edit'
+import PersonIcon from '@mui/icons-material/Person'
+import PlaceIcon from '@mui/icons-material/Place'
+import type {
+ CharacterState,
+ SessionResponse,
+ SituationState,
+ Suggestion,
+ TurnRequest,
+ UpdateSessionRequest,
+} from '../api/generated/index'
import {Configuration, SessionsApi, TurnsApi, UserActionRequestTypeEnum,} from '../api/generated/index'
import NarrativeView from '../components/NarrativeView'
import SuggestionList from '../components/SuggestionList'
@@ -40,6 +63,10 @@ export default function SessionPage() {
const [loading, setLoading] = useState(true)
const [submitting, setSubmitting] = useState(false)
const [error, setError] = useState(null)
+ const [sceneDialogOpen, setSceneDialogOpen] = useState(false)
+ const [sceneDraft, setSceneDraft] = useState({})
+ const [charactersDialogOpen, setCharactersDialogOpen] = useState(false)
+ const [charactersDraft, setCharactersDraft] = useState([])
/** Load existing session on mount. */
useEffect(() => {
@@ -84,6 +111,74 @@ export default function SessionPage() {
void handleTurnSubmit(request)
}
+ /** Open scene edit dialog with current situation. */
+ const openSceneDialog = () => {
+ setSceneDraft({
+ setting: session?.situation?.setting ?? '',
+ currentScene: session?.situation?.currentScene ?? '',
+ })
+ setSceneDialogOpen(true)
+ }
+
+ /** Save scene and PATCH session. */
+ const saveScene = async () => {
+ if (!sessionId) return
+ setError(null)
+ try {
+ const request: UpdateSessionRequest = { situation: sceneDraft }
+ const updated = await sessionsApi.updateSession({ sessionId, updateSessionRequest: request })
+ setSession(updated)
+ setNarrative(updated.narrative ?? '')
+ setSuggestions(updated.suggestions ?? [])
+ setTurnNumber(updated.turnNumber)
+ setSceneDialogOpen(false)
+ } catch {
+ setError('Failed to update scene.')
+ }
+ }
+
+ /** Open characters edit dialog with current characters. */
+ const openCharactersDialog = () => {
+ setCharactersDraft(session?.characters ? [...session.characters] : [])
+ setCharactersDialogOpen(true)
+ }
+
+ /** Save characters and PATCH session. */
+ const saveCharacters = async () => {
+ if (!sessionId) return
+ setError(null)
+ try {
+ const request: UpdateSessionRequest = { characters: charactersDraft }
+ const updated = await sessionsApi.updateSession({ sessionId, updateSessionRequest: request })
+ setSession(updated)
+ setNarrative(updated.narrative ?? '')
+ setSuggestions(updated.suggestions ?? [])
+ setTurnNumber(updated.turnNumber)
+ setCharactersDialogOpen(false)
+ } catch {
+ setError('Failed to update characters.')
+ }
+ }
+
+ /** Add a new character to the draft list. */
+ const addCharacterDraft = () => {
+ setCharactersDraft((prev) => [...prev, { id: '', name: '', role: '', isUserCharacter: false }])
+ }
+
+ /** Update a character in the draft list. */
+ const updateCharacterDraft = (index: number, patch: Partial) => {
+ setCharactersDraft((prev) => {
+ const next = [...prev]
+ next[index] = { ...next[index], ...patch }
+ return next
+ })
+ }
+
+ /** Remove a character from the draft list. */
+ const removeCharacterDraft = (index: number) => {
+ setCharactersDraft((prev) => prev.filter((_, i) => i !== index))
+ }
+
return (
)}
+ {(session?.situation?.setting || session?.situation?.currentScene) && (
+
+
+
+
+
+ Scene
+
+
+
+ {session.situation.setting && (
+
+ Setting: {session.situation.setting}
+
+ )}
+ {session.situation.currentScene && (
+
+ Current: {session.situation.currentScene}
+
+ )}
+
+
+ )}
+
+ {session?.characters && session.characters.length > 0 && (
+
+
+
+
+
+ Characters
+
+
+
+
+ {session.characters.map((c, i) => (
+
+
+
+ ))}
+
+
+
+ )}
+
{suggestions.length > 0 && (
<>
@@ -140,6 +287,115 @@ export default function SessionPage() {
)}
+
+
+
+
)
}
diff --git a/src/main/web/src/pages/StartPage.tsx b/src/main/web/src/pages/StartPage.tsx
index 2657666..a92b818 100644
--- a/src/main/web/src/pages/StartPage.tsx
+++ b/src/main/web/src/pages/StartPage.tsx
@@ -1,13 +1,24 @@
import {useEffect, useState} from 'react'
import {useNavigate} from 'react-router-dom'
import {
+ Accordion,
+ AccordionDetails,
+ AccordionSummary,
Alert,
Box,
Button,
CircularProgress,
Container,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
FormControl,
+ IconButton,
InputLabel,
+ List,
+ ListItem,
+ ListItemText,
MenuItem,
Paper,
Select,
@@ -16,8 +27,22 @@ import {
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'
+import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
+import PersonAddIcon from '@mui/icons-material/PersonAdd'
+import DeleteIcon from '@mui/icons-material/Delete'
+import EditIcon from '@mui/icons-material/Edit'
+import type {
+ CharacterDefinition,
+ CreateSessionRequest,
+ ModelInfo,
+ ScenarioSetup,
+} 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'
@@ -39,6 +64,19 @@ export default function StartPage() {
const [starting, setStarting] = useState(false)
const [error, setError] = useState(null)
+ /** Optional scenario (collapsible). */
+ const [scenarioExpanded, setScenarioExpanded] = useState(false)
+ const [setting, setSetting] = useState('')
+ const [initialConflict, setInitialConflict] = useState('')
+ const [userCharacter, setUserCharacter] = useState(null)
+ const [aiCharacters, setAiCharacters] = useState([])
+ /** Dialog for add/edit character: 'user' | 'ai', and index (-1 = add). */
+ const [characterDialog, setCharacterDialog] = useState<{
+ mode: 'user' | 'ai'
+ index: number
+ draft: CharacterDefinition
+ } | null>(null)
+
/** Load available models on mount. */
useEffect(() => {
modelsApi.listModels()
@@ -56,6 +94,68 @@ export default function StartPage() {
setSelectedModel(event.target.value)
}
+ /** Build scenario from form state if any field is filled. */
+ const buildScenario = (): ScenarioSetup | undefined => {
+ const hasSetting = setting.trim() !== ''
+ const hasConflict = initialConflict.trim() !== ''
+ const hasUser = userCharacter && userCharacter.id.trim() && userCharacter.name.trim() && userCharacter.role.trim()
+ const hasAi = aiCharacters.length > 0 && aiCharacters.every(
+ (c) => c.id.trim() !== '' && c.name.trim() !== '' && c.role.trim() !== ''
+ )
+ if (!hasSetting && !hasConflict && !hasUser && !hasAi) return undefined
+ return {
+ setting: hasSetting ? setting.trim() : undefined,
+ initialConflict: hasConflict ? initialConflict.trim() : undefined,
+ userCharacter: hasUser ? userCharacter : undefined,
+ aiCharacters: hasAi ? aiCharacters : undefined,
+ }
+ }
+
+ /** Open dialog to add or edit user character. */
+ const openUserCharacterDialog = () => {
+ setCharacterDialog({
+ mode: 'user',
+ index: -1,
+ draft: userCharacter ?? { id: 'player', name: '', role: '' },
+ })
+ }
+
+ /** Open dialog to add or edit an AI character. */
+ const openAiCharacterDialog = (index: number) => {
+ const list = index >= 0 ? aiCharacters[index] : { id: '', name: '', role: '' }
+ setCharacterDialog({
+ mode: 'ai',
+ index,
+ draft: { ...list, personalityTraits: list.personalityTraits ?? [], goals: list.goals ?? [] },
+ })
+ }
+
+ /** Save character from dialog and close. */
+ const saveCharacterDialog = () => {
+ if (!characterDialog) return
+ const { draft } = characterDialog
+ if (!draft.id?.trim() || !draft.name?.trim() || !draft.role?.trim()) return
+ if (characterDialog.mode === 'user') {
+ setUserCharacter({ ...draft, id: draft.id.trim(), name: draft.name.trim(), role: draft.role.trim() })
+ } else {
+ const next = [...aiCharacters]
+ const char = { ...draft, id: draft.id.trim(), name: draft.name.trim(), role: draft.role.trim() }
+ const idx = characterDialog.index
+ if (idx !== undefined && idx >= 0) {
+ next[idx] = char
+ } else {
+ next.push(char)
+ }
+ setAiCharacters(next)
+ }
+ setCharacterDialog(null)
+ }
+
+ /** Remove an AI character. */
+ const removeAiCharacter = (index: number) => {
+ setAiCharacters((prev) => prev.filter((_, i) => i !== index))
+ }
+
/** Create a new session and navigate to the session page. */
const handleStart = async () => {
if (!selectedModel) return
@@ -66,6 +166,7 @@ export default function StartPage() {
model: selectedModel,
language,
safetyLevel: CreateSessionRequestSafetyLevelEnum.standard,
+ scenario: buildScenario(),
}
const session = await sessionsApi.createSession({createSessionRequest: request})
navigate(`/session/${session.sessionId}`)
@@ -128,6 +229,89 @@ export default function StartPage() {
fullWidth
/>
+ setScenarioExpanded((b) => !b)}
+ disabled={starting}
+ sx={{width: '100%'}}
+ >
+ }>
+ Scenario (optional)
+
+
+
+ setSetting(e.target.value)}
+ placeholder="Place, time, and atmosphere"
+ fullWidth
+ size="small"
+ />
+ setInitialConflict(e.target.value)}
+ placeholder="The hook or starting conflict"
+ fullWidth
+ size="small"
+ />
+
+
+ Your character
+
+ {userCharacter ? (
+
+
+
+ }
+ >
+
+
+ ) : (
+
+ )}
+
+
+
+ AI characters
+
+
+ {aiCharacters.map((c, i) => (
+
+ openAiCharacterDialog(i)}>
+
+
+ removeAiCharacter(i)}>
+
+
+
+ }
+ >
+
+
+ ))}
+
+
+
+
+
+
+