Skip to content

ECHO-334 Implement biome rules#337

Merged
spashii merged 9 commits intomainfrom
implement-biome-rules
Oct 20, 2025
Merged

ECHO-334 Implement biome rules#337
spashii merged 9 commits intomainfrom
implement-biome-rules

Conversation

@ussaama
Copy link
Copy Markdown
Contributor

@ussaama ussaama commented Oct 17, 2025

Summary by CodeRabbit

  • New Features

    • Announcement error state with retry
    • Improved upload experience: inline file renaming and per-file upload status
  • Bug Fixes

    • Better error handling and recovery for mutations and queries
    • Fixes to conversation duration and related edge cases
  • Improvements

    • Refined routing/navigation structure
    • Improved optimistic updates with rollback
    • Enhanced participant onboarding and UI polish

@linear
Copy link
Copy Markdown

linear bot commented Oct 17, 2025

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Oct 17, 2025

Caution

Review failed

Failed to post review comments

Walkthrough

Removed legacy ESLint/Prettier configs and migrated formatting/linting to Biome; reorganized imports to prefer type-only imports; flattened and rewired frontend routes/layouts; introduced announcement error UI and optimistic announcement hooks; and applied broad refactors and typing/pagination improvements across many components and hooks.

Changes

Cohort / File(s) Summary
Tooling & Config
echo/frontend/.eslintrc, echo/frontend/.prettierrc, echo/frontend/biome.json, echo/frontend/package.json, echo/frontend/postcss.config.js
Deleted legacy ESLint config and removed Prettier Tailwind plugin; added/updated Biome config and scripts; removed ESLint/Prettier deps; adjusted PostCSS plugin order.
Router & Layouts
echo/frontend/src/Router.tsx, echo/frontend/src/components/layout/...
Rewrote router to a flatter route tree (AuthLayout/Protected/LanguageLayout), reordered imports, refactored many layout components and sidebar hook return ordering.
Announcements
echo/frontend/src/components/announcement/...
Added AnnouncementErrorState; refactored announcement hooks for optimistic updates, rollback, queryClient integration; extracted processing utils and adjusted types/formatting.
Chat & Conversation
echo/frontend/src/components/chat/..., echo/frontend/src/components/conversation/..., echo/frontend/src/components/chat/hooks/index.ts, echo/frontend/src/components/conversation/hooks/index.ts
Large refactor of chat/conversation hooks and components: explicit queryKey arrays, pagination (getNextPageParam/initialPageParam), improved error handling/optimistic flows, UI restructuring (ChatHistoryMessage, ChatAccordion, Conversation components).
Common UI & Utilities
echo/frontend/src/components/common/...
Many components switched to type-only React imports, introduced stronger generics for LazyRoute helpers, refactored NavigationButton typing/render, i18nLink prefix guard, Toaster export, TipBanner color mapping and general JSX/formatting cleanups.
Dropzone & Uploads
echo/frontend/src/components/dropzone/...
Reworked UploadConversationDropzone with richer file editing/upload state and useFileNameEditor, added explicit onError handlers and typing refinements.
Auth, Forms & Error Handling
echo/frontend/src/components/auth/..., echo/frontend/src/components/form/..., echo/frontend/src/components/error/...
Standardized auth hooks and mutations, added explicit error handling and toasts, replaced debounced saves with save-on-blur in EditableTextBox, and applied import/formatting updates in error boundary/page.
Participant & Library
echo/frontend/src/components/participant/..., echo/frontend/src/components/library/...
Participant body: consolidated health/streams and message assembly; form uses zodResolver; library hooks and templates formatting/ordering updates.
Miscellaneous formatting/type-only imports
many files under echo/frontend/src/...
Widespread formatting, import reordering, minor API typings changes (mostly parameter ordering/type-only imports), and non-semantic refactors across numerous components and hooks.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Rationale: broad, heterogeneous changes across 100+ frontend files—mix of architectural routing/BIOME/tooling shifts and substantive hook/pagination/mutation refactors requiring cross-file reasoning.

Possibly related PRs

LGTM.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 2 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.36% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Linked Issues Check ❓ Inconclusive The PR successfully addresses most objectives from ECHO-334: biome configuration is properly set up in biome.json with comprehensive linting and formatting rules [issue objectives 1, 3], package.json is updated to run biome check/format/lint/fix scripts, ESLint/Prettier dependencies are removed, and the "any" type prevention is implemented via biome-ignore comments throughout the codebase indicating the noExplicitAny rule is active. However, the second objective requiring CI integration to enforce code quality is not visible in the provided raw summary, making it impossible to verify whether lint rules have been wired into CI workflows to ensure enforcement on commits or PRs. Verify that CI/workflow files (e.g., .github/workflows) have been updated to run biome check as part of the PR validation pipeline. The raw summary provided focuses on frontend source changes but does not show CI configuration modifications. Confirm that lint rule enforcement is integrated into CI as stated in the linked issue objective.
Out of Scope Changes Check ❓ Inconclusive The majority of changes—formatting adjustments, import reordering, type annotation removals, and configuration updates—are in-scope for implementing biome rules and are expected as part of lint rule application. However, several changes appear to exceed typical biome implementation scope: Router.tsx includes a significant architectural refactoring from deeply nested to flattened hierarchical route composition; ParticipantBody.tsx involves substantial functional refactoring of message assembly and rendering logic; and multiple component files receive non-formatting refactorings (e.g., Announcements extracting error state into dedicated component). While these may be beneficial code quality improvements, they extend beyond standard "configure linting tool and apply formatting/lint compliance" work. Clarify whether the architectural and functional refactorings (Router restructuring, ParticipantBody message assembly refactoring, Announcements component extraction) were necessary to achieve biome lint rule compliance (e.g., complexity rules, code organization rules) or if they represent scope creep. If they were necessary for compliance, document the specific lint rules that triggered these changes; if not, consider separating them into a follow-up PR focused on code quality improvements.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "ECHO-334 Implement biome rules" is clear, concise, and directly summarizes the primary change. It accurately reflects the main objective of configuring biome as the linting and formatting tool and applying these rules across the frontend codebase. The title is specific enough that a teammate scanning history would immediately understand this PR is about biome implementation and rule enforcement.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch implement-biome-rules

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

};
// use browser history to navigate to the new language path
// otherwise the language change found to be inconsistent!
window.location.href = `/${selectedLanguage}${newPathname}`;

Check failure

Code scanning / CodeQL

DOM text reinterpreted as HTML High

DOM text
is reinterpreted as HTML without escaping meta-characters.

Copilot Autofix

AI 6 months ago

To fix this issue, we must ensure that the selectedLanguage variable is always constrained to known, safe values before using it to build a URL. An effective way is to check—after reading from e.target.value—whether the value exists in the SUPPORTED_LANGUAGES array. If not, we simply abort the navigation or handle it gracefully. We do not need additional libraries, nor is any escaping required, as we are essentially enforcing a whitelist.

Specifically, in LanguagePicker.tsx within the handleChange function, add a check immediately after assigning selectedLanguage. Only continue if selectedLanguage is present in SUPPORTED_LANGUAGES. This prevents any unexpected or malicious values from being used in redirection.


Suggested changeset 1
echo/frontend/src/components/language/LanguagePicker.tsx

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/echo/frontend/src/components/language/LanguagePicker.tsx b/echo/frontend/src/components/language/LanguagePicker.tsx
--- a/echo/frontend/src/components/language/LanguagePicker.tsx
+++ b/echo/frontend/src/components/language/LanguagePicker.tsx
@@ -60,6 +60,9 @@
 	const handleChange = (e: ChangeEvent<HTMLSelectElement>) => {
 		const selectedLanguage = e.target.value;
 
+		// Ensure selectedLanguage is a supported language to prevent injection
+		if (!SUPPORTED_LANGUAGES.includes(selectedLanguage)) return;
+
 		// If the selected language is the same as the current language, do nothing
 		if (selectedLanguage === currentLanguage) return;
 
EOF
@@ -60,6 +60,9 @@
const handleChange = (e: ChangeEvent<HTMLSelectElement>) => {
const selectedLanguage = e.target.value;

// Ensure selectedLanguage is a supported language to prevent injection
if (!SUPPORTED_LANGUAGES.includes(selectedLanguage)) return;

// If the selected language is the same as the current language, do nothing
if (selectedLanguage === currentLanguage) return;

Copilot is powered by AI and may make mistakes. Always verify output.
@ussaama ussaama requested a review from spashii October 17, 2025 12:40
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 122

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (11)
echo/frontend/src/components/form/UnsavedChanges.tsx (1)

2-2: Nix the dead import (Biome will bark).

Tooltip is unused. Drop it.

-import { Group, Text, Tooltip } from "@mantine/core";
+import { Group, Text } from "@mantine/core";
echo/frontend/package.json (1)

64-66: Malformed dependency entry breaks installs.

The key contains a newline and duplicates Mantine notifications via a pnpm link alias. This will break package.json parsing and cause resolution weirdness.

Apply:

-    "notifications\n": "link:@mantine/notifications\n",

You already have "@mantine/notifications": "^7.17.8" on Line 40. Keep only that. Based on PR objectives.

echo/frontend/src/components/conversation/RetranscribeConversation.tsx (1)

83-99: Invalid Sonner API: action.actionButtonStyle is not supported.

Sonner action accepts label and onClick (and optional altText). actionButtonStyle isn’t a documented prop and will be ignored or fail types.

Use global classNames (already configured in Toaster) or per‑toast style/className on the toast, not on action. Minimal fix:

-					action: {
-						actionButtonStyle: {
-							color: "blue",
-						},
-						label: t`Go to new conversation`,
-						onClick: () => {
+					action: {
+						label: t`Go to new conversation`,
+						onClick: () => {
 							navigate(
 								`/projects/${projectId}/conversation/${new_conversation_id}/transcript`,
 							);
 						},
 					},

If you need per‑toast styling, consider toast.success(msg, { className: "...", action: { label, onClick } }).

echo/frontend/src/components/conversation/ConversationAccordion.tsx (2)

230-239: Infinite scroll hook deps: stable functions in array are fine; optional nit.

If you see redundant re‑fires, wrap fetchNextPage/hasNextPage with refs or rely on query’s isFetchingNextPage; otherwise looks good.


208-214: Effect deps nit.

useEffect depends on form.reset; react-hook-form’s reset is stable, but dependency on the function is unnecessary. Prefer [opened] to avoid surprise re-runs on re-renders.

-	}, [opened, form.reset]);
+	}, [opened]);
echo/frontend/src/components/conversation/hooks/index.ts (5)

128-147: Critical: transient tag validation failure can delete all tags. Abort on validation errors.

If fetching valid tags fails, validTagsIds becomes [], causing needToDelete to include all existing tags and wiping them. Throw and handle via onError instead of defaulting to empty.

-      let validTagsIds: string[] = [];
+      let validTagsIds: string[] = [];
       try {
         const validTags = await directus.request<ProjectTag[]>(
           readItems("project_tag", {
             fields: ["*"],
             filter: {
               id: { _in: projectTagIdList },
               project_id: { _eq: projectId },
             },
           }),
         );
         validTagsIds = validTags.map((tag) => tag.id);
-      } catch (_error) {
-        validTagsIds = [];
+      } catch (error) {
+        Sentry.captureException(error);
+        throw new Error("Failed to validate tags, aborting update");
       }
@@
   await Promise.all([deletePromise, createPromise]);
@@
   return directus.request<Conversation>(readItem("conversation", conversationId, { fields: ["*"] }));
 },
-  onSuccess: (_values, variables) => {
+  onSuccess: (_values, variables) => {
     queryClient.invalidateQueries({ queryKey: ["conversations", variables.conversationId] });
     queryClient.invalidateQueries({ queryKey: ["projects", variables.projectId] });
   },
+  onError: (error) => {
+    Sentry.captureException(error);
+    toast.error(t`Failed to update conversation tags. No changes were applied.`);
+  },

Also applies to: 166-214, 222-230


560-575: LGTM; small nit on queryKey naming consistency.

enabled + gcTime + staleTime are on point. Consider aligning key namespace with others (use "conversations" instead of "conversation") to keep cache taxonomy consistent.

-  queryKey: ["conversation", conversationId, "chunk", chunkId, "audio-url"],
+  queryKey: ["conversations", conversationId, "chunk", chunkId, "audio-url"],

969-987: Return a number from aggregate count.

Directus aggregate returns string counts; coerce to number to keep API consistent.

-      return response[0].count;
+      return Number(response[0].count ?? 0);

Based on learnings.


743-747: Type the chunk in .some(); avoid any.

Define a ConversationChunk shape or reuse your generated type for the lambda param.

- const hasRecentChunks = conversation.chunks?.some((chunk: any) => {
+ const hasRecentChunks = conversation.chunks?.some((chunk: ConversationChunk) => {

Also applies to: 945-951


1-989: Fix remaining any and @ts-expect-error violations in conversation hooks.

The audit found 9+ violations in this file. Prioritize these fixes:

  • Lines 300, 309–310, 315, 319: Replace (context as any) with proper type narrowing or interface extension for mutation context objects.
  • Line 641: Type fields parameter instead of fields as any—use QueryFields<CustomDirectusTypes, ConversationChunk>[].
  • Lines 670, 864: Replace @ts-expect-error with explicit Directus deep query typing or a properly typed helper.
  • Lines 743, 945: Replace (chunk: any) with (chunk: ConversationChunk).
  • Line 832: Replace as any with explicit array field definition.

These are not generated types or Directus SDK limitations—they're opportunities to strengthen the type system and eliminate type-check bypasses.

echo/frontend/src/components/common/DiffViewer.tsx (1)

40-42: Discriminated PlanStep union; drop casts.

Make plan typed; remove as any[] and the ignore.

 type SplitMode = "line" | "sentence";
 
+type PlanStep =
+  | { type: "show"; from: number; to: number }
+  | { type: "gap"; at: number; count: number; key: string };
+
@@
-const plan = useMemo(() => {
+const plan = useMemo<PlanStep[]>(() => {
   if (!isCompact)
     return rows.length
       ? [{ from: 0, to: rows.length - 1, type: "show" }]
       : [];
@@
-  const out: Array<
-    | { type: "show"; from: number; to: number }
-    | { type: "gap"; at: number; count: number; key: string }
-  > = [];
+  const out: PlanStep[] = [];
@@
   return out;
 }, [rows, isCompact, collapseThreshold]);
@@
-// biome-ignore lint/suspicious/noExplicitAny: needs to be fixed
-for (const step of plan as any[]) {
+for (const step of plan) {

Also applies to: 232-270, 343-345

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d3e7349 and 3e89182.

⛔ Files ignored due to path filters (1)
  • echo/frontend/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (107)
  • echo/frontend/.eslintrc (0 hunks)
  • echo/frontend/.prettierrc (0 hunks)
  • echo/frontend/biome.json (1 hunks)
  • echo/frontend/lingui.config.ts (1 hunks)
  • echo/frontend/package.json (1 hunks)
  • echo/frontend/postcss.config.js (1 hunks)
  • echo/frontend/src/App.tsx (1 hunks)
  • echo/frontend/src/Router.tsx (1 hunks)
  • echo/frontend/src/components/announcement/AnnouncementDrawerHeader.tsx (1 hunks)
  • echo/frontend/src/components/announcement/AnnouncementErrorState.tsx (1 hunks)
  • echo/frontend/src/components/announcement/AnnouncementIcon.tsx (1 hunks)
  • echo/frontend/src/components/announcement/AnnouncementItem.tsx (1 hunks)
  • echo/frontend/src/components/announcement/AnnouncementSkeleton.tsx (1 hunks)
  • echo/frontend/src/components/announcement/Announcements.tsx (1 hunks)
  • echo/frontend/src/components/announcement/TopAnnouncementBar.tsx (1 hunks)
  • echo/frontend/src/components/announcement/hooks/index.ts (1 hunks)
  • echo/frontend/src/components/announcement/hooks/useProcessedAnnouncements.ts (1 hunks)
  • echo/frontend/src/components/announcement/utils/dateUtils.ts (1 hunks)
  • echo/frontend/src/components/aspect/AspectCard.tsx (2 hunks)
  • echo/frontend/src/components/aspect/hooks/useCopyAspect.tsx (1 hunks)
  • echo/frontend/src/components/aspect/hooks/useCopyQuote.ts (1 hunks)
  • echo/frontend/src/components/auth/hooks/index.ts (1 hunks)
  • echo/frontend/src/components/auth/utils/errorUtils.ts (1 hunks)
  • echo/frontend/src/components/chat/BaseMessage.tsx (1 hunks)
  • echo/frontend/src/components/chat/ChatAccordion.tsx (2 hunks)
  • echo/frontend/src/components/chat/ChatContextProgress.tsx (1 hunks)
  • echo/frontend/src/components/chat/ChatHistoryMessage.tsx (1 hunks)
  • echo/frontend/src/components/chat/ChatMessage.tsx (1 hunks)
  • echo/frontend/src/components/chat/ChatSkeleton.tsx (1 hunks)
  • echo/frontend/src/components/chat/ChatTemplatesMenu.tsx (2 hunks)
  • echo/frontend/src/components/chat/References.tsx (1 hunks)
  • echo/frontend/src/components/chat/Sources.tsx (1 hunks)
  • echo/frontend/src/components/chat/SourcesSearch.tsx (1 hunks)
  • echo/frontend/src/components/chat/SourcesSearched.tsx (1 hunks)
  • echo/frontend/src/components/chat/TemplatesModal.tsx (1 hunks)
  • echo/frontend/src/components/chat/chatUtils.ts (1 hunks)
  • echo/frontend/src/components/chat/hooks/index.ts (1 hunks)
  • echo/frontend/src/components/chat/templates.ts (5 hunks)
  • echo/frontend/src/components/common/BaseSkeleton.tsx (1 hunks)
  • echo/frontend/src/components/common/Breadcrumbs.tsx (1 hunks)
  • echo/frontend/src/components/common/ClosableAlert.tsx (1 hunks)
  • echo/frontend/src/components/common/ConnectionHealthStatus.tsx (1 hunks)
  • echo/frontend/src/components/common/CopyIconButton.tsx (1 hunks)
  • echo/frontend/src/components/common/CopyRichTextIconButton.tsx (1 hunks)
  • echo/frontend/src/components/common/DembraneLoadingSpinner/DembraneLoading.css (1 hunks)
  • echo/frontend/src/components/common/DembraneLoadingSpinner/index.tsx (1 hunks)
  • echo/frontend/src/components/common/DiffViewer.tsx (2 hunks)
  • echo/frontend/src/components/common/Drawer.tsx (1 hunks)
  • echo/frontend/src/components/common/DynamicLucideIcon.tsx (1 hunks)
  • echo/frontend/src/components/common/ExponentialProgress.tsx (1 hunks)
  • echo/frontend/src/components/common/InformationTooltip.tsx (1 hunks)
  • echo/frontend/src/components/common/LazyRoute.tsx (1 hunks)
  • echo/frontend/src/components/common/LoadingSpinner.tsx (1 hunks)
  • echo/frontend/src/components/common/Logo.tsx (1 hunks)
  • echo/frontend/src/components/common/Markdown.tsx (1 hunks)
  • echo/frontend/src/components/common/NavigationButton.tsx (1 hunks)
  • echo/frontend/src/components/common/Protected.tsx (1 hunks)
  • echo/frontend/src/components/common/QRCode.tsx (1 hunks)
  • echo/frontend/src/components/common/ReferencesIconButton.tsx (1 hunks)
  • echo/frontend/src/components/common/ScrollToBottom.tsx (1 hunks)
  • echo/frontend/src/components/common/SummaryCard.tsx (1 hunks)
  • echo/frontend/src/components/common/TipBanner.tsx (1 hunks)
  • echo/frontend/src/components/common/Toaster.tsx (1 hunks)
  • echo/frontend/src/components/common/i18nLink.tsx (1 hunks)
  • echo/frontend/src/components/conversation/AutoSelectConversations.tsx (1 hunks)
  • echo/frontend/src/components/conversation/ConversationAccordion.tsx (7 hunks)
  • echo/frontend/src/components/conversation/ConversationDangerZone.tsx (1 hunks)
  • echo/frontend/src/components/conversation/ConversationEdit.tsx (1 hunks)
  • echo/frontend/src/components/conversation/ConversationLink.tsx (2 hunks)
  • echo/frontend/src/components/conversation/ConversationLinks.tsx (1 hunks)
  • echo/frontend/src/components/conversation/MoveConversationButton.tsx (1 hunks)
  • echo/frontend/src/components/conversation/OngoingConversationsSummaryCard.tsx (1 hunks)
  • echo/frontend/src/components/conversation/OpenForParticipationSummaryCard.tsx (1 hunks)
  • echo/frontend/src/components/conversation/RetranscribeConversation.tsx (1 hunks)
  • echo/frontend/src/components/conversation/hooks/index.ts (25 hunks)
  • echo/frontend/src/components/dropzone/Dropzone.tsx (1 hunks)
  • echo/frontend/src/components/dropzone/UploadConversationDropzone.tsx (2 hunks)
  • echo/frontend/src/components/dropzone/hooks/index.ts (1 hunks)
  • echo/frontend/src/components/error/ErrorBoundary.tsx (1 hunks)
  • echo/frontend/src/components/error/ErrorPage.tsx (1 hunks)
  • echo/frontend/src/components/form/EditableTextBox.tsx (1 hunks)
  • echo/frontend/src/components/form/FormLabel.tsx (1 hunks)
  • echo/frontend/src/components/form/MarkdownWYSIWYG/MarkdownWYSIWYG.tsx (1 hunks)
  • echo/frontend/src/components/form/SaveStatus.tsx (1 hunks)
  • echo/frontend/src/components/form/UnsavedChanges.tsx (1 hunks)
  • echo/frontend/src/components/insight/Insight.tsx (1 hunks)
  • echo/frontend/src/components/language/LanguagePicker.tsx (1 hunks)
  • echo/frontend/src/components/layout/AuthLayout.tsx (1 hunks)
  • echo/frontend/src/components/layout/BaseLayout.tsx (1 hunks)
  • echo/frontend/src/components/layout/Footer.tsx (1 hunks)
  • echo/frontend/src/components/layout/Header.tsx (1 hunks)
  • echo/frontend/src/components/layout/I18nProvider.tsx (1 hunks)
  • echo/frontend/src/components/layout/LanguageLayout.tsx (1 hunks)
  • echo/frontend/src/components/layout/ParticipantLayout.tsx (1 hunks)
  • echo/frontend/src/components/layout/ProjectConversationLayout.tsx (1 hunks)
  • echo/frontend/src/components/layout/ProjectLayout.tsx (1 hunks)
  • echo/frontend/src/components/layout/ProjectLibraryLayout.tsx (1 hunks)
  • echo/frontend/src/components/layout/ProjectOverviewLayout.tsx (1 hunks)
  • echo/frontend/src/components/layout/TabsWithRouter.tsx (1 hunks)
  • echo/frontend/src/components/layout/hooks/useSidebar.tsx (1 hunks)
  • echo/frontend/src/components/library/LibraryTemplatesMenu.tsx (1 hunks)
  • echo/frontend/src/components/library/hooks/index.ts (1 hunks)
  • echo/frontend/src/components/participant/EchoErrorAlert.tsx (1 hunks)
  • echo/frontend/src/components/participant/MicrophoneTest.tsx (1 hunks)
  • echo/frontend/src/components/participant/ParticipantBody.tsx (1 hunks)
  • echo/frontend/src/components/participant/ParticipantInitiateForm.tsx (1 hunks)
  • echo/frontend/src/components/participant/ParticipantOnboardingCards.css (1 hunks)
💤 Files with no reviewable changes (2)
  • echo/frontend/.eslintrc
  • echo/frontend/.prettierrc
🧰 Additional context used
🧠 Learnings (8)
📚 Learning: 2025-09-16T08:34:38.109Z
Learnt from: ussaama
PR: Dembrane/echo#293
File: echo/frontend/src/components/chat/ChatTemplatesMenu.tsx:15-16
Timestamp: 2025-09-16T08:34:38.109Z
Learning: In ChatTemplatesMenu.tsx, titles are preferred over IDs for template selection logic since titles are unique one-liners and work effectively as identifiers. IDs were added primarily for better list rendering rather than selection purposes.

Applied to files:

  • echo/frontend/src/components/chat/ChatTemplatesMenu.tsx
📚 Learning: 2025-08-19T10:22:55.323Z
Learnt from: ussaama
PR: Dembrane/echo#266
File: echo/frontend/src/components/conversation/ConversationAccordion.tsx:675-678
Timestamp: 2025-08-19T10:22:55.323Z
Learning: In echo/frontend/src/components/conversation/hooks/index.ts, the useConversationsCountByProjectId hook uses useSuspenseQuery with Directus aggregate, which always returns string numbers like "0", "1", "2" and suspends during loading instead of returning undefined. Therefore, Number(conversationsCountQuery.data) ?? 0 is safe and the Number() conversion is necessary for type conversion from string to number.

Applied to files:

  • echo/frontend/src/components/chat/hooks/index.ts
  • echo/frontend/src/components/conversation/hooks/index.ts
📚 Learning: 2025-09-16T08:35:18.796Z
Learnt from: ussaama
PR: Dembrane/echo#293
File: echo/frontend/src/components/chat/TemplatesModal.tsx:34-41
Timestamp: 2025-09-16T08:35:18.796Z
Learning: In TemplatesModal.tsx, the user confirmed that titles should be used as template keys rather than IDs, consistent with their established pattern. IDs were added primarily for list rendering, not for selection logic.

Applied to files:

  • echo/frontend/src/components/chat/TemplatesModal.tsx
📚 Learning: 2025-08-08T10:39:31.114Z
Learnt from: ussaama
PR: Dembrane/echo#259
File: echo/frontend/src/components/layout/ParticipantLayout.tsx:33-33
Timestamp: 2025-08-08T10:39:31.114Z
Learning: In echo/frontend/src/components/layout/ParticipantLayout.tsx, prefer using simple pathname.includes("start") and pathname.includes("finish") to control the settings button visibility. No need to switch to segment-based matching or add a useEffect to auto-close the modal for these routes, per ussaama’s preference in PR #259.

Applied to files:

  • echo/frontend/src/components/layout/ParticipantLayout.tsx
📚 Learning: 2025-08-06T13:38:30.769Z
Learnt from: ussaama
PR: Dembrane/echo#256
File: echo/frontend/src/components/participant/MicrophoneTest.tsx:54-54
Timestamp: 2025-08-06T13:38:30.769Z
Learning: In echo/frontend/src/components/participant/MicrophoneTest.tsx, the useDisclosure hook is used where only the `close` function is needed locally (called in handleConfirmMicChange and handleCancelMicChange), while `opened` and `open` are unused because the modal state is managed by a parent component. The `showSecondModal` state variable is used separately to control content switching within the modal between the main UI and confirmation step.

Applied to files:

  • echo/frontend/src/components/participant/MicrophoneTest.tsx
📚 Learning: 2025-06-16T11:51:33.799Z
Learnt from: ussaama
PR: Dembrane/echo#185
File: echo/frontend/src/App.tsx:25-36
Timestamp: 2025-06-16T11:51:33.799Z
Learning: In echo/frontend/src/App.tsx, user ussaama prefers explicit code over terse/inline code for React useEffect cleanup functions, choosing clarity and future maintainability over brevity when assigning cleanup functions to variables before returning them.

Applied to files:

  • echo/frontend/src/App.tsx
📚 Learning: 2025-08-07T10:19:42.859Z
Learnt from: spashii
PR: Dembrane/echo#257
File: echo/frontend/src/App.tsx:14-14
Timestamp: 2025-08-07T10:19:42.859Z
Learning: React Query devtools (tanstack/react-query-devtools) are automatically excluded from production bundles when process.env.NODE_ENV !== 'development' through tree-shaking and dead code elimination. Manual environment checks or conditional imports are unnecessary complexity.

Applied to files:

  • echo/frontend/src/App.tsx
📚 Learning: 2025-08-07T10:19:42.859Z
Learnt from: spashii
PR: Dembrane/echo#257
File: echo/frontend/src/App.tsx:14-14
Timestamp: 2025-08-07T10:19:42.859Z
Learning: React Query devtools (tanstack/react-query-devtools) are automatically excluded from production bundles when process.env.NODE_ENV !== 'development', so manual gating with environment checks is unnecessary.

Applied to files:

  • echo/frontend/src/App.tsx
🧬 Code graph analysis (69)
echo/frontend/src/components/layout/LanguageLayout.tsx (1)
echo/frontend/src/components/layout/I18nProvider.tsx (1)
  • I18nProvider (6-14)
echo/frontend/src/components/common/Protected.tsx (1)
echo/frontend/src/components/auth/hooks/index.ts (1)
  • useAuthenticated (186-218)
echo/frontend/src/components/conversation/ConversationLinks.tsx (2)
echo/server/dembrane/reply_utils.py (1)
  • Conversation (29-33)
echo/frontend/src/components/common/i18nLink.tsx (1)
  • I18nLink (6-29)
echo/frontend/src/components/aspect/hooks/useCopyQuote.ts (1)
echo/frontend/src/lib/directus.ts (1)
  • directus (6-14)
echo/frontend/src/Router.tsx (18)
echo/frontend/src/components/common/LazyRoute.tsx (2)
  • createLazyNamedRoute (44-61)
  • createLazyRoute (28-41)
echo/frontend/src/routes/participant/ParticipantReport.tsx (1)
  • ParticipantReport (13-78)
echo/frontend/src/routes/project/unsubscribe/ProjectUnsubscribe.tsx (1)
  • ProjectUnsubscribe (10-90)
echo/frontend/src/routes/Debug.tsx (1)
  • DebugPage (172-314)
echo/frontend/src/components/layout/AuthLayout.tsx (1)
  • AuthLayout (10-38)
echo/frontend/src/routes/project/ProjectRoutes.tsx (2)
  • ProjectSettingsRoute (13-57)
  • ProjectPortalSettingsRoute (59-89)
echo/frontend/src/components/layout/ProjectOverviewLayout.tsx (1)
  • ProjectOverviewLayout (11-40)
echo/frontend/src/components/layout/ProjectConversationLayout.tsx (1)
  • ProjectConversationLayout (11-52)
echo/frontend/src/components/layout/ProjectLibraryLayout.tsx (1)
  • ProjectLibraryLayout (5-8)
echo/frontend/src/components/layout/ProjectLayout.tsx (1)
  • ProjectLayout (10-80)
echo/frontend/src/components/common/Protected.tsx (1)
  • Protected (5-23)
echo/frontend/src/components/layout/BaseLayout.tsx (1)
  • BaseLayout (8-25)
echo/frontend/src/components/error/ErrorPage.tsx (1)
  • ErrorPage (6-37)
echo/frontend/src/components/layout/LanguageLayout.tsx (1)
  • LanguageLayout (4-10)
echo/frontend/src/routes/participant/ParticipantStart.tsx (1)
  • ParticipantStartRoute (105-157)
echo/frontend/src/routes/participant/ParticipantConversation.tsx (2)
  • ParticipantConversationAudioRoute (59-720)
  • ParticipantConversationTextRoute (722-930)
echo/frontend/src/routes/participant/ParticipantPostConversation.tsx (1)
  • ParticipantPostConversation (27-264)
echo/frontend/src/components/layout/ParticipantLayout.tsx (1)
  • ParticipantLayout (28-72)
echo/frontend/src/components/common/Markdown.tsx (1)
echo/frontend/src/lib/utils.ts (1)
  • cn (4-6)
echo/frontend/src/components/layout/Footer.tsx (1)
echo/frontend/src/config.ts (1)
  • PRIVACY_POLICY_URL (37-38)
echo/frontend/src/components/aspect/hooks/useCopyAspect.tsx (1)
echo/frontend/src/lib/directus.ts (1)
  • directus (6-14)
echo/frontend/src/components/chat/ChatTemplatesMenu.tsx (2)
echo/frontend/src/components/chat/templates.ts (2)
  • quickAccessTemplates (78-78)
  • Templates (11-76)
echo/frontend/src/components/chat/TemplatesModal.tsx (1)
  • TemplatesModal (26-134)
echo/frontend/src/components/error/ErrorPage.tsx (2)
echo/frontend/src/components/layout/BaseLayout.tsx (1)
  • BaseLayout (8-25)
echo/frontend/src/config.ts (1)
  • DEBUG_MODE (46-46)
echo/frontend/src/components/conversation/MoveConversationButton.tsx (5)
echo/frontend/src/components/conversation/ConversationAccordion.tsx (1)
  • MoveConversationButton (172-345)
echo/frontend/src/components/project/hooks/index.ts (1)
  • useInfiniteProjects (215-249)
echo/frontend/src/components/conversation/hooks/index.ts (1)
  • useMoveConversationMutation (252-281)
echo/frontend/src/hooks/useI18nNavigate.ts (1)
  • useI18nNavigate (10-38)
echo/frontend/src/components/form/FormLabel.tsx (1)
  • FormLabel (11-30)
echo/frontend/src/components/common/ScrollToBottom.tsx (1)
echo/frontend/src/lib/utils.ts (1)
  • scrollToBottom (12-16)
echo/frontend/src/components/announcement/utils/dateUtils.ts (1)
echo/frontend/src/hooks/useLanguage.ts (1)
  • useLanguage (24-45)
echo/frontend/src/components/common/QRCode.tsx (1)
echo/frontend/src/components/common/Logo.tsx (1)
  • CURRENT_BRAND (55-55)
echo/frontend/src/components/announcement/hooks/useProcessedAnnouncements.ts (1)
echo/frontend/src/lib/typesDirectusContent.ts (2)
  • Announcement (1101-1109)
  • AnnouncementTranslations (1111-1116)
echo/frontend/src/components/announcement/AnnouncementDrawerHeader.tsx (1)
echo/frontend/src/components/announcement/hooks/index.ts (1)
  • useUnreadAnnouncements (426-490)
echo/frontend/src/components/form/UnsavedChanges.tsx (1)
echo/frontend/src/components/common/LoadingSpinner.tsx (1)
  • LoadingSpinner (8-10)
echo/frontend/src/components/layout/I18nProvider.tsx (1)
echo/frontend/src/hooks/useLanguage.ts (1)
  • useLanguage (24-45)
echo/frontend/src/components/common/DembraneLoadingSpinner/index.tsx (1)
echo/frontend/src/lib/utils.ts (1)
  • cn (4-6)
echo/frontend/src/components/layout/BaseLayout.tsx (2)
echo/frontend/src/components/layout/Header.tsx (1)
  • Header (72-187)
echo/frontend/src/components/error/ErrorBoundary.tsx (1)
  • ErrorBoundary (13-48)
echo/frontend/src/components/dropzone/UploadConversationDropzone.tsx (3)
echo/frontend/src/components/dropzone/hooks/index.ts (1)
  • useConversationUploader (62-216)
echo/frontend/src/components/project/hooks/index.ts (1)
  • useProjectById (251-276)
echo/frontend/src/components/dropzone/Dropzone.tsx (1)
  • CommonDropzone (20-58)
echo/frontend/src/components/announcement/hooks/index.ts (3)
echo/frontend/src/components/auth/hooks/index.ts (1)
  • useCurrentUser (17-27)
echo/frontend/src/lib/directus.ts (1)
  • directus (6-14)
echo/frontend/src/lib/typesDirectusContent.ts (2)
  • CustomDirectusTypes (1127-1234)
  • Announcement (1101-1109)
echo/frontend/src/components/conversation/ConversationDangerZone.tsx (3)
echo/frontend/src/components/conversation/hooks/index.ts (1)
  • useDeleteConversationByIdMutation (233-250)
echo/frontend/src/hooks/useI18nNavigate.ts (1)
  • useI18nNavigate (10-38)
echo/frontend/src/lib/api.ts (1)
  • getConversationContentLink (877-883)
echo/frontend/src/components/library/hooks/index.ts (2)
echo/frontend/src/lib/api.ts (2)
  • getProjectViews (198-218)
  • generateProjectLibrary (892-899)
echo/frontend/src/lib/directus.ts (1)
  • directus (6-14)
echo/frontend/src/components/common/i18nLink.tsx (2)
echo/frontend/src/hooks/useLanguage.ts (1)
  • useLanguage (24-45)
echo/frontend/src/config.ts (1)
  • SUPPORTED_LANGUAGES (29-35)
echo/frontend/src/components/common/Logo.tsx (1)
echo/frontend/src/lib/utils.ts (1)
  • cn (4-6)
echo/frontend/src/components/participant/ParticipantInitiateForm.tsx (2)
echo/frontend/src/hooks/useI18nNavigate.ts (1)
  • useI18nNavigate (10-38)
echo/frontend/src/components/participant/hooks/index.ts (1)
  • useInitiateConversationMutation (243-253)
echo/frontend/src/components/chat/ChatContextProgress.tsx (3)
echo/frontend/src/components/chat/hooks/index.ts (1)
  • useProjectChatContext (111-117)
echo/frontend/src/config.ts (1)
  • ENABLE_CHAT_AUTO_SELECT (40-41)
echo/frontend/src/lib/utils.ts (1)
  • capitalize (8-10)
echo/frontend/src/components/chat/hooks/index.ts (2)
echo/frontend/src/lib/api.ts (3)
  • getChatHistory (975-1014)
  • lockConversations (969-973)
  • getProjectChatContext (936-938)
echo/frontend/src/lib/directus.ts (1)
  • directus (6-14)
echo/frontend/src/components/conversation/OngoingConversationsSummaryCard.tsx (2)
echo/frontend/src/lib/directus.ts (1)
  • directus (6-14)
echo/frontend/src/components/common/SummaryCard.tsx (1)
  • SummaryCard (11-24)
echo/frontend/src/components/chat/TemplatesModal.tsx (1)
echo/frontend/src/components/chat/templates.ts (2)
  • Templates (11-76)
  • Template (4-9)
echo/frontend/src/components/common/NavigationButton.tsx (3)
echo/frontend/src/components/common/LoadingSpinner.tsx (1)
  • LoadingSpinner (8-10)
echo/frontend/src/lib/utils.ts (1)
  • cn (4-6)
echo/frontend/src/components/common/i18nLink.tsx (1)
  • I18nLink (6-29)
echo/frontend/src/components/chat/chatUtils.ts (1)
echo/frontend/src/components/announcement/utils/dateUtils.ts (1)
  • formatDate (16-30)
echo/frontend/src/components/conversation/ConversationAccordion.tsx (1)
echo/frontend/src/components/project/hooks/index.ts (1)
  • useProjectById (251-276)
echo/frontend/src/components/chat/Sources.tsx (1)
echo/frontend/src/components/common/i18nLink.tsx (1)
  • I18nLink (6-29)
echo/frontend/src/components/common/DiffViewer.tsx (1)
echo/frontend/src/lib/utils.ts (1)
  • cn (4-6)
echo/frontend/src/components/layout/ParticipantLayout.tsx (3)
echo/frontend/src/components/common/Logo.tsx (1)
  • Logo (57-63)
echo/frontend/src/components/layout/I18nProvider.tsx (1)
  • I18nProvider (6-14)
echo/frontend/src/components/participant/ParticipantSettingsModal.tsx (1)
  • ParticipantSettingsModal (11-39)
echo/frontend/src/components/chat/ChatSkeleton.tsx (2)
echo/frontend/src/components/common/LoadingSpinner.tsx (1)
  • LoadingSpinner (8-10)
echo/frontend/src/components/common/BaseSkeleton.tsx (1)
  • BaseSkeleton (11-26)
echo/frontend/src/components/form/FormLabel.tsx (1)
echo/frontend/src/lib/utils.ts (1)
  • cn (4-6)
echo/frontend/src/components/form/SaveStatus.tsx (1)
echo/frontend/src/components/common/LoadingSpinner.tsx (1)
  • LoadingSpinner (8-10)
echo/frontend/src/components/chat/ChatAccordion.tsx (4)
echo/frontend/src/components/chat/hooks/index.ts (4)
  • useDeleteChatMutation (64-79)
  • useUpdateChatMutation (81-109)
  • useInfiniteProjectChats (158-197)
  • useProjectChatsCount (199-224)
echo/frontend/src/hooks/useI18nNavigate.ts (1)
  • useI18nNavigate (10-38)
echo/frontend/src/components/common/NavigationButton.tsx (1)
  • NavigationButton (27-118)
echo/frontend/src/components/chat/ChatSkeleton.tsx (1)
  • ChatSkeleton (6-23)
echo/frontend/src/components/conversation/OpenForParticipationSummaryCard.tsx (2)
echo/frontend/src/components/project/hooks/index.ts (2)
  • useProjectById (251-276)
  • useUpdateProjectByIdMutation (184-196)
echo/frontend/src/components/common/SummaryCard.tsx (1)
  • SummaryCard (11-24)
echo/frontend/src/components/conversation/hooks/index.ts (2)
echo/frontend/src/lib/api.ts (1)
  • getConversationContentLink (877-883)
echo/frontend/src/lib/directus.ts (1)
  • directus (6-14)
echo/frontend/src/components/layout/Header.tsx (7)
echo/frontend/src/components/auth/hooks/index.ts (3)
  • useLogoutMutation (155-184)
  • useAuthenticated (186-218)
  • useCurrentUser (17-27)
echo/frontend/src/config.ts (1)
  • ENABLE_ANNOUNCEMENTS (51-52)
echo/frontend/src/components/announcement/TopAnnouncementBar.tsx (1)
  • TopAnnouncementBar (16-114)
echo/frontend/src/components/common/i18nLink.tsx (1)
  • I18nLink (6-29)
echo/frontend/src/components/common/Logo.tsx (1)
  • Logo (57-63)
echo/frontend/src/components/announcement/AnnouncementIcon.tsx (1)
  • AnnouncementIcon (9-70)
echo/frontend/src/components/language/LanguagePicker.tsx (1)
  • LanguagePicker (56-100)
echo/frontend/src/components/insight/Insight.tsx (1)
echo/frontend/src/components/common/i18nLink.tsx (1)
  • I18nLink (6-29)
echo/frontend/src/components/announcement/Announcements.tsx (9)
echo/frontend/src/components/announcement/hooks/index.ts (4)
  • useAnnouncementDrawer (492-518)
  • useMarkAsReadMutation (167-283)
  • useMarkAllAsReadMutation (285-424)
  • useInfiniteAnnouncements (80-165)
echo/frontend/src/hooks/useLanguage.ts (1)
  • useLanguage (24-45)
echo/frontend/src/lib/typesDirectusContent.ts (1)
  • Announcement (1101-1109)
echo/frontend/src/components/announcement/hooks/useProcessedAnnouncements.ts (1)
  • useProcessedAnnouncements (22-41)
echo/frontend/src/components/common/Drawer.tsx (1)
  • Drawer (14-35)
echo/frontend/src/components/announcement/AnnouncementDrawerHeader.tsx (1)
  • AnnouncementDrawerHeader (7-57)
echo/frontend/src/components/announcement/AnnouncementErrorState.tsx (1)
  • AnnouncementErrorState (10-40)
echo/frontend/src/components/announcement/AnnouncementSkeleton.tsx (1)
  • AnnouncementSkeleton (4-67)
echo/frontend/src/components/announcement/AnnouncementItem.tsx (1)
  • AnnouncementItem (37-158)
echo/frontend/src/components/common/LazyRoute.tsx (1)
echo/frontend/src/components/error/ErrorBoundary.tsx (1)
  • ErrorBoundary (13-48)
echo/frontend/src/components/layout/ProjectOverviewLayout.tsx (2)
echo/frontend/src/components/project/hooks/index.ts (1)
  • useProjectById (251-276)
echo/frontend/src/components/layout/TabsWithRouter.tsx (1)
  • TabsWithRouter (24-75)
echo/frontend/src/components/announcement/AnnouncementIcon.tsx (5)
echo/frontend/src/components/announcement/hooks/index.ts (3)
  • useAnnouncementDrawer (492-518)
  • useLatestAnnouncement (16-78)
  • useUnreadAnnouncements (426-490)
echo/frontend/src/hooks/useLanguage.ts (1)
  • useLanguage (24-45)
echo/frontend/src/components/announcement/hooks/useProcessedAnnouncements.ts (1)
  • getTranslatedContent (3-19)
echo/frontend/src/lib/typesDirectusContent.ts (2)
  • Announcement (1101-1109)
  • AnnouncementActivity (1118-1125)
echo/frontend/src/components/common/Markdown.tsx (1)
  • Markdown (6-39)
echo/frontend/src/components/aspect/AspectCard.tsx (3)
echo/frontend/src/components/project/hooks/index.ts (1)
  • useProjectById (251-276)
echo/frontend/src/components/common/i18nLink.tsx (1)
  • I18nLink (6-29)
echo/frontend/src/lib/utils.ts (2)
  • cn (4-6)
  • sanitizeImageUrl (36-42)
echo/frontend/src/components/announcement/AnnouncementItem.tsx (3)
echo/frontend/src/lib/typesDirectusContent.ts (1)
  • Announcement (1101-1109)
echo/frontend/src/components/announcement/utils/dateUtils.ts (2)
  • formatDate (16-30)
  • useFormatDate (32-38)
echo/frontend/src/components/common/Markdown.tsx (1)
  • Markdown (6-39)
echo/frontend/src/components/conversation/ConversationLink.tsx (1)
echo/frontend/src/components/common/i18nLink.tsx (1)
  • I18nLink (6-29)
echo/frontend/src/components/announcement/TopAnnouncementBar.tsx (5)
echo/frontend/src/components/announcement/hooks/index.ts (3)
  • useLatestAnnouncement (16-78)
  • useMarkAsReadMutation (167-283)
  • useAnnouncementDrawer (492-518)
echo/frontend/src/hooks/useLanguage.ts (1)
  • useLanguage (24-45)
echo/frontend/src/lib/typesDirectusContent.ts (2)
  • AnnouncementActivity (1118-1125)
  • Announcement (1101-1109)
echo/frontend/src/components/announcement/hooks/useProcessedAnnouncements.ts (1)
  • getTranslatedContent (3-19)
echo/frontend/src/components/common/Markdown.tsx (1)
  • Markdown (6-39)
echo/frontend/src/components/layout/ProjectLayout.tsx (3)
echo/frontend/src/components/layout/hooks/useSidebar.tsx (1)
  • useSidebar (3-29)
echo/frontend/src/lib/utils.ts (1)
  • cn (4-6)
echo/frontend/src/components/project/ProjectSidebar.tsx (1)
  • ProjectSidebar (25-144)
echo/frontend/src/App.tsx (5)
echo/frontend/src/config.ts (1)
  • USE_PARTICIPANT_ROUTER (1-2)
echo/frontend/src/Router.tsx (2)
  • participantRouter (258-298)
  • mainRouter (93-256)
echo/frontend/src/lib/analytics.ts (1)
  • analytics (10-13)
echo/frontend/src/theme.tsx (1)
  • theme (5-251)
echo/frontend/src/components/layout/I18nProvider.tsx (1)
  • I18nProvider (6-14)
echo/frontend/src/components/form/EditableTextBox.tsx (1)
echo/frontend/src/lib/utils.ts (1)
  • cn (4-6)
echo/frontend/src/components/conversation/AutoSelectConversations.tsx (5)
echo/frontend/src/components/project/hooks/index.ts (1)
  • useProjectById (251-276)
echo/frontend/src/components/chat/hooks/index.ts (1)
  • useProjectChatContext (111-117)
echo/frontend/src/components/conversation/hooks/index.ts (2)
  • useAddChatContextMutation (283-413)
  • useDeleteChatContextMutation (415-555)
echo/frontend/src/lib/analytics.ts (1)
  • analytics (10-13)
echo/frontend/src/lib/links.ts (1)
  • SalesLinks (1-3)
echo/frontend/src/components/layout/ProjectConversationLayout.tsx (3)
echo/frontend/src/components/conversation/hooks/index.ts (2)
  • useConversationById (787-840)
  • CONVERSATION_FIELDS_WITHOUT_PROCESSING_STATUS (758-785)
echo/frontend/src/components/conversation/ConversationAccordion.tsx (1)
  • ConversationStatusIndicators (347-438)
echo/frontend/src/components/layout/TabsWithRouter.tsx (1)
  • TabsWithRouter (24-75)
echo/frontend/src/components/layout/AuthLayout.tsx (4)
echo/frontend/src/hooks/useI18nNavigate.ts (1)
  • useI18nNavigate (10-38)
echo/frontend/src/components/auth/hooks/index.ts (1)
  • useAuthenticated (186-218)
echo/frontend/src/components/layout/Footer.tsx (1)
  • Footer (5-16)
echo/frontend/src/components/common/Toaster.tsx (1)
  • Toaster (34-34)
echo/frontend/src/components/language/LanguagePicker.tsx (2)
echo/frontend/src/config.ts (1)
  • SUPPORTED_LANGUAGES (29-35)
echo/frontend/src/hooks/useLanguage.ts (1)
  • useLanguage (24-45)
echo/frontend/src/components/chat/References.tsx (1)
echo/frontend/src/components/common/i18nLink.tsx (1)
  • I18nLink (6-29)
echo/frontend/src/components/auth/hooks/index.ts (5)
echo/frontend/src/lib/directus.ts (1)
  • directus (6-14)
echo/frontend/src/hooks/useI18nNavigate.ts (1)
  • useI18nNavigate (10-38)
echo/frontend/src/components/auth/utils/errorUtils.ts (1)
  • throwWithMessage (2-24)
echo/frontend/src/components/common/Toaster.tsx (1)
  • toast (34-34)
echo/frontend/src/config.ts (1)
  • ADMIN_BASE_URL (3-4)
echo/frontend/src/components/conversation/ConversationEdit.tsx (2)
echo/frontend/src/components/conversation/hooks/index.ts (2)
  • useUpdateConversationByIdMutation (86-112)
  • useUpdateConversationTagsMutation (115-231)
echo/frontend/src/hooks/useAutoSave.ts (1)
  • useAutoSave (5-63)
echo/frontend/src/components/common/Breadcrumbs.tsx (1)
echo/frontend/src/components/common/i18nLink.tsx (1)
  • I18nLink (6-29)
echo/frontend/src/components/layout/TabsWithRouter.tsx (1)
echo/frontend/src/hooks/useI18nNavigate.ts (1)
  • useI18nNavigate (10-38)
echo/frontend/src/components/participant/ParticipantBody.tsx (6)
echo/frontend/src/components/participant/hooks/index.ts (4)
  • useParticipantProjectById (279-284)
  • useConversationChunksQuery (348-358)
  • useConversationRepliesQuery (308-324)
  • combineUserChunks (295-306)
echo/frontend/src/hooks/useOnlineStatus.ts (1)
  • useOnlineStatus (3-20)
echo/frontend/src/components/participant/hooks/useConversationsHealthStream.ts (1)
  • useConversationsHealthStream (10-79)
echo/frontend/src/components/participant/hooks/useConversationIssueBanner.ts (1)
  • useConversationIssueBanner (4-30)
echo/frontend/src/components/common/ConnectionHealthStatus.tsx (1)
  • ConnectionHealthStatus (10-35)
echo/frontend/src/components/common/TipBanner.tsx (1)
  • TipBanner (12-91)
echo/frontend/src/components/dropzone/hooks/index.ts (2)
echo/frontend/src/lib/api.ts (2)
  • initiateAndUploadConversationChunk (709-840)
  • uploadResourceByProjectId (157-177)
echo/frontend/src/components/common/Toaster.tsx (1)
  • toast (34-34)
echo/frontend/src/components/chat/ChatHistoryMessage.tsx (11)
echo/frontend/src/components/chat/chatUtils.ts (1)
  • extractMessageMetadata (25-52)
echo/frontend/src/config.ts (1)
  • ENABLE_CHAT_AUTO_SELECT (40-41)
echo/frontend/src/components/chat/Sources.tsx (1)
  • Sources (5-45)
echo/frontend/src/components/chat/ChatMessage.tsx (1)
  • ChatMessage (11-41)
echo/frontend/src/lib/utils.ts (1)
  • cn (4-6)
echo/frontend/src/components/announcement/utils/dateUtils.ts (1)
  • formatDate (16-30)
echo/frontend/src/components/common/CopyRichTextIconButton.tsx (1)
  • CopyRichTextIconButton (5-25)
echo/frontend/src/components/common/ReferencesIconButton.tsx (1)
  • ReferencesIconButton (5-30)
echo/frontend/src/components/common/Markdown.tsx (1)
  • Markdown (6-39)
echo/frontend/src/components/chat/References.tsx (1)
  • References (5-57)
echo/frontend/src/components/conversation/ConversationLinks.tsx (1)
  • ConversationLinks (5-29)
echo/frontend/src/components/chat/ChatMessage.tsx (1)
echo/frontend/src/lib/utils.ts (1)
  • cn (4-6)
🪛 GitHub Check: CodeQL
echo/frontend/src/components/language/LanguagePicker.tsx

[warning] 90-90: DOM text reinterpreted as HTML
DOM text is reinterpreted as HTML without escaping meta-characters.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ci-check-server

Comment on lines +10 to +14
data,
className,
}: {
data: Aspect;
className?: string;
data: Aspect;
className?: string;
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

🧩 Analysis chain

Missing type import for Aspect.

The component uses the Aspect type in the props definition (line 13) but there's no corresponding import statement visible. This will cause a TypeScript error unless the type is available through ambient types or a global declaration.

Please verify the source of the Aspect type and add the import if needed:


🏁 Script executed:

#!/bin/bash
# Search for Aspect type definition and existing imports
rg -nP "type Aspect|interface Aspect|export.*Aspect" --type=ts --type=tsx
rg -nP "from.*import.*Aspect" --type=ts --type=tsx echo/frontend/src/components/aspect/

Length of output: 55



---

🏁 Script executed:

```shell
#!/bin/bash
# Search for Aspect type definition across the codebase
rg -n "type Aspect|interface Aspect" --glob="*.ts" --glob="*.tsx"

# Search specifically in AspectCard.tsx file and nearby for Aspect import
rg -n "import.*Aspect|from.*aspect" echo/frontend/src/components/aspect/ --glob="*.tsx" --glob="*.ts"

# Also check the actual AspectCard.tsx file for all imports
echo "=== Checking AspectCard.tsx imports ===" 
fd "AspectCard.tsx" echo/frontend/src/components/aspect/ --exec head -20 {}

Length of output: 772


Add missing Aspect type import.

The Aspect type is defined in echo/frontend/src/lib/typesDirectus.d.ts but not imported in AspectCard.tsx. This breaks TypeScript compilation—the component can't reference an undefined type in its props.

Add this import at the top of the file:

import { Aspect } from "@/lib/typesDirectus";
🤖 Prompt for AI Agents
In echo/frontend/src/components/aspect/AspectCard.tsx around lines 10 to 14, the
props reference the Aspect type but the file does not import it; add an import
for Aspect from "@/lib/typesDirectus" at the top of the file so the component's
props compile correctly (e.g., add: import { Aspect } from
"@/lib/typesDirectus";).

Comment on lines 2 to +4
import { readItem } from "@directus/sdk";
import useCopyToRichText from "@/hooks/useCopyToRichText";
import { useParams } from "react-router";

interface Quote {
quote_id: {
text: string;
conversation_id: {
id: string;
participant_name: string;
};
};
}
import useCopyToRichText from "@/hooks/useCopyToRichText";
import { directus } from "@/lib/directus";
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

🧩 Analysis chain

Missing type imports for type assertions used below.

The code uses AspectSegment, ConversationSegment, and Conversation types in lines 58, 66-70 but doesn't import them. Ship it once you add the type imports at the top.

Add the missing type imports:

 import { readItem } from "@directus/sdk";
 import { useParams } from "react-router";
+import type { AspectSegment, ConversationSegment, Conversation } from "@/types/aspect";
 import useCopyToRichText from "@/hooks/useCopyToRichText";
 import { directus } from "@/lib/directus";

Note: Adjust the import path based on where these types are actually defined in your codebase.


🏁 Script executed:

#!/bin/bash
# Find type definitions for AspectSegment, ConversationSegment, and Conversation
ast-grep --pattern 'type AspectSegment = $$$'
ast-grep --pattern 'interface AspectSegment { $$$ }'
ast-grep --pattern 'type ConversationSegment = $$$'
ast-grep --pattern 'interface ConversationSegment { $$$ }'
ast-grep --pattern 'type Conversation = $$$'
ast-grep --pattern 'interface Conversation { $$$ }'

Length of output: 4208


🏁 Script executed:

#!/bin/bash
# Get full content of useCopyAspect.tsx to check imports and type assertions
cat echo/frontend/src/components/aspect/hooks/useCopyAspect.tsx

Length of output: 2374


Missing type imports for type assertions used below—fix the import path.

The code uses AspectSegment, ConversationSegment, and Conversation types in type assertions (lines 57, 66–68) but doesn't import them. Add the missing type imports from the correct module:

 import { readItem } from "@directus/sdk";
 import { useParams } from "react-router";
+import type { AspectSegment, ConversationSegment, Conversation } from "@/lib/typesDirectus";
 import useCopyToRichText from "@/hooks/useCopyToRichText";
 import { directus } from "@/lib/directus";
📝 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
import { readItem } from "@directus/sdk";
import useCopyToRichText from "@/hooks/useCopyToRichText";
import { useParams } from "react-router";
interface Quote {
quote_id: {
text: string;
conversation_id: {
id: string;
participant_name: string;
};
};
}
import useCopyToRichText from "@/hooks/useCopyToRichText";
import { directus } from "@/lib/directus";
import { readItem } from "@directus/sdk";
import { useParams } from "react-router";
import type { AspectSegment, ConversationSegment, Conversation } from "@/lib/typesDirectus";
import useCopyToRichText from "@/hooks/useCopyToRichText";
import { directus } from "@/lib/directus";
🤖 Prompt for AI Agents
In echo/frontend/src/components/aspect/hooks/useCopyAspect.tsx around lines 1 to
4 and usages at lines ~57 and ~66–68, the types AspectSegment,
ConversationSegment, and Conversation are used in type assertions but not
imported; add named type imports for these three from the correct module (e.g.,
import type { AspectSegment, ConversationSegment, Conversation } from the module
that exports your conversation/aspect types) at the top of the file and ensure
the import uses the proper path used elsewhere in the project.

Comment on lines +10 to +37
const copyAspect = async (aspectId: string) => {
const stringBuilder: string[] = [];
const aspect = await directus.request(
readItem("aspect", aspectId, {
fields: [
"id",
"name",
"short_summary",
"long_summary",
"image_url",
"view_id",
{
aspect_segment: [
{
segment: [
{
conversation_id: ["id", "participant_name"],
},
"description",
"verbatim_transcript",
"relevant_index",
],
},
],
},
],
}),
);
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 | 🟠 Major

Add error handling for the Directus request.

Network calls can fail, database might be down, aspectId might be invalid - you know the drill. Wrap this in try-catch so users don't get an unhandled rejection in prod.

 const copyAspect = async (aspectId: string) => {
+	try {
 		const stringBuilder: string[] = [];
 		const aspect = await directus.request(
 			readItem("aspect", aspectId, {
 				fields: [
 					"id",
 					"name",
 					"short_summary",
 					"long_summary",
 					"image_url",
 					"view_id",
 					{
 						aspect_segment: [
 							{
 								segment: [
 									{
 										conversation_id: ["id", "participant_name"],
 									},
 									"description",
 									"verbatim_transcript",
 									"relevant_index",
 								],
 							},
 						],
 					},
 				],
 			}),
 		);
 
 		// ... rest of the function
 		
 		copy(stringBuilder.join("\n"));
+	} catch (error) {
+		console.error("Failed to copy aspect:", error);
+		// TODO: Show error toast to user
+	}
 };

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In echo/frontend/src/components/aspect/hooks/useCopyAspect.tsx around lines 10
to 37, the Directus request call is unprotected and can throw an unhandled
rejection; wrap the await directus.request(readItem(...)) in a try-catch block,
handle errors by logging the error (e.g., console.error or app logger), surface
a user-friendly notification/toast or return an early failure value, and ensure
any cleanup or state updates (loading flags) are done in finally so the UI stays
consistent.

Comment on lines +57 to +59
const quotes = Array.isArray(aspect.aspect_segment)
? (aspect.aspect_segment as AspectSegment[])
: [];
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 | 🟠 Major

Type assertion bypasses compile-time safety.

Casting aspect.aspect_segment to AspectSegment[] without runtime validation is sketch. If the API returns something unexpected, this will blow up at runtime.

Since your PR is all about code quality and avoiding unsafe types, consider adding a runtime check or using a type guard:

-const quotes = Array.isArray(aspect.aspect_segment)
-	? (aspect.aspect_segment as AspectSegment[])
-	: [];
+const isAspectSegmentArray = (data: unknown): data is AspectSegment[] => {
+	return Array.isArray(data) && data.every(item => item && typeof item === 'object' && 'segment' in item);
+};
+
+const quotes = isAspectSegmentArray(aspect.aspect_segment)
+	? aspect.aspect_segment
+	: [];

Or at minimum, add a TODO comment acknowledging this needs proper typing.

📝 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
const quotes = Array.isArray(aspect.aspect_segment)
? (aspect.aspect_segment as AspectSegment[])
: [];
const isAspectSegmentArray = (data: unknown): data is AspectSegment[] => {
return Array.isArray(data) && data.every(item => item && typeof item === 'object' && 'segment' in item);
};
const quotes = isAspectSegmentArray(aspect.aspect_segment)
? aspect.aspect_segment
: [];
🤖 Prompt for AI Agents
In echo/frontend/src/components/aspect/hooks/useCopyAspect.tsx around lines 57
to 59, the code unsafely casts aspect.aspect_segment to AspectSegment[] which
bypasses compile-time checks and can crash at runtime; replace the direct type
assertion with a runtime guard that validates aspect.aspect_segment is an array
of objects matching the expected shape (e.g., check Array.isArray and the
presence/types of required fields) before using it, falling back to an empty
array if validation fails, or at minimum add a TODO comment noting this must be
properly typed/validated and avoid the blanket cast.

Comment on lines +66 to +71
const conversationId = (quote.segment as ConversationSegment)
.conversation_id as string;
const description = quote.description ?? "No description available";
const conversation = (quote.segment as ConversationSegment)
?.conversation_id as Conversation;
const participantName = conversation?.participant_name ?? "Unknown";
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

Refactor: Extract conversation once, avoid duplicate casts.

You're casting and extracting conversation_id twice here (lines 66-67 for the string ID, lines 69-70 for the conversation object). This is redundant and makes the code harder to follow.

Simplify:

-const conversationId = (quote.segment as ConversationSegment)
-	.conversation_id as string;
-const description = quote.description ?? "No description available";
-const conversation = (quote.segment as ConversationSegment)
-	?.conversation_id as Conversation;
-const participantName = conversation?.participant_name ?? "Unknown";
+const segment = quote.segment as ConversationSegment;
+const conversation = segment.conversation_id as Conversation;
+const conversationId = typeof conversation === 'object' ? conversation.id : String(conversation);
+const participantName = conversation?.participant_name ?? "Unknown";
+const description = quote.description ?? "No description available";

This casts once, extracts the data, and makes the relationship between the variables clearer.

🤖 Prompt for AI Agents
In echo/frontend/src/components/aspect/hooks/useCopyAspect.tsx around lines 66
to 71, the code casts quote.segment twice to get conversation_id and the
Conversation object separately; instead cast quote.segment once to a
ConversationSegment, assign it to a local variable (e.g., const segment =
quote.segment as ConversationSegment), then derive conversationId from
segment.conversation_id as string and the conversation object or participantName
from segment.conversation_id (or segment.conversation if that property holds the
Conversation) to avoid duplicate casts and make intent clearer; update usages to
reference the new local segment variable.

<Trans>Changes will be saved automatically</Trans>
)}
</Text>
{isSaving && <LoadingSpinner />}
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

Spinner a11y: announce saving state.

Wrap the spinner so AT users get updates.

-{isSaving && <LoadingSpinner />}
+{isSaving && (
+  <span aria-live="polite" aria-busy="true">
+    <LoadingSpinner />
+  </span>
+)}

Optionally, forward ARIA props in LoadingSpinner and set role="status" there for reuse.

🤖 Prompt for AI Agents
In echo/frontend/src/components/form/UnsavedChanges.tsx around line 23, the
LoadingSpinner is rendered without any accessible status announcement; wrap the
spinner in an element (or update LoadingSpinner) that exposes an ARIA status so
AT users are notified when saving starts/completes. Fix by either wrapping the
spinner here with a container element that has role="status" and
aria-live="polite" and includes a visually-hidden text node like "Saving…"
(shown only when isSaving), or update LoadingSpinner to accept and forward ARIA
props and set role="status" internally so you can render <LoadingSpinner
aria-live="polite" aria-label="Saving…" /> instead; ensure the live region is
updated only when isSaving changes.

Comment on lines +119 to +120
analyserRef.current.getByteTimeDomainData(
dataArrayRef.current as Uint8Array<ArrayBuffer>,
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Oct 17, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Type assertion is incorrect.

Uint8Array<ArrayBuffer> is invalid TypeScript—Uint8Array doesn't accept a type parameter. Since dataArrayRef.current is already typed as Uint8Array (and null-checked on line 117), the cast is both redundant and syntactically wrong.

Apply this diff:

-				analyserRef.current.getByteTimeDomainData(
-					dataArrayRef.current as Uint8Array<ArrayBuffer>,
-				);
+				analyserRef.current.getByteTimeDomainData(dataArrayRef.current);
📝 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
analyserRef.current.getByteTimeDomainData(
dataArrayRef.current as Uint8Array<ArrayBuffer>,
analyserRef.current.getByteTimeDomainData(dataArrayRef.current);
🤖 Prompt for AI Agents
In echo/frontend/src/components/participant/MicrophoneTest.tsx around lines
119-120, the code uses an invalid type assertion `Uint8Array<ArrayBuffer>` when
calling analyserRef.current.getByteTimeDomainData(...); remove the incorrect
generic and pass the existing Uint8Array directly (or use a non-null assertion
like dataArrayRef.current! if needed), e.g. call
getByteTimeDomainData(dataArrayRef.current) so the cast is neither redundant nor
syntactically wrong.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

no we get this error if not typed 'Argument of type 'Uint8Array' is not assignable to parameter of type 'Uint8Array'."

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.

@ussaama You're absolutely right, and I apologize for the incorrect flag. 🙏

The type assertion as Uint8Array<ArrayBuffer> is valid and necessary. I was mistaken when I said Uint8Array doesn't accept a type parameter—it actually does: Uint8Array<TArrayBuffer extends ArrayBufferLike>.

The issue you're encountering is that TypeScript's type inference may widen the Uint8Array to use ArrayBufferLike (which is a union of ArrayBuffer | SharedArrayBuffer), while the Web Audio API's getByteTimeDomainData method specifically expects Uint8Array<ArrayBuffer>. The type assertion correctly narrows this type to satisfy the API requirement.

Your code is correct as-is. LGTM! 👍


🧠 Learnings used
Learnt from: ussaama
PR: Dembrane/echo#256
File: echo/frontend/src/components/participant/MicrophoneTest.tsx:54-54
Timestamp: 2025-08-06T13:38:30.769Z
Learning: In echo/frontend/src/components/participant/MicrophoneTest.tsx, the useDisclosure hook is used where only the `close` function is needed locally (called in handleConfirmMicChange and handleCancelMicChange), while `opened` and `open` are unused because the modal state is managed by a parent component. The `showSecondModal` state variable is used separately to control content switching within the modal between the main UI and confirmation step.

Comment on lines +275 to +279
const handleConfirmMicChange = () => {
// Apply the pending device change
setSelectedDeviceId(displayDeviceId);
onContinue(selectedDeviceId);
};
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

State timing bug in handleConfirmMicChange.

Line 278 calls onContinue(selectedDeviceId), but selectedDeviceId still holds the old value because the state update on line 277 hasn't taken effect yet. You're confirming a device change but passing the previous device to the parent callback.

Apply this diff:

 const handleConfirmMicChange = () => {
   // Apply the pending device change
   setSelectedDeviceId(displayDeviceId);
-  onContinue(selectedDeviceId);
+  onContinue(displayDeviceId);
 };
📝 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
const handleConfirmMicChange = () => {
// Apply the pending device change
setSelectedDeviceId(displayDeviceId);
onContinue(selectedDeviceId);
};
const handleConfirmMicChange = () => {
// Apply the pending device change
setSelectedDeviceId(displayDeviceId);
onContinue(displayDeviceId);
};
🤖 Prompt for AI Agents
In echo/frontend/src/components/participant/MicrophoneTest.tsx around lines 275
to 279, the handler sets state with setSelectedDeviceId(displayDeviceId) then
calls onContinue(selectedDeviceId), but selectedDeviceId is still the old value;
change the call to pass the new id directly (onContinue(displayDeviceId)) or
otherwise ensure the callback receives displayDeviceId (e.g., capture it in a
local const) so the parent receives the confirmed device immediately.

Comment on lines +64 to 71
const replies = (repliesQuery.data ?? [])
.filter((m) => ["assistant_reply"].includes(m.type ?? ""))
.map((m) => ({
data: m,
timestamp: new Date(m.date_created ?? ""),
type: "assistant_chunk" as const,
}));

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 | 🟠 Major

Harden timestamp parsing; avoid NaN sort and drop bad rows.

new Date("") → Invalid Date → NaN comparator. Filter invalid timestamps before sorting.

- const userChunks = (chunksQuery.data ?? []).map((chunk) => ({
-   data: chunk,
-   timestamp: new Date(chunk.timestamp),
-   type: "user_chunk" as const,
- }));
+ const userChunks = (chunksQuery.data ?? [])
+   .flatMap((chunk) => {
+     const ts = new Date(chunk.timestamp);
+     return Number.isFinite(ts.getTime())
+       ? [{ data: chunk, timestamp: ts, type: "user_chunk" as const }]
+       : [];
+   });

- const replies = (repliesQuery.data ?? [])
-   .filter((m) => ["assistant_reply"].includes(m.type ?? ""))
-   .map((m) => ({
-     data: m,
-     timestamp: new Date(m.date_created ?? ""),
-     type: "assistant_chunk" as const,
-   }));
+ const replies = (repliesQuery.data ?? [])
+   .filter((m) => m.type === "assistant_reply" && m.date_created)
+   .flatMap((m) => {
+     const ts = new Date(m.date_created!);
+     return Number.isFinite(ts.getTime())
+       ? [{ data: m, timestamp: ts, type: "assistant_chunk" as const }]
+       : [];
+   });

- if (currentUserChunks.length > 0) {
+ if (currentUserChunks.length > 0) {

Also applies to: 72-74, 95-101

🤖 Prompt for AI Agents
In echo/frontend/src/components/participant/ParticipantBody.tsx around lines
64-71 (and also apply same fix at 72-74 and 95-101), timestamp parsing uses new
Date(m.date_created ?? "") which may produce Invalid Date and NaN during
sorting; validate the parsed timestamp and drop rows with invalid dates before
mapping/sorting. Replace direct new Date(...) with a guarded parse: attempt to
create the Date, check isFinite(date.getTime()) (or !isNaN(date.getTime())), and
only include items with valid timestamps in the resulting array (otherwise
filter them out or handle them explicitly) so downstream sorting/comparisons
never see NaN timestamps.

Comment on lines +213 to +221
{chunksQuery.data
?.sort(
(a, b) =>
new Date(a.timestamp).getTime() -
new Date(b.timestamp).getTime(),
)
.map((chunk) => (
<UserChunkMessage key={chunk.id} chunk={chunk} />
))}
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

DRY the duplicate sorts for chunks.

You sort chunks twice (list + modal). Memoize a single sortedChunks and reuse.

Also applies to: 243-251

🤖 Prompt for AI Agents
In echo/frontend/src/components/participant/ParticipantBody.tsx around lines
213-221 and 243-251, duplicate sorting of chunks is being performed; create a
single memoized sortedChunks (e.g. useMemo(() => (chunksQuery.data ??
[]).slice().sort((a,b)=>new Date(a.timestamp).getTime()-new
Date(b.timestamp).getTime()), [chunksQuery.data])) and replace both inline sorts
with sortedChunks so the list and modal reuse the same pre-sorted array; ensure
you clone before sorting to avoid mutating original data and keep existing
keys/props when rendering.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 95

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
echo/frontend/src/components/conversation/ConversationLinks.tsx (1)

1-4: Import organization looks clean, but verify type imports vs. value imports.

Make sure any imports are using type keyword where appropriate (e.g., import type { Conversation } vs. import { Conversation }). This aligns with the PR objective of stricter type handling and the broader Biome migration.

echo/frontend/src/components/conversation/ConversationAccordion.tsx (1)

816-818: Unacceptable biome-ignore usage in a lint rules PR.

The whole point of ECHO-334 is to enforce code quality via lint rules. You can't ship biome-ignore comments with just <todo> as justification. Either fix the a11y violation (add keyboard handler or use a proper button) or provide a real explanation for why it's safe to ignore here.

Apply this diff to fix it properly:

-{/** biome-ignore lint/a11y/noStaticElementInteractions: <todo> */}
-{/** biome-ignore lint/a11y/useKeyWithClickEvents: <todo> */}
-<div onClick={(e) => e.stopPropagation()}>
+<div 
+  role="button" 
+  tabIndex={0}
+  onClick={(e) => e.stopPropagation()}
+  onKeyDown={(e) => {
+    if (e.key === 'Enter' || e.key === ' ') {
+      e.stopPropagation();
+    }
+  }}
+>
   <UploadConversationDropzone projectId={projectId} />
 </div>

Or if stopPropagation is the only goal and there's no actual interaction:

-{/** biome-ignore lint/a11y/noStaticElementInteractions: <todo> */}
-{/** biome-ignore lint/a11y/useKeyWithClickEvents: <todo> */}
-<div onClick={(e) => e.stopPropagation()}>
-  <UploadConversationDropzone projectId={projectId} />
-</div>
+<UploadConversationDropzone projectId={projectId} onWrapperClick={(e) => e.stopPropagation()} />
echo/frontend/src/components/conversation/hooks/index.ts (1)

262-274: Yo, double toast bug detected 🐛

You're showing an error toast at line 269 inside the try-catch AND again at line 273 in onError. That means users get two error toasts for a single failure.

Remove the try-catch block or the onError handler—not both.

Apply this diff:

 	mutationFn: async ({
 		conversationId,
 		targetProjectId,
 	}: {
 		conversationId: string;
 		targetProjectId: string;
 	}) => {
-		try {
-			await directus.request(
-				updateItem("conversation", conversationId, {
-					project_id: targetProjectId,
-				}),
-			);
-		} catch (_error) {
-			toast.error("Failed to move conversation.");
-		}
+		await directus.request(
+			updateItem("conversation", conversationId, {
+				project_id: targetProjectId,
+			}),
+		);
 	},
 	onError: (error: Error) => {
 		toast.error(`Failed to move conversation: ${error.message}`);
 	},
echo/frontend/package.json (1)

64-66: Malformed dependency key — likely to break installs.

This entry appears corrupted (newline in key/value). Remove it; you already depend on @mantine/notifications elsewhere.

-    "notifications\n": "link:@mantine/notifications\n",
♻️ Duplicate comments (41)
echo/frontend/src/components/auth/utils/errorUtils.ts (1)

3-23: ** Biome-ignore suppressions contradict PR objectives—type-guard this properly.**

This is the exact issue flagged in the previous review. You're shipping biome rules to enforce no-any, but then immediately suppressing them with "need to address this later" comments. That's not enforcement—that's kicking the can.

The prior reviewer nailed it: replace the any casts with a proper DirectusError type guard. No casting, no suppressions, no tech debt on day one. Here's the move:

+interface DirectusError {
+  errors: Array<{ message: string }>;
+}
+
+function isDirectusError(e: unknown): e is DirectusError {
+  return (
+    typeof e === "object" &&
+    e !== null &&
+    "errors" in e &&
+    Array.isArray((e as Record<string, unknown>).errors) &&
+    (e as Record<string, unknown>).errors.length > 0 &&
+    typeof ((e as Record<string, unknown>).errors[0] as Record<string, unknown>).message === "string"
+  );
+}
+
 export function throwWithMessage(e: unknown): never {
-	if (
-		e &&
-		typeof e === "object" &&
-		"errors" in e &&
-		// biome-ignore lint/suspicious/noExplicitAny: need to address this later
-		Array.isArray((e as any).errors)
-	) {
-		// Handle Directus error format
-		// biome-ignore lint/suspicious/noExplicitAny: need to address this later
-		const message = (e as any).errors[0].message;
-		console.log(message);
-		throw new Error(message);
+	if (isDirectusError(e)) {
+		const message = e.errors[0].message;
+		console.log(message);
+		throw new Error(message);

Clean, type-safe, and actually enforces the no-any rule. Let's ship it.

echo/frontend/src/components/common/BaseSkeleton.tsx (1)

20-23: Yo, this was already called out. Fix the comment or fix the key.

Look, index-as-key for stateless skeletons that never reorder? Totally fine. But that "needs to be fixed" comment is lying to your future self and creating fake tech debt.

Either:

  • Ship it with key={skeleton-${index}} and drop the ignore, or
  • Keep the index but update the comment to "acceptable for stateless skeletons"

Don't leave misleading comments in prod. Previous review already covered this.

Apply this if you want the explicit key:

-			{Array.from({ length: count }).map((_, index) => (
-				// biome-ignore lint/suspicious/noArrayIndexKey: needs to be fixed
-				<Skeleton key={index} height={height} width={width} radius={radius} />
-			))}
+			{Array.from({ length: count }).map((_, index) => (
+				<Skeleton key={`skeleton-${index}`} height={height} width={width} radius={radius} />
+			))}

Or just update the comment:

-				// biome-ignore lint/suspicious/noArrayIndexKey: needs to be fixed
+				// biome-ignore lint/suspicious/noArrayIndexKey: acceptable for stateless loading skeletons
echo/frontend/src/components/common/ScrollToBottom.tsx (2)

14-16: Still duplicating utils.scrollToBottom.

Previous review already caught this — the local helper here is identical to the shared util. DRY principle says nuke the duplication and import from lib/utils.

Based on relevant code snippets showing utils.scrollToBottom exists.


18-18: isVisible naming still misleading.

Prior review nailed this — isVisible reads backwards. The button hides when you're already at the bottom, so isAtBottom would be way clearer. The comment patch helps but doesn't fix the root naming confusion.

echo/frontend/src/components/conversation/OngoingConversationsSummaryCard.tsx (1)

16-16: Yo, this FIXME is still chilling here unaddressed.

Your past review already called out that Aggregate would be way more performant than pulling all chunks client-side. FIXME acknowledged but not shipped yet.

If you're planning to knock this out, ship it. If you're deferring, maybe open an issue and drop the FIXME. Either way, LGTM for now but don't let this sit forever.

echo/frontend/src/components/aspect/hooks/useCopyAspect.tsx (4)

1-4: Ship it once you add those type imports, chief.

You're using AspectSegment, ConversationSegment, and Conversation types down in lines 58, 66-70 but haven't imported them. In a PR literally about implementing biome rules and code quality, this should've been caught. Add the imports:

 import { readItem } from "@directus/sdk";
 import { useParams } from "react-router";
+import type { AspectSegment, ConversationSegment, Conversation } from "@/lib/typesDirectus";
 import useCopyToRichText from "@/hooks/useCopyToRichText";
 import { directus } from "@/lib/directus";

10-37: Bro, where's your error handling?

Network calls can fail. Database might be down. aspectId might be garbage. You know the drill. In a PR about code quality, leaving this unprotected is not the move. Wrap it:

 const copyAspect = async (aspectId: string) => {
+	try {
 		const stringBuilder: string[] = [];
 		const aspect = await directus.request(
 			readItem("aspect", aspectId, {
 				fields: [
 					"id",
 					"name",
 					"short_summary",
 					"long_summary",
 					"image_url",
 					"view_id",
 					{
 						aspect_segment: [
 							{
 								segment: [
 									{
 										conversation_id: ["id", "participant_name"],
 									},
 									"description",
 									"verbatim_transcript",
 									"relevant_index",
 								],
 							},
 						],
 					},
 				],
 			}),
 		);
 
 		// ... rest of the function
 		
 		copy(stringBuilder.join("\n"));
+	} catch (error) {
+		console.error("Failed to copy aspect:", error);
+		// TODO: Show error toast to user
+	}
 };

57-59: Type assertions without runtime validation? Not very 100x of you.

Casting aspect.aspect_segment to AspectSegment[] is sketch. If the API returns something unexpected, this crashes at runtime. Your PR is literally about avoiding unsafe types - why leave this?

Add a type guard or at minimum acknowledge this needs proper validation:

-const quotes = Array.isArray(aspect.aspect_segment)
-	? (aspect.aspect_segment as AspectSegment[])
-	: [];
+const isAspectSegmentArray = (data: unknown): data is AspectSegment[] => {
+	return Array.isArray(data) && data.every(item => item && typeof item === 'object' && 'segment' in item);
+};
+
+const quotes = isAspectSegmentArray(aspect.aspect_segment)
+	? aspect.aspect_segment
+	: [];

66-71: Double casting is not clean code, fam.

You're extracting conversation_id twice here - once as a string (lines 66-67), once as a Conversation object (lines 69-70). This is redundant and makes the code harder to follow.

Simplify:

-const conversationId = (quote.segment as ConversationSegment)
-	.conversation_id as string;
-const description = quote.description ?? "No description available";
-const conversation = (quote.segment as ConversationSegment)
-	?.conversation_id as Conversation;
-const participantName = conversation?.participant_name ?? "Unknown";
+const segment = quote.segment as ConversationSegment;
+const conversation = segment.conversation_id as Conversation;
+const conversationId = typeof conversation === 'object' ? conversation.id : String(conversation);
+const description = quote.description ?? "No description available";
+const participantName = conversation?.participant_name ?? "Unknown";
echo/frontend/src/components/conversation/AutoSelectConversations.tsx (8)

1-1: Fix the Lingui import path—won't compile.

Ship it with @lingui/macro instead of @lingui/react/macro. The compile-time macro lives at the correct path.

-import { Trans } from "@lingui/react/macro";
+import { Trans } from "@lingui/macro";

24-36: Type your params to eliminate those ?? "" fallbacks.

Untyped params = defensive ?? "" everywhere = potential empty-string requests. Type them and let the hooks gate on enabled.

-const { chatId, projectId } = useParams();
+const { chatId = "", projectId = "" } = useParams<{ chatId: string; projectId: string }>();

 const { data: project } = useProjectById({
-  projectId: projectId ?? "",
+  projectId,
   query: {
     fields: ["is_enhanced_audio_processing_enabled"],
   },
 });

-const projectChatContextQuery = useProjectChatContext(chatId ?? "");
+const projectChatContextQuery = useProjectChatContext(chatId);

Optional but recommended: gate useProjectById with enabled: Boolean(projectId) in the hook definition.


26-31: Loading state shows "Upgrade" before data arrives—confusing UX.

During load, project is undefined so isDisabled is truthy, showing the locked/upgrade state prematurely. Destructure isLoading and render a neutral badge while fetching.

-const { data: project } = useProjectById({
+const { data: project, isLoading: projectLoading } = useProjectById({
   projectId,
   query: {
     fields: ["is_enhanced_audio_processing_enabled"],
   },
 });

Then in renderFeatureIndicator():

if (projectLoading) {
  return <Badge color="gray" variant="light"><Trans>Loading</Trans></Badge>;
}

40-42: Gate on chatId too—empty ID should disable the control.

Right now only the project flag matters. If chatId is missing, the UI shouldn't be active.

-const isDisabled = !project?.is_enhanced_audio_processing_enabled;
+const isDisabled = !chatId || !project?.is_enhanced_audio_processing_enabled;
 const isAvailableButNotEnabled = !autoSelect && !isDisabled;

43-59: Guard mutations against empty chatId—don't fire with "" IDs.

Early-return before mutating if chatId is falsy. Prevents server calls with invalid input.

 const handleCheckboxChange = (checked: boolean) => {
   if (isDisabled) {
     return;
   }
+  if (!chatId) {
+    console.warn("Missing chatId; ignoring toggle");
+    return;
+  }

   if (checked) {
     addChatContextMutation.mutate({
       auto_select_bool: true,
-      chatId: chatId ?? "",
+      chatId,
     });
   } else {
     deleteChatContextMutation.mutate({
       auto_select_bool: false,
-      chatId: chatId ?? "",
+      chatId,
     });
   }
 };

61-76: Guard mutation and secure the window.open call.

Two issues:

  1. Same mutation guard needed (check chatId before mutate)
  2. window.open with _blank needs noopener,noreferrer to prevent reverse-tabnabbing
 const enableAutoSelect = () => {
   if (!isDisabled) {
+    if (!chatId) {
+      console.warn("Missing chatId; ignoring enable");
+      return;
+    }
     addChatContextMutation.mutate({
       auto_select_bool: true,
-      chatId: chatId ?? "",
+      chatId,
     });
   } else {
     try {
       analytics.trackEvent(events.AUTO_SELECT_CONTACT_SALES);
     } catch (error) {
       console.warn("Analytics tracking failed:", error);
     }
-    window.open(SalesLinks.AUTO_SELECT_CONTACT, "_blank");
+    window.open(SalesLinks.AUTO_SELECT_CONTACT, "_blank", "noopener,noreferrer");
   }
 };

113-113: Drop cursor-pointer or wire an onClick—pick one.

The pointer cursor signals clickability but there's no handler. Easiest: nix the class.

-<Box className="relative cursor-pointer border border-gray-200 hover:bg-gray-50">
+<Box className="relative border border-gray-200 hover:bg-gray-50">

130-150: Associate the label with the checkbox for accessibility.

Screen readers need a programmatic link between the Text and Checkbox. Add id and aria-labelledby.

-<Text className="font-medium">
+<Text id="auto-select-label" className="font-medium">
   <Trans>Auto-select</Trans>
 </Text>

 {renderFeatureIndicator()}

 {/* ... */}

 <Checkbox
   size="md"
   checked={autoSelect}
   disabled={isDisabled}
   color="green"
   onClick={(e) => e.stopPropagation()}
   onChange={(e) => handleCheckboxChange(e.currentTarget.checked)}
+  aria-labelledby="auto-select-label"
 />
echo/frontend/src/components/participant/MicrophoneTest.tsx (2)

119-120: Still shipping invalid TypeScript syntax.

Uint8Array<ArrayBuffer> is invalid - Uint8Array doesn't take type params. The cast is redundant since dataArrayRef.current is already typed correctly and null-checked at line 117.

Apply this diff:

-				analyserRef.current.getByteTimeDomainData(
-					dataArrayRef.current as Uint8Array<ArrayBuffer>,
-				);
+				analyserRef.current.getByteTimeDomainData(dataArrayRef.current);

275-279: Classic React state gotcha - still passing stale state.

Line 278 calls onContinue(selectedDeviceId) but that's the old value because setState is async. Parent gets the previous device instead of the confirmed one.

Apply this diff:

 const handleConfirmMicChange = () => {
   // Apply the pending device change
   setSelectedDeviceId(displayDeviceId);
-  onContinue(selectedDeviceId);
+  onContinue(displayDeviceId);
 };
echo/frontend/src/components/form/EditableTextBox.tsx (5)

36-43: State drift on save failure - same issue as before.

When saveChanges bombs, localValue stays edited but the backend rejected it. Your UI is lying to the user. You need to revert localValue back to value and log that error for debugging.

Apply this diff:

 	const saveChanges = async (newValue: string) => {
 		try {
 			await onChange(newValue.trim());
 			setError(null);
-		} catch (_err) {
+		} catch (err) {
+			console.error("Failed to save editable text:", err);
 			setError("Failed to save changes. Please try again.");
+			setLocalValue(value);
 		}
 	};

45-49: Type suppression defeats the whole point of this PR.

You're implementing biome rules to enforce code quality and avoid loose typing, but here you're using @ts-expect-error instead of just typing the event correctly. This is basically "any" with extra steps.

Apply this diff:

-	const handleChange = (e: React.ChangeEvent) => {
-		// @ts-expect-error e.target.value is not typed
+	const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
 		const newValue = e.target.value;
 		setLocalValue(newValue);
 	};

51-54: Still hammering the backend on every blur.

Same as before - you're calling saveChanges even when nothing changed. If onChange hits the network, you're wasting bandwidth.

Apply this diff:

 	const handleBlur = () => {
 		setIsEditing(false);
-		saveChanges(localValue);
+		if (localValue.trim() !== value.trim()) {
+			saveChanges(localValue);
+		}
 	};

88-98: Data loss bug - save icon doesn't save.

Same critical issue: clicking the floppy disk icon just toggles editing mode but doesn't actually persist changes. Users will lose data when they click what looks like a save button.

Apply this diff:

+	const handleIconClick = () => {
+		if (isEditing) {
+			handleBlur();
+		} else {
+			setIsEditing(true);
+		}
+	};
+
 	return (
 		// biome-ignore lint/a11y/useSemanticElements: role group here is not an ARIA attribute
 		<div
 			<Tooltip label={isEditing ? "Save changes" : "Edit text"}>
 				<ActionIcon
-					onClick={() => setIsEditing(!isEditing)}
+					onClick={handleIconClick}
 					aria-label={isEditing ? "Save changes" : "Edit text"}
 					variant="transparent"
 				>

100-104: Error message will render inline with the input.

Same layout issue - the error <p> is a flex sibling with items-center, so it'll appear next to the input instead of below it. Looks janky.

Wrap the input+icon in a container div and move the error outside:

 	return (
-		// biome-ignore lint/a11y/useSemanticElements: role group here is not an ARIA attribute
 		<div
-			className="flex items-center justify-start"
+			className="flex flex-col"
 			role="group"
 			aria-label="Editable text"
 		>
+			<div className="flex items-center justify-start">
 			<InputBase
 				ref={inputRef}
 				value={localValue}
 				onChange={handleChange}
 				onBlur={handleBlur}
 				onKeyDown={handleKeyDown}
 				disabled={disabled}
 				placeholder={placeholder}
 				aria-label="Editable text input"
 				{...inputProps}
 				className={cn("mr-2 min-w-[100px] py-1", inputProps?.className)}
 			/>
 			<Tooltip label={isEditing ? "Save changes" : "Edit text"}>
 				<ActionIcon
 					onClick={handleIconClick}
 					aria-label={isEditing ? "Save changes" : "Edit text"}
 					variant="transparent"
 				>
 					{isEditing ? (
 						<IconDeviceFloppy aria-hidden="true" color="gray" />
 					) : (
 						<IconPencil aria-hidden="true" color="gray" />
 					)}
 				</ActionIcon>
 			</Tooltip>
+			</div>
 			{error && (
 				<p className="mt-1 text-sm text-red-500" role="alert">
 					{error}
 				</p>
 			)}
 		</div>
 	);
echo/frontend/src/components/aspect/hooks/useCopyQuote.ts (4)

1-4: Ship the type-only imports.

You reference AspectSegment/ConversationSegment/Conversation but never import them. Add type-only imports to satisfy Biome and kill implicit any drift.

 import { readItem } from "@directus/sdk";
 import { useParams } from "react-router";
+import type {
+  AspectSegment,
+  ConversationSegment,
+  Conversation,
+} from "@/types";
 import useCopyToRichText from "@/hooks/useCopyToRichText";
 import { directus } from "@/lib/directus";

17-34: Drop the ts-expect-error; add runtime-safe fetch with error handling.

Don’t suppress types; handle shape+failures explicitly.

-    // @ts-expect-error - Directus SDK has incorrect types for nested fields
-    const quote: AspectSegment = await directus.request(
-      readItem("aspect_segment", quoteId, {
-        fields: [
-          "id",
-          "description",
-          "verbatim_transcript",
-          "relevant_index",
-          {
-            segment: [
-              {
-                conversation_id: ["id", "participant_name", "created_at"],
-              },
-            ],
-          },
-        ],
-      }),
-    );
+    let quote: AspectSegment;
+    try {
+      const res = await directus.request(
+        readItem("aspect_segment", quoteId, {
+          fields: [
+            "id",
+            "description",
+            "verbatim_transcript",
+            "relevant_index",
+            {
+              segment: [
+                {
+                  conversation_id: ["id", "participant_name", "created_at"],
+                },
+              ],
+            },
+          ],
+        }),
+      );
+      quote = res as unknown as AspectSegment;
+    } catch (err) {
+      console.error("useCopyQuote: failed to load aspect_segment", err);
+      return;
+    }

36-37: Remove brittle double assertions; validate conversation presence.

Runtime check > “as X as Y” chain.

-    const conversation = (quote.segment as ConversationSegment)
-      ?.conversation_id as Conversation;
+    const segment = (quote.segment as Partial<ConversationSegment> | null) ?? null;
+    const conversation = (segment?.conversation_id as Conversation | undefined) ?? null;
+    if (!conversation || !conversation.id) {
+      console.error("useCopyQuote: conversation missing in quote", { quoteId, segment });
+      return;
+    }

Follow-up: with the guard, you can drop optional chaining on conversation below.


42-48: Delete dead code or replace with a single TODO.

Commented blocks rot fast—keep intent concise.

-    // // Format tags if available
-    // const tags = ((quote.segment as ConversationSegment)?.conversation_id as Conversation)?.tags
-    //   ?.map(
-    //     (tag: ConversationProjectTag) =>
-    //       tag.project_tag_id?.text ?? "",
-    //   )
-    //   .join(", ");
+    // TODO(ECHO-XXX): Add tags support when schema is ready.
@@
-    // if (tags) {
-    //   stringBuilder.push(`**Tags:** ${tags}`);
-    //   stringBuilder.push("");
-    // }
+    // TODO(ECHO-XXX): Include formatted tags above when available.

Also applies to: 93-97

echo/frontend/src/components/participant/ParticipantBody.tsx (2)

57-74: Harden timestamp parsing; drop bad rows to avoid NaN comparator.

Validate dates for both chunks and replies; use strict type filter. Prevents unstable sort and weird ordering.

-  const userChunks = (chunksQuery.data ?? []).map((chunk) => ({
-    data: chunk,
-    timestamp: new Date(chunk.timestamp),
-    type: "user_chunk" as const,
-  }));
+  const userChunks = (chunksQuery.data ?? []).flatMap((chunk) => {
+    const ts = new Date(chunk.timestamp);
+    return Number.isFinite(ts.getTime())
+      ? [{ data: chunk, timestamp: ts, type: "user_chunk" as const }]
+      : [];
+  });

-  const replies = (repliesQuery.data ?? [])
-    .filter((m) => ["assistant_reply"].includes(m.type ?? ""))
-    .map((m) => ({
-      data: m,
-      timestamp: new Date(m.date_created ?? ""),
-      type: "assistant_chunk" as const,
-    }));
+  const replies = (repliesQuery.data ?? [])
+    .filter((m) => m.type === "assistant_reply" && m.date_created)
+    .flatMap((m) => {
+      const ts = new Date(m.date_created!);
+      return Number.isFinite(ts.getTime())
+        ? [{ data: m, timestamp: ts, type: "assistant_chunk" as const }]
+        : [];
+    });

213-221: Stop in‑place sorting query data; DRY the sort.

.sort() here mutates chunksQuery.data (TanStack Query cache) and can desync useMemo deps. Memoize a cloned, validated sortedChunks once and reuse in both places.

Add once near other hooks:

const sortedChunks = useMemo(
  () =>
    (chunksQuery.data ?? [])
      .filter((c) => Number.isFinite(new Date(c.timestamp).getTime()))
      .slice()
      .sort(
        (a, b) =>
          new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(),
      ),
  [chunksQuery.data],
);

Then apply:

- {chunksQuery.data
-   ?.sort(
-     (a, b) =>
-       new Date(a.timestamp).getTime() -
-       new Date(b.timestamp).getTime(),
-   )
-   .map((chunk) => (
-     <UserChunkMessage key={chunk.id} chunk={chunk} />
-   ))}
+ {sortedChunks.map((chunk) => (
+   <UserChunkMessage key={chunk.id} chunk={chunk} />
+ ))}

And:

- {chunksQuery.data
-   ?.sort(
-     (a, b) =>
-       new Date(a.timestamp).getTime() -
-       new Date(b.timestamp).getTime(),
-   )
-   .map((chunk) => (
-     <UserChunkMessage key={chunk.id} chunk={chunk} />
-   ))}
+ {sortedChunks.map((chunk) => (
+   <UserChunkMessage key={chunk.id} chunk={chunk} />
+ ))}

Also applies to: 243-251

echo/frontend/src/components/aspect/AspectCard.tsx (1)

10-15: Compile blocker: missing Aspect type import (use type‑only).

Aspect is referenced in props but never imported; TS will fail. Import it as a type to satisfy Biome’s type‑imports preference.

 import { cn, sanitizeImageUrl } from "@/lib/utils";
+import type { Aspect } from "@/lib/typesDirectus";
echo/frontend/src/components/form/UnsavedChanges.tsx (1)

10-10: Still seeing unused isDirty prop.

Previous review caught this—isDirty is declared but never used. Should clean it up.

Apply the fix:

 }: {
-	isDirty?: boolean;
 	isSaving?: boolean;
 	lastSavedAt?: Date;
echo/frontend/src/components/common/DembraneLoadingSpinner/index.tsx (1)

30-37: The past review comment about exhaustive deps wasn't addressed, bro. 🔧

The biome-ignore on line 30 is suppressing a legit exhaustive-dependencies warning. The interval callback captures messages.length, but the messages array is recreated on every render (it's defined inside the component). This means the effect should either:

  1. Move messages outside the component (preferred—it's static)
  2. Use useMemo with empty deps if the messages need to be inside

Apply this diff to fix it properly:

+const messages = [
+	t`Welcome to Dembrane!`,
+	t`Loading`,
+	t`Preparing your experience`,
+	t`Almost there`,
+	t`Just a moment`,
+];
+
 const DembraneLoadingSpinner: React.FC<DembraneLoadingSpinnerProps> = ({
 	isLoading,
 	className,
 	showMessage = true,
 }) => {
 	const [messageIndex, setMessageIndex] = useState(0);
 	const [visible, setVisible] = useState(true);
 
-	const messages = [
-		t`Welcome to Dembrane!`,
-		t`Loading`,
-		t`Preparing your experience`,
-		t`Almost there`,
-		t`Just a moment`,
-	];
 
-	// biome-ignore lint/correctness/useExhaustiveDependencies: needs to be looked at
 	useEffect(() => {
 		const interval = setInterval(() => {
 			setMessageIndex((index) => (index + 1) % messages.length);
 		}, 3000);
 
 		return () => clearInterval(interval);
-	}, []);
+	}, [messages.length]);

Based on learnings

echo/frontend/src/components/conversation/MoveConversationButton.tsx (3)

51-68: The past review comment about projectId source is legit, bro. 🎯

Line 58 pulls projectId from URL params, but it should use conversation.project_id directly—the URL-based projectId can diverge from the conversation's actual project if routes change or URLs are manipulated. Also missing the enabled: opened optimization to prevent unnecessary queries when the modal is closed.

Apply this diff:

 	const projectsQuery = useInfiniteProjects({
+		enabled: opened,
 		options: {
 			initialLimit: 10,
 		},
 		query: {
 			filter: {
 				id: {
-					_neq: projectId as string,
+					_neq: conversation.project_id as string,
 				},
 				...(debouncedSearchValue && {
 					name: {
 						_icontains: debouncedSearchValue,
 					},
 				}),
 			},
 			sort: "-updated_at",
 		},
 	});

142-148: The past review about FormLabel is valid—it's overkill here, my dude. 🧹

Line 143 uses FormLabel with isDirty={false} hardcoded, which makes the component pointless. The search field isn't part of form submission, so no need for the dirty state indicator.

Apply this diff:

 							<TextInput
-								label={<FormLabel label={t`Search`} isDirty={false} />}
+								label={t`Search`}
 								placeholder={t`Search projects...`}
 								leftSection={<IconSearch size={16} />}
 								value={search}
 								onChange={(e) => setSearch(e.currentTarget.value)}
 							/>

202-220: The past review about dirtyFields logic is on point, bro. 🔘

Lines 214-215 check dirtyFields.targetProjectId which only tells you if the field was touched, not if it has a value. If React Hook Form doesn't mark the radio selection as dirty in some edge case, the button stays disabled even with a valid selection.

The old code used form.watch("targetProjectId") which is more direct and reliable.

Apply this diff:

 	const {
 		control,
 		handleSubmit,
 		reset,
-		formState: { dirtyFields },
+		watch,
 	} = useForm({
 		defaultValues: {
 			targetProjectId: "",
 		},
 		mode: "onChange",
 	});
+
+	const targetProjectId = watch("targetProjectId");
 
 	// ... later in the JSX ...
 
 							<Button
 								type="submit"
 								loading={moveConversationMutation.isPending}
 								disabled={
-									!dirtyFields.targetProjectId ||
+									!targetProjectId ||
 									moveConversationMutation.isPending
 								}
 							>
 								{t`Move`}
 							</Button>

Props on the type="button" on the cancel button though—prevents accidental form submission. That's 100x!

echo/frontend/src/components/language/LanguagePicker.tsx (2)

88-91: CodeQL “DOM text reinterpreted as HTML” — likely a false positive here, but sanitize anyway.

You’re writing to location.href, not innerHTML. Still, the whitelist check above neutralizes the finding. Please re-run CodeQL after the change to close the alert.


60-91: Whitelist the selected language before redirect.

selectedLanguage comes from DOM; guard it against SUPPORTED_LANGUAGES to prevent malformed paths and accidental redirects. Minimal patch:

-  const selectedLanguage = e.target.value;
+  const selectedLanguage = e.target.value;
+  if (!SUPPORTED_LANGUAGES.includes(selectedLanguage as (typeof SUPPORTED_LANGUAGES)[number])) {
+    console.warn("Unsupported language selection:", selectedLanguage);
+    return;
+  }
echo/frontend/src/components/chat/ChatAccordion.tsx (2)

100-130: Type suppressions already flagged.

The @ts-expect-error comments here were noted in previous reviews. Per project learnings, ts-ignore for Directus SDK filter issues is acceptable.


133-157: Duplicate type suppression pattern already noted.

This count query filter logic mirrors the main query suppressions flagged in previous reviews.

echo/frontend/src/components/chat/ChatContextProgress.tsx (1)

78-85: Use stable message IDs for keys (not indices).

Same feedback as earlier review; index-based keys break reconciliation on reorder/filter. Prefer m.id (or a stable message_id).

-        {chatContextQuery.data?.messages.map((m, idx) => (
+        {chatContextQuery.data?.messages.map((m, idx) => (
           <Tooltip
-            key={`message-${m.role}-${idx}`}
+            key={m.id ?? `message-${m.role}-${idx}`}
             label={t`Messages from ${capitalize(m.role)} - ${Math.ceil(m.token_usage * 100)}%`}
           >
             <Progress.Section value={m.token_usage * 100} color="gray.5" />
           </Tooltip>
         ))}

import { IconSpeakerphone } from "@tabler/icons-react";
import { useLatestAnnouncement, useUnreadAnnouncements } from "./hooks";
import { useLanguage } from "@/hooks/useLanguage";
import { useAnnouncementDrawer } from "@/components/announcement/hooks";
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

Import path inconsistency – pick a lane.

You're mixing absolute (@/components/announcement/hooks) and relative (./hooks) imports for what appears to be the same directory. Standardize on one approach for consistency.

If they're the same location, use the same import style:

-import { useAnnouncementDrawer } from "@/components/announcement/hooks";
+import { useAnnouncementDrawer, useLatestAnnouncement, useUnreadAnnouncements } from "./hooks";
 import { getTranslatedContent } from "@/components/announcement/hooks/useProcessedAnnouncements";
 import { Markdown } from "@/components/common/Markdown";
 import { useLanguage } from "@/hooks/useLanguage";
-import { useLatestAnnouncement, useUnreadAnnouncements } from "./hooks";

Also applies to: 7-7

🤖 Prompt for AI Agents
In echo/frontend/src/components/announcement/AnnouncementIcon.tsx around lines 3
to 7, the file mixes absolute imports (e.g. "@/components/announcement/hooks")
and relative imports (e.g. "./hooks") for the same module; standardize to one
import style across the file (and the project convention) by replacing the
differing import(s) so both references use the chosen form (either convert both
to "@/components/announcement/hooks" or both to "./hooks"), ensure the import
paths resolve correctly after change, and run a quick build/typecheck to verify
no path errors remain.

Comment on lines 9 to 70
export const AnnouncementIcon = () => {
const { open } = useAnnouncementDrawer();
const { language } = useLanguage();
const { data: latestAnnouncement, isLoading: isLoadingLatest } =
useLatestAnnouncement();
const { data: unreadCount, isLoading: isLoadingUnread } =
useUnreadAnnouncements();
const { open } = useAnnouncementDrawer();
const { language } = useLanguage();
const { data: latestAnnouncement, isLoading: isLoadingLatest } =
useLatestAnnouncement();
const { data: unreadCount, isLoading: isLoadingUnread } =
useUnreadAnnouncements();

// Get latest urgent announcement message
const message = latestAnnouncement
? getTranslatedContent(latestAnnouncement as Announcement, language).message
: "";
// Get latest urgent announcement message
const message = latestAnnouncement
? getTranslatedContent(latestAnnouncement as Announcement, language).message
: "";

// Check if the latest announcement is unread
const isUnread = latestAnnouncement
? !latestAnnouncement.activity?.some(
(activity: AnnouncementActivity) => activity.read === true,
)
: false;
// Check if the latest announcement is unread
const isUnread = latestAnnouncement
? !latestAnnouncement.activity?.some(
(activity: AnnouncementActivity) => activity.read === true,
)
: false;

const showMessage =
isUnread && message && latestAnnouncement?.level === "info";
const showMessage =
isUnread && message && latestAnnouncement?.level === "info";

const isLoading = isLoadingLatest || isLoadingUnread;
const isLoading = isLoadingLatest || isLoadingUnread;

return (
<Group onClick={open} gap="sm" align="center" className="cursor-pointer">
<Box>
<Indicator
inline
offset={4}
color="blue"
label={
<Box px={2} className="text-xs">
{unreadCount || 0}
</Box>
}
size={20}
disabled={(unreadCount || 0) === 0}
withBorder
>
<ActionIcon color="gray" variant="transparent">
{isLoading ? (
<Loader size="xs" />
) : (
<IconSpeakerphone className="me-1 rotate-[330deg]" />
)}
</ActionIcon>
</Indicator>
</Box>
return (
<Group onClick={open} gap="sm" align="center" className="cursor-pointer">
<Box>
<Indicator
inline
offset={4}
color="blue"
label={
<Box px={2} className="text-xs">
{unreadCount || 0}
</Box>
}
size={20}
disabled={(unreadCount || 0) === 0}
withBorder
>
<ActionIcon color="gray" variant="transparent">
{isLoading ? (
<Loader size="xs" />
) : (
<IconSpeakerphone className="me-1 rotate-[330deg]" />
)}
</ActionIcon>
</Indicator>
</Box>

{showMessage && (
<Box
className="hidden max-w-xs [mask-image:linear-gradient(to_right,black_80%,transparent)] md:block"
style={{ maxWidth: "400px" }}
>
<Markdown content={message} className="line-clamp-1" />
</Box>
)}
</Group>
);
{showMessage && (
<Box
className="hidden max-w-xs [mask-image:linear-gradient(to_right,black_80%,transparent)] md:block"
style={{ maxWidth: "400px" }}
>
<Markdown content={message} className="line-clamp-1" />
</Box>
)}
</Group>
);
};
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

Component logic ships clean.

The announcement icon implementation looks solid:

  • Proper loading states
  • Clean conditional rendering
  • Unread logic is correctly derived
  • No accessibility blockers
  • That gradient mask on line 62 for text truncation is chef's kiss 👨‍🍳

One minor optimization: the as Announcement cast on line 19 might be unnecessary if useLatestAnnouncement properly types its return value. Consider removing it if the hook already returns the correct type.

🤖 Prompt for AI Agents
In echo/frontend/src/components/announcement/AnnouncementIcon.tsx around lines 9
to 70, remove the unnecessary "as Announcement" type assertion on
latestAnnouncement when calling getTranslatedContent; instead rely on the hook's
proper return type (or update the useLatestAnnouncement hook/type if needed) so
getTranslatedContent receives the correctly typed value without an explicit
cast, then run TypeScript typecheck and adjust generics/signatures (either of
the hook or getTranslatedContent) if the compiler reports mismatches.

: "";
// Get latest urgent announcement message
const message = latestAnnouncement
? getTranslatedContent(latestAnnouncement as Announcement, language).message
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

Missing type imports – TypeScript will be unhappy.

The types Announcement (line 19) and AnnouncementActivity (line 25) are referenced but not imported. These should be pulled in from your types file.

Add the missing imports at the top:

 import { ActionIcon, Box, Group, Indicator, Loader } from "@mantine/core";
 import { IconSpeakerphone } from "@tabler/icons-react";
+import type { Announcement, AnnouncementActivity } from "@/lib/typesDirectusContent";
 import { useAnnouncementDrawer } from "@/components/announcement/hooks";

Also applies to: 25-25

🤖 Prompt for AI Agents
In echo/frontend/src/components/announcement/AnnouncementIcon.tsx around lines
19 and 25, the types Announcement and AnnouncementActivity are referenced but
not imported; add an import for Announcement and AnnouncementActivity from the
project types file (e.g. echo/frontend/src/types or the correct relative path to
your shared types file) at the top of the file so TypeScript recognizes these
types.

className?: string;
}) => {
const { projectId } = useParams();
const { projectId } = useParams();
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

Tighten types for route params.

Make useParams explicit to avoid accidental string widening and to improve DX.

-const { projectId } = useParams();
+const { projectId } = useParams<{ projectId?: string }>();
📝 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
const { projectId } = useParams();
const { projectId } = useParams<{ projectId?: string }>();
🤖 Prompt for AI Agents
In echo/frontend/src/components/aspect/AspectCard.tsx around line 16, the call
const { projectId } = useParams(); is untyped and allows string widening;
tighten the route param types by defining an explicit params interface (e.g.
interface RouteParams { projectId: string } ) and pass it as a generic to
useParams<RouteParams>(); then adjust any downstream uses (parsing to number or
handling undefined) to match the typed projectId. Ensure imports remain the same
and handle optionality if route param can be missing.

Comment on lines +29 to +30
to={`/projects/${projectId}/library/views/${data.view_id}/aspects/${data.id}`}
>
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 | 🟠 Major

Prevent undefined in URLs.

When projectId is absent, the current template literal yields /projects/undefined/.... At minimum, coerce to empty; ideally, don’t render the link until projectId is present.

- to={`/projects/${projectId}/library/views/${data.view_id}/aspects/${data.id}`}
+ to={`/projects/${projectId ?? ""}/library/views/${data.view_id}/aspects/${data.id}`}

Optional: conditionally render the <I18nLink> only if projectId exists.

📝 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
to={`/projects/${projectId}/library/views/${data.view_id}/aspects/${data.id}`}
>
to={`/projects/${projectId ?? ""}/library/views/${data.view_id}/aspects/${data.id}`}
>
🤖 Prompt for AI Agents
In echo/frontend/src/components/aspect/AspectCard.tsx around lines 29-30, the
template literal for the link can produce `/projects/undefined/...` when
projectId is missing; change it to avoid undefined by either coercing projectId
to an empty string in the path (e.g. use projectId ?? '' or String(projectId ||
'')) or, preferably, only render the <I18nLink> when projectId is present (wrap
the link in a conditional that returns null or a non-link fallback when
projectId is falsy) so no invalid URL is generated.

Comment on lines 70 to 76
const StatusIcon = ({
icon: Icon,
children,
icon: Icon,
children,
}: {
icon: any;
children: React.ReactNode;
icon: React.ElementType;
children: React.ReactNode;
}) => {
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

Good move away from any type.

Tightening the icon prop from any to React.ElementType directly addresses the PR objective to discourage the any type. This is solid progress.

One small note: if stricter typing is desired down the road, you could narrow this further to React.ComponentType<{ size?: number }> to match the icon prop being passed on line 79, but the current change is good and maintainable.

🤖 Prompt for AI Agents
In echo/frontend/src/components/form/SaveStatus.tsx around lines 70 to 76, the
icon prop is typed as React.ElementType which is fine, but to better reflect the
actual usage (we pass a size prop to the icon at line 79) change the prop type
to React.ComponentType<{ size?: number }> so the component type accepts the size
prop; update the prop type annotation and any callers if needed to satisfy the
new tighter type.

const [micAccessDenied, setMicAccessDenied] = useState(false);
const [isMicTestSuccessful, setIsMicTestSuccessful] = useState(false);
const isMicSuccessRef = useRef(false);
const displayLevel = Math.min(Math.sqrt(level / 255) * 100, 100);
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

Minor perf nit: displayLevel computed on every render.

This calc runs on every render even when not shown. Could wrap in useMemo with [level] dependency or compute inline where used (line 329).

-	const displayLevel = Math.min(Math.sqrt(level / 255) * 100, 100);

Then at line 329:

-		value={displayLevel}
+		value={Math.min(Math.sqrt(level / 255) * 100, 100)}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In echo/frontend/src/components/participant/MicrophoneTest.tsx around line 43,
the displayLevel calculation is executed on every render even when the
component/visual is not shown; change it to be memoized or moved to where it’s
used: wrap the calculation in a useMemo with [level] as the dependency (or
remove the const and compute the expression inline at the usage site on line
329) so the expensive Math.sqrt/Math.min work only when level changes or when
the value is actually required.

Comment on lines +53 to +55
} = useConversationsHealthStream(
ENABLE_CONVERSATION_HEALTH ? [conversationId] : undefined,
);
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

Don’t stream “undefined”: guard conversationId before subscribing.

Passing [conversationId] when conversationId is undefined yields ...ids=undefined. Gate on truthiness.

-  } = useConversationsHealthStream(
-    ENABLE_CONVERSATION_HEALTH ? [conversationId] : undefined,
-  );
+  } = useConversationsHealthStream(
+    ENABLE_CONVERSATION_HEALTH && conversationId ? [conversationId] : undefined,
+  );
📝 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
} = useConversationsHealthStream(
ENABLE_CONVERSATION_HEALTH ? [conversationId] : undefined,
);
} = useConversationsHealthStream(
ENABLE_CONVERSATION_HEALTH && conversationId ? [conversationId] : undefined,
);
🤖 Prompt for AI Agents
In echo/frontend/src/components/participant/ParticipantBody.tsx around lines 53
to 55, the call to useConversationsHealthStream passes [conversationId] even
when conversationId is undefined which results in streaming "...ids=undefined";
change the second argument so you only pass an array when conversationId is
truthy (e.g. ENABLE_CONVERSATION_HEALTH ? (conversationId ? [conversationId] :
undefined) : undefined) so the hook receives undefined until a real id exists.

Comment on lines +76 to +101
const combinedResult = [];
let currentUserChunks = [];

for (let i = 0; i < allMessages.length; i++) {
const message = allMessages[i];
if (message.type === "user_chunk") {
currentUserChunks.push(message);
} else {
if (currentUserChunks.length > 0) {
if (currentUserChunks.length > 1) {
combinedResult.push(combineUserChunks(currentUserChunks));
} else {
combinedResult.push(currentUserChunks[0]);
}
currentUserChunks = [];
}
combinedResult.push(message);
}
}
if (currentUserChunks.length > 0) {
if (currentUserChunks.length > 1) {
combinedResult.push(combineUserChunks(currentUserChunks));
} else {
combinedResult.push(currentUserChunks[0]);
}
}
for (let i = 0; i < allMessages.length; i++) {
const message = allMessages[i];
if (message.type === "user_chunk") {
currentUserChunks.push(message);
} else {
if (currentUserChunks.length > 0) {
if (currentUserChunks.length > 1) {
combinedResult.push(combineUserChunks(currentUserChunks));
} else {
combinedResult.push(currentUserChunks[0]);
}
currentUserChunks = [];
}
combinedResult.push(message);
}
}
if (currentUserChunks.length > 0) {
if (currentUserChunks.length > 1) {
combinedResult.push(combineUserChunks(currentUserChunks));
} else {
combinedResult.push(currentUserChunks[0]);
}
}
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

Make the message assembly typed; avoid implicit any[].

Biome “no‑any” goal: seed types for combinedResult and currentUserChunks to keep the union precise and eliminate implicit anys.

Right after computing userChunks and replies:

type UserMsg = (typeof userChunks)[number];
type AssistantMsg = (typeof replies)[number];
type CombinedMessage = UserMsg | AssistantMsg;

Then:

- const combinedResult = [];
- let currentUserChunks = [];
+ const combinedResult: CombinedMessage[] = [];
+ let currentUserChunks: UserMsg[] = [];
🤖 Prompt for AI Agents
In echo/frontend/src/components/participant/ParticipantBody.tsx around lines 76
to 101, the arrays combinedResult and currentUserChunks are implicitly any[];
declare explicit types to avoid implicit any by deriving message types from
userChunks and replies (e.g. create UserMsg = (typeof userChunks)[number],
AssistantMsg = (typeof replies)[number], CombinedMessage = UserMsg |
AssistantMsg) then annotate combinedResult as CombinedMessage[] and
currentUserChunks as UserMsg[]; ensure combineUserChunks is typed to return
UserMsg (or CombinedMessage) so push operations are type-safe and update any
downstream usages to accept CombinedMessage.

Comment on lines +108 to +112
useEffect(() => {
if (interleaveMessages && bottomRef.current) {
bottomRef.current.scrollIntoView();
}
}, [interleaveMessages]);
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 | 🟠 Major

Auto‑scroll currently NOP: effect watches the wrong dep and the target ref is hidden.

You scroll when interleaveMessages toggles, but the sentinel <div> is hidden unless viewResponses is true. Result: no scroll in the interleaved view.

 useEffect(() => {
-  if (interleaveMessages && bottomRef.current) {
-    bottomRef.current.scrollIntoView();
-  }
-}, [interleaveMessages]);
+  if (interleaveMessages && bottomRef.current) {
+    bottomRef.current.scrollIntoView({ behavior: "smooth", block: "end" });
+  }
+}, [interleaveMessages, combinedMessages.length]);
-  <div ref={bottomRef} className={viewResponses ? "" : "hidden"} />
+  <div ref={bottomRef} />

Also applies to: 258-259

🤖 Prompt for AI Agents
In echo/frontend/src/components/participant/ParticipantBody.tsx around lines
108-112, the effect only watches interleaveMessages and tries to scroll a
sentinel that is hidden unless viewResponses is true, so auto-scroll never
happens in the interleaved view; update the effect to include viewResponses (and
any message list/state that should trigger a scroll) in its dependency array and
guard the scroll call with a check that bottomRef.current exists and
viewResponses is true (or target the visible container element instead), and
apply the same fix to the similar effect at lines 258-259 so the scroll runs
only when the sentinel is present and the relevant state changes.

@spashii spashii added this pull request to the merge queue Oct 20, 2025
Merged via the queue into main with commit 447421e Oct 20, 2025
11 checks passed
@spashii spashii deleted the implement-biome-rules branch October 20, 2025 08:41
spashii pushed a commit that referenced this pull request Nov 18, 2025
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
  * Announcement error state with retry
* Improved upload experience: inline file renaming and per-file upload
status

* **Bug Fixes**
  * Better error handling and recovery for mutations and queries
  * Fixes to conversation duration and related edge cases

* **Improvements**
  * Refined routing/navigation structure
  * Improved optimistic updates with rollback
  * Enhanced participant onboarding and UI polish
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants