more cleanup on translations view

This commit is contained in:
hzrd149 2023-12-23 18:22:06 -06:00
parent f2a5359c82
commit cbe7ef63e0
15 changed files with 418 additions and 395 deletions

View File

@ -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>
);
}

View File

@ -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";

View File

@ -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>

View File

@ -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();

View 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} />}
</>
);
}

View File

@ -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;
}

View 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>
</>
);
}

View 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>
</>
);
}

View 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>
);
}

View 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 { 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} />}
</>
);
}

View File

@ -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;
}

View File

@ -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} />}
</>
);
}

View 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 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>
</>
);
}

View File

@ -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({

View File

@ -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";