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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,19 @@ Modules are discovered dynamically by the modular-rest framework. Entry point: [
- **i18n**: strings in [frontend/locales/en.json](frontend/locales/en.json) β€” only English is wired up today.
- **UI components**: prefer **pilotui** (in-house Vue 3 + Tailwind library) before hand-rolling. Components are organized by category path (`pilotui/elements`, `pilotui/form`, `pilotui/shell`, etc.), use the `CL` prefix (e.g. `<CLButton>`), and require wrapping the app in `AppRoot` for theming. Registered in [frontend/plugins/component-library.ts](frontend/plugins/component-library.ts). **LLM-friendly docs**: <https://codebridger.github.io/lib-vue-components/llm.md> β€” fetch when adding or editing UI to see component APIs.

## Commits & versioning

This repo uses **semantic versioning**, and commit titles follow **Conventional Commits** so the type prefix maps to the intended `vMAJOR.MINOR.PATCH` bump. Pick the type by the change's real impact, not by habit:

- `feat:` β†’ **minor** (`0.X.0`) β€” new user-facing capability
- `fix:` / `perf:` β†’ **patch** (`0.0.X`) β€” bug fix or performance
- `feat!:` / `fix!:` / a `BREAKING CHANGE:` footer β†’ **major** (`X.0.0`)
- `refactor:` / `chore:` / `docs:` / `test:` / `style:` / `ci:` / `build:` β†’ **no bump**

Don't dress a real feature as `refactor`/`chore` (it would skip a release) or inflate a refactor into `feat` (it over-bumps). If you squash-merge a PR, the **PR title** becomes the commit message, so it must follow the same convention.

> Release automation is **not** wired up in this repo yet (no `semantic-release`, no tags, `server/package.json` is `0.0.0`) β€” the convention currently records the *intended* bump. The sibling **subturtle-extension-apps** repo enforces the identical mapping automatically via `semantic-release`.

## Gotchas

- **Gemini, not OpenAI**, for new live-session work.
Expand Down
11 changes: 5 additions & 6 deletions frontend/components/bundle/PhraseCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@
</div>

<div class="flex-1">
<TextArea type="text" :label="isLinguisticPhrase ? t('definition') : t('translation')"
:placeholder="isLinguisticPhrase ? t('bundle.phrase_card.definition_placeholder') : t('bundle.phrase_card.translation_placeholder')"
<TextArea type="text" :label="t('translation')"
:placeholder="t('bundle.phrase_card.translation_placeholder')"
v-model="translation" :error="!!error" :error-message="error || ''"
:loading="!!props.newPhrase && isSubmitting" :disabled="isLinguisticPhrase" />
</div>
Expand Down Expand Up @@ -97,11 +97,10 @@ const isLinguisticPhrase = computed(() => {
return props.phrase?.type === 'linguistic';
});

// Computed property for translation/definition value
// Computed property for the translation value.
// Show the same translation the user saw in the extension (translation.phrase),
// not linguistic_data.definition (a whole-phrase explanation never shown on save).
const translationValue = computed(() => {
if (isLinguisticPhrase.value) {
return props.phrase?.linguistic_data?.definition || '';
}
return props.phrase?.translation || '';
});

Expand Down
34 changes: 18 additions & 16 deletions frontend/components/practice/LeitnerReviewSession.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,13 @@
</div>

<div v-else class="flex h-full w-full flex-col items-center p-5 md:px-16 md:py-14">
<!-- Fixed height (viewport units) so the card never resizes when flipping between
front/back; long content scrolls inside the card instead of growing the page. -->
<div
:class="['w-full flex-1 transition-all duration-300 ease-in-out', 'md:max-h-[80%] md:max-w-[80%]', 'lg:max-h-[65%] lg:max-w-[65%]']">
<!-- Use WidgetFlashCard based on phrase type -->
:class="['h-[58vh] w-full transition-all duration-300 ease-in-out', 'md:max-w-[80%]', 'lg:max-w-[65%]']">
<Transition name="fade-slide" mode="out-in">
<WidgetFlashCard v-if="currentPhrase && currentPhrase.type === 'normal'"
:key="`normal-${currentIndex}`" ref="flashCardRef" :phrase-type="'normal'"
:front="currentPhrase.phrase" :back="currentPhrase.translation" :context="currentPhrase.context"
:translation-language="currentPhrase.translation_language" />

<WidgetFlashCard v-else-if="currentPhrase && currentPhrase.type === 'linguistic'"
:key="`linguistic-${currentIndex}`" ref="flashCardRef" :phrase-type="'linguistic'"
:front="currentPhrase.phrase"
:back="currentPhrase.linguistic_data?.definition || currentPhrase.phrase"
:context="currentPhrase.context" :direction="currentPhrase.direction"
:language-info="currentPhrase.language_info" :linguistic-data="currentPhrase.linguistic_data" />

<WidgetFlashCard v-else-if="currentPhrase" :key="`fallback-${currentIndex}`" ref="flashCardRef"
:front="currentPhrase.phrase" :back="currentPhrase.translation || 'No translation'" />
<WidgetFlashCard v-if="currentPhrase" :key="currentIndex" ref="flashCardRef"
:phrase="currentPhrase" :leitner-level="cardLevel" />
</Transition>
</div>

Expand Down Expand Up @@ -93,6 +82,13 @@ const currentItem = computed(() => props.items[currentIndex.value]);
const currentPhrase = computed(() => currentItem.value?.phrase as PhraseType);
const totalItems = computed(() => props.items.length);

// Leitner level drives the L3+ cloze. get-review-session returns raw Mongoose docs, so schema fields
// like boxLevel sit under `_doc` rather than at the top level β€” read both so the level is found either way.
const cardLevel = computed<number | undefined>(() => {
const item = currentItem.value as any;
return item?.boxLevel ?? item?._doc?.boxLevel;
});

onMounted(() => {
window.addEventListener('keydown', handleKeyDown);
});
Expand All @@ -104,6 +100,12 @@ onUnmounted(() => {
function handleKeyDown(event: KeyboardEvent) {
if (props.loading || props.items.length === 0) return;

// Don't hijack typing: while the cloze answer input (or any field) is focused, let keys through.
const target = event.target as HTMLElement | null;
if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)) {
return;
}

switch (event.code) {
case 'Space':
event.preventDefault();
Expand Down
Loading
Loading