Skip to content
Closed
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
5 changes: 5 additions & 0 deletions echo/directus/sync/collections/folders.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
[
{
"name": "custom_logos",
"parent": null,
"_syncId": "416965c6-7695-4235-8322-8515c9a05820"
},
{
"name": "Public",
"parent": null,
Expand Down
7 changes: 7 additions & 0 deletions echo/directus/sync/collections/permissions.json
Original file line number Diff line number Diff line change
Expand Up @@ -2123,6 +2123,13 @@
"_and": [
{
"_or": [
{
"folder": {
"name": {
"_contains": "custom_logos"
}
}
},
{
"folder": {
"name": {
Expand Down
94 changes: 94 additions & 0 deletions echo/frontend/src/components/common/RedactedText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
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;

const REDACTED_LABELS: Record<string, string> = {
address: "Address",
card: "Card",
email: "Email",
iban: "IBAN",
id: "ID",
license_plate: "License Plate",
name: "Name",
phone: "Phone",
username: "Username",
};
Comment on lines +7 to +17
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Label strings aren't wrapped with t — won't be extracted for localization.

Since the localization workflow is active, these user-visible labels ("Address", "Email", etc.) should go through Lingui so translators can pick them up.

✏️ Example fix
+import { t } from "@lingui/core/macro";
+
 const REDACTED_LABELS: Record<string, string> = {
-	address: "Address",
-	card: "Card",
-	email: "Email",
+	address: t`Address`,
+	card: t`Card`,
+	email: t`Email`,
 	// ... etc
 };

Note: since t is a macro evaluated at render time, you'd need to make this a function or compute labels inside the component/badge. Alternatively, move formatLabel to use t dynamically.

As per coding guidelines: "Localization workflow is active: keep Lingui extract/compile scripts in mind when touching t/Trans strings."

🤖 Prompt for AI Agents
In `@echo/frontend/src/components/common/RedactedText.tsx` around lines 7 - 17,
REDACTED_LABELS contains user-visible label strings that are not passed through
the Lingui t macro, so extractable translations are missed; update the code so
labels are generated at render-time via t (e.g., replace the static
REDACTED_LABELS map with a function getRedactedLabel(key: string) that returns
t`...` for each label or move the mapping logic into formatLabel/component
rendering and call t for each user-facing string), ensuring every label
(Address, Email, Card, IBAN, ID, License Plate, Name, Phone, Username) is
wrapped with t so Lingui can extract them.


const formatLabel = (key: string): string => {
if (key in REDACTED_LABELS) {
return REDACTED_LABELS[key];
}
return key
.split("_")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
};

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}
>
{label}
</Text>
</Tooltip>
);
};

/**
* Parses a text string and replaces `<redacted_*>` placeholders with
* styled inline badges that show a human-readable label and tooltip.
*
* Returns the original string unchanged if no placeholders are found.
*/
export const parseRedactedText = (text: string): ReactNode[] | string => {
if (!text || !text.includes("<redacted_")) {
return text;
}

const parts: ReactNode[] = [];
let lastIndex = 0;

const regex = new RegExp(REDACTED_PATTERN);
for (let match = regex.exec(text); match !== null; match = regex.exec(text)) {
if (match.index > lastIndex) {
parts.push(text.slice(lastIndex, match.index));
}
parts.push(<RedactedBadge key={`${match.index}-${match[1]}`} type={match[1]} />);
lastIndex = regex.lastIndex;
}

if (lastIndex < text.length) {
parts.push(text.slice(lastIndex));
}

return parts;
};

/**
* Component that renders text with `<redacted_*>` placeholders replaced
* by subtle inline badges with tooltips.
*/
export const RedactedText = ({
children,
className,
}: {
children: string;
className?: string;
}) => {
const rendered = useMemo(() => parseRedactedText(children), [children]);

if (typeof rendered === "string") {
return <span className={className}>{rendered}</span>;
}

return <span className={className}>{rendered}</span>;
Comment on lines +86 to +93
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Dead branch — both paths return the same JSX.

The typeof rendered === "string" check differentiates nothing here since both branches render identical markup. Simplify.

♻️ Simplify
 export const RedactedText = ({
 	children,
 	className,
 }: {
 	children: string;
 	className?: string;
 }) => {
 	const rendered = useMemo(() => parseRedactedText(children), [children]);
-
-	if (typeof rendered === "string") {
-		return <span className={className}>{rendered}</span>;
-	}
-
 	return <span className={className}>{rendered}</span>;
 };
🤖 Prompt for AI Agents
In `@echo/frontend/src/components/common/RedactedText.tsx` around lines 86 - 93,
The conditional in the RedactedText component around the `rendered` value is
dead — both branches return identical JSX — so remove the `if (typeof rendered
=== "string")` check and simplify the render to a single return that outputs
`<span className={className}>{rendered}</span>`; update the component using
`useMemo(() => parseRedactedText(children), [children])` (and keep `rendered`,
`className`, `children`, and `parseRedactedText` references intact) to avoid
duplicated markup and improve clarity.

};
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import {
useMediaQuery,
useSessionStorage,
} from "@mantine/hooks";
import { ShieldCheckIcon } from "@phosphor-icons/react";
import { DetectiveIcon } from "@phosphor-icons/react";
import {
IconArrowsExchange,
IconArrowsUpDown,
Expand Down Expand Up @@ -587,7 +587,7 @@ const ConversationAccordionItem = ({
size={18}
style={{ cursor: "default" }}
>
<ShieldCheckIcon />
<DetectiveIcon />
</ThemeIcon>
</Tooltip>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { t } from "@lingui/core/macro";
import { Divider, Skeleton, Text } from "@mantine/core";

import { BaseMessage } from "../chat/BaseMessage";
import { RedactedText } from "../common/RedactedText";
import { useConversationChunkContentUrl } from "./hooks";

export const ConversationChunkAudioTranscript = ({
Expand Down Expand Up @@ -65,13 +66,13 @@ export const ConversationChunkAudioTranscript = ({
<span className="italic text-gray-500">{t`Transcript not available yet`}</span>
)} */}

{chunk.error ? (
<span className="italic text-gray-500">{t`Unable to process this chunk`}</span>
) : !chunk.transcript ? (
<span className="italic text-gray-500">{t`Transcribing...`}</span>
) : (
chunk.transcript
)}
{chunk.error ? (
<span className="italic text-gray-500">{t`Unable to process this chunk`}</span>
) : !chunk.transcript ? (
<span className="italic text-gray-500">{t`Transcribing...`}</span>
) : (
<RedactedText>{chunk.transcript}</RedactedText>
)}
</Text>
</BaseMessage>
);
Expand Down
19 changes: 12 additions & 7 deletions echo/frontend/src/components/participant/UserChunkMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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 @@ -101,14 +102,18 @@ const UserChunkMessage = ({
</Menu.Dropdown>
</Menu>
</div>
<Paper className="my-2 rounded-t-xl rounded-bl-xl border-0 bg-gray-100 p-4">
<Text className="prose text-sm">
{chunk.transcript == null && (
<Markdown content={t`*Transcription in progress.*`} />
)}
<Paper className="my-2 rounded-t-xl rounded-bl-xl border-0 bg-gray-100 p-4">
<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 ?? ""} />
</Text>
</Paper>
)}
</Text>
</Paper>
Comment on lines +105 to +116
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bug: double-render when transcript is null — both "Transcription in progress" AND an empty <Markdown> will render.

When chunk.transcript is null, the first condition (line 107) renders the progress message. But the ternary on line 110 also evaluates: null?.includes(...) is undefined (falsy), so it falls through to <Markdown content="" />. You get two elements rendered.

Need a proper if/else chain or early return.

🐛 Proposed fix
 		<Paper className="my-2 rounded-t-xl rounded-bl-xl border-0 bg-gray-100 p-4">
 			<Text className="prose text-sm">
-				{chunk.transcript == null && (
+				{chunk.transcript == null ? (
 					<Markdown content={t`*Transcription in progress.*`} />
-				)}
-				{chunk.transcript?.includes("<redacted_") ? (
+				) : chunk.transcript.includes("<redacted_") ? (
 					<RedactedText>{chunk.transcript}</RedactedText>
 				) : (
 					<Markdown content={chunk.transcript ?? ""} />
 				)}
 			</Text>
 		</Paper>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Paper className="my-2 rounded-t-xl rounded-bl-xl border-0 bg-gray-100 p-4">
<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 ?? ""} />
</Text>
</Paper>
)}
</Text>
</Paper>
<Paper className="my-2 rounded-t-xl rounded-bl-xl border-0 bg-gray-100 p-4">
<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 ?? ""} />
)}
</Text>
</Paper>
🤖 Prompt for AI Agents
In `@echo/frontend/src/components/participant/UserChunkMessage.tsx` around lines
105 - 116, The JSX currently renders both the "Transcription in progress" and an
empty <Markdown> because the ternary checks chunk.transcript?.includes(...)
after chunk.transcript == null; change the conditional flow in UserChunkMessage
(around the Paper/Text block) to an if/else chain: if chunk.transcript == null
render the Markdown with "*Transcription in progress.*" and return/skip the
other branch; else if chunk.transcript.includes("<redacted_") render
<RedactedText>{chunk.transcript}</RedactedText>; otherwise render <Markdown
content={chunk.transcript} /> so only one of the three render paths
(in-progress, redacted, or transcript) runs.

</div>
);
};
Expand Down
17 changes: 11 additions & 6 deletions echo/frontend/src/components/project/ProjectPortalEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import {
TextInput,
Title,
} from "@mantine/core";
import { ShieldCheckIcon } from "@phosphor-icons/react";
import { IconEye, IconEyeOff, IconRefresh, IconX } from "@tabler/icons-react";
import { DetectiveIcon } from "@phosphor-icons/react";
import { IconEye, IconEyeOff, IconInfoCircle, IconRefresh, IconRosetteDiscountCheck, IconX } from "@tabler/icons-react";
import { useQueryClient } from "@tanstack/react-query";
import { Resizable } from "re-resizable";
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
Expand All @@ -30,7 +30,6 @@ import { useAutoSave } from "@/hooks/useAutoSave";
import { useLanguage } from "@/hooks/useLanguage";
import type { VerificationTopicsResponse } from "@/lib/api";
import { testId } from "@/lib/testUtils";
import { Logo } from "../common/Logo";
import { toast } from "../common/Toaster";
import { FormLabel } from "../form/FormLabel";
import { MarkdownWYSIWYG } from "../form/MarkdownWYSIWYG/MarkdownWYSIWYG";
Expand Down Expand Up @@ -634,7 +633,6 @@ const ProjectPortalEditorComponent: React.FC<ProjectPortalEditorProps> = ({
<Title order={4}>
<Trans>Explore</Trans>
</Title>
<Logo hideTitle alwaysDembrane />
<Badge color="mauve" c="graphite" size="sm">
<Trans id="dashboard.dembrane.concrete.beta">
Beta
Expand Down Expand Up @@ -841,7 +839,10 @@ const ProjectPortalEditorComponent: React.FC<ProjectPortalEditorProps> = ({
Verify
</Trans>
</Title>
<Logo hideTitle alwaysDembrane />
<IconRosetteDiscountCheck
size={20}
color="var(--mantine-color-primary-filled)"
/>
<Badge color="mauve" c="graphite" size="sm">
<Trans id="dashboard.dembrane.verify.beta">
Beta
Expand Down Expand Up @@ -1171,7 +1172,7 @@ const ProjectPortalEditorComponent: React.FC<ProjectPortalEditorProps> = ({
<Title order={4}>
<Trans>Anonymize Transcripts</Trans>
</Title>
<ShieldCheckIcon
<DetectiveIcon
size={20}
color="var(--mantine-color-primary-filled)"
/>
Expand Down Expand Up @@ -1219,6 +1220,10 @@ const ProjectPortalEditorComponent: React.FC<ProjectPortalEditorProps> = ({
<Title order={4}>
<Trans>Auto-generate Titles</Trans>
</Title>
<IconInfoCircle
size={20}
className="text-gray-400"
/>
Comment on lines +1223 to +1226
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Use a CSS variable instead of text-gray-400 for theme consistency.

Every other icon in this file uses color="var(--mantine-color-primary-filled)" via an inline style prop. This one breaks the pattern with a hardcoded Tailwind color class, which won't respect theme changes.

Proposed fix
 <IconInfoCircle
   size={20}
-  className="text-gray-400"
+  style={{ color: "var(--mantine-color-dimmed)" }}
 />

As per coding guidelines: "Keep static utility classes (borders, spacing, layout) in Tailwind; move theme-dependent colors to CSS variables" and "Use var(--app-background) and var(--app-text) instead of hardcoded colors like #F6F4F1 or #2D2D2C to ensure theme changes propagate".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<IconInfoCircle
size={20}
className="text-gray-400"
/>
<IconInfoCircle
size={20}
style={{ color: "var(--mantine-color-dimmed)" }}
/>
🤖 Prompt for AI Agents
In `@echo/frontend/src/components/project/ProjectPortalEditor.tsx` around lines
1223 - 1226, Replace the hardcoded Tailwind color class on the IconInfoCircle
(remove className="text-gray-400") and use the same theme-aware pattern as other
icons by passing the color via the inline prop (e.g.,
color="var(--mantine-color-primary-filled)") so the icon follows CSS variables
and theme changes; update the IconInfoCircle element to use that color prop and
remove the static utility class.

<Badge color="mauve" c="graphite" size="sm">
<Trans>Beta</Trans>
</Badge>
Expand Down
19 changes: 10 additions & 9 deletions echo/frontend/src/components/quote/Quote.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { useCopyQuote } from "@/components/aspect/hooks/useCopyQuote";
import { cn } from "@/lib/utils";
import { CopyIconButton } from "../common/CopyIconButton";
import { I18nLink } from "../common/i18nLink";
import { RedactedText } from "../common/RedactedText";

// replacement for AspectSegment
export const Quote = ({
Expand Down Expand Up @@ -132,12 +133,12 @@ export const Quote = ({
>
{showTranscript ? (
<div className="space-y-3">
<Text
size="sm"
className="whitespace-pre-wrap italic leading-relaxed"
>
{data.verbatim_transcript}
</Text>
<Text
size="sm"
className="whitespace-pre-wrap italic leading-relaxed"
>
<RedactedText>{data.verbatim_transcript ?? ""}</RedactedText>
</Text>
{data.relevant_index && (
<div className="border-t border-gray-300 pt-2 dark:border-gray-600">
<Text size="xs" c="dimmed">
Expand All @@ -149,9 +150,9 @@ export const Quote = ({
</div>
) : (
<div>
<Text size="sm" className="italic" lineClamp={2}>
"{transcriptExcerpt}"
</Text>
<Text size="sm" className="italic" lineClamp={2}>
"<RedactedText>{transcriptExcerpt}</RedactedText>"
</Text>
{hasTranscript && (
<Text size="xs" c="dimmed" mt="xs">
Click to see full context
Expand Down
32 changes: 12 additions & 20 deletions echo/frontend/src/components/settings/WhitelabelLogoCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,10 @@ export const WhitelabelLogoCard = () => {
},
});

const handleUpload = () => {
if (file) {
uploadMutation.mutate(file);
const handleFileChange = (selectedFile: File | null) => {
setFile(selectedFile);
if (selectedFile) {
uploadMutation.mutate(selectedFile);
}
};

Expand Down Expand Up @@ -135,23 +136,14 @@ export const WhitelabelLogoCard = () => {
</Text>
)}

<Stack gap="xs">
<FileInput
accept="image/png,image/jpeg,image/svg+xml,image/webp"
placeholder={t`Choose a logo file`}
value={file}
onChange={setFile}
leftSection={<IconUpload size={16} />}
/>
<Button
size="compact-sm"
disabled={!file}
loading={uploadMutation.isPending}
onClick={handleUpload}
>
<Trans>Upload</Trans>
</Button>
</Stack>
<FileInput
accept="image/png,image/jpeg,image/svg+xml,image/webp"
placeholder={t`Choose a logo file`}
value={file}
onChange={handleFileChange}
leftSection={<IconUpload size={16} />}
disabled={uploadMutation.isPending}
/>
</Stack>
</Card>
);
Expand Down
Loading