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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 7 additions & 55 deletions frontend/components/message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,44 +5,14 @@ import { getToolName, isToolUIPart } from "ai";
import equal from "fast-deep-equal";
import Image from "next/image";
import { memo } from "react";
import { TOOL_PANELS } from "@/features/tool-panel-registry";
import type { ChatMessage } from "@/lib/types";
import { cn, sanitizeText } from "@/lib/utils";
import { BouncingDots } from "./elements/bouncing-dots";
import { MessageContent } from "./elements/message";
import { Response } from "./elements/response";
import { ToolCall } from "./elements/tool-call";
import { MessageActions } from "./message-actions";
import { RetrievedTracksPanel } from "./retrieved-tracks-panel";
import { SessionListPanel } from "./session-list-panel";
import { SessionVisualization } from "./session-visualization";

function extractSessionId(input: unknown): string | null {
if (input && typeof input === "object" && "session_id" in input) {
const value = (input as { session_id: unknown }).session_id;
if (typeof value === "string" && value.length > 0) {
return value;
}
}
return null;
}

// The list_sessions tool embeds each session's UUID in an HTML comment
// (`<!-- id=... -->`) on its line so the agent can resolve "Session 07" → UUID
// without leaking the UUID to the user. We extract the same comments here so
// the rendered panel mirrors exactly what the agent decided to show — order
// and all — instead of duplicating the tool's filter logic on the frontend.
const SESSION_ID_COMMENT = /<!--\s*id=([0-9a-f-]+)\s*-->/gi;

function extractSessionIdsFromOutput(output: unknown): string[] | null {
if (typeof output !== "string") {
return null;
}
const ids: string[] = [];
for (const match of output.matchAll(SESSION_ID_COMMENT)) {
ids.push(match[1]);
}
return ids.length > 0 ? ids : null;
}

const AssistantAvatar = ({ isLoading }: { isLoading?: boolean }) => (
<div
Expand Down Expand Up @@ -129,36 +99,18 @@ const PurePreviewMessage = ({
}
if (isToolUIPart(part)) {
const toolName = getToolName(part);
const isOutputAvailable = part.state === "output-available";
const analyzeSessionId =
toolName === "analyze_session" && isOutputAvailable
? extractSessionId(part.input)
: null;
const retrievalSessionId =
toolName === "retrieve_tracks_from_brain_state" &&
isOutputAvailable
? extractSessionId(part.input)
: null;
const showSessionList =
toolName === "list_sessions" && isOutputAvailable;
const sessionListIds = showSessionList
? extractSessionIdsFromOutput(part.output)
: null;
const entry = TOOL_PANELS[toolName];
const showPanel =
entry != null && part.state === "output-available";
return (
<div className="flex flex-col gap-2" key={key}>
<ToolCall
hideOutput={showSessionList}
hideOutput={showPanel && entry.hideRawOutput === true}
isStreaming={isLoading && !hasTextParts}
part={part}
/>
{analyzeSessionId && (
<SessionVisualization sessionId={analyzeSessionId} />
)}
{retrievalSessionId && (
<RetrievedTracksPanel sessionId={retrievalSessionId} />
)}
{showSessionList && (
<SessionListPanel sessionIds={sessionListIds} />
{showPanel && (
<entry.Panel input={part.input} output={part.output} />
)}
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { WaveformViz } from "@/components/waveform-viz";
import { BACKEND_URL } from "@/lib/constants";
import { cn } from "@/lib/utils";
import { WaveformViz } from "./waveform-viz";

// Backend serves the cached m4a under its own CORS policy so wavesurfer can
// fetch + decodeAudioData; Apple's preview CDN does not reliably set CORS
Expand Down
20 changes: 20 additions & 0 deletions frontend/features/retrieval/tool-panels.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"use client";

import type {
ToolPanelEntry,
ToolPanelProps,
} from "@/features/tool-panel-registry";
import { extractSessionId } from "@/lib/extract-session-id";
import { RetrievedTracksPanel } from "./retrieved-tracks-panel";

function RetrieveTracksPanel({ input }: ToolPanelProps) {
const sessionId = extractSessionId(input);
if (!sessionId) {
return null;
}
return <RetrievedTracksPanel sessionId={sessionId} />;
}

export const RETRIEVAL_TOOL_PANELS: Record<string, ToolPanelEntry> = {
retrieve_tracks_from_brain_state: { Panel: RetrieveTracksPanel },
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
import { memo } from "react";
import type { SessionSummarySchema } from "@/api/generated/types.gen";
import { useEnrichedSessions } from "@/api/hooks/sessions";
import { useChatActions } from "@/components/chat-actions-provider";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { useChatActions } from "./chat-actions-provider";

const QUADRANT_ORDER = ["relaxed", "calm", "excited", "stressed"] as const;
type Quadrant = (typeof QUADRANT_ORDER)[number];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ import type {
TrajectorySummary,
} from "@/api/generated/types.gen";
import { useSessionSegments } from "@/api/hooks/sessions";
import { EmotionTrajectory } from "@/components/emotion-trajectory";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { EmotionTrajectory } from "./emotion-trajectory";

type Props = {
sessionId: string;
Expand Down
44 changes: 44 additions & 0 deletions frontend/features/sessions/tool-panels.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"use client";

import type {
ToolPanelEntry,
ToolPanelProps,
} from "@/features/tool-panel-registry";
import { extractSessionId } from "@/lib/extract-session-id";
import { SessionListPanel } from "./session-list-panel";
import { SessionVisualization } from "./session-visualization";

// The list_sessions tool embeds each session's UUID in an HTML comment
// (`<!-- id=... -->`) on its line so the agent can resolve "Session 07" → UUID
// without leaking the UUID to the user. We extract the same comments here so
// the rendered panel mirrors exactly what the agent decided to show — order
// and all — instead of duplicating the tool's filter logic on the frontend.
const SESSION_ID_COMMENT = /<!--\s*id=([0-9a-f-]+)\s*-->/gi;

function extractSessionIdsFromOutput(output: unknown): string[] | null {
if (typeof output !== "string") {
return null;
}
const ids: string[] = [];
for (const match of output.matchAll(SESSION_ID_COMMENT)) {
ids.push(match[1]);
}
return ids.length > 0 ? ids : null;
}

function AnalyzeSessionPanel({ input }: ToolPanelProps) {
const sessionId = extractSessionId(input);
if (!sessionId) {
return null;
}
return <SessionVisualization sessionId={sessionId} />;
}

function ListSessionsPanel({ output }: ToolPanelProps) {
return <SessionListPanel sessionIds={extractSessionIdsFromOutput(output)} />;
}

export const SESSIONS_TOOL_PANELS: Record<string, ToolPanelEntry> = {
analyze_session: { Panel: AnalyzeSessionPanel },
list_sessions: { Panel: ListSessionsPanel, hideRawOutput: true },
};
19 changes: 19 additions & 0 deletions frontend/features/tool-panel-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { ComponentType } from "react";
import { RETRIEVAL_TOOL_PANELS } from "./retrieval/tool-panels";
import { SESSIONS_TOOL_PANELS } from "./sessions/tool-panels";

export type ToolPanelProps = { input: unknown; output: unknown };

export type ToolPanelEntry = {
Panel: ComponentType<ToolPanelProps>;
/** Suppress <ToolCall>'s raw output block when the panel replaces it. */
hideRawOutput?: boolean;
};

// Composition root: the only file that imports across feature slices.
// Each slice exports its own tool-name -> panel map; adding a feature is
// one spread here. message.tsx does a lookup — never per-tool branching.
export const TOOL_PANELS: Record<string, ToolPanelEntry> = {
...SESSIONS_TOOL_PANELS,
...RETRIEVAL_TOOL_PANELS,
};
9 changes: 9 additions & 0 deletions frontend/lib/extract-session-id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export function extractSessionId(input: unknown): string | null {
if (input && typeof input === "object" && "session_id" in input) {
const value = (input as { session_id: unknown }).session_id;
if (typeof value === "string" && value.length > 0) {
return value;
}
}
return null;
}
Loading