mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-10 12:49:29 +02:00
more cleanup on translations view
This commit is contained in:
parent
f2a5359c82
commit
cbe7ef63e0
@ -1,254 +0,0 @@
|
||||
import { MouseEventHandler, useCallback, useState } from "react";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Button,
|
||||
Card,
|
||||
CardHeader,
|
||||
Flex,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
ModalProps,
|
||||
Select,
|
||||
Spacer,
|
||||
Spinner,
|
||||
Text,
|
||||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
import dayjs from "dayjs";
|
||||
import codes from "iso-language-codes";
|
||||
|
||||
import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import { getEventUID } from "../../helpers/nostr/events";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import UserAvatarLink from "../user-avatar-link";
|
||||
import UserLink from "../user-link";
|
||||
import { useSigningContext } from "../../providers/signing-provider";
|
||||
import relayScoreboardService from "../../services/relay-scoreboard";
|
||||
import NostrPublishAction from "../../classes/nostr-publish-action";
|
||||
import clientRelaysService from "../../services/client-relays";
|
||||
import { NoteContents } from "../note/text-note-contents";
|
||||
import Timestamp from "../timestamp";
|
||||
import { readablizeSats } from "../../helpers/bolt11";
|
||||
import { LightningIcon } from "../icons";
|
||||
import {
|
||||
DVM_STATUS_KIND,
|
||||
DVM_TRANSLATE_JOB_KIND,
|
||||
DVM_TRANSLATE_RESULT_KIND,
|
||||
getRequestInputParam,
|
||||
} from "../../helpers/nostr/dvm";
|
||||
|
||||
function getTranslationRequestLanguage(request: NostrEvent) {
|
||||
const targetLanguage = getRequestInputParam(request, "language", false);
|
||||
return codes.find((code) => code.iso639_1 === targetLanguage);
|
||||
}
|
||||
|
||||
function TranslationRequest({ request }: { request: NostrEvent }) {
|
||||
const lang = getTranslationRequestLanguage(request);
|
||||
const requestRelays = request.tags.find((t) => t[0] === "relays")?.slice(1);
|
||||
const readRelays = useReadRelayUrls();
|
||||
|
||||
const timeline = useTimelineLoader(`${getEventUID(request)}-offers-results`, requestRelays || readRelays, {
|
||||
kinds: [DVM_STATUS_KIND, DVM_TRANSLATE_RESULT_KIND],
|
||||
"#e": [request.id],
|
||||
});
|
||||
|
||||
const events = useSubject(timeline.timeline);
|
||||
const dvmStatuses: Record<string, NostrEvent> = {};
|
||||
for (const event of events) {
|
||||
if (
|
||||
(event.kind === DVM_STATUS_KIND || event.kind === DVM_TRANSLATE_RESULT_KIND) &&
|
||||
(!dvmStatuses[event.pubkey] || dvmStatuses[event.pubkey].created_at < event.created_at)
|
||||
) {
|
||||
dvmStatuses[event.pubkey] = event;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card variant="outline">
|
||||
<CardHeader px="4" py="4" pb="2" display="flex" gap="2" alignItems="center" flexWrap="wrap">
|
||||
<UserAvatarLink pubkey={request.pubkey} size="sm" />
|
||||
<UserLink pubkey={request.pubkey} fontWeight="bold" />
|
||||
<Text>
|
||||
Requested translation to <strong>{lang?.nativeName}</strong>
|
||||
</Text>
|
||||
<Timestamp timestamp={request.created_at} />
|
||||
</CardHeader>
|
||||
{Object.keys(dvmStatuses).length === 0 && (
|
||||
<Flex gap="2" alignItems="center" m="4">
|
||||
<Spinner />
|
||||
Waiting for offers
|
||||
</Flex>
|
||||
)}
|
||||
<Accordion allowMultiple>
|
||||
{Object.values(dvmStatuses).map((event) => {
|
||||
switch (event.kind) {
|
||||
case DVM_STATUS_KIND:
|
||||
return <TranslationOffer key={event.id} offer={event} />;
|
||||
case DVM_TRANSLATE_RESULT_KIND:
|
||||
return <TranslationResult key={event.id} result={event} />;
|
||||
}
|
||||
})}
|
||||
</Accordion>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function TranslationOffer({ offer }: { offer: NostrEvent }) {
|
||||
const toast = useToast();
|
||||
|
||||
const amountTag = offer.tags.find((t) => t[0] === "amount" && t[1] && t[2]);
|
||||
const amountMsat = amountTag?.[1] && parseInt(amountTag[1]);
|
||||
const invoice = amountTag?.[2];
|
||||
|
||||
const [paid, setPaid] = useState(false);
|
||||
const [paying, setPaying] = useState(false);
|
||||
const payInvoice: MouseEventHandler = async (e) => {
|
||||
try {
|
||||
if (window.webln && invoice) {
|
||||
setPaying(true);
|
||||
e.stopPropagation();
|
||||
await window.webln.sendPayment(invoice);
|
||||
setPaid(true);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) toast({ status: "error", description: e.message });
|
||||
}
|
||||
setPaying(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<AccordionItem>
|
||||
<AccordionButton>
|
||||
<Flex gap="2" alignItems="center" grow={1}>
|
||||
<UserAvatarLink pubkey={offer.pubkey} size="sm" />
|
||||
<UserLink pubkey={offer.pubkey} fontWeight="bold" />
|
||||
<Text>Offered</Text>
|
||||
<Spacer />
|
||||
|
||||
{invoice && amountMsat && (
|
||||
<Button
|
||||
colorScheme="yellow"
|
||||
size="sm"
|
||||
variant="solid"
|
||||
leftIcon={<LightningIcon />}
|
||||
onClick={payInvoice}
|
||||
isLoading={paying || paid}
|
||||
isDisabled={!window.webln}
|
||||
>
|
||||
Pay {readablizeSats(amountMsat / 1000)} sats
|
||||
</Button>
|
||||
)}
|
||||
<AccordionIcon />
|
||||
</Flex>
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4}>
|
||||
<Text>{offer.content}</Text>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
);
|
||||
}
|
||||
|
||||
function TranslationResult({ result }: { result: NostrEvent }) {
|
||||
return (
|
||||
<AccordionItem>
|
||||
<AccordionButton>
|
||||
<Flex gap="2" alignItems="center" grow={1}>
|
||||
<UserAvatarLink pubkey={result.pubkey} size="sm" />
|
||||
<UserLink pubkey={result.pubkey} fontWeight="bold" />
|
||||
<Text>Translated Note</Text>
|
||||
<AccordionIcon ml="auto" />
|
||||
</Flex>
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4}>
|
||||
<NoteContents event={result} />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
);
|
||||
}
|
||||
|
||||
export function NoteTranslationsPage({ note }: { note: NostrEvent }) {
|
||||
const { requestSignature } = useSigningContext();
|
||||
const toast = useToast();
|
||||
|
||||
const [lang, setLang] = useState(navigator.language.split("-")[0] ?? "en");
|
||||
const readRelays = useReadRelayUrls();
|
||||
const requestTranslation = useCallback(async () => {
|
||||
try {
|
||||
const top8Relays = relayScoreboardService.getRankedRelays(readRelays).slice(0, 8);
|
||||
const draft: DraftNostrEvent = {
|
||||
kind: DVM_TRANSLATE_JOB_KIND,
|
||||
content: "",
|
||||
created_at: dayjs().unix(),
|
||||
tags: [
|
||||
["i", note.id, "event"],
|
||||
["param", "language", lang],
|
||||
["output", "text/plain"],
|
||||
["relays", ...top8Relays],
|
||||
],
|
||||
};
|
||||
|
||||
const signed = await requestSignature(draft);
|
||||
new NostrPublishAction("Request Translation", clientRelaysService.getWriteUrls(), signed);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) toast({ status: "error", description: e.message });
|
||||
}
|
||||
}, [requestSignature, note, readRelays]);
|
||||
|
||||
const timeline = useTimelineLoader(`${getEventUID(note)}-translations`, readRelays, {
|
||||
kinds: [DVM_TRANSLATE_JOB_KIND],
|
||||
"#i": [note.id],
|
||||
});
|
||||
|
||||
const events = useSubject(timeline.timeline);
|
||||
const jobs = events.filter((e) => e.kind === DVM_TRANSLATE_JOB_KIND);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex gap="2">
|
||||
<Select value={lang} onChange={(e) => setLang(e.target.value)} w="60">
|
||||
{codes.map((code) => (
|
||||
<option value={code.iso639_1}>
|
||||
{code.name} ({code.nativeName})
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<Button size="md" variant="solid" colorScheme="primary" onClick={requestTranslation} flexShrink={0}>
|
||||
Request new translation
|
||||
</Button>
|
||||
</Flex>
|
||||
{jobs.map((event) => (
|
||||
<TranslationRequest key={event.id} request={event} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function NoteTranslationModal({
|
||||
onClose,
|
||||
isOpen,
|
||||
note,
|
||||
...props
|
||||
}: Omit<ModalProps, "children"> & { note: NostrEvent }) {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="4xl" {...props}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader p="4">Note Translations</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody px="4" pt="0" pb="4" display="flex" gap="2" flexDirection="column">
|
||||
<NoteTranslationsPage note={note} />
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
@ -9,7 +9,7 @@ import NoteDebugModal from "../debug-modals/note-debug-modal";
|
||||
import clientRelaysService from "../../services/client-relays";
|
||||
import { handleEventFromRelay } from "../../services/event-relays";
|
||||
import NostrPublishAction from "../../classes/nostr-publish-action";
|
||||
import NoteTranslationModal from "../note-translation-modal";
|
||||
import NoteTranslationModal from "../../views/tools/transform-note/translation";
|
||||
import Translate01 from "../icons/translate-01";
|
||||
import InfoCircle from "../icons/info-circle";
|
||||
import PinNoteMenuItem from "../common-menu-items/pin-note";
|
||||
|
@ -3,7 +3,7 @@ import { Flex, Spacer, Spinner, Tab, TabList, TabPanel, TabPanels, Tabs } from "
|
||||
import useParamsEventPointer from "../../../hooks/use-params-event-pointer";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import useSingleEvent from "../../../hooks/use-single-event";
|
||||
import { NoteTranslationsPage } from "../../../components/note-translation-modal";
|
||||
import { NoteTranslationsPage } from "./translation";
|
||||
import { NoteContents } from "../../../components/note/text-note-contents";
|
||||
import UserAvatarLink from "../../../components/user-avatar-link";
|
||||
import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon";
|
||||
@ -18,7 +18,7 @@ function TransformNotePage({ note }: { note: NostrEvent }) {
|
||||
<Tabs colorScheme="primary" isLazy>
|
||||
<TabList>
|
||||
<Tab>Original</Tab>
|
||||
<Tab>Translate</Tab>
|
||||
<Tab>Translation</Tab>
|
||||
<Tab>Text to speech</Tab>
|
||||
</TabList>
|
||||
|
||||
|
@ -1,17 +1,5 @@
|
||||
import { MouseEventHandler, useCallback, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardHeader,
|
||||
Flex,
|
||||
IconButton,
|
||||
Select,
|
||||
Spacer,
|
||||
Spinner,
|
||||
Text,
|
||||
useDisclosure,
|
||||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Button, Flex, Select, useToast } from "@chakra-ui/react";
|
||||
import dayjs from "dayjs";
|
||||
import codes from "iso-language-codes";
|
||||
|
||||
@ -19,141 +7,20 @@ import { useReadRelayUrls } from "../../../../hooks/use-client-relays";
|
||||
import useTimelineLoader from "../../../../hooks/use-timeline-loader";
|
||||
import { getEventUID } from "../../../../helpers/nostr/events";
|
||||
import {
|
||||
DVMJob,
|
||||
DVMResponse,
|
||||
DVM_STATUS_KIND,
|
||||
DVM_TTS_JOB_KIND,
|
||||
DVM_TTS_RESULT_KIND,
|
||||
getRequestInputParam,
|
||||
groupEventsIntoJobs,
|
||||
} from "../../../../helpers/nostr/dvm";
|
||||
import useSubject from "../../../../hooks/use-subject";
|
||||
import { DraftNostrEvent, NostrEvent } from "../../../../types/nostr-event";
|
||||
import UserAvatarLink from "../../../../components/user-avatar-link";
|
||||
import UserLink from "../../../../components/user-link";
|
||||
import Timestamp from "../../../../components/timestamp";
|
||||
import { CodeIcon, LightningIcon } from "../../../../components/icons";
|
||||
import { readablizeSats } from "../../../../helpers/bolt11";
|
||||
import { useSigningContext } from "../../../../providers/signing-provider";
|
||||
import relayScoreboardService from "../../../../services/relay-scoreboard";
|
||||
import NostrPublishAction from "../../../../classes/nostr-publish-action";
|
||||
import clientRelaysService from "../../../../services/client-relays";
|
||||
import useCurrentAccount from "../../../../hooks/use-current-account";
|
||||
import { NostrQuery } from "../../../../types/nostr-query";
|
||||
import NoteDebugModal from "../../../../components/debug-modals/note-debug-modal";
|
||||
|
||||
function getTranslationRequestLanguage(request: NostrEvent) {
|
||||
const targetLanguage = getRequestInputParam(request, "language", false);
|
||||
return codes.find((code) => code.iso639_1 === targetLanguage);
|
||||
}
|
||||
|
||||
function TextToSpeechJob({ job }: { job: DVMJob }) {
|
||||
const lang = getTranslationRequestLanguage(job.request);
|
||||
const debug = useDisclosure();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card variant="outline">
|
||||
<CardHeader px="4" py="4" pb="2" display="flex" gap="2" alignItems="center" flexWrap="wrap">
|
||||
<UserAvatarLink pubkey={job.request.pubkey} size="sm" />
|
||||
<UserLink pubkey={job.request.pubkey} fontWeight="bold" />
|
||||
<Text>
|
||||
Requested reading in <strong>{lang?.nativeName}</strong>
|
||||
</Text>
|
||||
<Timestamp timestamp={job.request.created_at} />
|
||||
<Spacer />
|
||||
<IconButton
|
||||
icon={<CodeIcon />}
|
||||
aria-label="Show Raw"
|
||||
title="Show Raw"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={debug.onOpen}
|
||||
/>
|
||||
</CardHeader>
|
||||
{job.responses.length === 0 && (
|
||||
<Flex gap="2" alignItems="center" m="4">
|
||||
<Spinner />
|
||||
Waiting for response
|
||||
</Flex>
|
||||
)}
|
||||
{Object.values(job.responses).map((response) => (
|
||||
<TextToSpeechResponse key={response.pubkey} response={response} />
|
||||
))}
|
||||
</Card>
|
||||
{debug.isOpen && <NoteDebugModal isOpen onClose={debug.onClose} event={job.request} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function TextToSpeechResponse({ response }: { response: DVMResponse }) {
|
||||
if (response.result) return <TextToSpeechResult result={response.result} />;
|
||||
if (response.status) return <TextToSpeechStatus status={response.status} />;
|
||||
return null;
|
||||
}
|
||||
|
||||
function TextToSpeechStatus({ status }: { status: NostrEvent }) {
|
||||
const toast = useToast();
|
||||
|
||||
const amountTag = status.tags.find((t) => t[0] === "amount" && t[1] && t[2]);
|
||||
const amountMsat = amountTag?.[1] && parseInt(amountTag[1]);
|
||||
const invoice = amountTag?.[2];
|
||||
|
||||
const [paid, setPaid] = useState(false);
|
||||
const [paying, setPaying] = useState(false);
|
||||
const payInvoice: MouseEventHandler = async (e) => {
|
||||
try {
|
||||
if (window.webln && invoice) {
|
||||
setPaying(true);
|
||||
e.stopPropagation();
|
||||
await window.webln.sendPayment(invoice);
|
||||
setPaid(true);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) toast({ status: "error", description: e.message });
|
||||
}
|
||||
setPaying(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex gap="2" alignItems="center" grow={1}>
|
||||
<UserAvatarLink pubkey={status.pubkey} size="sm" />
|
||||
<UserLink pubkey={status.pubkey} fontWeight="bold" />
|
||||
<Text>Offered</Text>
|
||||
<Spacer />
|
||||
|
||||
{invoice && amountMsat && (
|
||||
<Button
|
||||
colorScheme="yellow"
|
||||
size="sm"
|
||||
variant="solid"
|
||||
leftIcon={<LightningIcon />}
|
||||
onClick={payInvoice}
|
||||
isLoading={paying || paid}
|
||||
isDisabled={!window.webln}
|
||||
>
|
||||
Pay {readablizeSats(amountMsat / 1000)} sats
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
<Text>{status.content}</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function TextToSpeechResult({ result }: { result: NostrEvent }) {
|
||||
return (
|
||||
<>
|
||||
<Flex gap="2" alignItems="center" grow={1}>
|
||||
<UserAvatarLink pubkey={result.pubkey} size="sm" />
|
||||
<UserLink pubkey={result.pubkey} fontWeight="bold" />
|
||||
<Text>Finished job</Text>
|
||||
</Flex>
|
||||
<Text>{result.content}</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
import TextToSpeechJob from "./tts-job";
|
||||
|
||||
export default function NoteTextToSpeechPage({ note }: { note: NostrEvent }) {
|
||||
const { requestSignature } = useSigningContext();
|
||||
|
57
src/views/tools/transform-note/text-to-speech/tts-job.tsx
Normal file
57
src/views/tools/transform-note/text-to-speech/tts-job.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { Card, CardBody, CardHeader, Flex, IconButton, Spacer, Spinner, Text, useDisclosure } from "@chakra-ui/react";
|
||||
import codes from "iso-language-codes";
|
||||
|
||||
import { DVMJob, getRequestInputParam } from "../../../../helpers/nostr/dvm";
|
||||
import { NostrEvent } from "../../../../types/nostr-event";
|
||||
import UserAvatarLink from "../../../../components/user-avatar-link";
|
||||
import UserLink from "../../../../components/user-link";
|
||||
import Timestamp from "../../../../components/timestamp";
|
||||
import { CodeIcon } from "../../../../components/icons";
|
||||
import TextToSpeechResponse from "./tts-response";
|
||||
import NoteDebugModal from "../../../../components/debug-modals/note-debug-modal";
|
||||
|
||||
function getTranslationRequestLanguage(request: NostrEvent) {
|
||||
const targetLanguage = getRequestInputParam(request, "language", false);
|
||||
return codes.find((code) => code.iso639_1 === targetLanguage);
|
||||
}
|
||||
|
||||
export default function TextToSpeechJob({ job }: { job: DVMJob }) {
|
||||
const lang = getTranslationRequestLanguage(job.request);
|
||||
const debug = useDisclosure();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card variant="outline">
|
||||
<CardHeader px="4" py="4" pb="2" display="flex" gap="2" alignItems="center" flexWrap="wrap">
|
||||
<UserAvatarLink pubkey={job.request.pubkey} size="sm" />
|
||||
<UserLink pubkey={job.request.pubkey} fontWeight="bold" />
|
||||
<Text>
|
||||
Requested reading in <strong>{lang?.nativeName}</strong>
|
||||
</Text>
|
||||
<Timestamp timestamp={job.request.created_at} />
|
||||
<Spacer />
|
||||
<IconButton
|
||||
icon={<CodeIcon />}
|
||||
aria-label="Show Raw"
|
||||
title="Show Raw"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={debug.onOpen}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardBody px="4" py="4" gap="2" display="flex" flexDirection="column">
|
||||
{job.responses.length === 0 && (
|
||||
<Flex gap="2" alignItems="center">
|
||||
<Spinner />
|
||||
Waiting for response
|
||||
</Flex>
|
||||
)}
|
||||
{Object.values(job.responses).map((response) => (
|
||||
<TextToSpeechResponse key={response.pubkey} response={response} />
|
||||
))}
|
||||
</CardBody>
|
||||
</Card>
|
||||
{debug.isOpen && <NoteDebugModal isOpen onClose={debug.onClose} event={job.request} />}
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
import { DVMResponse } from "../../../../helpers/nostr/dvm";
|
||||
import TextToSpeechResult from "./tts-result";
|
||||
import TextToSpeechStatus from "./tts-status";
|
||||
|
||||
export default function TextToSpeechResponse({ response }: { response: DVMResponse }) {
|
||||
if (response.result) return <TextToSpeechResult result={response.result} />;
|
||||
if (response.status) return <TextToSpeechStatus status={response.status} />;
|
||||
return null;
|
||||
}
|
18
src/views/tools/transform-note/text-to-speech/tts-result.tsx
Normal file
18
src/views/tools/transform-note/text-to-speech/tts-result.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import { Flex, Text } from "@chakra-ui/react";
|
||||
|
||||
import UserAvatarLink from "../../../../components/user-avatar-link";
|
||||
import UserLink from "../../../../components/user-link";
|
||||
import { NostrEvent } from "../../../../types/nostr-event";
|
||||
|
||||
export default function TextToSpeechResult({ result }: { result: NostrEvent }) {
|
||||
return (
|
||||
<>
|
||||
<Flex gap="2" alignItems="center" grow={1}>
|
||||
<UserAvatarLink pubkey={result.pubkey} size="sm" />
|
||||
<UserLink pubkey={result.pubkey} fontWeight="bold" />
|
||||
<Text>Finished job</Text>
|
||||
</Flex>
|
||||
<Text>{result.content}</Text>
|
||||
</>
|
||||
);
|
||||
}
|
58
src/views/tools/transform-note/text-to-speech/tts-status.tsx
Normal file
58
src/views/tools/transform-note/text-to-speech/tts-status.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { MouseEventHandler, useState } from "react";
|
||||
import { Button, Flex, Spacer, Text, useToast } from "@chakra-ui/react";
|
||||
|
||||
import { NostrEvent } from "../../../../types/nostr-event";
|
||||
import UserAvatarLink from "../../../../components/user-avatar-link";
|
||||
import UserLink from "../../../../components/user-link";
|
||||
import { LightningIcon } from "../../../../components/icons";
|
||||
import { readablizeSats } from "../../../../helpers/bolt11";
|
||||
|
||||
export default function TextToSpeechStatus({ status }: { status: NostrEvent }) {
|
||||
const toast = useToast();
|
||||
|
||||
const amountTag = status.tags.find((t) => t[0] === "amount" && t[1] && t[2]);
|
||||
const amountMsat = amountTag?.[1] && parseInt(amountTag[1]);
|
||||
const invoice = amountTag?.[2];
|
||||
|
||||
const [paid, setPaid] = useState(false);
|
||||
const [paying, setPaying] = useState(false);
|
||||
const payInvoice: MouseEventHandler = async (e) => {
|
||||
try {
|
||||
if (window.webln && invoice) {
|
||||
setPaying(true);
|
||||
e.stopPropagation();
|
||||
await window.webln.sendPayment(invoice);
|
||||
setPaid(true);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) toast({ status: "error", description: e.message });
|
||||
}
|
||||
setPaying(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex gap="2" alignItems="center" grow={1}>
|
||||
<UserAvatarLink pubkey={status.pubkey} size="sm" />
|
||||
<UserLink pubkey={status.pubkey} fontWeight="bold" />
|
||||
<Text>Offered</Text>
|
||||
<Spacer />
|
||||
|
||||
{invoice && amountMsat && (
|
||||
<Button
|
||||
colorScheme="yellow"
|
||||
size="sm"
|
||||
variant="solid"
|
||||
leftIcon={<LightningIcon />}
|
||||
onClick={payInvoice}
|
||||
isLoading={paying || paid}
|
||||
isDisabled={!window.webln}
|
||||
>
|
||||
Pay {readablizeSats(amountMsat / 1000)} sats
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
<Text>{status.content}</Text>
|
||||
</>
|
||||
);
|
||||
}
|
120
src/views/tools/transform-note/translation/index.tsx
Normal file
120
src/views/tools/transform-note/translation/index.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Flex,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
ModalProps,
|
||||
Select,
|
||||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
import dayjs from "dayjs";
|
||||
import codes from "iso-language-codes";
|
||||
|
||||
import { DraftNostrEvent, NostrEvent } from "../../../../types/nostr-event";
|
||||
import useTimelineLoader from "../../../../hooks/use-timeline-loader";
|
||||
import { getEventUID } from "../../../../helpers/nostr/events";
|
||||
import { useReadRelayUrls } from "../../../../hooks/use-client-relays";
|
||||
import useSubject from "../../../../hooks/use-subject";
|
||||
import { useSigningContext } from "../../../../providers/signing-provider";
|
||||
import relayScoreboardService from "../../../../services/relay-scoreboard";
|
||||
import NostrPublishAction from "../../../../classes/nostr-publish-action";
|
||||
import clientRelaysService from "../../../../services/client-relays";
|
||||
import {
|
||||
DVM_STATUS_KIND,
|
||||
DVM_TRANSLATE_JOB_KIND,
|
||||
DVM_TRANSLATE_RESULT_KIND,
|
||||
groupEventsIntoJobs,
|
||||
} from "../../../../helpers/nostr/dvm";
|
||||
import useCurrentAccount from "../../../../hooks/use-current-account";
|
||||
import { NostrQuery } from "../../../../types/nostr-query";
|
||||
import TranslationJob from "./translation-job";
|
||||
|
||||
export function NoteTranslationsPage({ note }: { note: NostrEvent }) {
|
||||
const account = useCurrentAccount();
|
||||
const { requestSignature } = useSigningContext();
|
||||
const toast = useToast();
|
||||
|
||||
const [lang, setLang] = useState(navigator.language.split("-")[0] ?? "en");
|
||||
const readRelays = useReadRelayUrls();
|
||||
const requestTranslation = useCallback(async () => {
|
||||
try {
|
||||
const top8Relays = relayScoreboardService.getRankedRelays(readRelays).slice(0, 8);
|
||||
const draft: DraftNostrEvent = {
|
||||
kind: DVM_TRANSLATE_JOB_KIND,
|
||||
content: "",
|
||||
created_at: dayjs().unix(),
|
||||
tags: [
|
||||
["i", note.id, "event"],
|
||||
["param", "language", lang],
|
||||
["output", "text/plain"],
|
||||
["relays", ...top8Relays],
|
||||
],
|
||||
};
|
||||
|
||||
const signed = await requestSignature(draft);
|
||||
new NostrPublishAction("Request Translation", clientRelaysService.getWriteUrls(), signed);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) toast({ status: "error", description: e.message });
|
||||
}
|
||||
}, [requestSignature, note, readRelays]);
|
||||
|
||||
const timeline = useTimelineLoader(
|
||||
`${getEventUID(note)}-translations`,
|
||||
readRelays,
|
||||
[
|
||||
{
|
||||
kinds: [DVM_TRANSLATE_JOB_KIND, DVM_TRANSLATE_RESULT_KIND],
|
||||
"#i": [note.id],
|
||||
},
|
||||
account && { kinds: [DVM_STATUS_KIND], "#p": [account.pubkey] },
|
||||
].filter(Boolean) as NostrQuery[],
|
||||
);
|
||||
|
||||
const events = useSubject(timeline.timeline);
|
||||
const jobs = Object.values(groupEventsIntoJobs(events));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex gap="2">
|
||||
<Select value={lang} onChange={(e) => setLang(e.target.value)} w="60">
|
||||
{codes.map((code) => (
|
||||
<option value={code.iso639_1}>
|
||||
{code.name} ({code.nativeName})
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<Button size="md" variant="solid" colorScheme="primary" onClick={requestTranslation} flexShrink={0}>
|
||||
Request new translation
|
||||
</Button>
|
||||
</Flex>
|
||||
{jobs.map((job) => (
|
||||
<TranslationJob key={job.request.id} job={job} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function NoteTranslationModal({
|
||||
onClose,
|
||||
isOpen,
|
||||
note,
|
||||
...props
|
||||
}: Omit<ModalProps, "children"> & { note: NostrEvent }) {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="4xl" {...props}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader p="4">Note Translations</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody px="4" pt="0" pb="4" display="flex" gap="2" flexDirection="column">
|
||||
<NoteTranslationsPage note={note} />
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
import { Card, CardBody, CardHeader, Flex, IconButton, Spacer, Spinner, Text, useDisclosure } from "@chakra-ui/react";
|
||||
import codes from "iso-language-codes";
|
||||
|
||||
import { DVMJob, getRequestInputParam } from "../../../../helpers/nostr/dvm";
|
||||
import { NostrEvent } from "../../../../types/nostr-event";
|
||||
import { CodeIcon } from "../../../../components/icons";
|
||||
import Timestamp from "../../../../components/timestamp";
|
||||
import UserLink from "../../../../components/user-link";
|
||||
import UserAvatarLink from "../../../../components/user-avatar-link";
|
||||
import NoteDebugModal from "../../../../components/debug-modals/note-debug-modal";
|
||||
import TranslationResponse from "./translation-response";
|
||||
|
||||
function getTranslationJobLanguage(request: NostrEvent) {
|
||||
const targetLanguage = getRequestInputParam(request, "language", false);
|
||||
return codes.find((code) => code.iso639_1 === targetLanguage);
|
||||
}
|
||||
|
||||
export default function TranslationJob({ job }: { job: DVMJob }) {
|
||||
const lang = getTranslationJobLanguage(job.request);
|
||||
const debug = useDisclosure();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card variant="outline">
|
||||
<CardHeader px="4" pt="4" pb="0" display="flex" gap="2" alignItems="center" flexWrap="wrap">
|
||||
<UserAvatarLink pubkey={job.request.pubkey} size="sm" />
|
||||
<UserLink pubkey={job.request.pubkey} fontWeight="bold" />
|
||||
<Text>
|
||||
Requested translation to <strong>{lang?.nativeName}</strong>
|
||||
</Text>
|
||||
<Timestamp timestamp={job.request.created_at} />
|
||||
<Spacer />
|
||||
<IconButton
|
||||
icon={<CodeIcon />}
|
||||
aria-label="Show Raw"
|
||||
title="Show Raw"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={debug.onOpen}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardBody px="4" py="4" gap="2" display="flex" flexDirection="column">
|
||||
{job.responses.length === 0 && (
|
||||
<Flex gap="2" alignItems="center" m="4">
|
||||
<Spinner />
|
||||
Waiting for response
|
||||
</Flex>
|
||||
)}
|
||||
{job.responses.map((response) => (
|
||||
<TranslationResponse key={response.pubkey} response={response} />
|
||||
))}
|
||||
</CardBody>
|
||||
</Card>
|
||||
{debug.isOpen && <NoteDebugModal isOpen onClose={debug.onClose} event={job.request} />}
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
import { DVMResponse } from "../../../../helpers/nostr/dvm";
|
||||
import TranslationResult from "./translation-result";
|
||||
import TranslationStatus from "./translation-status";
|
||||
|
||||
export default function TranslationResponse({ response }: { response: DVMResponse }) {
|
||||
if (response.result) return <TranslationResult result={response.result} />;
|
||||
if (response.status) return <TranslationStatus status={response.status} />;
|
||||
return null;
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
import { Button, Flex, Text, useDisclosure } from "@chakra-ui/react";
|
||||
|
||||
import UserAvatarLink from "../../../../components/user-avatar-link";
|
||||
import UserLink from "../../../../components/user-link";
|
||||
import { NoteContents } from "../../../../components/note/text-note-contents";
|
||||
import { NostrEvent } from "../../../../types/nostr-event";
|
||||
|
||||
export default function TranslationResult({ result }: { result: NostrEvent }) {
|
||||
const content = useDisclosure();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex gap="2" alignItems="center" grow={1} wrap="wrap">
|
||||
<UserAvatarLink pubkey={result.pubkey} size="sm" />
|
||||
<UserLink pubkey={result.pubkey} fontWeight="bold" />
|
||||
<Text>Translated Note</Text>
|
||||
<Button size="sm" onClick={content.onToggle}>
|
||||
{content.isOpen ? "Hide" : "Show"} Content
|
||||
</Button>
|
||||
</Flex>
|
||||
{content.isOpen && <NoteContents event={result} />}
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
import { MouseEventHandler, useState } from "react";
|
||||
import { Button, Flex, Spacer, Text, useToast } from "@chakra-ui/react";
|
||||
|
||||
import { NostrEvent } from "../../../../types/nostr-event";
|
||||
import UserAvatarLink from "../../../../components/user-avatar-link";
|
||||
import UserLink from "../../../../components/user-link";
|
||||
import { LightningIcon } from "../../../../components/icons";
|
||||
import { readablizeSats } from "../../../../helpers/bolt11";
|
||||
|
||||
export default function TranslationStatus({ status }: { status: NostrEvent }) {
|
||||
const toast = useToast();
|
||||
|
||||
const amountTag = status.tags.find((t) => t[0] === "amount" && t[1] && t[2]);
|
||||
const amountMsat = amountTag?.[1] && parseInt(amountTag[1]);
|
||||
const invoice = amountTag?.[2];
|
||||
|
||||
const [paid, setPaid] = useState(false);
|
||||
const [paying, setPaying] = useState(false);
|
||||
const payInvoice: MouseEventHandler = async (e) => {
|
||||
try {
|
||||
if (window.webln && invoice) {
|
||||
setPaying(true);
|
||||
e.stopPropagation();
|
||||
await window.webln.sendPayment(invoice);
|
||||
setPaid(true);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) toast({ status: "error", description: e.message });
|
||||
}
|
||||
setPaying(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex gap="2" alignItems="center" grow={1}>
|
||||
<UserAvatarLink pubkey={status.pubkey} size="sm" />
|
||||
<UserLink pubkey={status.pubkey} fontWeight="bold" />
|
||||
<Text>Responded</Text>
|
||||
<Spacer />
|
||||
|
||||
{invoice && amountMsat && (
|
||||
<Button
|
||||
colorScheme="yellow"
|
||||
size="sm"
|
||||
variant="solid"
|
||||
leftIcon={<LightningIcon />}
|
||||
onClick={payInvoice}
|
||||
isLoading={paying || paid}
|
||||
isDisabled={!window.webln}
|
||||
>
|
||||
Pay {readablizeSats(amountMsat / 1000)} sats
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
<Text>{status.content}</Text>
|
||||
</>
|
||||
);
|
||||
}
|
@ -9,7 +9,7 @@ import DeleteEventMenuItem from "../../../components/common-menu-items/delete-ev
|
||||
import Translate01 from "../../../components/icons/translate-01";
|
||||
import { CodeIcon } from "../../../components/icons";
|
||||
import NoteDebugModal from "../../../components/debug-modals/note-debug-modal";
|
||||
import NoteTranslationModal from "../../../components/note-translation-modal";
|
||||
import NoteTranslationModal from "../../tools/transform-note/translation";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
|
||||
export default function TorrentCommentMenu({
|
||||
|
@ -4,7 +4,7 @@ import { CustomMenuIconButton, MenuIconButtonProps } from "../../../components/m
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { CodeIcon, TranslateIcon } from "../../../components/icons";
|
||||
import DeleteEventMenuItem from "../../../components/common-menu-items/delete-event";
|
||||
import NoteTranslationModal from "../../../components/note-translation-modal";
|
||||
import NoteTranslationModal from "../../tools/transform-note/translation";
|
||||
import NoteDebugModal from "../../../components/debug-modals/note-debug-modal";
|
||||
import MuteUserMenuItem from "../../../components/common-menu-items/mute-user";
|
||||
import OpenInAppMenuItem from "../../../components/common-menu-items/open-in-app";
|
||||
|
Loading…
x
Reference in New Issue
Block a user