Compare commits
No commits in common. "a8cd8d80602a0496a83ff31e4f927919a54bdf0e" and "8b687e973733a410bff0f8c40325b44fc49770f8" have entirely different histories.
a8cd8d8060
...
8b687e9737
15 changed files with 38 additions and 1018 deletions
|
|
@ -1,7 +1,6 @@
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from ...languages import SUPPORTED_LANGUAGES
|
|
||||||
from ...outbound.anthropic.adventure_prompts import (
|
from ...outbound.anthropic.adventure_prompts import (
|
||||||
build_conversation_messages,
|
build_conversation_messages,
|
||||||
build_entry_system_prompt,
|
build_entry_system_prompt,
|
||||||
|
|
@ -22,11 +21,8 @@ from ...outbound.postgres.repositories.adventure_repository import (
|
||||||
PostgresAdventureRepository,
|
PostgresAdventureRepository,
|
||||||
)
|
)
|
||||||
from ...storage import upload_audio
|
from ...storage import upload_audio
|
||||||
from ..models.adventure import (
|
from ..models.adventure import Adventure, AdventureEntry, AdventureEntryPossibleChoiceDecision
|
||||||
Adventure,
|
from ...languages import SUPPORTED_LANGUAGES
|
||||||
AdventureEntry,
|
|
||||||
AdventureEntryPossibleChoiceDecision,
|
|
||||||
)
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -64,7 +60,6 @@ class AdventureService:
|
||||||
setting: list[str],
|
setting: list[str],
|
||||||
vibes: list[str],
|
vibes: list[str],
|
||||||
protagonist: list[str],
|
protagonist: list[str],
|
||||||
entry_word_count_range: list[int],
|
|
||||||
max_entry_count: int = 6,
|
max_entry_count: int = 6,
|
||||||
) -> tuple[Adventure, AdventureEntry]:
|
) -> tuple[Adventure, AdventureEntry]:
|
||||||
"""Creates the adventure and a placeholder for the first entry.
|
"""Creates the adventure and a placeholder for the first entry.
|
||||||
|
|
@ -81,10 +76,7 @@ class AdventureService:
|
||||||
vibes=vibes,
|
vibes=vibes,
|
||||||
protagonist=protagonist,
|
protagonist=protagonist,
|
||||||
max_entry_count=max_entry_count,
|
max_entry_count=max_entry_count,
|
||||||
entry_story_text_target_length={
|
entry_story_text_target_length={"min": 700, "max": 800},
|
||||||
"min": entry_word_count_range[0],
|
|
||||||
"max": entry_word_count_range[1],
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
first_entry = await self.entry_repo.create(
|
first_entry = await self.entry_repo.create(
|
||||||
adventure_id=uuid.UUID(adventure.id),
|
adventure_id=uuid.UUID(adventure.id),
|
||||||
|
|
@ -162,14 +154,10 @@ class AdventureService:
|
||||||
is_final_entry = current_entry.entry_index + 1 == adventure.max_entry_count
|
is_final_entry = current_entry.entry_index + 1 == adventure.max_entry_count
|
||||||
|
|
||||||
prior_entries = await self._load_prior_entries_with_metadata(
|
prior_entries = await self._load_prior_entries_with_metadata(
|
||||||
all_entries=[
|
all_entries=[e for e in all_entries if e.entry_index < current_entry.entry_index],
|
||||||
e for e in all_entries if e.entry_index < current_entry.entry_index
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
language_name = SUPPORTED_LANGUAGES.get(
|
language_name = SUPPORTED_LANGUAGES.get(adventure.language, adventure.language)
|
||||||
adventure.language, adventure.language
|
|
||||||
)
|
|
||||||
competency = adventure.competencies[0] if adventure.competencies else "B1"
|
competency = adventure.competencies[0] if adventure.competencies else "B1"
|
||||||
system_prompt = build_entry_system_prompt(
|
system_prompt = build_entry_system_prompt(
|
||||||
language_name=language_name,
|
language_name=language_name,
|
||||||
|
|
@ -205,15 +193,10 @@ class AdventureService:
|
||||||
if not is_final_entry:
|
if not is_final_entry:
|
||||||
await self.choice_repo.create_many(
|
await self.choice_repo.create_many(
|
||||||
entry_id=entry_id,
|
entry_id=entry_id,
|
||||||
choices=[
|
choices=[(i, label, text) for i, (label, text) in enumerate(choices_parsed)],
|
||||||
(i, label, text)
|
|
||||||
for i, (label, text) in enumerate(choices_parsed)
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
translated = await self.deepl_client.translate(
|
translated = await self.deepl_client.translate(story_text, adventure.source_language)
|
||||||
story_text, adventure.source_language
|
|
||||||
)
|
|
||||||
await self.translation_repo.create(
|
await self.translation_repo.create(
|
||||||
entry_id=entry_id,
|
entry_id=entry_id,
|
||||||
component_type="story_text",
|
component_type="story_text",
|
||||||
|
|
@ -235,9 +218,7 @@ class AdventureService:
|
||||||
|
|
||||||
if is_first_entry:
|
if is_first_entry:
|
||||||
title_system = build_title_system_prompt()
|
title_system = build_title_system_prompt()
|
||||||
title_user = build_title_user_message(
|
title_user = build_title_user_message(story_text, language_name, adventure.genres)
|
||||||
story_text, language_name, adventure.genres
|
|
||||||
)
|
|
||||||
title_raw, _ = await self.anthropic_client.complete(
|
title_raw, _ = await self.anthropic_client.complete(
|
||||||
system_prompt=title_system,
|
system_prompt=title_system,
|
||||||
messages=[{"role": "user", "content": title_user}],
|
messages=[{"role": "user", "content": title_user}],
|
||||||
|
|
@ -249,17 +230,13 @@ class AdventureService:
|
||||||
)
|
)
|
||||||
|
|
||||||
new_status = "complete" if is_final_entry else "active"
|
new_status = "complete" if is_final_entry else "active"
|
||||||
await self.adventure_repo.update_status(
|
await self.adventure_repo.update_status(adventure_id=adventure_id, status=new_status)
|
||||||
adventure_id=adventure_id, status=new_status
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Entry pipeline failed for entry %s", entry_id)
|
logger.exception("Entry pipeline failed for entry %s", entry_id)
|
||||||
try:
|
try:
|
||||||
await self.entry_repo.update_status(entry_id=entry_id, status="error")
|
await self.entry_repo.update_status(entry_id=entry_id, status="error")
|
||||||
await self.adventure_repo.update_status(
|
await self.adventure_repo.update_status(adventure_id=adventure_id, status="error")
|
||||||
adventure_id=adventure_id, status="error"
|
|
||||||
)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Failed to mark entry/adventure as error")
|
logger.exception("Failed to mark entry/adventure as error")
|
||||||
|
|
||||||
|
|
@ -281,11 +258,7 @@ class AdventureService:
|
||||||
next_entry = sorted_entries[i + 1]
|
next_entry = sorted_entries[i + 1]
|
||||||
if next_entry.generated_from_choice_id:
|
if next_entry.generated_from_choice_id:
|
||||||
chosen = next(
|
chosen = next(
|
||||||
(
|
(c for c in choices if c.id == next_entry.generated_from_choice_id),
|
||||||
c
|
|
||||||
for c in choices
|
|
||||||
if c.id == next_entry.generated_from_choice_id
|
|
||||||
),
|
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
if chosen:
|
if chosen:
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ from fastapi import APIRouter, Depends, HTTPException
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from ... import worker
|
|
||||||
from ...auth import verify_token
|
from ...auth import verify_token
|
||||||
from ...config import settings
|
from ...config import settings
|
||||||
from ...domain.services.adventure_service import AdventureService
|
from ...domain.services.adventure_service import AdventureService
|
||||||
|
|
@ -24,6 +23,7 @@ from ...outbound.postgres.repositories.adventure_repository import (
|
||||||
PostgresAdventureEntryTranslationRepository,
|
PostgresAdventureEntryTranslationRepository,
|
||||||
PostgresAdventureRepository,
|
PostgresAdventureRepository,
|
||||||
)
|
)
|
||||||
|
from ... import worker
|
||||||
|
|
||||||
router = APIRouter(prefix="/adventures", tags=["adventures"])
|
router = APIRouter(prefix="/adventures", tags=["adventures"])
|
||||||
|
|
||||||
|
|
@ -44,7 +44,8 @@ _STUB_ENTRY_RESPONSE = (
|
||||||
"no notes"
|
"no notes"
|
||||||
)
|
)
|
||||||
_STUB_TITLE_RESPONSE = (
|
_STUB_TITLE_RESPONSE = (
|
||||||
"La Nuit Parisienne\nUne aventure mystérieuse dans les rues sombres de Paris."
|
"La Nuit Parisienne\n"
|
||||||
|
"Une aventure mystérieuse dans les rues sombres de Paris."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -56,12 +57,7 @@ class _StubAnthropicClient:
|
||||||
model: str = "claude-sonnet-4-6",
|
model: str = "claude-sonnet-4-6",
|
||||||
max_tokens: int = 2048,
|
max_tokens: int = 2048,
|
||||||
) -> tuple[str, dict]:
|
) -> tuple[str, dict]:
|
||||||
usage = {
|
usage = {"provider": "stub", "model": "stub", "input_tokens": 0, "output_tokens": 0}
|
||||||
"provider": "stub",
|
|
||||||
"model": "stub",
|
|
||||||
"input_tokens": 0,
|
|
||||||
"output_tokens": 0,
|
|
||||||
}
|
|
||||||
if "game master" in system_prompt.lower():
|
if "game master" in system_prompt.lower():
|
||||||
return _STUB_ENTRY_RESPONSE, usage
|
return _STUB_ENTRY_RESPONSE, usage
|
||||||
return _STUB_TITLE_RESPONSE, usage
|
return _STUB_TITLE_RESPONSE, usage
|
||||||
|
|
@ -71,9 +67,7 @@ class _StubDeepLClient:
|
||||||
def can_translate_to(self, lang: str) -> bool:
|
def can_translate_to(self, lang: str) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def translate(
|
async def translate(self, text: str, to_language: str, context: str | None = None) -> str:
|
||||||
self, text: str, to_language: str, context: str | None = None
|
|
||||||
) -> str:
|
|
||||||
return f"[STUB] {text[:120]}"
|
return f"[STUB] {text[:120]}"
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -95,7 +89,6 @@ class _StubGeminiClient:
|
||||||
# Service factory
|
# Service factory
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def _make_service(db: AsyncSession) -> AdventureService:
|
def _make_service(db: AsyncSession) -> AdventureService:
|
||||||
if settings.stub_generation:
|
if settings.stub_generation:
|
||||||
anthropic = _StubAnthropicClient() # type: ignore[assignment]
|
anthropic = _StubAnthropicClient() # type: ignore[assignment]
|
||||||
|
|
@ -130,7 +123,6 @@ async def _run_entry_pipeline_task(
|
||||||
# Request / response models
|
# Request / response models
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class CreateAdventureRequest(BaseModel):
|
class CreateAdventureRequest(BaseModel):
|
||||||
language: str
|
language: str
|
||||||
source_language: str
|
source_language: str
|
||||||
|
|
@ -139,7 +131,6 @@ class CreateAdventureRequest(BaseModel):
|
||||||
setting: list[str]
|
setting: list[str]
|
||||||
vibes: list[str]
|
vibes: list[str]
|
||||||
protagonist: list[str]
|
protagonist: list[str]
|
||||||
entry_word_count_range: str
|
|
||||||
max_entry_count: int = 6
|
max_entry_count: int = 6
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -205,7 +196,6 @@ class EntryDetailResponse(BaseModel):
|
||||||
# Helpers
|
# Helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def _to_adventure_response(adventure) -> AdventureResponse:
|
def _to_adventure_response(adventure) -> AdventureResponse:
|
||||||
return AdventureResponse(
|
return AdventureResponse(
|
||||||
id=adventure.id,
|
id=adventure.id,
|
||||||
|
|
@ -236,7 +226,6 @@ def _parse_adventure_id(adventure_id: str) -> uuid.UUID:
|
||||||
# Endpoints
|
# Endpoints
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
@router.post("", response_model=AdventureResponse, status_code=201)
|
@router.post("", response_model=AdventureResponse, status_code=201)
|
||||||
async def create_adventure(
|
async def create_adventure(
|
||||||
body: CreateAdventureRequest,
|
body: CreateAdventureRequest,
|
||||||
|
|
@ -251,30 +240,13 @@ async def create_adventure(
|
||||||
detail=f"Unsupported language '{body.language}'. Supported: {list(SUPPORTED_LANGUAGES)}",
|
detail=f"Unsupported language '{body.language}'. Supported: {list(SUPPORTED_LANGUAGES)}",
|
||||||
)
|
)
|
||||||
|
|
||||||
deepl_client = (
|
deepl_client = DeepLClient(settings.deepl_api_key) if not settings.stub_generation else _StubDeepLClient() # type: ignore[assignment]
|
||||||
DeepLClient(settings.deepl_api_key)
|
|
||||||
if not settings.stub_generation
|
|
||||||
else _StubDeepLClient()
|
|
||||||
) # type: ignore[assignment]
|
|
||||||
if not deepl_client.can_translate_to(body.source_language):
|
if not deepl_client.can_translate_to(body.source_language):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail=f"Cannot translate to source language '{body.source_language}'",
|
detail=f"Cannot translate to source language '{body.source_language}'",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Word count is e.g. "100-200 Words", convert to a tuple of ints (100, 200)
|
|
||||||
try:
|
|
||||||
word_count_range = tuple(
|
|
||||||
int(x.strip().split()[0]) for x in body.entry_word_count_range.split("-")
|
|
||||||
)
|
|
||||||
if len(word_count_range) != 2 or word_count_range[0] >= word_count_range[1]:
|
|
||||||
raise ValueError()
|
|
||||||
except ValueError:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=400,
|
|
||||||
detail="Invalid entry_word_count_range. Expected format 'min-max Words', e.g. '100-200 Words'",
|
|
||||||
)
|
|
||||||
|
|
||||||
adventure, first_entry = await _make_service(db).create_adventure_for_user(
|
adventure, first_entry = await _make_service(db).create_adventure_for_user(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
language=body.language,
|
language=body.language,
|
||||||
|
|
@ -285,12 +257,9 @@ async def create_adventure(
|
||||||
vibes=body.vibes,
|
vibes=body.vibes,
|
||||||
protagonist=body.protagonist,
|
protagonist=body.protagonist,
|
||||||
max_entry_count=body.max_entry_count,
|
max_entry_count=body.max_entry_count,
|
||||||
entry_word_count_range=word_count_range,
|
|
||||||
)
|
)
|
||||||
await worker.enqueue(
|
await worker.enqueue(
|
||||||
partial(
|
partial(_run_entry_pipeline_task, uuid.UUID(adventure.id), uuid.UUID(first_entry.id))
|
||||||
_run_entry_pipeline_task, uuid.UUID(adventure.id), uuid.UUID(first_entry.id)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
return _to_adventure_response(adventure)
|
return _to_adventure_response(adventure)
|
||||||
|
|
||||||
|
|
@ -312,9 +281,7 @@ async def get_adventure(
|
||||||
token_data: dict = Depends(verify_token),
|
token_data: dict = Depends(verify_token),
|
||||||
) -> AdventureResponse:
|
) -> AdventureResponse:
|
||||||
user_id = uuid.UUID(token_data["sub"])
|
user_id = uuid.UUID(token_data["sub"])
|
||||||
adventure = await PostgresAdventureRepository(db).get_by_id(
|
adventure = await PostgresAdventureRepository(db).get_by_id(_parse_adventure_id(adventure_id))
|
||||||
_parse_adventure_id(adventure_id)
|
|
||||||
)
|
|
||||||
if adventure is None or adventure.user_id != str(user_id):
|
if adventure is None or adventure.user_id != str(user_id):
|
||||||
raise HTTPException(status_code=404, detail="Adventure not found")
|
raise HTTPException(status_code=404, detail="Adventure not found")
|
||||||
return _to_adventure_response(adventure)
|
return _to_adventure_response(adventure)
|
||||||
|
|
@ -334,9 +301,7 @@ async def delete_adventure(
|
||||||
await repo.soft_delete(uuid.UUID(adventure.id))
|
await repo.soft_delete(uuid.UUID(adventure.id))
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post("/{adventure_id}/decisions", response_model=DecisionResponse, status_code=201)
|
||||||
"/{adventure_id}/decisions", response_model=DecisionResponse, status_code=201
|
|
||||||
)
|
|
||||||
async def record_decision(
|
async def record_decision(
|
||||||
adventure_id: str,
|
adventure_id: str,
|
||||||
body: CreateDecisionRequest,
|
body: CreateDecisionRequest,
|
||||||
|
|
@ -351,9 +316,7 @@ async def record_decision(
|
||||||
raise HTTPException(status_code=400, detail="Invalid choice_id")
|
raise HTTPException(status_code=400, detail="Invalid choice_id")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
decision, next_entry = await _make_service(
|
decision, next_entry = await _make_service(db).record_decision_and_prepare_next_entry(
|
||||||
db
|
|
||||||
).record_decision_and_prepare_next_entry(
|
|
||||||
adventure_id=_parse_adventure_id(adventure_id),
|
adventure_id=_parse_adventure_id(adventure_id),
|
||||||
choice_id=choice_id,
|
choice_id=choice_id,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
|
|
@ -455,8 +418,7 @@ async def get_entry(
|
||||||
story_text=entry.story_text,
|
story_text=entry.story_text,
|
||||||
created_at=entry.created_at.isoformat(),
|
created_at=entry.created_at.isoformat(),
|
||||||
choices=[
|
choices=[
|
||||||
ChoiceResponse(id=c.id, index=c.index, label=c.label, text=c.text)
|
ChoiceResponse(id=c.id, index=c.index, label=c.label, text=c.text) for c in choices
|
||||||
for c in choices
|
|
||||||
],
|
],
|
||||||
translation=translation.translated_text if translation else None,
|
translation=translation.translated_text if translation else None,
|
||||||
audio_file_name=audio.file_name if audio else None,
|
audio_file_name=audio.file_name if audio else None,
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -312,12 +312,6 @@ body {
|
||||||
color: var(--color-on-surface-variant);
|
color: var(--color-on-surface-variant);
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-fieldset {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-input {
|
.field-input {
|
||||||
background-color: var(--color-surface-container-high);
|
background-color: var(--color-surface-container-high);
|
||||||
color: var(--color-on-surface);
|
color: var(--color-on-surface);
|
||||||
|
|
@ -522,24 +516,3 @@ body {
|
||||||
color: #b3261e;
|
color: #b3261e;
|
||||||
border-left: 3px solid #b3261e;
|
border-left: 3px solid #b3261e;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
LAYOUT: APP PAGE
|
|
||||||
*/
|
|
||||||
|
|
||||||
.app-page {
|
|
||||||
max-width: 52rem;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: var(--space-8) var(--space-6);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--space-6);
|
|
||||||
|
|
||||||
.page-title {
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-size: var(--text-headline-lg);
|
|
||||||
font-weight: var(--weight-semibold);
|
|
||||||
line-height: var(--leading-snug);
|
|
||||||
color: var(--color-on-surface);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -192,68 +192,6 @@ export type AddWordRequest = {
|
||||||
source_article_id?: string | null;
|
source_article_id?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* AdventureResponse
|
|
||||||
*/
|
|
||||||
export type AdventureResponse = {
|
|
||||||
/**
|
|
||||||
* Id
|
|
||||||
*/
|
|
||||||
id: string;
|
|
||||||
/**
|
|
||||||
* User Id
|
|
||||||
*/
|
|
||||||
user_id: string;
|
|
||||||
/**
|
|
||||||
* Status
|
|
||||||
*/
|
|
||||||
status: string;
|
|
||||||
/**
|
|
||||||
* Language
|
|
||||||
*/
|
|
||||||
language: string;
|
|
||||||
/**
|
|
||||||
* Source Language
|
|
||||||
*/
|
|
||||||
source_language: string;
|
|
||||||
/**
|
|
||||||
* Competencies
|
|
||||||
*/
|
|
||||||
competencies: Array<string>;
|
|
||||||
/**
|
|
||||||
* Max Entry Count
|
|
||||||
*/
|
|
||||||
max_entry_count: number;
|
|
||||||
/**
|
|
||||||
* Title
|
|
||||||
*/
|
|
||||||
title: string;
|
|
||||||
/**
|
|
||||||
* Description
|
|
||||||
*/
|
|
||||||
description: string | null;
|
|
||||||
/**
|
|
||||||
* Genres
|
|
||||||
*/
|
|
||||||
genres: Array<string>;
|
|
||||||
/**
|
|
||||||
* Setting
|
|
||||||
*/
|
|
||||||
setting: Array<string>;
|
|
||||||
/**
|
|
||||||
* Vibes
|
|
||||||
*/
|
|
||||||
vibes: Array<string>;
|
|
||||||
/**
|
|
||||||
* Protagonist
|
|
||||||
*/
|
|
||||||
protagonist: Array<string>;
|
|
||||||
/**
|
|
||||||
* Created At
|
|
||||||
*/
|
|
||||||
created_at: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ArticleDetail
|
* ArticleDetail
|
||||||
*/
|
*/
|
||||||
|
|
@ -362,76 +300,6 @@ export type ArticleListResponse = {
|
||||||
articles: Array<ArticleItem>;
|
articles: Array<ArticleItem>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* ChoiceResponse
|
|
||||||
*/
|
|
||||||
export type ChoiceResponse = {
|
|
||||||
/**
|
|
||||||
* Id
|
|
||||||
*/
|
|
||||||
id: string;
|
|
||||||
/**
|
|
||||||
* Index
|
|
||||||
*/
|
|
||||||
index: number;
|
|
||||||
/**
|
|
||||||
* Label
|
|
||||||
*/
|
|
||||||
label: string;
|
|
||||||
/**
|
|
||||||
* Text
|
|
||||||
*/
|
|
||||||
text: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CreateAdventureRequest
|
|
||||||
*/
|
|
||||||
export type CreateAdventureRequest = {
|
|
||||||
/**
|
|
||||||
* Language
|
|
||||||
*/
|
|
||||||
language: string;
|
|
||||||
/**
|
|
||||||
* Source Language
|
|
||||||
*/
|
|
||||||
source_language: string;
|
|
||||||
/**
|
|
||||||
* Competencies
|
|
||||||
*/
|
|
||||||
competencies: Array<string>;
|
|
||||||
/**
|
|
||||||
* Genres
|
|
||||||
*/
|
|
||||||
genres: Array<string>;
|
|
||||||
/**
|
|
||||||
* Setting
|
|
||||||
*/
|
|
||||||
setting: Array<string>;
|
|
||||||
/**
|
|
||||||
* Vibes
|
|
||||||
*/
|
|
||||||
vibes: Array<string>;
|
|
||||||
/**
|
|
||||||
* Protagonist
|
|
||||||
*/
|
|
||||||
protagonist: Array<string>;
|
|
||||||
/**
|
|
||||||
* Max Entry Count
|
|
||||||
*/
|
|
||||||
max_entry_count?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CreateDecisionRequest
|
|
||||||
*/
|
|
||||||
export type CreateDecisionRequest = {
|
|
||||||
/**
|
|
||||||
* Choice Id
|
|
||||||
*/
|
|
||||||
choice_id: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CreatePackRequest
|
* CreatePackRequest
|
||||||
*/
|
*/
|
||||||
|
|
@ -466,108 +334,6 @@ export type CreatePackRequest = {
|
||||||
proficiencies?: Array<string>;
|
proficiencies?: Array<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* DecisionResponse
|
|
||||||
*/
|
|
||||||
export type DecisionResponse = {
|
|
||||||
/**
|
|
||||||
* Id
|
|
||||||
*/
|
|
||||||
id: string;
|
|
||||||
/**
|
|
||||||
* Choice Id
|
|
||||||
*/
|
|
||||||
choice_id: string;
|
|
||||||
/**
|
|
||||||
* User Id
|
|
||||||
*/
|
|
||||||
user_id: string;
|
|
||||||
/**
|
|
||||||
* Created At
|
|
||||||
*/
|
|
||||||
created_at: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* EntryDetailResponse
|
|
||||||
*/
|
|
||||||
export type EntryDetailResponse = {
|
|
||||||
/**
|
|
||||||
* Id
|
|
||||||
*/
|
|
||||||
id: string;
|
|
||||||
/**
|
|
||||||
* Adventure Id
|
|
||||||
*/
|
|
||||||
adventure_id: string;
|
|
||||||
/**
|
|
||||||
* Generated From Choice Id
|
|
||||||
*/
|
|
||||||
generated_from_choice_id: string | null;
|
|
||||||
/**
|
|
||||||
* Status
|
|
||||||
*/
|
|
||||||
status: string;
|
|
||||||
/**
|
|
||||||
* Entry Index
|
|
||||||
*/
|
|
||||||
entry_index: number;
|
|
||||||
/**
|
|
||||||
* Story Text
|
|
||||||
*/
|
|
||||||
story_text: string | null;
|
|
||||||
/**
|
|
||||||
* Created At
|
|
||||||
*/
|
|
||||||
created_at: string;
|
|
||||||
/**
|
|
||||||
* Choices
|
|
||||||
*/
|
|
||||||
choices: Array<ChoiceResponse>;
|
|
||||||
/**
|
|
||||||
* Translation
|
|
||||||
*/
|
|
||||||
translation: string | null;
|
|
||||||
/**
|
|
||||||
* Audio File Name
|
|
||||||
*/
|
|
||||||
audio_file_name: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* EntryResponse
|
|
||||||
*/
|
|
||||||
export type EntryResponse = {
|
|
||||||
/**
|
|
||||||
* Id
|
|
||||||
*/
|
|
||||||
id: string;
|
|
||||||
/**
|
|
||||||
* Adventure Id
|
|
||||||
*/
|
|
||||||
adventure_id: string;
|
|
||||||
/**
|
|
||||||
* Generated From Choice Id
|
|
||||||
*/
|
|
||||||
generated_from_choice_id: string | null;
|
|
||||||
/**
|
|
||||||
* Status
|
|
||||||
*/
|
|
||||||
status: string;
|
|
||||||
/**
|
|
||||||
* Entry Index
|
|
||||||
*/
|
|
||||||
entry_index: number;
|
|
||||||
/**
|
|
||||||
* Story Text
|
|
||||||
*/
|
|
||||||
story_text: string | null;
|
|
||||||
/**
|
|
||||||
* Created At
|
|
||||||
*/
|
|
||||||
created_at: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FlashcardEventResponse
|
* FlashcardEventResponse
|
||||||
*/
|
*/
|
||||||
|
|
@ -1253,14 +1019,6 @@ export type SenseCandidateResponse = {
|
||||||
tags: Array<string>;
|
tags: Array<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* SenseMatch
|
|
||||||
*/
|
|
||||||
export type SenseMatch = {
|
|
||||||
sense: SenseResponse;
|
|
||||||
lemma: LemmaResponse;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SenseResponse
|
* SenseResponse
|
||||||
*/
|
*/
|
||||||
|
|
@ -1800,42 +1558,6 @@ export type SearchWordformsPrefixApiDictionarySearchGetResponses = {
|
||||||
|
|
||||||
export type SearchWordformsPrefixApiDictionarySearchGetResponse = SearchWordformsPrefixApiDictionarySearchGetResponses[keyof SearchWordformsPrefixApiDictionarySearchGetResponses];
|
export type SearchWordformsPrefixApiDictionarySearchGetResponse = SearchWordformsPrefixApiDictionarySearchGetResponses[keyof SearchWordformsPrefixApiDictionarySearchGetResponses];
|
||||||
|
|
||||||
export type SearchSensesApiDictionarySensesGetData = {
|
|
||||||
body?: never;
|
|
||||||
path?: never;
|
|
||||||
query: {
|
|
||||||
/**
|
|
||||||
* Lang Code
|
|
||||||
*/
|
|
||||||
lang_code: string;
|
|
||||||
/**
|
|
||||||
* Text
|
|
||||||
*/
|
|
||||||
text: string;
|
|
||||||
};
|
|
||||||
url: '/api/dictionary/senses';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SearchSensesApiDictionarySensesGetErrors = {
|
|
||||||
/**
|
|
||||||
* Validation Error
|
|
||||||
*/
|
|
||||||
422: HttpValidationError;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SearchSensesApiDictionarySensesGetError = SearchSensesApiDictionarySensesGetErrors[keyof SearchSensesApiDictionarySensesGetErrors];
|
|
||||||
|
|
||||||
export type SearchSensesApiDictionarySensesGetResponses = {
|
|
||||||
/**
|
|
||||||
* Response Search Senses Api Dictionary Senses Get
|
|
||||||
*
|
|
||||||
* Successful Response
|
|
||||||
*/
|
|
||||||
200: Array<SenseMatch>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SearchSensesApiDictionarySensesGetResponse = SearchSensesApiDictionarySensesGetResponses[keyof SearchSensesApiDictionarySensesGetResponses];
|
|
||||||
|
|
||||||
export type SearchWordformsApiDictionaryWordformsGetData = {
|
export type SearchWordformsApiDictionaryWordformsGetData = {
|
||||||
body?: never;
|
body?: never;
|
||||||
path?: never;
|
path?: never;
|
||||||
|
|
@ -2658,205 +2380,6 @@ export type RemoveFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsTe
|
||||||
|
|
||||||
export type RemoveFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsTemplateIdDeleteResponse = RemoveFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsTemplateIdDeleteResponses[keyof RemoveFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsTemplateIdDeleteResponses];
|
export type RemoveFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsTemplateIdDeleteResponse = RemoveFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsTemplateIdDeleteResponses[keyof RemoveFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsTemplateIdDeleteResponses];
|
||||||
|
|
||||||
export type ListAdventuresApiAdventuresGetData = {
|
|
||||||
body?: never;
|
|
||||||
path?: never;
|
|
||||||
query?: never;
|
|
||||||
url: '/api/adventures';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ListAdventuresApiAdventuresGetResponses = {
|
|
||||||
/**
|
|
||||||
* Response List Adventures Api Adventures Get
|
|
||||||
*
|
|
||||||
* Successful Response
|
|
||||||
*/
|
|
||||||
200: Array<AdventureResponse>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ListAdventuresApiAdventuresGetResponse = ListAdventuresApiAdventuresGetResponses[keyof ListAdventuresApiAdventuresGetResponses];
|
|
||||||
|
|
||||||
export type CreateAdventureApiAdventuresPostData = {
|
|
||||||
body: CreateAdventureRequest;
|
|
||||||
path?: never;
|
|
||||||
query?: never;
|
|
||||||
url: '/api/adventures';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CreateAdventureApiAdventuresPostErrors = {
|
|
||||||
/**
|
|
||||||
* Validation Error
|
|
||||||
*/
|
|
||||||
422: HttpValidationError;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CreateAdventureApiAdventuresPostError = CreateAdventureApiAdventuresPostErrors[keyof CreateAdventureApiAdventuresPostErrors];
|
|
||||||
|
|
||||||
export type CreateAdventureApiAdventuresPostResponses = {
|
|
||||||
/**
|
|
||||||
* Successful Response
|
|
||||||
*/
|
|
||||||
201: AdventureResponse;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CreateAdventureApiAdventuresPostResponse = CreateAdventureApiAdventuresPostResponses[keyof CreateAdventureApiAdventuresPostResponses];
|
|
||||||
|
|
||||||
export type DeleteAdventureApiAdventuresAdventureIdDeleteData = {
|
|
||||||
body?: never;
|
|
||||||
path: {
|
|
||||||
/**
|
|
||||||
* Adventure Id
|
|
||||||
*/
|
|
||||||
adventure_id: string;
|
|
||||||
};
|
|
||||||
query?: never;
|
|
||||||
url: '/api/adventures/{adventure_id}';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DeleteAdventureApiAdventuresAdventureIdDeleteErrors = {
|
|
||||||
/**
|
|
||||||
* Validation Error
|
|
||||||
*/
|
|
||||||
422: HttpValidationError;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DeleteAdventureApiAdventuresAdventureIdDeleteError = DeleteAdventureApiAdventuresAdventureIdDeleteErrors[keyof DeleteAdventureApiAdventuresAdventureIdDeleteErrors];
|
|
||||||
|
|
||||||
export type DeleteAdventureApiAdventuresAdventureIdDeleteResponses = {
|
|
||||||
/**
|
|
||||||
* Successful Response
|
|
||||||
*/
|
|
||||||
204: void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DeleteAdventureApiAdventuresAdventureIdDeleteResponse = DeleteAdventureApiAdventuresAdventureIdDeleteResponses[keyof DeleteAdventureApiAdventuresAdventureIdDeleteResponses];
|
|
||||||
|
|
||||||
export type GetAdventureApiAdventuresAdventureIdGetData = {
|
|
||||||
body?: never;
|
|
||||||
path: {
|
|
||||||
/**
|
|
||||||
* Adventure Id
|
|
||||||
*/
|
|
||||||
adventure_id: string;
|
|
||||||
};
|
|
||||||
query?: never;
|
|
||||||
url: '/api/adventures/{adventure_id}';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetAdventureApiAdventuresAdventureIdGetErrors = {
|
|
||||||
/**
|
|
||||||
* Validation Error
|
|
||||||
*/
|
|
||||||
422: HttpValidationError;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetAdventureApiAdventuresAdventureIdGetError = GetAdventureApiAdventuresAdventureIdGetErrors[keyof GetAdventureApiAdventuresAdventureIdGetErrors];
|
|
||||||
|
|
||||||
export type GetAdventureApiAdventuresAdventureIdGetResponses = {
|
|
||||||
/**
|
|
||||||
* Successful Response
|
|
||||||
*/
|
|
||||||
200: AdventureResponse;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetAdventureApiAdventuresAdventureIdGetResponse = GetAdventureApiAdventuresAdventureIdGetResponses[keyof GetAdventureApiAdventuresAdventureIdGetResponses];
|
|
||||||
|
|
||||||
export type RecordDecisionApiAdventuresAdventureIdDecisionsPostData = {
|
|
||||||
body: CreateDecisionRequest;
|
|
||||||
path: {
|
|
||||||
/**
|
|
||||||
* Adventure Id
|
|
||||||
*/
|
|
||||||
adventure_id: string;
|
|
||||||
};
|
|
||||||
query?: never;
|
|
||||||
url: '/api/adventures/{adventure_id}/decisions';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RecordDecisionApiAdventuresAdventureIdDecisionsPostErrors = {
|
|
||||||
/**
|
|
||||||
* Validation Error
|
|
||||||
*/
|
|
||||||
422: HttpValidationError;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RecordDecisionApiAdventuresAdventureIdDecisionsPostError = RecordDecisionApiAdventuresAdventureIdDecisionsPostErrors[keyof RecordDecisionApiAdventuresAdventureIdDecisionsPostErrors];
|
|
||||||
|
|
||||||
export type RecordDecisionApiAdventuresAdventureIdDecisionsPostResponses = {
|
|
||||||
/**
|
|
||||||
* Successful Response
|
|
||||||
*/
|
|
||||||
201: DecisionResponse;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RecordDecisionApiAdventuresAdventureIdDecisionsPostResponse = RecordDecisionApiAdventuresAdventureIdDecisionsPostResponses[keyof RecordDecisionApiAdventuresAdventureIdDecisionsPostResponses];
|
|
||||||
|
|
||||||
export type ListEntriesApiAdventuresAdventureIdEntriesGetData = {
|
|
||||||
body?: never;
|
|
||||||
path: {
|
|
||||||
/**
|
|
||||||
* Adventure Id
|
|
||||||
*/
|
|
||||||
adventure_id: string;
|
|
||||||
};
|
|
||||||
query?: never;
|
|
||||||
url: '/api/adventures/{adventure_id}/entries';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ListEntriesApiAdventuresAdventureIdEntriesGetErrors = {
|
|
||||||
/**
|
|
||||||
* Validation Error
|
|
||||||
*/
|
|
||||||
422: HttpValidationError;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ListEntriesApiAdventuresAdventureIdEntriesGetError = ListEntriesApiAdventuresAdventureIdEntriesGetErrors[keyof ListEntriesApiAdventuresAdventureIdEntriesGetErrors];
|
|
||||||
|
|
||||||
export type ListEntriesApiAdventuresAdventureIdEntriesGetResponses = {
|
|
||||||
/**
|
|
||||||
* Response List Entries Api Adventures Adventure Id Entries Get
|
|
||||||
*
|
|
||||||
* Successful Response
|
|
||||||
*/
|
|
||||||
200: Array<EntryResponse>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ListEntriesApiAdventuresAdventureIdEntriesGetResponse = ListEntriesApiAdventuresAdventureIdEntriesGetResponses[keyof ListEntriesApiAdventuresAdventureIdEntriesGetResponses];
|
|
||||||
|
|
||||||
export type GetEntryApiAdventuresAdventureIdEntriesEntryIdGetData = {
|
|
||||||
body?: never;
|
|
||||||
path: {
|
|
||||||
/**
|
|
||||||
* Adventure Id
|
|
||||||
*/
|
|
||||||
adventure_id: string;
|
|
||||||
/**
|
|
||||||
* Entry Id
|
|
||||||
*/
|
|
||||||
entry_id: string;
|
|
||||||
};
|
|
||||||
query?: never;
|
|
||||||
url: '/api/adventures/{adventure_id}/entries/{entry_id}';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetEntryApiAdventuresAdventureIdEntriesEntryIdGetErrors = {
|
|
||||||
/**
|
|
||||||
* Validation Error
|
|
||||||
*/
|
|
||||||
422: HttpValidationError;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetEntryApiAdventuresAdventureIdEntriesEntryIdGetError = GetEntryApiAdventuresAdventureIdEntriesEntryIdGetErrors[keyof GetEntryApiAdventuresAdventureIdEntriesEntryIdGetErrors];
|
|
||||||
|
|
||||||
export type GetEntryApiAdventuresAdventureIdEntriesEntryIdGetResponses = {
|
|
||||||
/**
|
|
||||||
* Successful Response
|
|
||||||
*/
|
|
||||||
200: EntryDetailResponse;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetEntryApiAdventuresAdventureIdEntriesEntryIdGetResponse = GetEntryApiAdventuresAdventureIdEntriesEntryIdGetResponses[keyof GetEntryApiAdventuresAdventureIdEntriesEntryIdGetResponses];
|
|
||||||
|
|
||||||
export type GetAccountBffAccountGetData = {
|
export type GetAccountBffAccountGetData = {
|
||||||
body?: never;
|
body?: never;
|
||||||
path?: never;
|
path?: never;
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1 @@
|
||||||
// place files you want to import through the `$lib` alias in this folder.
|
// place files you want to import through the `$lib` alias in this folder.
|
||||||
export { shuffleArray } from './shuffleArray';
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
export function shuffleArray<T>(data: Array<T>): Array<T> {
|
|
||||||
return data.sort(() => Math.random() - 0.5);
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
<script>
|
|
||||||
import { resolve } from '$app/paths';
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { getAdventures } from './getAdventures.remote';
|
|
||||||
onMount(async () => {
|
|
||||||
const _adventures = await getAdventures('');
|
|
||||||
if (_adventures) {
|
|
||||||
adventures = _adventures;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let adventures = $state([]);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="app-page">
|
|
||||||
<header class="page-header">
|
|
||||||
<h1 class="page-title">Adventures</h1>
|
|
||||||
|
|
||||||
<a href={resolve('/app/adventures/new')} class="btn">Create</a>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{#each adventures as adventure (adventure.id)}
|
|
||||||
<div class="adventure-card">
|
|
||||||
<h2>{adventure.title}</h2>
|
|
||||||
<p>{adventure.description}</p>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import { getRequestEvent, query } from '$app/server';
|
|
||||||
import { listAdventuresApiAdventuresGet } from '@client';
|
|
||||||
|
|
||||||
export const getAdventures = query('unchecked', async () => {
|
|
||||||
const request = getRequestEvent();
|
|
||||||
const { data } = await listAdventuresApiAdventuresGet({
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${request.locals.authToken}`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
});
|
|
||||||
|
|
@ -1,167 +0,0 @@
|
||||||
import { error, type Action, type Actions } from '@sveltejs/kit';
|
|
||||||
import type { PageServerLoad } from './$types';
|
|
||||||
import { createAdventureApiAdventuresPost } from '@client';
|
|
||||||
import * as v from 'valibot';
|
|
||||||
import { shuffleArray } from '$lib';
|
|
||||||
|
|
||||||
export const load: PageServerLoad = async () => {
|
|
||||||
return {
|
|
||||||
lengths: ['100-200 Words', '200-350 Words', '350-500 Words', '500-750 Words'],
|
|
||||||
competencies: ['A1', 'A2', 'B1', 'B2', 'C1'],
|
|
||||||
languages: [
|
|
||||||
{ code: 'fr', name: 'French' },
|
|
||||||
{ code: 'de', name: 'German' },
|
|
||||||
{ code: 'it', name: 'Italian' },
|
|
||||||
{ code: 'es', name: 'Spanish' },
|
|
||||||
{ code: 'pt', name: 'Portuguese' }
|
|
||||||
],
|
|
||||||
genres: shuffleArray([
|
|
||||||
'Crime Fiction',
|
|
||||||
'Crime noir',
|
|
||||||
'Who-dun-it mystery',
|
|
||||||
'Paranormal',
|
|
||||||
'Horror',
|
|
||||||
'Psychological thriller',
|
|
||||||
'Romance',
|
|
||||||
'Family',
|
|
||||||
'Fantasy',
|
|
||||||
'Science Fiction'
|
|
||||||
]),
|
|
||||||
eras: [
|
|
||||||
'Ancient/Classical',
|
|
||||||
'Medieval',
|
|
||||||
'1500-1800',
|
|
||||||
'Renaissance',
|
|
||||||
'19th century',
|
|
||||||
'Early 20th century',
|
|
||||||
'Mid-century',
|
|
||||||
'Contemporary',
|
|
||||||
'Near future',
|
|
||||||
'Far future'
|
|
||||||
],
|
|
||||||
settings: [
|
|
||||||
'Capital city',
|
|
||||||
'Large city (not the capital)',
|
|
||||||
'Urban',
|
|
||||||
'The suburbs',
|
|
||||||
'Rural',
|
|
||||||
'Pastoral',
|
|
||||||
'Small town',
|
|
||||||
'Wilderness',
|
|
||||||
'Space',
|
|
||||||
'Another planet'
|
|
||||||
],
|
|
||||||
vibes: [
|
|
||||||
'Melancholic',
|
|
||||||
'Gothic',
|
|
||||||
'Sun-drenched',
|
|
||||||
'Bleak',
|
|
||||||
'Whimsical',
|
|
||||||
'Eerie',
|
|
||||||
'Cosy',
|
|
||||||
'Tense',
|
|
||||||
'Witty',
|
|
||||||
'Propulsive',
|
|
||||||
'Mentor and student',
|
|
||||||
'Unlikely duo',
|
|
||||||
'Lone wolf',
|
|
||||||
'Queer-norm',
|
|
||||||
'Class tensions',
|
|
||||||
'Chosen family',
|
|
||||||
'Diaspora',
|
|
||||||
'Academia',
|
|
||||||
'Small town',
|
|
||||||
'The sea',
|
|
||||||
'Grand house',
|
|
||||||
'Road trip',
|
|
||||||
'A single night',
|
|
||||||
'Heist',
|
|
||||||
'Mystery box',
|
|
||||||
'Reluctant hero',
|
|
||||||
'Redemption',
|
|
||||||
'Animal companions',
|
|
||||||
'Gentle',
|
|
||||||
'Happy ever after',
|
|
||||||
'Bittersweet',
|
|
||||||
'Epistolary (letters / diary entries)',
|
|
||||||
"A big city that isn't the capital",
|
|
||||||
'Parenthood',
|
|
||||||
'Sly',
|
|
||||||
'Slapstick',
|
|
||||||
'Recovery',
|
|
||||||
'Political',
|
|
||||||
'Apocalyptic',
|
|
||||||
'Post-apocalyptic',
|
|
||||||
'Survival',
|
|
||||||
'War',
|
|
||||||
'Spy thriller',
|
|
||||||
'Time travel'
|
|
||||||
]
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const CreateFormSchema = v.object({
|
|
||||||
vibes: v.array(v.string()),
|
|
||||||
genre: v.string(),
|
|
||||||
setting: v.string(),
|
|
||||||
competency: v.pipe(v.string(), v.picklist(['A1', 'A2', 'B1', 'B2', 'C1'])),
|
|
||||||
language: v.pipe(v.string(), v.picklist(['fr', 'it', 'de', 'it', 'es'])),
|
|
||||||
length: v.string(),
|
|
||||||
protagonist_gender: v.string(),
|
|
||||||
protagonist_age: v.string()
|
|
||||||
});
|
|
||||||
|
|
||||||
export const actions = {
|
|
||||||
default: async ({ locals, request }) => {
|
|
||||||
const { authToken } = locals;
|
|
||||||
|
|
||||||
const formData = await request.formData();
|
|
||||||
|
|
||||||
const data = v.safeParse(CreateFormSchema, {
|
|
||||||
vibes: formData.getAll('vibes') as string[],
|
|
||||||
genre: formData.get('genre') as string,
|
|
||||||
setting: formData.get('setting') as string,
|
|
||||||
competency: formData.get('competency') as string,
|
|
||||||
language: formData.get('language') as string,
|
|
||||||
length: formData.get('length') as string,
|
|
||||||
protagonist_gender: formData.get('protagonist_gender') as string,
|
|
||||||
protagonist_age: formData.get('protagonist_age') as string
|
|
||||||
});
|
|
||||||
|
|
||||||
if (data.success == false) {
|
|
||||||
throw error(400, {
|
|
||||||
message: data.issues.map((e) => e.message).join(', ')
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
competency,
|
|
||||||
language,
|
|
||||||
length,
|
|
||||||
genre,
|
|
||||||
setting,
|
|
||||||
protagonist_gender,
|
|
||||||
protagonist_age,
|
|
||||||
vibes
|
|
||||||
} = data.output;
|
|
||||||
|
|
||||||
const { data: apiData, error: apiError } = await createAdventureApiAdventuresPost({
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${authToken}`
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
competencies: [competency],
|
|
||||||
language: language,
|
|
||||||
genres: [genre],
|
|
||||||
setting: [setting],
|
|
||||||
vibes: vibes,
|
|
||||||
protagonist: [protagonist_gender, protagonist_age],
|
|
||||||
source_language: 'en',
|
|
||||||
entry_word_count_range: length,
|
|
||||||
max_entry_count: 6
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log({ apiData, apiError });
|
|
||||||
}
|
|
||||||
} satisfies Actions;
|
|
||||||
|
|
@ -1,123 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { shuffleArray } from '$lib';
|
|
||||||
import type { ChangeEventHandler } from 'svelte/elements';
|
|
||||||
|
|
||||||
import type { PageServerData } from './$types';
|
|
||||||
|
|
||||||
const { data }: { data: PageServerData } = $props();
|
|
||||||
|
|
||||||
let theVibes = $state(shuffleArray(data.vibes).slice(0, 7));
|
|
||||||
|
|
||||||
const handleVibeSelected: ChangeEventHandler<HTMLInputElement> = (e) => {
|
|
||||||
const { checked, value } = e.currentTarget;
|
|
||||||
|
|
||||||
if (checked) {
|
|
||||||
selectedVibes = [...selectedVibes, value as string];
|
|
||||||
} else {
|
|
||||||
selectedVibes = selectedVibes.filter((v) => v !== value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let selectedVibes = $state([]);
|
|
||||||
let vibeShuffleCount = $state(0);
|
|
||||||
let shuffleTheVibes = () => (theVibes = shuffleArray(data.vibes).slice(0, 5));
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<section class="app-page">
|
|
||||||
<div class="page-header">
|
|
||||||
<div class="page-title">Create new Adventure</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form class="form" method="POST">
|
|
||||||
<div class="field">
|
|
||||||
<label for="genre" class="label">Genre</label>
|
|
||||||
<select name="genre" id="genre">
|
|
||||||
{#each data.genres as genre (genre)}
|
|
||||||
<option value={genre}>{genre}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<label for="setting" class="label">Setting</label>
|
|
||||||
<select name="setting" id="setting">
|
|
||||||
{#each ['Urban', 'Rural', 'Coastal', 'Mountain', 'Forest', 'Desert'] as setting (setting)}
|
|
||||||
<option value={setting}>{setting}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<fieldset class="field-fieldset">
|
|
||||||
<legend>Entry length</legend>
|
|
||||||
{#each data.lengths as length (length)}
|
|
||||||
{@const inputId = `length-${length}`}
|
|
||||||
<div class="radio-option">
|
|
||||||
<input type="radio" name="length" value={length} id={inputId} />
|
|
||||||
<label for={inputId}>{length}</label>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<fieldset class="field-fieldset">
|
|
||||||
<legend>Vibes (pick two)</legend>
|
|
||||||
{#each theVibes as vibe (vibe)}
|
|
||||||
{@const theId = `vibe-${vibe}`}
|
|
||||||
<label for={theId} class="label">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
disabled={selectedVibes.length >= 2 && !selectedVibes.includes(vibe)}
|
|
||||||
id={theId}
|
|
||||||
value={vibe}
|
|
||||||
onchange={handleVibeSelected}
|
|
||||||
/>
|
|
||||||
{vibe}
|
|
||||||
</label>
|
|
||||||
{/each}
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<label for="language" class="label">Language</label>
|
|
||||||
<select name="language" id="language">
|
|
||||||
{#each data.languages as language (language.code)}
|
|
||||||
<option value={language.code}>{language.name}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<label for="competency" class="label">Competency</label>
|
|
||||||
<select name="competency" id="competency">
|
|
||||||
{#each data.competencies as competency (competency)}
|
|
||||||
<option value={competency}>{competency}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<fieldset class="field-fieldset">
|
|
||||||
<legend>Protagonist gender</legend>
|
|
||||||
{#each ['male', 'female', 'non-binary', 'any'] as gender (gender)}
|
|
||||||
{@const inputId = `gender-${gender}`}
|
|
||||||
<div class="radio-option">
|
|
||||||
<input type="radio" name="protagonist_gender" value={gender} id={inputId} />
|
|
||||||
<label for={inputId}>{gender}</label>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<label for="protagonist_age" class="label">Protagonist age</label>
|
|
||||||
<select name="protagonist_age" id="protagonist_age">
|
|
||||||
{#each ['Adult', 'Child', 'Teen', 'Young Adult', 'Older Adult', 'Middle-aged', 'Older'] as age (age)}
|
|
||||||
<option value={age}>{age}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input type="submit" value="Create Adventure" class="submit-button" />
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
}).format(new Date(iso));
|
}).format(new Date(iso));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="app-page">
|
<div class="page">
|
||||||
<header class="page-header">
|
<header class="page-header">
|
||||||
<p class="form-eyebrow">Reading</p>
|
<p class="form-eyebrow">Reading</p>
|
||||||
<h1 class="form-title">Articles</h1>
|
<h1 class="form-title">Articles</h1>
|
||||||
|
|
@ -44,9 +44,7 @@
|
||||||
</div>
|
</div>
|
||||||
<h2 class="article-title">{article.target_title}</h2>
|
<h2 class="article-title">{article.target_title}</h2>
|
||||||
<p class="article-source">{article.source_title}</p>
|
<p class="article-source">{article.source_title}</p>
|
||||||
<time class="article-date label-md" datetime={article.published_at}
|
<time class="article-date label-md" datetime={article.published_at}>{fmt(article.published_at)}</time>
|
||||||
>{fmt(article.published_at)}</time
|
|
||||||
>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
@ -60,6 +58,15 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.page {
|
||||||
|
max-width: 52rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: var(--space-8) var(--space-6);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
/* --- Article list --- */
|
/* --- Article list --- */
|
||||||
|
|
||||||
.article-list {
|
.article-list {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ const config = {
|
||||||
remoteFunctions: true
|
remoteFunctions: true
|
||||||
},
|
},
|
||||||
alias: {
|
alias: {
|
||||||
'@client': 'src/client/sdk.gen'
|
'@client': 'src/client/client.gen.ts'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
compilerOptions: {
|
compilerOptions: {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue