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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ dist-ssr
coverage/
.sonar/
*.tsbuildinfo
jwt_private_key.pem
Original file line number Diff line number Diff line change
Expand Up @@ -94,21 +94,41 @@ export function CardBubble({ message }: { readonly message: CardMessage }) {
p: ({ node, ...props }) => (
<p className="mb-1.5 last:mb-0 leading-snug" {...props} />
),

ol: ({ node, ...props }) => (
<ol className="my-1.5 list-decimal pl-6 [&>li]:mb-0.5 [&>li:last-child]:mb-0" {...props} />
),
ul: ({ node, ...props }) => (
<ul className="my-1.5 list-disc pl-6 [&>li]:mb-0.5 [&>li:last-child]:mb-0" {...props} />
),

li: ({ node, ...props }) => (
<li className="pl-1" {...props} />
<li className="pl-1" {...props} />
),

strong: ({ node, ...props }) => (
<span className="font-semibold text-foreground/90" {...props} />
),
table: ({ node, ...props }) => (
<div className="my-2 w-full overflow-x-auto rounded-md border border-[#E5E7EB]">
<table className="w-full border-collapse text-sm" {...props} />
</div>
),
thead: ({ node, ...props }) => (
<thead className="bg-[#F9FAFB]" {...props} />
),
tbody: ({ node, ...props }) => (
<tbody className="bg-white" {...props} />
),
tr: ({ node, ...props }) => (
<tr className="border-b border-[#E5E7EB] last:border-b-0" {...props} />
),
th: ({ node, ...props }) => (
<th className="px-3 py-2 text-left text-xs font-semibold tracking-wide text-[#111827]" {...props} />
),
td: ({ node, ...props }) => (
<td className="px-3 py-2 align-top text-sm text-[#111827]" {...props} />
),
}}
>
{message.body}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ import { MessageChrome } from "./message-chrome";
import { ChatMessage } from "./bubbles/chat-types";
import { Bubble } from "./bubbles";
import { AILoader } from "./ai-loader";
import {
markAnswerRendered,
logResponseEvent,
startTelemetry,
endTelemetry,
} from "@/lib/telemetry";
import { useChatStore } from "@/hooks/store/chat";

/* eslint-disable no-unused-vars */
type MessageListProps = {
Expand All @@ -16,6 +23,7 @@ type MessageListProps = {

export function MessageList(props: MessageListProps) {
const bottomRef = useRef<HTMLDivElement | null>(null);
const loggedResponseQidsRef = useRef<Set<string>>(new Set());

useEffect(() => {
if (props.messages.length > 0) {
Expand All @@ -28,36 +36,40 @@ export function MessageList(props: MessageListProps) {
}
}

// Performance Tracking
const lastMessage = props.messages[props.messages.length - 1];
if (
lastMessage &&
lastMessage.role === "assistant" &&
lastMessage.type === "card" &&
lastMessage.questionId &&
!props.isAssistantTyping // Only log when fully done? Or maybe streams behave differently.
// In original OAN-UI it seemed to log after paint.
// We'll trust that isAssistantTyping false means done.
) {
// We need to import these functions or pass them down.
// Ideally we'd import them directly since they are singletons/globals basically.
import("@/lib/telemetry").then(({ markAnswerRendered, logResponseEvent }) => {
// We need the session ID, which isn't in props.
// But the store has it. Or we can just rely on the global state in telemetry if it persists?
// Actually `logResponseEvent` needs sessionId.
// Maybe we should pass sessionId to MessageList?
// Or we can import the store to get sessionId.
import("@/hooks/store/chat").then(({ useChatStore }) => {
const sessionId = useChatStore.getState().sessionId;
if(sessionId && lastMessage.questionText && lastMessage.body) {
const pipeline = lastMessage.type === "card" && "pipeline" in lastMessage ? lastMessage.pipeline : undefined;
markAnswerRendered(lastMessage.questionId!, () => {
logResponseEvent(lastMessage.questionId!, sessionId, lastMessage.questionText!, lastMessage.body, pipeline);
// endTelemetryWithWait is called in store action
});
}
});
});
// Performance + response telemetry tracking.
// Process all completed assistant messages so follow-up Q&A pairs are not skipped.
if (props.isAssistantTyping) return;

const sessionId = useChatStore.getState().sessionId;
const userDetails = useChatStore.getState().getUserForTelemetry();
if (!sessionId) return;

for (const msg of props.messages) {
if (msg.role !== "assistant" || msg.type !== "card") continue;
if (!msg.questionId || !msg.questionText || !msg.body) continue;
if (!msg.showListenRow) continue;

const questionId = msg.questionId;
if (loggedResponseQidsRef.current.has(questionId)) continue;

const pipeline = "pipeline" in msg ? msg.pipeline : undefined;
loggedResponseQidsRef.current.add(questionId);
markAnswerRendered(questionId, async () => {
try {
await startTelemetry(sessionId, userDetails);
logResponseEvent(
questionId,
sessionId,
msg.questionText!,
msg.body,
pipeline,
);
endTelemetry();
} catch (error) {
console.warn("Telemetry failed (response event)", error);
loggedResponseQidsRef.current.delete(questionId);
}
});
}
}, [props.messages.length, props.isAssistantTyping, props.messages]);

Expand Down
24 changes: 12 additions & 12 deletions src/hooks/store/chat/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,12 @@ function makeAssistantMessage(text: string, isError = false, showListenRow = fal
};
}

function normalizeAssistantBodyForDisplay(text: string): string {
// Backend guardrail prefixes milk-collection payloads with a success line.
// Remove that prefix so the chat starts directly with markdown sections/tables.
return text.replace(/^Farmer milk collection details fetched successfully:\s*\n*/i, "");
}

import { playTTS as playTTSHelper } from "@/lib/audio-utils";
import { ANONYMOUS_BOOTSTRAP_SESSION_KEY } from "@/lib/anonymous-bootstrap";

Expand Down Expand Up @@ -297,17 +303,18 @@ export const useChatStore = create<ChatStore>((set, get) => ({
}

set((state) => {
const displayBody = normalizeAssistantBodyForDisplay(streamingText);
const lastMsg = state.messages[state.messages.length - 1];
if (lastMsg && lastMsg.role === "assistant" && lastMsg.type === "card") {
return {
messages: [
...state.messages.slice(0, -1),
{ ...lastMsg, body: streamingText }
{ ...lastMsg, body: displayBody }
]
};
} else {
return {
messages: [...state.messages, { ...makeAssistantMessage(streamingText), questionId, questionText: trimmed, pipeline: useTranslationPipeline ? 'oss_translate' : 'default' }]
messages: [...state.messages, { ...makeAssistantMessage(displayBody), questionId, questionText: trimmed, pipeline: useTranslationPipeline ? 'oss_translate' : 'default' }]
};
}
});
Expand All @@ -329,14 +336,6 @@ export const useChatStore = create<ChatStore>((set, get) => ({
return { isAssistantTyping: false };
});

try {
const userDetailsResponse = get().getUserForTelemetry();
await telemetry.startTelemetry(currentSession, userDetailsResponse);
await telemetry.endTelemetryWithWait(questionId);
} catch (e) {
console.warn("Telemetry failed (response event)", e);
}

// Use inline suggestions from stream if available, fall back to API
const parsedInlineSuggestions = Array.isArray(inlineSuggestions) ? inlineSuggestions : [];
if (parsedInlineSuggestions.length > 0) {
Expand Down Expand Up @@ -532,7 +531,8 @@ export const useChatStore = create<ChatStore>((set, get) => ({
const msg = messages.find(m => m.id === messageId);
if (!msg) return;

const userMsg = messages.findLast((m) => m.role === 'user');
const feedbackQuestionId = msg.questionId || messageId;
const userMsg = messages.findLast((m) => m.role === 'user' && m.questionId === msg.questionId);
const questionText = userMsg && userMsg.type === 'text' ? userMsg.text : "";
const responseText = msg && msg.type === 'card' ? msg.body : "";
const feedbackType = isPositive ? "like" : "dislike";
Expand All @@ -547,7 +547,7 @@ export const useChatStore = create<ChatStore>((set, get) => ({
email: user?.email || ""
});
telemetry.logFeedbackEvent(
messageId,
feedbackQuestionId,
sessionId,
feedbackMsg,
feedbackType,
Expand Down
Loading