Boopster commited on
Commit
8fc5f54
Β·
1 Parent(s): b152b60

feat: Reworked the observability panel's conversation log into a chat-style UI with detailed message types and timestamps.

Browse files
frontend/src/components/ObservabilityPanel.tsx CHANGED
@@ -17,7 +17,7 @@ import {
17
  DollarSign,
18
  ChevronDown,
19
  ChevronRight,
20
- Terminal,
21
  } from "lucide-react";
22
  import { useObservability } from "@/hooks/useObservability";
23
  import { Message } from "@/hooks/useConversation";
@@ -557,25 +557,65 @@ function ToolCallRow({ tool }: { tool: { id: string; name: string; status: strin
557
  );
558
  }
559
 
560
- // ---- Conversation Log ----
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
561
 
562
- function LogDetailBlock({ data, label }: { data: Record<string, unknown>; label: string }) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
563
  const [expanded, setExpanded] = useState(false);
564
 
565
- // Compact one-liner summary
566
  const keys = Object.keys(data);
567
- const summary = keys.length <= 3
568
- ? keys.map((k) => {
569
- const v = data[k];
570
- const display = typeof v === "string"
571
- ? (v.length > 40 ? v.slice(0, 40) + "…" : v)
572
- : JSON.stringify(v);
573
- return `${k}: ${display}`;
574
- }).join(", ")
575
- : `${keys.length} fields`;
 
 
 
 
 
 
576
 
577
  return (
578
- <div style={{ marginTop: 2, marginLeft: 4 }}>
579
  <button
580
  onClick={() => setExpanded((p) => !p)}
581
  style={{
@@ -587,42 +627,48 @@ function LogDetailBlock({ data, label }: { data: Record<string, unknown>; label:
587
  alignItems: "center",
588
  gap: 4,
589
  fontSize: 11,
590
- fontFamily: "var(--font-mono)",
591
  color: "var(--color-text-muted)",
592
  }}
593
  >
594
  <span style={{ fontSize: 9, opacity: 0.7 }}>{expanded ? "β–Ύ" : "β–Έ"}</span>
595
- <span style={{
596
- padding: "1px 5px",
597
- borderRadius: 3,
598
- background: "rgba(255, 255, 255, 0.05)",
599
- border: "1px solid rgba(255, 255, 255, 0.08)",
600
- fontSize: 9,
601
- fontWeight: 700,
602
- textTransform: "uppercase",
603
- letterSpacing: "0.05em",
604
- }}>
 
 
605
  {label}
606
  </span>
607
  {!expanded && (
608
- <span style={{ opacity: 0.5, fontSize: 11 }}>{summary}</span>
 
 
609
  )}
610
  </button>
611
  {expanded && (
612
- <pre style={{
613
- marginTop: 4,
614
- padding: "8px 10px",
615
- background: "rgba(30, 30, 30, 0.8)",
616
- border: "1px solid rgba(255, 255, 255, 0.06)",
617
- borderRadius: 6,
618
- fontSize: 11,
619
- lineHeight: 1.5,
620
- color: "var(--color-text-secondary)",
621
- overflowX: "auto",
622
- maxHeight: 200,
623
- whiteSpace: "pre-wrap",
624
- wordBreak: "break-word",
625
- }}>
 
 
 
626
  {JSON.stringify(data, null, 2)}
627
  </pre>
628
  )}
@@ -633,6 +679,12 @@ function LogDetailBlock({ data, label }: { data: Record<string, unknown>; label:
633
  function ConversationLog({ messages }: { messages: Message[] }) {
634
  const [isOpen, setIsOpen] = useState(true);
635
  const scrollRef = useRef<HTMLDivElement>(null);
 
 
 
 
 
 
636
 
637
  useEffect(() => {
638
  if (isOpen && scrollRef.current) {
@@ -640,26 +692,11 @@ function ConversationLog({ messages }: { messages: Message[] }) {
640
  }
641
  }, [messages, isOpen]);
642
 
643
- const formatTime = (timestamp: string) => {
644
- try {
645
- const d = new Date(timestamp);
646
- return d.toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" });
647
- } catch {
648
- return "??:??:??";
649
- }
650
- };
651
-
652
- const roleConfig: Record<string, { label: string; color: string }> = {
653
- user: { label: "YOU", color: "var(--color-cta)" },
654
- assistant: { label: "AI", color: "var(--color-accent-cyan)" },
655
- tool: { label: "TOOL", color: "var(--color-accent-pink)" },
656
- system: { label: "SYS", color: "var(--color-text-muted)" },
657
- };
658
-
659
  const logMessages = messages.filter((m) => !m.isPartial);
660
 
661
  return (
662
  <div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
 
663
  <button
664
  onClick={() => setIsOpen((prev) => !prev)}
665
  style={{
@@ -674,17 +711,18 @@ function ConversationLog({ messages }: { messages: Message[] }) {
674
  }}
675
  >
676
  <SectionLabel
677
- icon={<Terminal className="w-3.5 h-3.5" />}
678
- label="Conversation Log"
679
  />
680
  <div style={{ marginLeft: "auto", display: "flex", alignItems: "center", gap: 4 }}>
681
  {logMessages.length > 0 && (
682
  <span className="pill pill-cyan text-[9px] font-bold">{logMessages.length}</span>
683
  )}
684
- {isOpen
685
- ? <ChevronDown className="w-3.5 h-3.5 text-muted" />
686
- : <ChevronRight className="w-3.5 h-3.5 text-muted" />
687
- }
 
688
  </div>
689
  </button>
690
 
@@ -694,103 +732,230 @@ function ConversationLog({ messages }: { messages: Message[] }) {
694
  style={{
695
  flex: 1,
696
  overflowY: "auto",
697
- background: "rgba(20, 20, 20, 0.9)",
698
  borderTop: "1px solid rgba(255, 255, 255, 0.06)",
699
- padding: "8px 20px",
700
- fontFamily: "var(--font-mono)",
701
- fontSize: 13,
702
- lineHeight: 1.7,
703
  }}
704
  >
705
  {logMessages.length === 0 ? (
706
- <p style={{ fontSize: 12, color: "var(--color-text-muted)", fontStyle: "italic", textAlign: "center", padding: "16px 0" }}>
707
- No conversation yet
708
- </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
709
  ) : (
710
  logMessages.map((msg) => {
711
- const cfg = roleConfig[msg.role] || roleConfig.assistant;
712
- const isSystem = msg.role === "system";
713
- const isTool = msg.role === "tool" && msg.tool;
714
 
715
- // Determine status pill for tool messages
716
- const toolStatusPill = isTool ? (
717
- <span
718
- className={`pill text-[9px] font-bold ${
719
- msg.tool!.status === "completed"
720
- ? "pill-cyan"
721
- : msg.tool!.status === "error"
722
- ? "pill-pink"
723
- : "pill-lavender"
724
- }`}
725
- style={{ marginLeft: 6 }}
726
- >
727
- {msg.tool!.status}
728
- </span>
729
- ) : null;
730
-
731
- // GenUI badge
732
- const genuiBadge = msg.component ? (
733
- <span style={{
734
- marginLeft: 6,
735
- padding: "1px 6px",
736
- background: "rgba(168, 218, 220, 0.12)",
737
- border: "1px solid rgba(168, 218, 220, 0.2)",
738
- borderRadius: 4,
739
- color: "var(--color-accent-cyan)",
740
- fontSize: 9,
741
- fontWeight: 600,
742
- }}>
743
- GenUI: {msg.component.name}
744
- </span>
745
- ) : null;
746
 
747
- return (
748
- <div key={msg.id} style={{ marginBottom: isSystem ? 2 : 4 }}>
749
- {/* Main row */}
750
- <div style={{
751
- display: "flex",
752
- gap: 6,
753
- alignItems: "baseline",
754
- ...(isSystem ? { opacity: 0.6, fontStyle: "italic" } : {}),
755
- }}>
756
- <span style={{ color: "var(--color-text-muted)", flexShrink: 0 }}>
757
- [{formatTime(msg.timestamp)}]
758
- </span>
759
- <span style={{ color: cfg.color, fontWeight: 700, flexShrink: 0, minWidth: 32 }}>
760
- {cfg.label}
761
- </span>
762
- <span style={{ color: "var(--color-text-muted)" }}>β–Έ</span>
763
- <span style={{ color: "var(--color-text-secondary)", wordBreak: "break-word", flex: 1 }}>
764
- {isTool ? (
765
- <>
766
- <span style={{ color: "var(--color-accent-pink)", fontWeight: 600 }}>
767
- {msg.tool!.name}
768
- </span>
769
- {toolStatusPill}
770
- </>
771
- ) : (
772
- msg.content || "(no text)"
773
- )}
774
- {genuiBadge}
775
  </span>
776
  </div>
777
-
778
- {/* Expandable detail blocks */}
779
- {isTool && msg.tool!.args && Object.keys(msg.tool!.args).length > 0 && (
780
- <div style={{ marginLeft: 90 }}>
781
- <LogDetailBlock data={msg.tool!.args} label="Input" />
782
- </div>
783
- )}
784
- {isTool && msg.tool!.result && Object.keys(msg.tool!.result).length > 0 && (
785
- <div style={{ marginLeft: 90 }}>
786
- <LogDetailBlock data={msg.tool!.result} label="Output" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
787
  </div>
788
- )}
789
- {msg.component && Object.keys(msg.component.props).length > 0 && (
790
- <div style={{ marginLeft: 90 }}>
791
- <LogDetailBlock data={msg.component.props} label="Props" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
792
  </div>
793
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
794
  </div>
795
  );
796
  })
 
17
  DollarSign,
18
  ChevronDown,
19
  ChevronRight,
20
+
21
  } from "lucide-react";
22
  import { useObservability } from "@/hooks/useObservability";
23
  import { Message } from "@/hooks/useConversation";
 
557
  );
558
  }
559
 
560
+ // ---- Conversation Log (Chat-style) ----
561
+
562
+ function relativeTime(timestamp: string): string {
563
+ try {
564
+ const now = Date.now();
565
+ const then = new Date(timestamp).getTime();
566
+ const diff = Math.max(0, Math.floor((now - then) / 1000));
567
+ if (diff < 5) return "just now";
568
+ if (diff < 60) return `${diff}s ago`;
569
+ const mins = Math.floor(diff / 60);
570
+ if (mins < 60) return `${mins}m ago`;
571
+ const hrs = Math.floor(mins / 60);
572
+ return `${hrs}h ${mins % 60}m ago`;
573
+ } catch {
574
+ return "";
575
+ }
576
+ }
577
 
578
+ function fullTime(timestamp: string): string {
579
+ try {
580
+ return new Date(timestamp).toLocaleTimeString("en-GB", {
581
+ hour: "2-digit",
582
+ minute: "2-digit",
583
+ second: "2-digit",
584
+ });
585
+ } catch {
586
+ return "";
587
+ }
588
+ }
589
+
590
+ /** Friendly tool name: "log_entry" β†’ "Log Entry" */
591
+ function friendlyToolName(name: string): string {
592
+ return name
593
+ .replace(/_/g, " ")
594
+ .replace(/\b\w/g, (c) => c.toUpperCase());
595
+ }
596
+
597
+ function ToolDetailBlock({ data, label }: { data: Record<string, unknown>; label: string }) {
598
  const [expanded, setExpanded] = useState(false);
599
 
 
600
  const keys = Object.keys(data);
601
+ const summary =
602
+ keys.length <= 3
603
+ ? keys
604
+ .map((k) => {
605
+ const v = data[k];
606
+ const display =
607
+ typeof v === "string"
608
+ ? v.length > 40
609
+ ? v.slice(0, 40) + "…"
610
+ : v
611
+ : JSON.stringify(v);
612
+ return `${k}: ${display}`;
613
+ })
614
+ .join(", ")
615
+ : `${keys.length} fields`;
616
 
617
  return (
618
+ <div style={{ marginTop: 4 }}>
619
  <button
620
  onClick={() => setExpanded((p) => !p)}
621
  style={{
 
627
  alignItems: "center",
628
  gap: 4,
629
  fontSize: 11,
 
630
  color: "var(--color-text-muted)",
631
  }}
632
  >
633
  <span style={{ fontSize: 9, opacity: 0.7 }}>{expanded ? "β–Ύ" : "β–Έ"}</span>
634
+ <span
635
+ style={{
636
+ padding: "1px 5px",
637
+ borderRadius: 3,
638
+ background: "rgba(255, 255, 255, 0.05)",
639
+ border: "1px solid rgba(255, 255, 255, 0.08)",
640
+ fontSize: 9,
641
+ fontWeight: 700,
642
+ textTransform: "uppercase",
643
+ letterSpacing: "0.05em",
644
+ }}
645
+ >
646
  {label}
647
  </span>
648
  {!expanded && (
649
+ <span style={{ opacity: 0.5, fontSize: 11, fontFamily: "var(--font-mono)" }}>
650
+ {summary}
651
+ </span>
652
  )}
653
  </button>
654
  {expanded && (
655
+ <pre
656
+ style={{
657
+ marginTop: 4,
658
+ padding: "8px 10px",
659
+ background: "rgba(20, 20, 20, 0.8)",
660
+ border: "1px solid rgba(255, 255, 255, 0.06)",
661
+ borderRadius: 6,
662
+ fontSize: 11,
663
+ lineHeight: 1.5,
664
+ fontFamily: "var(--font-mono)",
665
+ color: "var(--color-text-secondary)",
666
+ overflowX: "auto",
667
+ maxHeight: 200,
668
+ whiteSpace: "pre-wrap",
669
+ wordBreak: "break-word",
670
+ }}
671
+ >
672
  {JSON.stringify(data, null, 2)}
673
  </pre>
674
  )}
 
679
  function ConversationLog({ messages }: { messages: Message[] }) {
680
  const [isOpen, setIsOpen] = useState(true);
681
  const scrollRef = useRef<HTMLDivElement>(null);
682
+ // Re-render every 30s so relative timestamps update
683
+ const [, setTick] = useState(0);
684
+ useEffect(() => {
685
+ const id = setInterval(() => setTick((t) => t + 1), 30_000);
686
+ return () => clearInterval(id);
687
+ }, []);
688
 
689
  useEffect(() => {
690
  if (isOpen && scrollRef.current) {
 
692
  }
693
  }, [messages, isOpen]);
694
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
695
  const logMessages = messages.filter((m) => !m.isPartial);
696
 
697
  return (
698
  <div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
699
+ {/* Header toggle */}
700
  <button
701
  onClick={() => setIsOpen((prev) => !prev)}
702
  style={{
 
711
  }}
712
  >
713
  <SectionLabel
714
+ icon={<MessageSquare className="w-3.5 h-3.5" />}
715
+ label="Conversation"
716
  />
717
  <div style={{ marginLeft: "auto", display: "flex", alignItems: "center", gap: 4 }}>
718
  {logMessages.length > 0 && (
719
  <span className="pill pill-cyan text-[9px] font-bold">{logMessages.length}</span>
720
  )}
721
+ {isOpen ? (
722
+ <ChevronDown className="w-3.5 h-3.5 text-muted" />
723
+ ) : (
724
+ <ChevronRight className="w-3.5 h-3.5 text-muted" />
725
+ )}
726
  </div>
727
  </button>
728
 
 
732
  style={{
733
  flex: 1,
734
  overflowY: "auto",
735
+ background: "rgba(18, 18, 18, 0.95)",
736
  borderTop: "1px solid rgba(255, 255, 255, 0.06)",
737
+ padding: "16px 20px",
738
+ display: "flex",
739
+ flexDirection: "column",
740
+ gap: 6,
741
  }}
742
  >
743
  {logMessages.length === 0 ? (
744
+ <div
745
+ style={{
746
+ display: "flex",
747
+ flexDirection: "column",
748
+ alignItems: "center",
749
+ justifyContent: "center",
750
+ gap: 8,
751
+ padding: "32px 0",
752
+ color: "var(--color-text-muted)",
753
+ }}
754
+ >
755
+ <MessageSquare className="w-6 h-6" style={{ opacity: 0.3 }} />
756
+ <p style={{ fontSize: 13, fontStyle: "italic", margin: 0 }}>
757
+ Start talking to see the conversation here
758
+ </p>
759
+ </div>
760
  ) : (
761
  logMessages.map((msg) => {
762
+ const isUser = msg.role === "user";
 
 
763
 
764
+ const isTool = msg.role === "tool" && msg.tool;
765
+ const isSystem = msg.role === "system";
766
+ const isGenUI = msg.role === "assistant" && !!msg.component;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
767
 
768
+ // ── System event chip (centered) ──
769
+ if (isSystem) {
770
+ return (
771
+ <div
772
+ key={msg.id}
773
+ style={{
774
+ display: "flex",
775
+ justifyContent: "center",
776
+ padding: "4px 0",
777
+ }}
778
+ >
779
+ <span
780
+ title={fullTime(msg.timestamp)}
781
+ style={{
782
+ display: "inline-flex",
783
+ alignItems: "center",
784
+ gap: 6,
785
+ padding: "4px 14px",
786
+ borderRadius: 20,
787
+ background: "rgba(255, 255, 255, 0.04)",
788
+ border: "1px solid rgba(255, 255, 255, 0.08)",
789
+ fontSize: 11,
790
+ color: "var(--color-text-muted)",
791
+ fontWeight: 500,
792
+ }}
793
+ >
794
+ {msg.content}
 
795
  </span>
796
  </div>
797
+ );
798
+ }
799
+
800
+ // ── Tool call badge (compact inline) ──
801
+ if (isTool) {
802
+ const toolStatus = msg.tool!.status;
803
+ const statusEmoji =
804
+ toolStatus === "completed" ? "βœ“" : toolStatus === "error" ? "βœ—" : "β‹―";
805
+ const statusColor =
806
+ toolStatus === "completed"
807
+ ? "var(--color-success)"
808
+ : toolStatus === "error"
809
+ ? "var(--color-error)"
810
+ : "var(--color-accent-cyan)";
811
+
812
+ return (
813
+ <div key={msg.id} style={{ padding: "2px 0" }}>
814
+ <div
815
+ style={{
816
+ display: "inline-flex",
817
+ alignItems: "center",
818
+ gap: 6,
819
+ padding: "5px 12px",
820
+ borderRadius: 8,
821
+ background: "rgba(255, 193, 204, 0.06)",
822
+ border: "1px solid rgba(255, 193, 204, 0.12)",
823
+ fontSize: 12,
824
+ color: "var(--color-text-secondary)",
825
+ }}
826
+ >
827
+ <span style={{ fontSize: 13 }}>πŸ› </span>
828
+ <span style={{ fontWeight: 600, color: "var(--color-accent-pink)" }}>
829
+ {friendlyToolName(msg.tool!.name)}
830
+ </span>
831
+ <span
832
+ style={{
833
+ fontWeight: 700,
834
+ fontSize: 11,
835
+ color: statusColor,
836
+ }}
837
+ >
838
+ {statusEmoji}
839
+ </span>
840
+ <span
841
+ title={fullTime(msg.timestamp)}
842
+ style={{ fontSize: 10, color: "var(--color-text-muted)", marginLeft: 4 }}
843
+ >
844
+ {relativeTime(msg.timestamp)}
845
+ </span>
846
  </div>
847
+ {/* Expandable args/result (hidden by default) */}
848
+ {msg.tool!.args && Object.keys(msg.tool!.args).length > 0 && (
849
+ <div style={{ marginLeft: 16, marginTop: 2 }}>
850
+ <ToolDetailBlock data={msg.tool!.args} label="Input" />
851
+ </div>
852
+ )}
853
+ {msg.tool!.result && Object.keys(msg.tool!.result).length > 0 && (
854
+ <div style={{ marginLeft: 16, marginTop: 2 }}>
855
+ <ToolDetailBlock data={msg.tool!.result} label="Output" />
856
+ </div>
857
+ )}
858
+ </div>
859
+ );
860
+ }
861
+
862
+ // ── GenUI badge (compact) ──
863
+ if (isGenUI) {
864
+ return (
865
+ <div key={msg.id} style={{ padding: "2px 0" }}>
866
+ <div
867
+ style={{
868
+ display: "inline-flex",
869
+ alignItems: "center",
870
+ gap: 6,
871
+ padding: "5px 12px",
872
+ borderRadius: 8,
873
+ background: "rgba(168, 218, 220, 0.06)",
874
+ border: "1px solid rgba(168, 218, 220, 0.12)",
875
+ fontSize: 12,
876
+ }}
877
+ >
878
+ <span style={{ fontSize: 13 }}>πŸ–₯</span>
879
+ <span style={{ fontWeight: 600, color: "var(--color-accent-cyan)" }}>
880
+ {msg.component!.name}
881
+ </span>
882
+ <span style={{ fontSize: 10, color: "var(--color-text-muted)" }}>shown</span>
883
+ <span
884
+ title={fullTime(msg.timestamp)}
885
+ style={{ fontSize: 10, color: "var(--color-text-muted)", marginLeft: 4 }}
886
+ >
887
+ {relativeTime(msg.timestamp)}
888
+ </span>
889
  </div>
890
+ {Object.keys(msg.component!.props).length > 0 && (
891
+ <div style={{ marginLeft: 16, marginTop: 2 }}>
892
+ <ToolDetailBlock data={msg.component!.props} label="Props" />
893
+ </div>
894
+ )}
895
+ </div>
896
+ );
897
+ }
898
+
899
+ // ── Chat bubble (user = left, assistant = right) ──
900
+ return (
901
+ <div
902
+ key={msg.id}
903
+ style={{
904
+ display: "flex",
905
+ flexDirection: "column",
906
+ alignItems: isUser ? "flex-start" : "flex-end",
907
+ maxWidth: "85%",
908
+ alignSelf: isUser ? "flex-start" : "flex-end",
909
+ }}
910
+ >
911
+ {/* Sender label */}
912
+ <span
913
+ style={{
914
+ fontSize: 10,
915
+ fontWeight: 700,
916
+ textTransform: "uppercase",
917
+ letterSpacing: "0.08em",
918
+ color: isUser ? "var(--color-cta)" : "var(--color-accent-cyan)",
919
+ marginBottom: 3,
920
+ paddingLeft: isUser ? 2 : 0,
921
+ paddingRight: isUser ? 0 : 2,
922
+ }}
923
+ >
924
+ {isUser ? "You" : "Reachy"}
925
+ </span>
926
+ {/* Bubble */}
927
+ <div
928
+ style={{
929
+ padding: "10px 14px",
930
+ borderRadius: isUser ? "2px 14px 14px 14px" : "14px 2px 14px 14px",
931
+ background: isUser
932
+ ? "rgba(255, 255, 255, 0.06)"
933
+ : "rgba(168, 218, 220, 0.10)",
934
+ border: isUser
935
+ ? "1px solid rgba(255, 255, 255, 0.10)"
936
+ : "1px solid rgba(168, 218, 220, 0.18)",
937
+ fontSize: 14,
938
+ lineHeight: 1.6,
939
+ color: "var(--color-text-primary)",
940
+ wordBreak: "break-word",
941
+ }}
942
+ >
943
+ {msg.content || "(no text)"}
944
+ </div>
945
+ {/* Timestamp */}
946
+ <span
947
+ title={fullTime(msg.timestamp)}
948
+ style={{
949
+ fontSize: 10,
950
+ color: "var(--color-text-muted)",
951
+ marginTop: 3,
952
+ paddingLeft: isUser ? 2 : 0,
953
+ paddingRight: isUser ? 0 : 2,
954
+ opacity: 0.7,
955
+ }}
956
+ >
957
+ {relativeTime(msg.timestamp)}
958
+ </span>
959
  </div>
960
  );
961
  })
src/reachy_mini_conversation_app/wakeword_detector.py CHANGED
@@ -42,10 +42,11 @@ _CAPTURE_DIR = Path(__file__).parent.parent.parent.parent / "wakeword_captures"
42
  _AUDIO_HISTORY_CHUNKS = 50
43
 
44
  # Patience: number of consecutive frames above threshold required.
45
- # The hey_reachy model produces sharp single-frame peaks (score=1.0)
46
- # rather than sustained runs, so patience > 1 forces the user to
47
- # repeat themselves. The threshold alone provides sufficient filtering.
48
- _PATIENCE_FRAMES = 1
 
49
 
50
  # RMS energy gate: skip wakeword evaluation when audio is too quiet.
51
  # The model produces high false-positive scores on near-silence, so we
 
42
  _AUDIO_HISTORY_CHUNKS = 50
43
 
44
  # Patience: number of consecutive frames above threshold required.
45
+ # A real "Hey Reachy" utterance produces sustained high scores across
46
+ # multiple 80ms frames, while false positives from ambient noise tend
47
+ # to spike for only 1-2 frames. Requiring 3 consecutive hits (~240ms)
48
+ # filters out transient spikes without noticeably delaying detection.
49
+ _PATIENCE_FRAMES = 3
50
 
51
  # RMS energy gate: skip wakeword evaluation when audio is too quiet.
52
  # The model produces high false-positive scores on near-silence, so we