feat: add dual-action translation (cached/regenerate) in HypaV3 modal
Implements shift+click (desktop) and double tap (mobile) for regenerating translations while maintaining regular click/tap for cached translations.
This commit is contained in:
@@ -61,7 +61,10 @@
|
|||||||
expandedMessage: null,
|
expandedMessage: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
async function toggleTranslate(summary: ExtendedSummary): Promise<void> {
|
async function toggleTranslate(
|
||||||
|
summary: ExtendedSummary,
|
||||||
|
regenerate?: boolean
|
||||||
|
): Promise<void> {
|
||||||
if (summary.state.isTranslating) return;
|
if (summary.state.isTranslating) return;
|
||||||
|
|
||||||
if (summary.state.translation) {
|
if (summary.state.translation) {
|
||||||
@@ -72,7 +75,7 @@
|
|||||||
summary.state.isTranslating = true;
|
summary.state.isTranslating = true;
|
||||||
summary.state.translation = "Loading...";
|
summary.state.translation = "Loading...";
|
||||||
|
|
||||||
const result = await translate(summary.text);
|
const result = await translate(summary.text, regenerate);
|
||||||
|
|
||||||
summary.state.translation = result;
|
summary.state.translation = result;
|
||||||
summary.state.isTranslating = false;
|
summary.state.isTranslating = false;
|
||||||
@@ -144,7 +147,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function toggleTranslateRerolled(
|
async function toggleTranslateRerolled(
|
||||||
summary: ExtendedSummary
|
summary: ExtendedSummary,
|
||||||
|
regenerate?: boolean
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (summary.state.isRerolledTranslating) return;
|
if (summary.state.isRerolledTranslating) return;
|
||||||
|
|
||||||
@@ -158,13 +162,15 @@
|
|||||||
summary.state.isRerolledTranslating = true;
|
summary.state.isRerolledTranslating = true;
|
||||||
summary.state.rerolledTranslation = "Loading...";
|
summary.state.rerolledTranslation = "Loading...";
|
||||||
|
|
||||||
const result = await translate(summary.state.rerolledText);
|
const result = await translate(summary.state.rerolledText, regenerate);
|
||||||
|
|
||||||
summary.state.rerolledTranslation = result;
|
summary.state.rerolledTranslation = result;
|
||||||
summary.state.isRerolledTranslating = false;
|
summary.state.isRerolledTranslating = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleTranslateExpandedMessage(): Promise<void> {
|
async function toggleTranslateExpandedMessage(
|
||||||
|
regenerate?: boolean
|
||||||
|
): Promise<void> {
|
||||||
if (!modalState.expandedMessage || modalState.expandedMessage.isTranslating)
|
if (!modalState.expandedMessage || modalState.expandedMessage.isTranslating)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -180,7 +186,7 @@
|
|||||||
modalState.expandedMessage.isTranslating = true;
|
modalState.expandedMessage.isTranslating = true;
|
||||||
modalState.expandedMessage.translation = "Loading...";
|
modalState.expandedMessage.translation = "Loading...";
|
||||||
|
|
||||||
const result = await translate(messageData.data);
|
const result = await translate(messageData.data, regenerate);
|
||||||
|
|
||||||
modalState.expandedMessage.translation = result;
|
modalState.expandedMessage.translation = result;
|
||||||
modalState.expandedMessage.isTranslating = false;
|
modalState.expandedMessage.isTranslating = false;
|
||||||
@@ -235,13 +241,78 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function translate(text) {
|
async function translate(
|
||||||
|
text: string,
|
||||||
|
regenerate?: boolean
|
||||||
|
): Promise<string> {
|
||||||
try {
|
try {
|
||||||
return await translateHTML(text, false, "", -1);
|
return await translateHTML(text, false, "", -1, regenerate);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return `Translation failed: ${error}`;
|
return `Translation failed: ${error}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DualActionParams = {
|
||||||
|
onMainAction?: () => void;
|
||||||
|
onAlternativeAction?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleDualAction(node: HTMLElement, params: DualActionParams = {}) {
|
||||||
|
const state = {
|
||||||
|
lastTap: 0,
|
||||||
|
tapTimeout: null as any,
|
||||||
|
};
|
||||||
|
|
||||||
|
const DOUBLE_TAP_DELAY = 300;
|
||||||
|
|
||||||
|
function handleInteraction(event: Event) {
|
||||||
|
if ("ontouchend" in window) {
|
||||||
|
// Mobile environment
|
||||||
|
const currentTime = new Date().getTime();
|
||||||
|
const tapLength = currentTime - state.lastTap;
|
||||||
|
|
||||||
|
if (tapLength < DOUBLE_TAP_DELAY && tapLength > 0) {
|
||||||
|
// Double tap detected
|
||||||
|
event.preventDefault();
|
||||||
|
clearTimeout(state.tapTimeout); // Cancel the first tap timeout
|
||||||
|
params.onAlternativeAction?.();
|
||||||
|
state.lastTap = 0; // Reset state
|
||||||
|
} else {
|
||||||
|
// First tap
|
||||||
|
state.lastTap = currentTime;
|
||||||
|
|
||||||
|
// Delayed single tap execution
|
||||||
|
state.tapTimeout = setTimeout(() => {
|
||||||
|
if (state.lastTap === currentTime) {
|
||||||
|
// If no double tap occurred
|
||||||
|
params.onMainAction?.();
|
||||||
|
}
|
||||||
|
}, DOUBLE_TAP_DELAY);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Desktop environment
|
||||||
|
if ((event as MouseEvent).shiftKey) {
|
||||||
|
params.onAlternativeAction?.();
|
||||||
|
} else {
|
||||||
|
params.onMainAction?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
node.addEventListener("click", handleInteraction);
|
||||||
|
node.addEventListener("touchend", handleInteraction);
|
||||||
|
|
||||||
|
return {
|
||||||
|
destroy() {
|
||||||
|
node.removeEventListener("click", handleInteraction);
|
||||||
|
node.removeEventListener("touchend", handleInteraction);
|
||||||
|
clearTimeout(state.tapTimeout); // Cleanup timeout
|
||||||
|
},
|
||||||
|
update(newParams: DualActionParams) {
|
||||||
|
params = newParams;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="fixed inset-0 z-50 bg-black/50 p-4">
|
<div class="fixed inset-0 z-50 bg-black/50 p-4">
|
||||||
@@ -310,7 +381,10 @@
|
|||||||
<!-- Translate Button -->
|
<!-- Translate Button -->
|
||||||
<button
|
<button
|
||||||
class="p-2 text-zinc-400 hover:text-zinc-200 transition-colors"
|
class="p-2 text-zinc-400 hover:text-zinc-200 transition-colors"
|
||||||
onclick={async () => await toggleTranslate(summary)}
|
use:handleDualAction={{
|
||||||
|
onMainAction: () => toggleTranslate(summary, false),
|
||||||
|
onAlternativeAction: () => toggleTranslate(summary, true),
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<LanguagesIcon size={16} />
|
<LanguagesIcon size={16} />
|
||||||
</button>
|
</button>
|
||||||
@@ -367,8 +441,12 @@
|
|||||||
<!-- Translate Rerolled Button -->
|
<!-- Translate Rerolled Button -->
|
||||||
<button
|
<button
|
||||||
class="p-2 text-zinc-400 hover:text-zinc-200 transition-colors"
|
class="p-2 text-zinc-400 hover:text-zinc-200 transition-colors"
|
||||||
onclick={async () =>
|
use:handleDualAction={{
|
||||||
await toggleTranslateRerolled(summary)}
|
onMainAction: () =>
|
||||||
|
toggleTranslateRerolled(summary, false),
|
||||||
|
onAlternativeAction: () =>
|
||||||
|
toggleTranslateRerolled(summary, true),
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<LanguagesIcon size={16} />
|
<LanguagesIcon size={16} />
|
||||||
</button>
|
</button>
|
||||||
@@ -427,7 +505,11 @@
|
|||||||
<!-- Translate Message Button -->
|
<!-- Translate Message Button -->
|
||||||
<button
|
<button
|
||||||
class="p-2 text-zinc-400 hover:text-zinc-200 transition-colors"
|
class="p-2 text-zinc-400 hover:text-zinc-200 transition-colors"
|
||||||
onclick={async () => await toggleTranslateExpandedMessage()}
|
use:handleDualAction={{
|
||||||
|
onMainAction: () => toggleTranslateExpandedMessage(false),
|
||||||
|
onAlternativeAction: () =>
|
||||||
|
toggleTranslateExpandedMessage(true),
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<LanguagesIcon size={16} />
|
<LanguagesIcon size={16} />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -854,7 +854,7 @@ class HypaProcesserEx extends HypaProcesser {
|
|||||||
summaryChunkVectors: SummaryChunkVector[] = [];
|
summaryChunkVectors: SummaryChunkVector[] = [];
|
||||||
|
|
||||||
// Calculate dot product similarity between two vectors
|
// Calculate dot product similarity between two vectors
|
||||||
similarity(a: VectorArray, b: VectorArray) {
|
similarity(a: VectorArray, b: VectorArray): number {
|
||||||
let dot = 0;
|
let dot = 0;
|
||||||
|
|
||||||
for (let i = 0; i < a.length; i++) {
|
for (let i = 0; i < a.length; i++) {
|
||||||
@@ -864,7 +864,7 @@ class HypaProcesserEx extends HypaProcesser {
|
|||||||
return dot;
|
return dot;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addSummaryChunks(chunks: SummaryChunk[]) {
|
async addSummaryChunks(chunks: SummaryChunk[]): Promise<void> {
|
||||||
// Maintain the superclass's caching structure by adding texts
|
// Maintain the superclass's caching structure by adding texts
|
||||||
const texts = chunks.map((chunk) => chunk.text);
|
const texts = chunks.map((chunk) => chunk.text);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user