language-learning-app/frontend/src/routes/app/articles/[article_id]/TargetLanguageBody.svelte
wilson 13911331a3 feat: Add word-level translation panel to article page
- Add DeepL integration for word translation
- Parse article body into sentences and tokens
- Highlight active sentence and selected word
- Show translation in a sticky panel or mobile drawer
- Refactor audio timing and body parsing logic
- Enable SvelteKit remote functions and async compiler options
- Add dependencies: deepl-node, valibot
2026-03-31 07:21:20 +01:00

91 lines
2.2 KiB
Svelte

<script lang="ts">
import type { Paragraph, Sentence, SentenceToken } from './Transcript';
interface Props {
paragraphs: Paragraph[];
activeSentenceIdx: number;
selectedSentenceToken: SentenceToken | null;
onWordClick: (token: SentenceToken, sentence: Sentence) => void;
lang: string;
}
const { paragraphs, activeSentenceIdx, selectedSentenceToken, onWordClick, lang }: Props =
$props();
</script>
<div class="article-body" {lang}>
{#each paragraphs as para (para.index)}
<p class="paragraph">
{#each para.sentences as sentence (sentence.idx)}
<span class="sentence" class:sentence--active={activeSentenceIdx === sentence.idx}>
{#each sentence.tokens as token (token.idx)}
{#if !token.is_punct}
<button
class="word"
class:word--selected={selectedSentenceToken?.idx === token.idx}
onclick={() => onWordClick(token, sentence)}
>
{token.text}
</button>
{:else}
{token.text}
{/if}
{/each}
</span>
{/each}
</p>
{/each}
</div>
<style>
.article-body {
display: flex;
flex-direction: column;
gap: var(--space-4);
font-family: var(--font-body);
}
.paragraph {
font-family: var(--font-body);
font-size: var(--text-body-xl);
line-height: 2;
color: var(--color-on-surface);
}
/* Sentence: highlighted when audio is at that point */
.sentence {
border-radius: var(--radius-xs);
transition: background-color var(--duration-normal) var(--ease-standard);
}
.sentence--active {
background-color: var(--color-primary-container);
}
/* --- Word buttons --- */
.word {
display: inline;
background: none;
border: none;
padding: 0 0.1em;
margin: 0;
font: inherit;
color: inherit;
cursor: pointer;
border-radius: var(--radius-xs);
transition:
background-color var(--duration-fast) var(--ease-standard),
color var(--duration-fast) var(--ease-standard);
}
.word:hover {
background-color: color-mix(in srgb, var(--color-primary) 12%, transparent);
color: var(--color-primary);
}
.word--selected {
background-color: color-mix(in srgb, var(--color-primary) 20%, transparent);
color: var(--color-primary);
font-weight: var(--weight-medium);
}
</style>