Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
69dcf1b
feat: announce extension presence on dashboard origins for install nu…
navidshad May 13, 2026
9954c22
feat(save-modal): chunk highlights, AI advice chat, bundle suggestion
navidshad May 21, 2026
f766040
feat(save-modal): per-chunk definitions, merged pronunciation, reorde…
navidshad May 21, 2026
3b85dd8
chore(nibble): rename "Save & view" button to "Save and Learn"
navidshad May 21, 2026
58261fb
Merge pull request #24 from codebridger/CU-86exkh0z3_Step-3-First-run…
navidshad May 21, 2026
b392554
chore(release): 1.12.0-dev.1 [skip ci]
semantic-release-bot May 21, 2026
01e510c
refactor(save-modal): per-page bundle suggestion via dedicated RPC
navidshad May 21, 2026
ab00130
fix(save-modal): break circular import to console-crane store
navidshad May 21, 2026
374cbb4
feat(save-modal): in-field bundle chips with dirty-aware save + inlin…
navidshad May 21, 2026
1315cc8
feat(saved-phrase): DB-first lookup, reuse stored translation, no AI …
navidshad May 21, 2026
25499da
fix(save-modal): refetch bundle options so post-save chip shows title
navidshad May 22, 2026
224b9da
feat(console-crane): practice + flashcard-preview pages, near-transla…
navidshad May 23, 2026
e504570
test(console-crane): cover flashcard preview, word-detail actions, na…
navidshad May 24, 2026
b1688a1
Merge pull request #25 from codebridger/CU-86exnxbwb_Step-6-Build-PRF…
navidshad May 24, 2026
7bb19e2
chore(release): 1.12.0-dev.2 [skip ci]
semantic-release-bot May 24, 2026
db1a3fc
feat(practice-now): voice session config + dashboard deep-link
navidshad May 25, 2026
2f09e05
feat(practice-now): open config to logged-out users + clearer CTAs
navidshad May 25, 2026
8ff3408
feat(practice-now): emphasize practiced phrase + cover login flows
navidshad May 25, 2026
f78baf8
Merge pull request #26 from codebridger/CU-86exnxnw7_Practice-now-con…
navidshad May 25, 2026
946542e
chore(release): 1.12.0-dev.3 [skip ci]
semantic-release-bot May 25, 2026
85d40b5
Merge branch 'main' into dev
navidshad May 26, 2026
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
33 changes: 33 additions & 0 deletions CHANGELOG-DEV.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,36 @@
# [1.12.0-dev.3](https://github.com/codebridger/subturtle-extension-apps/compare/v1.12.0-dev.2...v1.12.0-dev.3) (2026-05-25)


### Features

* **practice-now:** emphasize practiced phrase + cover login flows ([8ff3408](https://github.com/codebridger/subturtle-extension-apps/commit/8ff340831d3ca35dcbc97ddfcf7088f7f382582b))
* **practice-now:** open config to logged-out users + clearer CTAs ([2f09e05](https://github.com/codebridger/subturtle-extension-apps/commit/2f09e0577de43a55d995e9277446d313b058beef))
* **practice-now:** voice session config + dashboard deep-link ([db1a3fc](https://github.com/codebridger/subturtle-extension-apps/commit/db1a3fc08f2579c2d6c958a2e8c289c95e592339))

# [1.12.0-dev.2](https://github.com/codebridger/subturtle-extension-apps/compare/v1.12.0-dev.1...v1.12.0-dev.2) (2026-05-24)


### Bug Fixes

* **save-modal:** break circular import to console-crane store ([ab00130](https://github.com/codebridger/subturtle-extension-apps/commit/ab00130647d72703147f8a96df6faa8e3b1fbbe5))
* **save-modal:** refetch bundle options so post-save chip shows title ([25499da](https://github.com/codebridger/subturtle-extension-apps/commit/25499da35925bebeb854f20973c3ae39b4dbd6f2))


### Features

* **console-crane:** practice + flashcard-preview pages, near-translation actions ([224b9da](https://github.com/codebridger/subturtle-extension-apps/commit/224b9da79e8b9d2f094ae3f63b1f8524fcb78299))
* **save-modal:** chunk highlights, AI advice chat, bundle suggestion ([9954c22](https://github.com/codebridger/subturtle-extension-apps/commit/9954c22f8822f5a06912247d0098d937c20ec6b5))
* **save-modal:** in-field bundle chips with dirty-aware save + inline removal ([374cbb4](https://github.com/codebridger/subturtle-extension-apps/commit/374cbb4b0c957c5def27d6b9f0211b3ed4fe17f5))
* **save-modal:** per-chunk definitions, merged pronunciation, reorder save ([f766040](https://github.com/codebridger/subturtle-extension-apps/commit/f76604023886a6d88b18831c9e492ef8253e11b7))
* **saved-phrase:** DB-first lookup, reuse stored translation, no AI re-call ([1315cc8](https://github.com/codebridger/subturtle-extension-apps/commit/1315cc86c3421ab64560e68e70d0b282c662d3ba))

# [1.12.0-dev.1](https://github.com/codebridger/subturtle-extension-apps/compare/v1.11.2-dev.1...v1.12.0-dev.1) (2026-05-21)


### Features

* announce extension presence on dashboard origins for install nudge [#86](https://github.com/codebridger/subturtle-extension-apps/issues/86)exkh0z3 ([69dcf1b](https://github.com/codebridger/subturtle-extension-apps/commit/69dcf1bc0c13f293c01592545ec2ccb1111d5f3e)), closes [#86exkh0z3](https://github.com/codebridger/subturtle-extension-apps/issues/86exkh0z3)

## [1.11.2-dev.1](https://github.com/codebridger/subturtle-extension-apps/compare/v1.11.1...v1.11.2-dev.1) (2026-05-06)


Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "subturtle-extension",
"version": "1.11.2",
"version": "1.12.0-dev.3",
"private": true,
"scripts": {
"dev": "webpack --watch",
Expand Down Expand Up @@ -60,4 +60,4 @@
"vue": "3.5.17",
"vue-router": "4.5.1"
}
}
}
30 changes: 30 additions & 0 deletions src/common/helper/url-normalise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Normalise a source page URL so saves from the same content group together.
* Mirrors the server-side normaliser (server/src/modules/translation/url-normalise.ts):
* drops query strings, fragments, and timestamps; lowercases the host; removes a
* trailing slash. Returns the input unchanged when it cannot be parsed.
*/
export function normaliseSourceUrl(raw: string): string {
if (!raw || typeof raw !== "string") return "";

const trimmed = raw.trim();

let url: URL;
try {
url = new URL(trimmed);
} catch {
return trimmed;
}

url.hash = "";
url.search = "";
url.hostname = url.hostname.toLowerCase();

let normalised = url.toString();

if (normalised.endsWith("/") && url.pathname !== "/") {
normalised = normalised.slice(0, -1);
}

return normalised;
}
56 changes: 56 additions & 0 deletions src/common/services/bundle-suggestion.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { authentication, functionProvider } from "@modular-rest/client";
import { normaliseSourceUrl } from "../helper/url-normalise";
import type { BundleSuggestion } from "../../console-crane/modules/word-detail/types";

/**
* Per-page bundle suggestion: which bundle the save modal should default to for
* the current page + logged-in user. Called once per page (first word-detail
* open) and cached client-side by normalised URL so repeated word lookups on
* the same page reuse the result.
*/
export class BundleSuggestionService {
static instance = new BundleSuggestionService();

// Cache of in-flight / resolved suggestions keyed by normalised URL.
private cache = new Map<string, Promise<BundleSuggestion>>();

/** Clear the cache (e.g. after a save creates a new bundle for this page). */
clear(url?: string) {
if (url) this.cache.delete(normaliseSourceUrl(url));
else this.cache.clear();
}

async getForCurrentPage(): Promise<BundleSuggestion> {
const empty: BundleSuggestion = { matchedBundle: null, suggestedName: null };

// Logged-in only; anonymous users get nothing to suggest.
if (!authentication.user?.id) return empty;
if (typeof location === "undefined") return empty;

const key = normaliseSourceUrl(location.href);
if (!key) return empty;

const cached = this.cache.get(key);
if (cached) return cached;

const pageTitle = typeof document !== "undefined" ? document.title : "";
const request = functionProvider
.run<BundleSuggestion>({
name: "getBundleSuggestionForPage",
args: {
refId: authentication.user?.id,
pageTitle,
pageUrl: location.href,
},
})
.catch((error) => {
// Best-effort; never block the save flow.
console.error("Bundle suggestion error:", error);
this.cache.delete(key);
return empty;
});

this.cache.set(key, request);
return request;
}
}
54 changes: 54 additions & 0 deletions src/common/services/live-voices.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { functionProvider } from "@modular-rest/client";

/**
* AI-coach voice as returned by the server's `get-live-session-voices`
* function. Kept structurally in sync with the dashboard's `CoachVoice`
* (server: live_session/voices.ts) β€” the two repos build separately so the
* shape is mirrored, not imported.
*/
export interface CoachVoice {
name: string;
label: string;
description?: string;
gender?: "female" | "male";
avatarColor?: string;
avatarUrl?: string | null;
}

/**
* Cached fetch of the coach voices. Singleton so every Practice now mount
* shares one network call (mirrors BundleSuggestionService / TranslateService).
*
* No offline fallback: if the server can't return voices, the dashboard live
* session can't run anyway, so an empty list is the honest result.
*/
export class LiveVoicesService {
private static _instance: LiveVoicesService | null = null;

static get instance(): LiveVoicesService {
if (!this._instance) this._instance = new LiveVoicesService();
return this._instance;
}

private cache: CoachVoice[] | null = null;
private inflight: Promise<CoachVoice[]> | null = null;

async getVoices(): Promise<CoachVoice[]> {
if (this.cache) return this.cache;
if (this.inflight) return this.inflight;

this.inflight = functionProvider
.run<CoachVoice[]>({ name: "get-live-session-voices", args: {} })
.then((res) => {
this.cache = res || [];
return this.cache;
})
// Don't cache failures, so a transient error retries on the next open.
.catch(() => [] as CoachVoice[])
.finally(() => {
this.inflight = null;
});

return this.inflight;
}
}
32 changes: 32 additions & 0 deletions src/common/services/phrase.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { dataProvider, authentication } from "@modular-rest/client";
import { COLLECTIONS, DATABASE } from "../static/global";
import type { PhraseType } from "../types/phrase.type";

/**
* Single source of truth for "has the user already saved this phrase?".
*
* Matches by phrase text (the saved unit) + owner only. The translation is
* intentionally excluded: the AI returns a slightly different translation on
* each call, so matching on it would make an already-saved phrase look unsaved.
*
* Returns the saved phrase document, or null when not logged in, the input is
* empty, the phrase isn't saved, or the lookup fails.
*/
export async function findSavedPhrase(
phrase: string
): Promise<PhraseType | null> {
const refId = authentication.user?.id;
const text = (phrase || "").trim();
if (!refId || !text) return null;

try {
const doc = await dataProvider.findOne<PhraseType>({
database: DATABASE.USER_CONTENT,
collection: COLLECTIONS.PHRASE,
query: { refId, phrase: text },
});
return (doc as PhraseType) || null;
} catch {
return null;
}
}
32 changes: 31 additions & 1 deletion src/common/services/translate.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import { Dictionary } from "../types/general.type";

import proxy from "./proxy.service";
import { functionProvider } from "@modular-rest/client";
import { LanguageLearningData } from "../../console-crane/modules/word-detail/types";
import {
Chunk,
LanguageLearningData,
TranslationAdvice,
} from "../../console-crane/modules/word-detail/types";
import { LanguageDetector } from "../helper/language-detection";
import { useSettingsStore } from "../store/settings";

Expand Down Expand Up @@ -171,6 +175,7 @@ export class TranslateService {
// Add context and phrase to the result
data.context = context;
data.phrase = text;
if (!Array.isArray(data.chunks)) data.chunks = [];

// Cache the result
this.cacheResult(cacheKey, data);
Expand All @@ -183,6 +188,31 @@ export class TranslateService {
}
}

/**
* Conversational advisor for the save modal's "fix this?" chat.
* Returns either a plain-text reply or an updated chunks list.
*/
async fetchTranslationAdvice(params: {
phrase: string;
context: string;
message: string;
currentChunks?: Chunk[];
history?: { role: "user" | "assistant"; text: string }[];
}): Promise<TranslationAdvice> {
return functionProvider.run<TranslationAdvice>({
name: "translationAdvice",
args: {
phrase: params.phrase,
context: params.context || "",
message: params.message,
currentChunks: params.currentChunks || [],
history: params.history || [],
sourceLanguage: "auto",
targetLanguage: this.languageTitle,
},
});
}

async translateByDictionaryapi(word: string) {
let url = {
url: "https://api.dictionaryapi.dev/api/v2/entries/en/" + encodeURI(word),
Expand Down
10 changes: 7 additions & 3 deletions src/common/static/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ export const VERSION = require("../../../package.json").version;

export const SUBTURTLE_DASHBOARD_URL = process.env.SUBTURTLE_DASHBOARD_URL;

export function getSubturtleDashboardUrlWithToken() {
export function getSubturtleDashboardUrlWithToken(redirectPath?: string) {
const token = authentication.getToken;
const url = `${process.env.SUBTURTLE_DASHBOARD_URL}/#/auth/login_with_token?token=${token}`;
console.log("Subturtle dashboard url", url);
let url = `${process.env.SUBTURTLE_DASHBOARD_URL}/#/auth/login_with_token?token=${token}`;
// The dashboard's login_with_token page reads `redirect`, validates same-origin,
// and pushes it after auth β€” so a deep-link survives the token handoff.
if (redirectPath) {
url += `&redirect=${encodeURIComponent(redirectPath)}`;
}
return url;
}

Expand Down
Loading
Loading