note translation sw & lru cache
continuous-integration/drone/push Build was killed Details

This commit is contained in:
Martti Malmi 2024-01-27 10:19:08 +02:00
parent dc99d2a653
commit 82d5b9fb64
6 changed files with 61 additions and 21 deletions

View File

@ -4,6 +4,7 @@ import React, { useCallback, useState } from "react";
import { useInView } from "react-intersection-observer"; import { useInView } from "react-intersection-observer";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { LRUCache } from "typescript-lru-cache";
import NoteHeader from "@/Components/Event/Note/NoteHeader"; import NoteHeader from "@/Components/Event/Note/NoteHeader";
import { NoteText } from "@/Components/Event/Note/NoteText"; import { NoteText } from "@/Components/Event/Note/NoteText";
@ -30,6 +31,7 @@ const defaultOptions = {
}; };
const canRenderAsTextNote = [EventKind.TextNote, EventKind.Polls]; const canRenderAsTextNote = [EventKind.TextNote, EventKind.Polls];
const translationCache = new LRUCache<string, NoteTranslation>({ maxSize: 300 });
export function Note(props: NoteProps) { export function Note(props: NoteProps) {
const { data: ev, highlight, options: opt, ignoreModeration = false, className, waitUntilInView } = props; const { data: ev, highlight, options: opt, ignoreModeration = false, className, waitUntilInView } = props;
@ -37,7 +39,14 @@ export function Note(props: NoteProps) {
const { isEventMuted } = useModeration(); const { isEventMuted } = useModeration();
const { ref, inView } = useInView({ triggerOnce: true, rootMargin: "2000px" }); const { ref, inView } = useInView({ triggerOnce: true, rootMargin: "2000px" });
const [showTranslation, setShowTranslation] = useState(true); const [showTranslation, setShowTranslation] = useState(true);
const [translated, setTranslated] = useState<NoteTranslation>(); const [translated, setTranslated] = useState<NoteTranslation>(translationCache.get(ev.id));
const cachedSetTranslated = useCallback(
(translation: NoteTranslation) => {
translationCache.set(ev.id, translation);
setTranslated(translation);
},
[ev.id],
);
const optionsMerged = { ...defaultOptions, ...opt }; const optionsMerged = { ...defaultOptions, ...opt };
const goToEvent = useGoToEvent(props, optionsMerged); const goToEvent = useGoToEvent(props, optionsMerged);
@ -50,7 +59,13 @@ export function Note(props: NoteProps) {
if (waitUntilInView && !inView) return null; if (waitUntilInView && !inView) return null;
return ( return (
<> <>
{optionsMerged.showHeader && <NoteHeader ev={ev} options={optionsMerged} setTranslated={setTranslated} />} {optionsMerged.showHeader && (
<NoteHeader
ev={ev}
options={optionsMerged}
setTranslated={translated === null ? cachedSetTranslated : undefined}
/>
)}
<div className="body" onClick={e => goToEvent(e, ev)}> <div className="body" onClick={e => goToEvent(e, ev)}>
<NoteText {...props} translated={translated} showTranslation={showTranslation} /> <NoteText {...props} translated={translated} showTranslation={showTranslation} />
{translated && <TranslationInfo translated={translated} setShowTranslation={setShowTranslation} />} {translated && <TranslationInfo translated={translated} setShowTranslation={setShowTranslation} />}

View File

@ -18,6 +18,7 @@ export interface NoteTranslation {
text: string; text: string;
fromLanguage: string; fromLanguage: string;
confidence: number; confidence: number;
skipped?: boolean;
} }
interface NosteContextMenuProps { interface NosteContextMenuProps {
@ -60,6 +61,7 @@ export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
} }
async function translate() { async function translate() {
if (!props.onTranslated) return;
const api = new SnortApi(); const api = new SnortApi();
const targetLang = lang.split("-")[0].toUpperCase(); const targetLang = lang.split("-")[0].toUpperCase();
const result = await api.translate({ const result = await api.translate({
@ -67,18 +69,23 @@ export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
target_lang: targetLang, target_lang: targetLang,
}); });
if ("translations" in result) { if (
if ( "translations" in result &&
typeof props.onTranslated === "function" && result.translations.length > 0 &&
result.translations.length > 0 && targetLang != result.translations[0].detected_source_language
targetLang != result.translations[0].detected_source_language ) {
) { props.onTranslated({
props.onTranslated({ text: result.translations[0].text,
text: result.translations[0].text, fromLanguage: langNames.of(result.translations[0].detected_source_language),
fromLanguage: langNames.of(result.translations[0].detected_source_language), confidence: 1,
confidence: 1, } as NoteTranslation);
} as NoteTranslation); } else {
} props.onTranslated({
text: "",
fromLanguage: "",
confidence: 0,
skipped: true,
});
} }
} }

View File

@ -17,7 +17,7 @@ import { setBookmarked, setPinned } from "@/Utils/Login";
export default function NoteHeader(props: { export default function NoteHeader(props: {
ev: TaggedNostrEvent; ev: TaggedNostrEvent;
options: NotePropsOptions; options: NotePropsOptions;
setTranslated: (t: NoteTranslation) => void; setTranslated?: (t: NoteTranslation) => void;
context?: React.ReactNode; context?: React.ReactNode;
}) { }) {
const [showReactions, setShowReactions] = useState(false); const [showReactions, setShowReactions] = useState(false);
@ -49,6 +49,8 @@ export default function NoteHeader(props: {
} }
} }
const onTranslated = setTranslated ? (t: NoteTranslation) => setTranslated(t) : undefined;
return ( return (
<div className="header flex"> <div className="header flex">
<ProfileImage <ProfileImage
@ -79,7 +81,7 @@ export default function NoteHeader(props: {
<NoteContextMenu <NoteContextMenu
ev={ev} ev={ev}
react={async () => {}} react={async () => {}}
onTranslated={t => setTranslated(t)} onTranslated={onTranslated}
setShowReactions={setShowReactions} setShowReactions={setShowReactions}
/> />
)} )}

View File

@ -15,8 +15,8 @@ export const NoteText = function InnerContent(
const { data: ev, options, translated, showTranslation } = props; const { data: ev, options, translated, showTranslation } = props;
const appData = useLogin(s => s.appData); const appData = useLogin(s => s.appData);
const [showMore, setShowMore] = useState(false); const [showMore, setShowMore] = useState(false);
const body = translated && showTranslation ? translated.text : ev?.content ?? ""; const body = translated && !translated.skipped && showTranslation ? translated.text : ev?.content ?? "";
const id = translated && showTranslation ? `${ev.id}-translated` : ev.id; const id = translated && !translated.skipped && showTranslation ? `${ev.id}-translated` : ev.id;
const shouldTruncate = options?.truncate && body.length > TEXT_TRUNCATE_LENGTH; const shouldTruncate = options?.truncate && body.length > TEXT_TRUNCATE_LENGTH;
const ToggleShowMore = () => ( const ToggleShowMore = () => (

View File

@ -23,7 +23,7 @@ export function TranslationInfo({ translated, setShowTranslation }: TranslationI
</span> </span>
</> </>
); );
} else if (translated) { } else if (translated && !translated.skipped) {
return ( return (
<p className="text-xs font-semibold text-gray-light"> <p className="text-xs font-semibold text-gray-light">
<FormattedMessage {...messages.TranslationFailed} /> <FormattedMessage {...messages.TranslationFailed} />

View File

@ -89,9 +89,25 @@ registerRoute(
registerRoute( registerRoute(
({ url }) => url.origin === "https://api.snort.social" && url.pathname.startsWith("/api/v1/preview"), ({ url }) => url.origin === "https://api.snort.social" && url.pathname.startsWith("/api/v1/preview"),
new StaleWhileRevalidate({ new CacheFirst({
cacheName: "preview-cache", cacheName: "preview-cache",
plugins: [new ExpirationPlugin({ maxAgeSeconds: 24 * 60 * 60 })], plugins: [
new ExpirationPlugin({ maxAgeSeconds: 24 * 60 * 60 }),
new CacheableResponsePlugin({ statuses: [0, 200] }),
],
}),
);
registerRoute(
({ url }) => url.origin === "https://api.snort.social" && url.pathname.startsWith("/api/v1/translate"),
new CacheFirst({
cacheName: "translate-cache",
plugins: [
new ExpirationPlugin({ maxEntries: 1000 }),
new CacheableResponsePlugin({
statuses: [0, 200, 204],
}),
],
}), }),
); );