From 9e999cc1629a37fd1b8c02c6d3d5be18874f98a3 Mon Sep 17 00:00:00 2001 From: Usama Date: Fri, 28 Nov 2025 09:39:57 +0000 Subject: [PATCH 1/4] - translations addition for verification topics - backend modification to include translations in the seed function to populate verification topics --- .../participant/verify/VerifySelection.tsx | 16 ++++++- .../project/ProjectPortalEditor.tsx | 19 ++++++-- echo/server/dembrane/seed.py | 48 +++++++++++++++++++ 3 files changed, 76 insertions(+), 7 deletions(-) diff --git a/echo/frontend/src/components/participant/verify/VerifySelection.tsx b/echo/frontend/src/components/participant/verify/VerifySelection.tsx index acc07643..b525ecdc 100644 --- a/echo/frontend/src/components/participant/verify/VerifySelection.tsx +++ b/echo/frontend/src/components/participant/verify/VerifySelection.tsx @@ -7,6 +7,7 @@ import { useParams, useSearchParams } from "react-router"; import { Logo } from "@/components/common/Logo"; import { toast } from "@/components/common/Toaster"; import { useI18nNavigate } from "@/hooks/useI18nNavigate"; +import { useLanguage } from "@/hooks/useLanguage"; import { useParticipantProjectById } from "../hooks"; import { startCooldown } from "../refine/hooks/useRefineSelectionCooldown"; import { @@ -15,7 +16,9 @@ import { } from "./hooks"; import { VerifyInstructions } from "./VerifyInstructions"; -const LANGUAGE_TO_LOCALE: Record = { +type LanguageCode = "de" | "en" | "es" | "fr" | "nl"; + +const LANGUAGE_TO_LOCALE: Record = { de: "de-DE", en: "en-US", es: "es-ES", @@ -23,6 +26,12 @@ const LANGUAGE_TO_LOCALE: Record = { nl: "nl-NL", }; +const localeFromLanguage = (language?: string) => { + if (!language) return undefined; + const iso = language.includes("-") ? language.split("-")[0] : language; + return LANGUAGE_TO_LOCALE[iso as LanguageCode]; +}; + export const TOPIC_ICON_MAP: Record = { actions: "↗️", agreements: "✅", @@ -50,8 +59,11 @@ export const VerifySelection = () => { const topicsQuery = useVerificationTopics(projectId); const projectLanguage = projectQuery.data?.language ?? "en"; + const { iso639_1: uiLanguageIso } = useLanguage(); const languageLocale = - LANGUAGE_TO_LOCALE[projectLanguage] ?? LANGUAGE_TO_LOCALE.en; + localeFromLanguage(uiLanguageIso) ?? + localeFromLanguage(projectLanguage) ?? + LANGUAGE_TO_LOCALE.en; const selectedTopics = topicsQuery.data?.selected_topics ?? []; const availableTopics = topicsQuery.data?.available_topics ?? []; diff --git a/echo/frontend/src/components/project/ProjectPortalEditor.tsx b/echo/frontend/src/components/project/ProjectPortalEditor.tsx index 8435a04c..0f0d8939 100644 --- a/echo/frontend/src/components/project/ProjectPortalEditor.tsx +++ b/echo/frontend/src/components/project/ProjectPortalEditor.tsx @@ -26,6 +26,7 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Controller, useForm, useWatch } from "react-hook-form"; import { z } from "zod"; import { useAutoSave } from "@/hooks/useAutoSave"; +import { useLanguage } from "@/hooks/useLanguage"; import type { VerificationTopicsResponse } from "@/lib/api"; import { Logo } from "../common/Logo"; import { toast } from "../common/Toaster"; @@ -55,7 +56,9 @@ const FormSchema = z.object({ type ProjectPortalFormValues = z.infer; -const LANGUAGE_TO_LOCALE: Record = { +type LanguageCode = "de" | "en" | "es" | "fr" | "nl"; + +const LANGUAGE_TO_LOCALE: Record = { de: "de-DE", en: "en-US", es: "es-ES", @@ -63,6 +66,9 @@ const LANGUAGE_TO_LOCALE: Record = { nl: "nl-NL", }; +const localeFromIso = (iso?: string) => + iso ? LANGUAGE_TO_LOCALE[iso as LanguageCode] : undefined; + const normalizeTopicList = (topics: string[]): string[] => Array.from( new Set(topics.map((topic) => topic.trim()).filter(Boolean)), @@ -193,8 +199,11 @@ const ProjectPortalEditorComponent: React.FC = ({ | "de" | "fr" | "es"; - const languageLocale = - LANGUAGE_TO_LOCALE[projectLanguageCode] ?? LANGUAGE_TO_LOCALE.en; + const { iso639_1: uiLanguageIso } = useLanguage(); + const translationLocale = + localeFromIso(uiLanguageIso) ?? + localeFromIso(projectLanguageCode) ?? + LANGUAGE_TO_LOCALE.en; const availableVerifyTopics = useMemo( () => @@ -204,11 +213,11 @@ const ProjectPortalEditorComponent: React.FC = ({ (topic.icon && !topic.icon.startsWith(":") ? topic.icon : undefined), key: topic.key, label: - topic.translations?.[languageLocale]?.label ?? + topic.translations?.[translationLocale]?.label ?? topic.translations?.["en-US"]?.label ?? topic.key, })), - [verificationTopics, languageLocale], + [verificationTopics, translationLocale], ); const selectedTopicDefaults = useMemo( diff --git a/echo/server/dembrane/seed.py b/echo/server/dembrane/seed.py index e0b6aab8..513b0129 100644 --- a/echo/server/dembrane/seed.py +++ b/echo/server/dembrane/seed.py @@ -68,6 +68,13 @@ async def seed_default_languages() -> None: "of mutual understanding. Output character should be diplomatic but precise, like meeting " "minutes with soul." ), + "translations": { + "en-US": {"label": "What we actually agreed on"}, + "nl-NL": {"label": "Waar we echt overeenkwamen"}, + "de-DE": {"label": "Worauf wir uns wirklich geeinigt haben"}, + "es-ES": {"label": "En qué acordamos realmente"}, + "fr-FR": {"label": "Ce sur quoi nous nous sommes vraiment mis d'accord"}, + }, }, { "key": "gems", @@ -82,6 +89,13 @@ async def seed_default_languages() -> None: "worth preserving, explaining why each gem matters. These are the insights people might forget but " "shouldn't. Output character should be excited and precise." ), + "translations": { + "en-US": {"label": "Hidden gems"}, + "nl-NL": {"label": "Verborgen parels"}, + "de-DE": {"label": "Verborgene Schätze"}, + "es-ES": {"label": "Joyas ocultas"}, + "fr-FR": {"label": "Pépites cachées"}, + }, }, { "key": "truths", @@ -96,6 +110,13 @@ async def seed_default_languages() -> None: "These truths are painful but necessary for genuine progress. Output character should be gentle but " "unflinching." ), + "translations": { + "en-US": {"label": "Painful truths"}, + "nl-NL": {"label": "Pijnlijke waarheden"}, + "de-DE": {"label": "Schmerzliche Wahrheiten"}, + "es-ES": {"label": "Verdades dolorosas"}, + "fr-FR": {"label": "Vérités douloureuses"}, + }, }, { "key": "moments", @@ -109,6 +130,13 @@ async def seed_default_languages() -> None: "made it possible. These are the moments when the conversation transcended its starting point. Output " "character should be energetic and forward-looking." ), + "translations": { + "en-US": {"label": "Breakthrough moments"}, + "nl-NL": {"label": "Doorbraakmomenten"}, + "de-DE": {"label": "Durchbruchmomente"}, + "es-ES": {"label": "Momentos decisivos"}, + "fr-FR": {"label": "Moments décisifs"}, + }, }, { "key": "actions", @@ -122,6 +150,13 @@ async def seed_default_languages() -> None: "provisional navigation rather than fixed commands. This is the group's best current thinking about the " "path forward. Output character should be pragmatic but inspirational." ), + "translations": { + "en-US": {"label": "What we think should happen"}, + "nl-NL": {"label": "Wat er volgens ons moet gebeuren"}, + "de-DE": {"label": "Was unserer Meinung nach passieren sollte"}, + "es-ES": {"label": "Lo que creemos que debería ocurrir"}, + "fr-FR": {"label": "Ce que nous pensons devoir se passer"}, + }, }, { "key": "disagreements", @@ -135,6 +170,13 @@ async def seed_default_languages() -> None: "each perspective has merit. These disagreements are features, not bugs - they prevent premature convergence " "and keep important tensions alive. Output character should be respectful and balanced." ), + "translations": { + "en-US": {"label": "Moments we agreed to disagree"}, + "nl-NL": {"label": "Momenten waarop we het eens waren om te verschillen"}, + "de-DE": {"label": "Momente, in denen wir uns auf Uneinigkeit geeinigt haben"}, + "es-ES": {"label": "Momentos en los que aceptamos discrepar"}, + "fr-FR": {"label": "Moments où nous sommes convenus d'être en désaccord"}, + }, }, ] @@ -166,6 +208,12 @@ async def seed_default_verification_topics() -> None: logger.info("Seeding verification topic '%s'", topic["key"]) translations_payload = [ + { + "languages_code": lang_code, + "label": translation["label"], + } + for lang_code, translation in (topic.get("translations") or {}).items() + ] or [ { "languages_code": DEFAULT_VERIFICATION_LANG, "label": topic["label"], From cf67aff763193229e0218711594b01b961d70cc4 Mon Sep 17 00:00:00 2001 From: Usama Date: Fri, 28 Nov 2025 12:52:18 +0000 Subject: [PATCH 2/4] - summarize functionality for conversations updated --- .../components/conversation/hooks/index.ts | 44 +++++++ .../ProjectConversationOverview.tsx | 112 ++++++++++++------ 2 files changed, 118 insertions(+), 38 deletions(-) diff --git a/echo/frontend/src/components/conversation/hooks/index.ts b/echo/frontend/src/components/conversation/hooks/index.ts index d9b358ca..db259d6b 100644 --- a/echo/frontend/src/components/conversation/hooks/index.ts +++ b/echo/frontend/src/components/conversation/hooks/index.ts @@ -1020,3 +1020,47 @@ export const useConversationsCountByProjectId = ( queryKey: ["projects", projectId, "conversations", "count", query], }); }; + +export const useConversationHasTranscript = ( + conversationId: string, + refetchInterval = 10000, + enabled = true, +) => { + return useQuery({ + enabled: enabled, + queryFn: async () => { + const response = await directus.request( + aggregate("conversation_chunk", { + aggregate: { + count: "*", + }, + query: { + filter: { + _and: [ + { + conversation_id: { + _eq: conversationId, + }, + }, + { + transcript: { + _nnull: true, + }, + }, + { + transcript: { + _nempty: true, + }, + }, + ], + }, + }, + }), + ); + const count = response[0]?.count; + return typeof count === "number" ? count : Number(count) || 0; + }, + queryKey: ["conversations", conversationId, "chunks", "transcript-count"], + refetchInterval, + }); +}; diff --git a/echo/frontend/src/routes/project/conversation/ProjectConversationOverview.tsx b/echo/frontend/src/routes/project/conversation/ProjectConversationOverview.tsx index 44b58881..6d122066 100644 --- a/echo/frontend/src/routes/project/conversation/ProjectConversationOverview.tsx +++ b/echo/frontend/src/routes/project/conversation/ProjectConversationOverview.tsx @@ -12,7 +12,7 @@ import { } from "@mantine/core"; import { useClipboard } from "@mantine/hooks"; import { IconRefresh } from "@tabler/icons-react"; -import { useMutation } from "@tanstack/react-query"; +import { useMutation, useMutationState } from "@tanstack/react-query"; import { useParams } from "react-router"; import { CopyIconButton } from "@/components/common/CopyIconButton"; import { Markdown } from "@/components/common/Markdown"; @@ -23,6 +23,7 @@ import { ConversationLink } from "@/components/conversation/ConversationLink"; import { useConversationById, useConversationChunks, + useConversationHasTranscript, } from "@/components/conversation/hooks"; import { VerifiedArtefactsSection } from "@/components/conversation/VerifiedArtefactsSection"; import { useProjectById } from "@/components/project/hooks"; @@ -58,44 +59,70 @@ export const ProjectConversationOverviewRoute = () => { }, }); + // Check if conversation has any transcript text using aggregate + const chunksWithTranscriptQuery = useConversationHasTranscript( + conversationId ?? "", + 10000, + !!conversationId && !conversationQuery.data?.summary, + ); + const hasTranscript = (chunksWithTranscriptQuery.data ?? 0) > 0; + const useHandleGenerateSummaryManually = useMutation({ - mutationFn: async () => { - const response = await generateConversationSummary(conversationId ?? ""); - toast.info( - t`The summary is being regenerated. Please wait for the new summary to be available.`, - ); - return response; - }, - onError: () => { - toast.error(t`Failed to regenerate the summary. Please try again later.`); + mutationFn: async (isRegeneration: boolean) => { + const promise = generateConversationSummary(conversationId ?? ""); + + toast.promise(promise, { + error: isRegeneration + ? t`Failed to regenerate the summary. Please try again later.` + : t`Failed to generate the summary. Please try again later.`, + loading: isRegeneration + ? t`Regenerating the summary. Please wait...` + : t`Generating the summary. Please wait...`, + success: (response) => { + // Show different message based on whether summary was generated or is being processed + if ( + response.status === "success" && + "summary" in response && + response.summary + ) { + return isRegeneration + ? t`Summary regenerated successfully.` + : t`Summary generated successfully.`; + } + return isRegeneration + ? t`The summary is being regenerated. Please wait for it to be available.` + : t`The summary is being generated. Please wait for it to be available.`; + }, + }); + + return promise; }, + mutationKey: ["generateSummary", conversationId], onSuccess: () => { conversationQuery.refetch(); }, }); - const clipboard = useClipboard(); + // Check if there's a pending mutation for this conversation across the app + const pendingMutations = useMutationState({ + filters: { + mutationKey: ["generateSummary", conversationId], + status: "pending", + }, + }); + const isMutationPending = pendingMutations.length > 0; - // Determine if summary section should be shown at all - const showSummarySection = - conversationQuery.data?.summary || - (conversationQuery.data?.source && - !conversationQuery.data.source.toLowerCase().includes("upload")); + const clipboard = useClipboard(); return ( {conversationChunksQuery.data && - conversationChunksQuery.data?.length > 0 && - showSummarySection && ( + conversationChunksQuery.data?.length > 0 && ( - {(conversationQuery.data?.summary || - (conversationQuery.data?.source && - !conversationQuery.data.source - .toLowerCase() - .includes("upload"))) && <Trans>Summary</Trans>} + <Trans>Summary</Trans> {conversationQuery.data?.summary && ( @@ -112,10 +139,11 @@ export const ProjectConversationOverviewRoute = () => { window.confirm( t`Are you sure you want to regenerate the summary? You will lose the current summary.`, - ) && useHandleGenerateSummaryManually.mutate() + ) && useHandleGenerateSummaryManually.mutate(true) } > @@ -136,22 +164,30 @@ export const ProjectConversationOverviewRoute = () => { /> {!conversationQuery.isFetching && - !conversationQuery.data?.summary && - conversationQuery.data?.source && - !conversationQuery.data.source - .toLowerCase() - .includes("upload") && ( + !conversationQuery.data?.summary && (
- + +
)} From 4fb10d6394295a5fb5c18636d9b2d8d2c24cee0c Mon Sep 17 00:00:00 2001 From: Usama Date: Fri, 28 Nov 2025 13:55:54 +0000 Subject: [PATCH 3/4] - added to context not showing in chat fixed --- echo/frontend/src/components/chat/ChatHistoryMessage.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/echo/frontend/src/components/chat/ChatHistoryMessage.tsx b/echo/frontend/src/components/chat/ChatHistoryMessage.tsx index 613ed455..5471a0b9 100644 --- a/echo/frontend/src/components/chat/ChatHistoryMessage.tsx +++ b/echo/frontend/src/components/chat/ChatHistoryMessage.tsx @@ -154,12 +154,14 @@ export const ChatHistoryMessage = ({ ); } } - if (message?._original?.added_conversations?.length > 0) { const conversations = message?._original?.added_conversations - .map((ac) => (ac as unknown as ConversationLink).source_conversation_id) + .map((conv: string | ProjectChatMessageConversation1) => + typeof conv === "object" && conv !== null + ? (conv as ProjectChatMessageConversation1).conversation_id + : null, + ) .filter((conv) => conv != null); - return conversations.length > 0 ? ( // biome-ignore lint/a11y/useValidAriaRole: role is a component prop for styling, not an ARIA attribute From ddb2fda20efd249f323ba2a8d410ca070aa5cdcf Mon Sep 17 00:00:00 2001 From: Usama Date: Fri, 28 Nov 2025 14:02:35 +0000 Subject: [PATCH 4/4] - disable auto select globally --- echo/frontend/src/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/echo/frontend/src/config.ts b/echo/frontend/src/config.ts index 51cdea13..774b14dc 100644 --- a/echo/frontend/src/config.ts +++ b/echo/frontend/src/config.ts @@ -42,7 +42,7 @@ export const PLAUSIBLE_API_HOST = export const DEBUG_MODE = import.meta.env.VITE_DEBUG_MODE === "1"; -export const ENABLE_CHAT_AUTO_SELECT = true; +export const ENABLE_CHAT_AUTO_SELECT = false; export const ENABLE_CONVERSATION_HEALTH = true; export const ENABLE_ANNOUNCEMENTS = true; export const ENABLE_DISPLAY_CONVERSATION_LINKS = true;