Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions echo/frontend/src/components/common/Markdown.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { useEffect } from "react";
import { useEffect, useMemo } from "react";
import type { Components } from "react-markdown";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import {
escapeRedactedTokens,
REDACTED_CODE_PREFIX,
RedactedBadge,
} from "@/components/common/RedactedText";
import { cn } from "@/lib/utils";

export const Markdown = ({
Expand All @@ -25,15 +31,39 @@ export const Markdown = ({
}
}, []);

const processedContent = useMemo(
() => escapeRedactedTokens(content),
[content],
);

const components = useMemo<Components>(
() => ({
code({ children, className: codeClassName, ...props }) {
const text = String(children).trim();
if (!codeClassName && text.startsWith(REDACTED_CODE_PREFIX)) {
const type = text.slice(REDACTED_CODE_PREFIX.length);
return <RedactedBadge type={type} />;
}
return (
<code className={codeClassName} {...props}>
{children}
</code>
);
},
}),
[],
);

return (
<ReactMarkdown
className={cn(
"prose prose-table:block prose-table:w-full prose-table:overflow-x-scroll",
className,
)}
remarkPlugins={[remarkGfm]}
components={components}
>
{content}
{processedContent}
</ReactMarkdown>
);
};
48 changes: 36 additions & 12 deletions echo/frontend/src/components/common/RedactedText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,37 @@ import { t } from "@lingui/core/macro";
import { Text, Tooltip } from "@mantine/core";
import { type ReactNode, useMemo } from "react";

const REDACTED_PATTERN = /<redacted_([a-z_]+)>/g;
export const REDACTED_PATTERN = /<redacted_([a-z_]+)>/g;
export const REDACTED_CODE_PREFIX = "redacted:";

const getRedactedLabels = (): Record<string, string> => ({
const SAFE_REDACTED_PATTERN = /`redacted:([a-z_]+)`/g;

/**
* Converts `<redacted_*>` tokens to backtick-wrapped inline code
* (`` `redacted:type` ``) that MDX/Markdown editors preserve verbatim.
*/
export const escapeRedactedTokens = (text: string): string => {
if (!text || !text.includes("<redacted_")) {
return text;
}
return text.replace(
REDACTED_PATTERN,
(_match, type: string) => `\`${REDACTED_CODE_PREFIX}${type}\``,
);
};

/**
* Reverses `escapeRedactedTokens`, restoring `` `redacted:type` ``
* back to `<redacted_type>` for downstream rendering.
*/
export const unescapeRedactedTokens = (text: string): string => {
if (!text || !text.includes("`redacted:")) {
return text;
}
return text.replace(SAFE_REDACTED_PATTERN, "<redacted_$1>");
};

export const getRedactedLabels = (): Record<string, string> => ({
address: t`Address`,
card: t`Card`,
email: t`Email`,
Expand All @@ -16,7 +44,7 @@ const getRedactedLabels = (): Record<string, string> => ({
username: t`Username`,
});

const formatLabel = (key: string): string => {
export const formatLabel = (key: string): string => {
const labels = getRedactedLabels();
if (key in labels) {
return labels[key];
Expand All @@ -27,17 +55,11 @@ const formatLabel = (key: string): string => {
.join(" ");
};

const RedactedBadge = ({ type }: { type: string }) => {
export const RedactedBadge = ({ type }: { type: string }) => {
const label = formatLabel(type);
return (
<Tooltip label={t`This information is anonymized`} withArrow>
<Text
component="span"
size="sm"
bg="primary.2"
px={6}
py={1}
>
<Text component="span" size="sm" bg="primary.2" px={6} py={1}>
{label}
</Text>
</Tooltip>
Expand All @@ -63,7 +85,9 @@ export const parseRedactedText = (text: string): ReactNode[] | string => {
if (match.index > lastIndex) {
parts.push(text.slice(lastIndex, match.index));
}
parts.push(<RedactedBadge key={`${match.index}-${match[1]}`} type={match[1]} />);
parts.push(
<RedactedBadge key={`${match.index}-${match[1]}`} type={match[1]} />,
);
lastIndex = regex.lastIndex;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,31 @@ import {
toolbarPlugin,
UndoRedo,
} from "@mdxeditor/editor";
import { useCallback, useMemo } from "react";

import "./styles.css";
import {
escapeRedactedTokens,
unescapeRedactedTokens,
} from "@/components/common/RedactedText";

export function MarkdownWYSIWYG({
markdown,
onChange,
...rest
}: MDXEditorProps) {
const safeMarkdown = useMemo(
() => escapeRedactedTokens(markdown ?? ""),
[markdown],
);

const handleChange = useCallback(
(value: string, initialMarkdownNormalize: boolean) => {
onChange?.(unescapeRedactedTokens(value), initialMarkdownNormalize);
},
[onChange],
);

export function MarkdownWYSIWYG(props: MDXEditorProps) {
return (
<MDXEditor
plugins={[
Expand All @@ -40,7 +62,9 @@ export function MarkdownWYSIWYG(props: MDXEditorProps) {
]}
contentEditableClassName="prose min-h-[200px] space-grotesk"
className="rounded border border-gray-200"
{...props}
{...rest}
markdown={safeMarkdown}
onChange={handleChange}
/>
);
}
3 changes: 0 additions & 3 deletions echo/frontend/src/components/participant/UserChunkMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { IconDotsVertical, IconTrash } from "@tabler/icons-react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useParams } from "react-router";
import { Markdown } from "@/components/common/Markdown";
import { RedactedText } from "@/components/common/RedactedText";
import { toast } from "@/components/common/Toaster";
import { deleteParticipantConversationChunk } from "@/lib/api";

Expand Down Expand Up @@ -106,8 +105,6 @@ const UserChunkMessage = ({
<Text className="prose text-sm">
{chunk.transcript == null ? (
<Markdown content={t`*Transcription in progress.*`} />
) : chunk.transcript.includes("<redacted_") ? (
<RedactedText>{chunk.transcript}</RedactedText>
) : (
<Markdown content={chunk.transcript} />
)}
Expand Down
26 changes: 13 additions & 13 deletions echo/frontend/src/locales/de-DE.po
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ msgstr "{readingNow} liest gerade"
msgid "library.conversations.still.processing"
msgstr "{0} werden noch verarbeitet."

#: src/components/participant/UserChunkMessage.tsx:108
#: src/components/participant/UserChunkMessage.tsx:107
msgid "*Transcription in progress.*"
msgstr "*Transkription wird durchgeführt.*"

Expand Down Expand Up @@ -480,7 +480,7 @@ msgstr "Kontext wird hinzugefügt:"
msgid "select.all.modal.loading.title"
msgstr "Unterhaltungen werden hinzugefügt"

#: src/components/common/RedactedText.tsx:8
#: src/components/common/RedactedText.tsx:36
msgid "Address"
msgstr "Adresse"

Expand Down Expand Up @@ -950,7 +950,7 @@ msgstr "Abbrechen"
msgid "Cannot add empty conversation"
msgstr "Leeres Gespräch kann nicht hinzugefügt werden"

#: src/components/common/RedactedText.tsx:9
#: src/components/common/RedactedText.tsx:37
msgid "Card"
msgstr "Karte"

Expand Down Expand Up @@ -1414,7 +1414,7 @@ msgstr "Projekt löschen"
msgid "Delete Webhook"
msgstr "Webhook löschen"

#: src/components/participant/UserChunkMessage.tsx:69
#: src/components/participant/UserChunkMessage.tsx:68
msgid "Deleted successfully"
msgstr "Erfolgreich gelöscht"

Expand Down Expand Up @@ -1625,7 +1625,7 @@ msgstr "Bearbeitungsmodus"

#: src/routes/auth/Login.tsx:251
#: src/routes/auth/Login.tsx:255
#: src/components/common/RedactedText.tsx:10
#: src/components/common/RedactedText.tsx:38
msgid "Email"
msgstr "E-Mail"

Expand Down Expand Up @@ -1922,7 +1922,7 @@ msgstr "Benutzerdefiniertes Thema konnte nicht erstellt werden"
msgid "Failed to delete custom topic"
msgstr "Benutzerdefiniertes Thema konnte nicht gelöscht werden"

#: src/components/participant/UserChunkMessage.tsx:46
#: src/components/participant/UserChunkMessage.tsx:45
msgid "Failed to delete response"
msgstr "Fehler beim Löschen der Antwort"

Expand Down Expand Up @@ -2310,11 +2310,11 @@ msgstr ""
msgid "participant.button.i.understand"
msgstr "Ich verstehe"

#: src/components/common/RedactedText.tsx:11
#: src/components/common/RedactedText.tsx:39
msgid "IBAN"
msgstr "IBAN"

#: src/components/common/RedactedText.tsx:12
#: src/components/common/RedactedText.tsx:40
msgid "ID"
msgstr "ID"

Expand Down Expand Up @@ -2523,7 +2523,7 @@ msgstr "Bibliothekserstellung läuft"
#~ msgid "library.regenerate"
#~ msgstr "Bibliothek neu generieren"

#: src/components/common/RedactedText.tsx:13
#: src/components/common/RedactedText.tsx:41
msgid "License Plate"
msgstr "Kennzeichen"

Expand Down Expand Up @@ -2698,7 +2698,7 @@ msgstr "Gespräch zu einem anderen Projekt verschieben"
#: src/components/project/ProjectBasicEdit.tsx:104
#: src/components/project/webhooks/WebhookSettingsCard.tsx:377
#: src/components/conversation/ConversationEdit.tsx:308
#: src/components/common/RedactedText.tsx:14
#: src/components/common/RedactedText.tsx:42
msgid "Name"
msgstr "Name"

Expand Down Expand Up @@ -3239,7 +3239,7 @@ msgstr "Vorlesen pausieren"
msgid "Pending"
msgstr "Ausstehend"

#: src/components/common/RedactedText.tsx:15
#: src/components/common/RedactedText.tsx:43
msgid "Phone"
msgstr "Telefon"

Expand Down Expand Up @@ -4526,7 +4526,7 @@ msgstr "Diese E-Mail ist bereits in der Liste."
msgid "participant.modal.refine.info.available.in"
msgstr "Diese Funktion wird in {remainingTime} Sekunden verfügbar sein."

#: src/components/common/RedactedText.tsx:33
#: src/components/common/RedactedText.tsx:61
msgid "This information is anonymized"
msgstr "Diese Informationen sind anonymisiert"

Expand Down Expand Up @@ -4981,7 +4981,7 @@ msgstr "PII Redaction verwenden"
msgid "Use Shift + Enter to add a new line"
msgstr "Verwenden Sie Shift + Enter, um eine neue Zeile hinzuzufügen"

#: src/components/common/RedactedText.tsx:16
#: src/components/common/RedactedText.tsx:44
msgid "Username"
msgstr "Benutzername"

Expand Down
Loading
Loading