forked from Kieran/snort
feat: auto translate
This commit is contained in:
parent
e0b68ae817
commit
6e349051a2
@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { HexKey, Lists, NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||
import { Menu, MenuItem } from "@szhsin/react-menu";
|
||||
@ -59,20 +59,27 @@ export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
|
||||
|
||||
async function translate() {
|
||||
const api = new SnortApi();
|
||||
const targetLang = lang.split("-")[0].toUpperCase();
|
||||
const result = await api.translate({
|
||||
text: [ev.content],
|
||||
target_lang: lang.split("-")[0].toUpperCase(),
|
||||
target_lang: targetLang,
|
||||
});
|
||||
|
||||
if (typeof props.onTranslated === "function" && result.translations.length > 0) {
|
||||
props.onTranslated({
|
||||
text: result.translations[0].text,
|
||||
fromLanguage: langNames.of(result.translations[0].detected_source_language),
|
||||
confidence: 1,
|
||||
} as NoteTranslation);
|
||||
if ("translations" in result) {
|
||||
if (typeof props.onTranslated === "function" && result.translations.length > 0 && targetLang != result.translations[0].detected_source_language) {
|
||||
props.onTranslated({
|
||||
text: result.translations[0].text,
|
||||
fromLanguage: langNames.of(result.translations[0].detected_source_language),
|
||||
confidence: 1,
|
||||
} as NoteTranslation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
translate();
|
||||
}, []);
|
||||
|
||||
async function copyId() {
|
||||
const link = NostrLink.fromEvent(ev).encode(CONFIG.eventLinkPrefix);
|
||||
await navigator.clipboard.writeText(link);
|
||||
|
@ -41,6 +41,7 @@ export function NoteInner(props: NoteProps) {
|
||||
const { pinned, bookmarked } = login;
|
||||
const { publisher, system } = useEventPublisher();
|
||||
const [translated, setTranslated] = useState<NoteTranslation>();
|
||||
const [showTranslation, setShowTranslation] = useState(true);
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const totalReactions = reactions.positive.length + reactions.negative.length + reposts.length + zaps.length;
|
||||
@ -78,10 +79,11 @@ export function NoteInner(props: NoteProps) {
|
||||
}
|
||||
|
||||
const innerContent = useMemo(() => {
|
||||
const body = ev?.content ?? "";
|
||||
const body = translated && showTranslation ? translated.text : ev?.content ?? "";
|
||||
const id = translated && showTranslation ? `${ev.id}-translated` : ev.id;
|
||||
return (
|
||||
<Text
|
||||
id={ev.id}
|
||||
id={id}
|
||||
highlighText={props.searchedValue}
|
||||
content={body}
|
||||
tags={ev.tags}
|
||||
@ -91,7 +93,7 @@ export function NoteInner(props: NoteProps) {
|
||||
disableMediaSpotlight={!(props.options?.showMediaSpotlight ?? true)}
|
||||
/>
|
||||
);
|
||||
}, [ev, props.searchedValue, props.depth, options.showMedia, props.options?.showMediaSpotlight]);
|
||||
}, [ev, translated, showTranslation, props.searchedValue, props.depth, options.showMedia, props.options?.showMediaSpotlight]);
|
||||
|
||||
const transformBody = () => {
|
||||
if (deletions?.length > 0) {
|
||||
@ -172,8 +174,8 @@ export function NoteInner(props: NoteProps) {
|
||||
const replyTo = thread?.replyTo ?? thread?.root;
|
||||
const replyLink = replyTo
|
||||
? NostrLink.fromTag(
|
||||
[replyTo.key, replyTo.value ?? "", replyTo.relay ?? "", replyTo.marker ?? ""].filter(a => a.length > 0),
|
||||
)
|
||||
[replyTo.key, replyTo.value ?? "", replyTo.relay ?? "", replyTo.marker ?? ""].filter(a => a.length > 0),
|
||||
)
|
||||
: undefined;
|
||||
const mentions: { pk: string; name: string; link: ReactNode }[] = [];
|
||||
for (const pk of thread?.pubKeys ?? []) {
|
||||
@ -243,17 +245,17 @@ export function NoteInner(props: NoteProps) {
|
||||
if (translated && translated.confidence > 0.5) {
|
||||
return (
|
||||
<>
|
||||
<p className="highlight">
|
||||
<span className="text-xs font-semibold text-gray-light select-none" onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowTranslation(s => !s)
|
||||
}}>
|
||||
<FormattedMessage {...messages.TranslatedFrom} values={{ lang: translated.fromLanguage }} />
|
||||
</p>
|
||||
<div className="card text">
|
||||
<div className="text-frag">{translated.text}</div>
|
||||
</div>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
} else if (translated) {
|
||||
return (
|
||||
<p className="highlight">
|
||||
<p className="text-xs font-semibold text-gray-light">
|
||||
<FormattedMessage {...messages.TranslationFailed} />
|
||||
</p>
|
||||
);
|
||||
@ -298,7 +300,7 @@ export function NoteInner(props: NoteProps) {
|
||||
{options.showContextMenu && (
|
||||
<NoteContextMenu
|
||||
ev={ev}
|
||||
react={async () => {}}
|
||||
react={async () => { }}
|
||||
onTranslated={t => setTranslated(t)}
|
||||
setShowReactions={setShowReactions}
|
||||
/>
|
||||
|
9
packages/app/src/External/SnortApi.ts
vendored
9
packages/app/src/External/SnortApi.ts
vendored
@ -1,6 +1,7 @@
|
||||
import { throwIfOffline } from "@snort/shared";
|
||||
import { EventKind, EventPublisher } from "@snort/system";
|
||||
import { ApiHost } from "Const";
|
||||
import { unwrap } from "SnortUtils";
|
||||
import { SubscriptionType } from "Subscription";
|
||||
|
||||
export interface RevenueToday {
|
||||
@ -117,7 +118,7 @@ export default class SnortApi {
|
||||
}
|
||||
|
||||
translate(tx: TranslationRequest) {
|
||||
return this.#getJson<TranslationResponse>("api/v1/translate", "POST", tx);
|
||||
return this.#getJson<TranslationResponse | object>("api/v1/translate", "POST", tx);
|
||||
}
|
||||
|
||||
async #getJsonAuthd<T>(
|
||||
@ -160,9 +161,9 @@ export default class SnortApi {
|
||||
});
|
||||
|
||||
if (rsp.ok) {
|
||||
const text = await rsp.text();
|
||||
if (text.length > 0) {
|
||||
const obj = JSON.parse(text);
|
||||
const text = (await rsp.text()) as string | null;
|
||||
if ((text?.length ?? 0) > 0) {
|
||||
const obj = JSON.parse(unwrap(text));
|
||||
if ("error" in obj) {
|
||||
throw new SubscriptionError(obj.error, obj.code);
|
||||
}
|
||||
|
@ -524,11 +524,11 @@ export function getDisplayNameOrPlaceHolder(user: UserMetadata | undefined, pubk
|
||||
|
||||
export function getCountry() {
|
||||
const tz = Intl.DateTimeFormat().resolvedOptions();
|
||||
const info = (TZ as Record<string, Array<string>>)[tz.timeZone];
|
||||
const [,lat, lon] = info[1].split(/[-+]/);
|
||||
const info = (TZ as Record<string, Array<string> | undefined>)[tz.timeZone];
|
||||
const [, lat, lon] = info?.[1].split(/[-+]/) ?? ["", "00", "000"];
|
||||
return {
|
||||
zone: tz.timeZone,
|
||||
country: info[0],
|
||||
country: info?.[0],
|
||||
lat: Number(lat) / Math.pow(10, lat.length - 2),
|
||||
lon: Number(lon) / Math.pow(10, lon.length - 3),
|
||||
};
|
||||
|
@ -716,6 +716,18 @@ div.form-col {
|
||||
color: var(--repost);
|
||||
}
|
||||
|
||||
.text-gray {
|
||||
color: var(--gray);
|
||||
}
|
||||
|
||||
.text-gray-medium {
|
||||
color: var(--gray-medium);
|
||||
}
|
||||
|
||||
.text-gray-light {
|
||||
color: var(--gray-light);
|
||||
}
|
||||
|
||||
.tweet {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
Loading…
Reference in New Issue
Block a user