diff --git a/echo/directus/sync/collections/operations.json b/echo/directus/sync/collections/operations.json
index c169726e..df2d31c5 100644
--- a/echo/directus/sync/collections/operations.json
+++ b/echo/directus/sync/collections/operations.json
@@ -79,7 +79,7 @@
"resolve": null,
"reject": null,
"flow": "17703446-fef0-49e9-bdc4-385db1311137",
- "_syncId": "615a54cd-a72e-41ad-9403-9577c80280d6"
+ "_syncId": "84c38ea6-5d15-429f-8c24-9485d54ba7be"
},
{
"name": "Email Send Operation Failed Dutch",
@@ -93,7 +93,7 @@
"resolve": null,
"reject": null,
"flow": "17703446-fef0-49e9-bdc4-385db1311137",
- "_syncId": "84c38ea6-5d15-429f-8c24-9485d54ba7be"
+ "_syncId": "615a54cd-a72e-41ad-9403-9577c80280d6"
},
{
"name": "failed",
@@ -107,7 +107,7 @@
"resolve": null,
"reject": null,
"flow": "17703446-fef0-49e9-bdc4-385db1311137",
- "_syncId": "8d8d787a-dbc4-44f9-9ab4-28e3f3d5f31c"
+ "_syncId": "eb6f8253-647f-4fb1-9010-e93594ba065e"
},
{
"name": "failed",
@@ -121,7 +121,7 @@
"resolve": null,
"reject": null,
"flow": "17703446-fef0-49e9-bdc4-385db1311137",
- "_syncId": "eb6f8253-647f-4fb1-9010-e93594ba065e"
+ "_syncId": "8d8d787a-dbc4-44f9-9ab4-28e3f3d5f31c"
},
{
"name": "Filter Emails",
@@ -132,10 +132,10 @@
"options": {
"code": "module.exports = async function(data) {\n\n const submissions = data.get_all_participants;\n \n // Filter submissions to only include those where email_opt_in is true\n const filteredSubmissions = submissions.filter(sub => sub.email_opt_in === true);\n\n // Create an array with email, project_id and an email_opt_out token for each submission\n const result = filteredSubmissions.map(sub => ({\n project_name: data.project_data[0].name || '',\n\t\tdefault_conversation_title: data.project_data[0].default_conversation_title || '',\n\t\tconversation_name: sub.conversation_id.participant_name || '',\n email: sub.email,\n project_id: sub.project_id || '',\n token: sub.email_opt_out_token,\n language: data.check_report_language[0].language || 'empty',\n ADMIN_BASE_URL: \"{{ $env.ADMIN_BASE_URL }}\" || \"http://localhost:5173\",\n PARTICIPANT_BASE_URL: \"{{ $env.PARTICIPANT_BASE_URL }}\" || \"http://localhost:5174\", \n }));\n \n return result;\n};"
},
- "resolve": "b8144cee-59f6-40d9-a849-dd0c639e4e31",
+ "resolve": "e101f00d-2fb8-4f40-9e0e-4d24da5bb1e9",
"reject": null,
"flow": "ec4e7ea5-72de-4365-b66f-d8f11b549495",
- "_syncId": "ca1ffbc5-cfce-4fb4-8f15-c128ea407d41"
+ "_syncId": "efb3982e-5703-4c07-8982-a6e1b5218e4a"
},
{
"name": "Filter Emails",
@@ -146,10 +146,10 @@
"options": {
"code": "module.exports = async function(data) {\n\n const submissions = data.get_all_participants;\n \n // Filter submissions to only include those where email_opt_in is true\n const filteredSubmissions = submissions.filter(sub => sub.email_opt_in === true);\n\n // Create an array with email, project_id and an email_opt_out token for each submission\n const result = filteredSubmissions.map(sub => ({\n project_name: data.project_data[0].name || '',\n\t\tdefault_conversation_title: data.project_data[0].default_conversation_title || '',\n\t\tconversation_name: sub.conversation_id.participant_name || '',\n email: sub.email,\n project_id: sub.project_id || '',\n token: sub.email_opt_out_token,\n language: data.check_report_language[0].language || 'empty',\n ADMIN_BASE_URL: \"{{ $env.ADMIN_BASE_URL }}\" || \"http://localhost:5173\",\n PARTICIPANT_BASE_URL: \"{{ $env.PARTICIPANT_BASE_URL }}\" || \"http://localhost:5174\", \n }));\n \n return result;\n};"
},
- "resolve": "e101f00d-2fb8-4f40-9e0e-4d24da5bb1e9",
+ "resolve": "b8144cee-59f6-40d9-a849-dd0c639e4e31",
"reject": null,
"flow": "ec4e7ea5-72de-4365-b66f-d8f11b549495",
- "_syncId": "efb3982e-5703-4c07-8982-a6e1b5218e4a"
+ "_syncId": "ca1ffbc5-cfce-4fb4-8f15-c128ea407d41"
},
{
"name": "log environment vars",
@@ -213,7 +213,7 @@
"resolve": null,
"reject": null,
"flow": "ec4e7ea5-72de-4365-b66f-d8f11b549495",
- "_syncId": "84852456-3f3a-4906-be94-8b750159883b"
+ "_syncId": "e8274ad4-5844-42cd-8a6b-d40d08cf83d3"
},
{
"name": "Report Not Published",
@@ -227,7 +227,7 @@
"resolve": null,
"reject": null,
"flow": "ec4e7ea5-72de-4365-b66f-d8f11b549495",
- "_syncId": "e8274ad4-5844-42cd-8a6b-d40d08cf83d3"
+ "_syncId": "84852456-3f3a-4906-be94-8b750159883b"
},
{
"name": "Send Email Dutch",
@@ -256,9 +256,9 @@
]
},
"resolve": null,
- "reject": "84c38ea6-5d15-429f-8c24-9485d54ba7be",
+ "reject": "615a54cd-a72e-41ad-9403-9577c80280d6",
"flow": "17703446-fef0-49e9-bdc4-385db1311137",
- "_syncId": "34fb6ee5-2813-484a-a1cc-f97de097509b"
+ "_syncId": "ea78ec02-364d-4f18-80f8-ea5ac4c787ed"
},
{
"name": "Send Email Dutch",
@@ -287,9 +287,9 @@
]
},
"resolve": null,
- "reject": "615a54cd-a72e-41ad-9403-9577c80280d6",
+ "reject": "84c38ea6-5d15-429f-8c24-9485d54ba7be",
"flow": "17703446-fef0-49e9-bdc4-385db1311137",
- "_syncId": "ea78ec02-364d-4f18-80f8-ea5ac4c787ed"
+ "_syncId": "34fb6ee5-2813-484a-a1cc-f97de097509b"
},
{
"name": "Send Email English",
@@ -318,9 +318,9 @@
]
},
"resolve": null,
- "reject": "2b24450b-6a2e-4452-aba1-9814d17fef42",
+ "reject": "920bd181-b2a2-4f0d-94dc-3b1a08c3f4ef",
"flow": "17703446-fef0-49e9-bdc4-385db1311137",
- "_syncId": "9390ed2f-7dc6-4a6a-83da-2d87d478261d"
+ "_syncId": "3dbf2ea1-17f8-4bde-aa89-43278fe9a00f"
},
{
"name": "Send Email English",
@@ -349,9 +349,9 @@
]
},
"resolve": null,
- "reject": "920bd181-b2a2-4f0d-94dc-3b1a08c3f4ef",
+ "reject": "2b24450b-6a2e-4452-aba1-9814d17fef42",
"flow": "17703446-fef0-49e9-bdc4-385db1311137",
- "_syncId": "3dbf2ea1-17f8-4bde-aa89-43278fe9a00f"
+ "_syncId": "9390ed2f-7dc6-4a6a-83da-2d87d478261d"
},
{
"name": "Trigger Email Flow",
diff --git a/echo/directus/sync/collections/policies.json b/echo/directus/sync/collections/policies.json
index 1dff0924..5687fce3 100644
--- a/echo/directus/sync/collections/policies.json
+++ b/echo/directus/sync/collections/policies.json
@@ -11,10 +11,6 @@
{
"role": "_sync_default_admin_role",
"sort": 1
- },
- {
- "role": null,
- "sort": null
}
],
"_syncId": "_sync_default_admin_policy"
diff --git a/echo/directus/sync/snapshot/fields/conversation/conversation_artifacts.json b/echo/directus/sync/snapshot/fields/conversation/conversation_artifacts.json
index 5104cc09..044057bb 100644
--- a/echo/directus/sync/snapshot/fields/conversation/conversation_artifacts.json
+++ b/echo/directus/sync/snapshot/fields/conversation/conversation_artifacts.json
@@ -16,7 +16,7 @@
"readonly": false,
"required": false,
"searchable": true,
- "sort": 26,
+ "sort": 27,
"special": [
"o2m"
],
diff --git a/echo/directus/sync/snapshot/fields/conversation/is_all_chunks_transcribed.json b/echo/directus/sync/snapshot/fields/conversation/is_all_chunks_transcribed.json
index dadd0a1e..ca2034d0 100644
--- a/echo/directus/sync/snapshot/fields/conversation/is_all_chunks_transcribed.json
+++ b/echo/directus/sync/snapshot/fields/conversation/is_all_chunks_transcribed.json
@@ -16,7 +16,7 @@
"readonly": false,
"required": false,
"searchable": true,
- "sort": 23,
+ "sort": 24,
"special": [
"cast-boolean"
],
diff --git a/echo/directus/sync/snapshot/fields/conversation/is_anonymized.json b/echo/directus/sync/snapshot/fields/conversation/is_anonymized.json
new file mode 100644
index 00000000..08dbba3f
--- /dev/null
+++ b/echo/directus/sync/snapshot/fields/conversation/is_anonymized.json
@@ -0,0 +1,46 @@
+{
+ "collection": "conversation",
+ "field": "is_anonymized",
+ "type": "boolean",
+ "meta": {
+ "collection": "conversation",
+ "conditions": null,
+ "display": null,
+ "display_options": null,
+ "field": "is_anonymized",
+ "group": null,
+ "hidden": false,
+ "interface": "boolean",
+ "note": null,
+ "options": null,
+ "readonly": false,
+ "required": false,
+ "searchable": true,
+ "sort": 21,
+ "special": [
+ "cast-boolean"
+ ],
+ "translations": null,
+ "validation": null,
+ "validation_message": null,
+ "width": "full"
+ },
+ "schema": {
+ "name": "is_anonymized",
+ "table": "conversation",
+ "data_type": "boolean",
+ "default_value": false,
+ "max_length": null,
+ "numeric_precision": null,
+ "numeric_scale": null,
+ "is_nullable": true,
+ "is_unique": false,
+ "is_indexed": false,
+ "is_primary_key": false,
+ "is_generated": false,
+ "generation_expression": null,
+ "has_auto_increment": false,
+ "foreign_key_table": null,
+ "foreign_key_column": null
+ }
+}
diff --git a/echo/directus/sync/snapshot/fields/conversation/is_audio_processing_finished.json b/echo/directus/sync/snapshot/fields/conversation/is_audio_processing_finished.json
index b4666d93..cc38c92a 100644
--- a/echo/directus/sync/snapshot/fields/conversation/is_audio_processing_finished.json
+++ b/echo/directus/sync/snapshot/fields/conversation/is_audio_processing_finished.json
@@ -16,7 +16,7 @@
"readonly": false,
"required": false,
"searchable": true,
- "sort": 22,
+ "sort": 23,
"special": [
"cast-boolean"
],
diff --git a/echo/directus/sync/snapshot/fields/conversation/is_finished.json b/echo/directus/sync/snapshot/fields/conversation/is_finished.json
index fe093e1a..39e45ab0 100644
--- a/echo/directus/sync/snapshot/fields/conversation/is_finished.json
+++ b/echo/directus/sync/snapshot/fields/conversation/is_finished.json
@@ -16,7 +16,7 @@
"readonly": false,
"required": false,
"searchable": true,
- "sort": 21,
+ "sort": 22,
"special": [
"cast-boolean"
],
diff --git a/echo/directus/sync/snapshot/fields/conversation/linked_conversations.json b/echo/directus/sync/snapshot/fields/conversation/linked_conversations.json
index af48f54d..d021d4e9 100644
--- a/echo/directus/sync/snapshot/fields/conversation/linked_conversations.json
+++ b/echo/directus/sync/snapshot/fields/conversation/linked_conversations.json
@@ -21,7 +21,7 @@
"readonly": false,
"required": false,
"searchable": true,
- "sort": 24,
+ "sort": 25,
"special": [
"o2m"
],
diff --git a/echo/directus/sync/snapshot/fields/conversation/linking_conversations.json b/echo/directus/sync/snapshot/fields/conversation/linking_conversations.json
index 6a8cd255..8df3c1a4 100644
--- a/echo/directus/sync/snapshot/fields/conversation/linking_conversations.json
+++ b/echo/directus/sync/snapshot/fields/conversation/linking_conversations.json
@@ -20,7 +20,7 @@
"readonly": false,
"required": false,
"searchable": true,
- "sort": 25,
+ "sort": 26,
"special": [
"o2m"
],
diff --git a/echo/frontend/src/components/conversation/ConversationAccordion.tsx b/echo/frontend/src/components/conversation/ConversationAccordion.tsx
index c714bf70..7b056e12 100644
--- a/echo/frontend/src/components/conversation/ConversationAccordion.tsx
+++ b/echo/frontend/src/components/conversation/ConversationAccordion.tsx
@@ -34,6 +34,7 @@ import {
useMediaQuery,
useSessionStorage,
} from "@mantine/hooks";
+import { ShieldCheckIcon } from "@phosphor-icons/react";
import {
IconArrowsExchange,
IconArrowsUpDown,
@@ -556,11 +557,13 @@ const ConversationAccordionItem = ({
{conversation.participant_name || conversation.title}
+
{conversation.title && conversation.participant_name && (
)}
+
{hasVerifiedArtefacts && (
)}
+
+ {conversation.is_anonymized && (
+
+
+
+
+
+ )}
+
{
- try { analytics.trackEvent(events.SELECT_ALL_CLICK); }
- catch (error) { console.warn("Analytics tracking failed:", error); }
+ try {
+ analytics.trackEvent(events.SELECT_ALL_CLICK);
+ } catch (error) {
+ console.warn("Analytics tracking failed:", error);
+ }
setSelectAllModalOpened(true);
setSelectAllResult(null);
};
@@ -927,8 +948,11 @@ export const ConversationAccordion = ({
return;
}
- try { analytics.trackEvent(events.SELECT_ALL_CONFIRM); }
- catch (error) { console.warn("Analytics tracking failed:", error); }
+ try {
+ analytics.trackEvent(events.SELECT_ALL_CONFIRM);
+ } catch (error) {
+ console.warn("Analytics tracking failed:", error);
+ }
setSelectAllLoading(true);
try {
@@ -940,11 +964,17 @@ export const ConversationAccordion = ({
verifiedOnly: showOnlyVerified || undefined,
});
setSelectAllResult(result);
- try { analytics.trackEvent(events.SELECT_ALL_SUCCESS); }
- catch (error) { console.warn("Analytics tracking failed:", error); }
+ try {
+ analytics.trackEvent(events.SELECT_ALL_SUCCESS);
+ } catch (error) {
+ console.warn("Analytics tracking failed:", error);
+ }
} catch (_error) {
- try { analytics.trackEvent(events.SELECT_ALL_ERROR); }
- catch (error) { console.warn("Analytics tracking failed:", error); }
+ try {
+ analytics.trackEvent(events.SELECT_ALL_ERROR);
+ } catch (error) {
+ console.warn("Analytics tracking failed:", error);
+ }
toast.error(t`Failed to add conversations to context`);
setSelectAllModalOpened(false);
} finally {
diff --git a/echo/frontend/src/components/conversation/ConversationDangerZone.tsx b/echo/frontend/src/components/conversation/ConversationDangerZone.tsx
index 486e099c..a1b642ec 100644
--- a/echo/frontend/src/components/conversation/ConversationDangerZone.tsx
+++ b/echo/frontend/src/components/conversation/ConversationDangerZone.tsx
@@ -1,6 +1,6 @@
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
-import { Button, Group, Stack } from "@mantine/core";
+import { Button, Group, Stack, Tooltip } from "@mantine/core";
import { IconDownload, IconTrash } from "@tabler/icons-react";
import { useParams } from "react-router";
import { MoveConversationButton } from "@/components/conversation/MoveConversationButton";
@@ -13,8 +13,10 @@ import { useDeleteConversationByIdMutation } from "./hooks";
export const ConversationDangerZone = ({
conversation,
+ disableDownloadAudio = false,
}: {
conversation: Conversation;
+ disableDownloadAudio?: boolean;
}) => {
const deleteConversationByIdMutation = useDeleteConversationByIdMutation();
const navigate = useI18nNavigate();
@@ -51,19 +53,29 @@ export const ConversationDangerZone = ({
- }
- component="a"
- target="_blank"
- href={getConversationContentLink(conversation.id)}
- onClick={handleDownloadAudio}
- {...testId("conversation-download-audio-button")}
+
-
- Download Audio
-
-
+ }
+ component="a"
+ target="_blank"
+ href={
+ disableDownloadAudio
+ ? undefined
+ : getConversationContentLink(conversation.id)
+ }
+ onClick={disableDownloadAudio ? undefined : handleDownloadAudio}
+ disabled={disableDownloadAudio}
+ {...testId("conversation-download-audio-button")}
+ >
+
+ Download Audio
+
+
+
)}
@@ -257,7 +263,10 @@ export const ProjectConversationOverviewRoute = () => {
) : null} */}
-
+
>
)}
diff --git a/echo/frontend/src/routes/project/conversation/ProjectConversationTranscript.tsx b/echo/frontend/src/routes/project/conversation/ProjectConversationTranscript.tsx
index 4a94c46c..0f491914 100644
--- a/echo/frontend/src/routes/project/conversation/ProjectConversationTranscript.tsx
+++ b/echo/frontend/src/routes/project/conversation/ProjectConversationTranscript.tsx
@@ -8,6 +8,7 @@ import {
Stack,
Switch,
Title,
+ Tooltip,
} from "@mantine/core";
import { IconAlertCircle } from "@tabler/icons-react";
import { useEffect } from "react";
@@ -30,7 +31,7 @@ export const ProjectConversationTranscript = () => {
conversationId: conversationId ?? "",
loadConversationChunks: false,
query: {
- fields: ["id", "participant_name", "is_finished"],
+ fields: ["id", "participant_name", "is_finished", "is_anonymized"],
},
});
const { ref: loadMoreRef, inView } = useInView();
@@ -58,6 +59,8 @@ export const ProjectConversationTranscript = () => {
const allChunks = (chunksData?.pages ?? []).flatMap((page) => page.chunks);
+ const isAnonymized = conversationQuery.data?.is_anonymized ?? false;
+
const hasValidTranscripts = allChunks.some(
(chunk) => chunk.transcript && chunk.transcript.trim().length > 0,
);
@@ -103,18 +106,26 @@ export const ProjectConversationTranscript = () => {
-
- setShowAudioPlayer(event.currentTarget.checked)
- }
- label={t`Show audio player`}
- {...testId("transcript-show-audio-player-toggle")}
- />
+
+
+ setShowAudioPlayer(event.currentTarget.checked)
+ }
+ disabled={isAnonymized}
+ label={t`Show audio player`}
+ {...testId("transcript-show-audio-player-toggle")}
+ />
+
@@ -149,7 +160,7 @@ export const ProjectConversationTranscript = () => {
timestamp: chunk.timestamp ?? "",
transcript: chunk.transcript ?? "",
}}
- showAudioPlayer={showAudioPlayer}
+ showAudioPlayer={isAnonymized ? false : showAudioPlayer}
/>
);
diff --git a/echo/frontend/src/styles/button.module.css b/echo/frontend/src/styles/button.module.css
index 8c5d6fa2..ba3b0f94 100644
--- a/echo/frontend/src/styles/button.module.css
+++ b/echo/frontend/src/styles/button.module.css
@@ -60,19 +60,24 @@
/* Disabled - gray bg, graphite text, interactive borders (Peach on hover, Salmon on click) */
/* Note: Loading buttons are disabled but styled differently (see above) */
-.root:disabled:not([data-loading="true"]) {
+/* Note: [data-disabled] is needed for anchor-based buttons (component="a") which lack :disabled */
+.root:disabled:not([data-loading="true"]),
+.root[data-disabled]:not([data-loading="true"]) {
background-color: var(--mantine-color-gray-3);
color: var(--mantine-color-graphite-6); /* Graphite */
opacity: 1; /* Override Mantine's default opacity reduction to keep text readable */
cursor: not-allowed;
+ pointer-events: all; /* Allow hover/active on disabled anchor buttons */
}
-.root:disabled:not([data-loading="true"]):hover {
+.root:disabled:not([data-loading="true"]):hover,
+.root[data-disabled]:not([data-loading="true"]):hover {
border: 1px solid var(--mantine-color-peach-6); /* Peach border on hover */
background-color: var(--mantine-color-gray-3); /* Keep gray background */
}
-.root:disabled:not([data-loading="true"]):active {
+.root:disabled:not([data-loading="true"]):active,
+.root[data-disabled]:not([data-loading="true"]):active {
border: 1px solid var(--mantine-color-salmon-6); /* Salmon border on click */
background-color: var(--mantine-color-gray-3); /* Keep gray background */
}
diff --git a/echo/server/dembrane/api/conversation.py b/echo/server/dembrane/api/conversation.py
index 9838a41a..171804fc 100644
--- a/echo/server/dembrane/api/conversation.py
+++ b/echo/server/dembrane/api/conversation.py
@@ -669,7 +669,9 @@ async def generate_title_for_conversation(
summary = conversation_data.get("summary")
if not summary:
- raise HTTPException(status_code=400, detail="Conversation has no summary. Generate a summary first.")
+ raise HTTPException(
+ status_code=400, detail="Conversation has no summary. Generate a summary first."
+ )
project_data = conversation_data["project_id"]
language = project_data.get("language", "en")
@@ -766,7 +768,12 @@ async def retranscribe_conversation(
project_data = await run_in_thread_pool(
admin_client.get_items,
"project",
- {"query": {"filter": {"id": {"_eq": project_id}}, "fields": ["anonymize_transcripts"]}},
+ {
+ "query": {
+ "filter": {"id": {"_eq": project_id}},
+ "fields": ["anonymize_transcripts"],
+ }
+ },
)
if project_data and len(project_data) > 0:
use_pii_redaction = bool(project_data[0].get("anonymize_transcripts", False))
@@ -815,6 +822,7 @@ async def retranscribe_conversation(
if original_conversation["participant_user_agent"]
else None,
"merged_audio_path": merged_audio_path,
+ "is_anonymized": use_pii_redaction,
},
)
diff --git a/echo/server/dembrane/service/conversation.py b/echo/server/dembrane/service/conversation.py
index 13d94325..75ccf2cd 100644
--- a/echo/server/dembrane/service/conversation.py
+++ b/echo/server/dembrane/service/conversation.py
@@ -290,6 +290,7 @@ def create(
"participant_email": participant_email,
"participant_user_agent": participant_user_agent,
"source": source,
+ "is_anonymized": bool(project.get("anonymize_transcripts", False)),
"tags": {
"create": [
{
diff --git a/echo/server/dembrane/tasks.py b/echo/server/dembrane/tasks.py
index 1a46b83b..67d63c61 100644
--- a/echo/server/dembrane/tasks.py
+++ b/echo/server/dembrane/tasks.py
@@ -579,22 +579,10 @@ def task_process_conversation_chunk(
conversation_id = chunk["conversation_id"]
logger.debug(f"Chunk {chunk_id} found in conversation: {conversation_id}")
- # Fetch anonymize_transcripts flag from the project
- anonymize_transcripts = False
- try:
- from dembrane.directus import directus as _directus
- conversation_data = _directus.get_items("conversation", {
- "query": {
- "filter": {"id": {"_eq": conversation_id}},
- "fields": ["project_id.anonymize_transcripts"],
- }
- })
- if conversation_data and len(conversation_data) > 0:
- project_data = conversation_data[0].get("project_id")
- if isinstance(project_data, dict):
- anonymize_transcripts = bool(project_data.get("anonymize_transcripts", False))
- except Exception as e:
- logger.warning(f"Failed to fetch anonymize_transcripts for chunk {chunk_id}: {e}")
+ # Read is_anonymized from the conversation itself
+ conversation = conversation_service.get_by_id_or_raise(conversation_id)
+ anonymize_transcripts = bool(conversation.get("is_anonymized", False))
+ logger.debug(f"Conversation {conversation_id} is_anonymized: {anonymize_transcripts}")
# critical section
with ProcessingStatusContext(