Skip to content
Draft
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
04e3d5a
chore(deps): bump docker/login-action from 4.1.0 to 4.4.0
dependabot[bot] Jul 8, 2026
9f84b86
chore(deps): bump sigstore/cosign-installer from 4.1.1 to 4.1.2
dependabot[bot] Jul 8, 2026
aa91fbc
feat(dashboard): add voice input to agent chat
swamimalode07 Jul 14, 2026
3d06054
fixed ui
swamimalode07 Jul 14, 2026
b406208
fixed conflicts
swamimalode07 Jul 14, 2026
f4a1802
added scale up based on mic volume intensity
swamimalode07 Jul 14, 2026
c85db98
fixed design and ux
swamimalode07 Jul 14, 2026
e77915b
added brand colors for the orb
swamimalode07 Jul 14, 2026
1ac0744
chore(deps): bump docker/metadata-action from 6.0.0 to 6.2.0
dependabot[bot] Jul 22, 2026
7a1f2cb
fix(ci): stabilize release and core e2e checks (#570)
izadoesdev Jul 26, 2026
2ab1cb6
Merge pull request #556 from databuddy-analytics/dependabot/github_ac…
izadoesdev Jul 26, 2026
fe015a4
Merge pull request #553 from databuddy-analytics/dependabot/github_ac…
izadoesdev Jul 26, 2026
a312518
Merge pull request #549 from databuddy-analytics/dependabot/github_ac…
izadoesdev Jul 26, 2026
e4c560f
Merge branch 'main' into feat/voice-mode
izadoesdev Jul 26, 2026
03ff434
fix(dashboard): prevent stale voice recognition sessions
izadoesdev Jul 26, 2026
f342eb5
Merge pull request #564 from swamimalode07/feat/voice-mode
izadoesdev Jul 26, 2026
4f5af2d
refactor(shared): sharpen insight output contract
izadoesdev Jul 26, 2026
0273788
fix(insights): improve investigation editorial quality
izadoesdev Jul 26, 2026
097c0d3
feat(dashboard): refine insights editorial feed
izadoesdev Jul 26, 2026
45cff73
fix(dashboard): restore insights header copy
izadoesdev Jul 26, 2026
4ba2441
feat(insights): recheck completed actions
izadoesdev Jul 26, 2026
a4495ca
fix(insights): recheck signals on manual runs
izadoesdev Jul 26, 2026
0ccd7c0
refactor(insights): tighten brief prose
izadoesdev Jul 26, 2026
0f8a234
feat(dashboard): make investigations actionable
izadoesdev Jul 27, 2026
e96de32
fix(dashboard): remove redundant insight chrome
izadoesdev Jul 27, 2026
48c5936
feat(insights): schedule measurable verification
izadoesdev Jul 27, 2026
572b5a2
feat(dashboard): link investigation evidence
izadoesdev Jul 27, 2026
1154779
refactor(dashboard): reduce insight visual noise
izadoesdev Jul 27, 2026
d281004
Revert "refactor(dashboard): reduce insight visual noise"
izadoesdev Jul 27, 2026
bdb8b40
docs(agents): preserve insight scan hierarchy
izadoesdev Jul 27, 2026
89dfb7e
feat(dashboard): foreground investigation outcomes
izadoesdev Jul 27, 2026
b9902a1
feat(insights): verify completed actions
izadoesdev Jul 27, 2026
0c1e3bc
docs(agents): clarify insight action verification
izadoesdev Jul 27, 2026
32f2ed4
feat(insights): apply executable goal actions
izadoesdev Jul 27, 2026
59abb05
docs(agents): clarify executable insight actions
izadoesdev Jul 27, 2026
322b0f4
refactor(dashboard): focus investigation actions
izadoesdev Jul 27, 2026
c154796
fix(basket,api): stop logging expected 4xx client errors as errors
izadoesdev Jul 27, 2026
172242d
fix(basket,notifications): degrade email alerts gracefully when RESEN…
izadoesdev Jul 27, 2026
aabac5f
feat(insights): recheck changed definitions
izadoesdev Jul 27, 2026
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
4 changes: 2 additions & 2 deletions .github/workflows/cache-benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
- name: Set up Docker Builder
uses: useblacksmith/setup-docker-builder@722e97d12b1d06a961800dd6c05d79d951ad3c80 # v1

- uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
- uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
Expand Down Expand Up @@ -63,7 +63,7 @@ jobs:
- name: Set up Docker Builder
uses: useblacksmith/setup-docker-builder@722e97d12b1d06a961800dd6c05d79d951ad3c80 # v1

- uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
- uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ jobs:
- name: Set up Docker Builder
uses: useblacksmith/setup-docker-builder@722e97d12b1d06a961800dd6c05d79d951ad3c80 # v1

- uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
- uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
Expand All @@ -142,7 +142,7 @@ jobs:

- name: Extract metadata (labels)
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
with:
images: ${{ env.IMAGE_PREFIX }}-${{ matrix.service }}
labels: |
Expand Down Expand Up @@ -186,7 +186,7 @@ jobs:

- name: Install cosign
if: always() && steps.build.outcome != 'cancelled'
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4

- name: Sign image
if: always() && steps.build.outcome != 'cancelled'
Expand All @@ -212,7 +212,7 @@ jobs:
service: ${{ fromJson(needs.detect.outputs.services) }}

steps:
- uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
- uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
Expand Down Expand Up @@ -267,7 +267,7 @@ jobs:
docker buildx imagetools inspect "$IMAGE:$TAG"

- name: Install cosign
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4

- name: Sign manifest
env:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ name: Release
on:
push:
branches: [main]
paths:
- ".changeset/**"

concurrency:
group: release-${{ github.ref }}
Expand Down
20 changes: 20 additions & 0 deletions apps/dashboard/components/agent/agent-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
AgentTextSwitch,
AGENT_INPUT_PLACEHOLDER_PHRASES,
} from "./agent-text-switch";
import { AgentVoiceDialog } from "./agent-voice-dialog";
import { useEnterSubmit } from "./hooks/use-enter-submit";
import { DropdownMenu } from "@databuddy/ui/client";
import { Button, Skeleton, Textarea, Tooltip } from "@databuddy/ui";
Expand Down Expand Up @@ -214,6 +215,19 @@ export function AgentInput() {
setMentions((prev) => prev.filter((m) => m.id !== id));
};

const applyVoiceTranscript = useCallback(
(transcript: string) => {
setInput((prev) => {
const existing = prev.trim();
return existing ? `${existing} ${transcript}` : transcript;
});
setCommandsDismissed(false);
setMentionsDismissed(false);
requestAnimationFrame(() => textareaRef.current?.focus());
},
[setInput]
);

const handleMessageKeyDown = (
event: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
Expand Down Expand Up @@ -342,6 +356,7 @@ export function AgentInput() {
canSend={Boolean(input.trim())}
isLoading={isLoading}
onStop={stop}
onVoiceTranscript={applyVoiceTranscript}
/>
</div>
);
Expand Down Expand Up @@ -385,10 +400,12 @@ const InputToolbar = memo(function InputToolbar({
canSend,
isLoading,
onStop,
onVoiceTranscript,
}: {
canSend: boolean;
isLoading: boolean;
onStop: () => void;
onVoiceTranscript: (transcript: string) => void;
}) {
return (
<div className="flex items-center justify-between gap-3 rounded border-border/60 bg-background px-1.5 py-1.5">
Expand All @@ -411,6 +428,9 @@ const InputToolbar = memo(function InputToolbar({

<div className="ml-auto flex shrink-0 items-center gap-3">
<KeyboardHints isLoading={isLoading} />
{isLoading ? null : (
<AgentVoiceDialog onTranscript={onVoiceTranscript} />
)}
{isLoading ? (
<Button
aria-label="Stop generation"
Expand Down
248 changes: 248 additions & 0 deletions apps/dashboard/components/agent/agent-voice-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
"use client";

import { Button, Tooltip } from "@databuddy/ui";
import { Dialog } from "@databuddy/ui/client";
import { MicrophoneIcon } from "@databuddy/ui/icons";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import {
useCallback,
useEffect,
useEffectEvent,
useRef,
useState,
} from "react";
import { Shimmer } from "@/components/ai-elements/shimmer";
import FluidOrb from "@/components/ui/fluid-orb";
import { cn } from "@/lib/utils";
import { useMicLevel } from "./hooks/use-mic-level";
import { useSpeechTranscription } from "./hooks/use-speech-transcription";

interface AgentVoiceDialogProps {
onTranscript: (transcript: string) => void;
}

const ORB_COLORS = ["#E3A514", "#B74677", "#453C7C"];

export function AgentVoiceDialog({ onTranscript }: AgentVoiceDialogProps) {
const [open, setOpen] = useState(false);
const [orbColor, setOrbColor] = useState(ORB_COLORS[0]);
const {
displayTranscript,
error,
finalTranscript,
interimTranscript,
isSupported,
start,
status,
stop,
} = useSpeechTranscription();
const hasTranscript = displayTranscript.trim().length > 0;
const reduceMotion = useReducedMotion();
const orbRef = useRef<HTMLDivElement | null>(null);
const orbScaleRef = useRef(1);
const transcriptRef = useRef<HTMLParagraphElement | null>(null);

useMicLevel(
open && status !== "error",
useCallback((level: number) => {
const target = 1 + Math.min(level * 0.9, 0.06);
orbScaleRef.current += (target - orbScaleRef.current) * 0.06;
orbRef.current?.style.setProperty(
"transform",
`scale(${orbScaleRef.current.toFixed(4)})`
);
}, [])
);

const handleOpenChange = useCallback(
(nextOpen: boolean) => {
setOpen(nextOpen);
if (nextOpen) {
orbScaleRef.current = 1;
setOrbColor(ORB_COLORS[Math.floor(Math.random() * ORB_COLORS.length)]);
start();
} else {
stop();
}
},
[start, stop]
);

const handleDone = useCallback(() => {
const nextTranscript = displayTranscript.trim();
if (!nextTranscript) {
return;
}

onTranscript(nextTranscript);
handleOpenChange(false);
}, [displayTranscript, handleOpenChange, onTranscript]);

const handleTranscriptKeyDown = useEffectEvent((event: KeyboardEvent) => {
if (event.key === "Enter") {
event.preventDefault();
handleDone();
}
});

useEffect(() => {
if (!(open && hasTranscript)) {
return;
}
const onKeyDown = (event: KeyboardEvent) => handleTranscriptKeyDown(event);
document.addEventListener("keydown", onKeyDown);
return () => document.removeEventListener("keydown", onKeyDown);
}, [open, hasTranscript]);

useEffect(() => {
transcriptRef.current?.scrollTo({
top: transcriptRef.current.scrollHeight,
});
}, [displayTranscript]);

if (!isSupported) {
return null;
}

return (
<Dialog onOpenChange={handleOpenChange} open={open}>
<Tooltip content="Voice input" side="top">
<Dialog.Trigger
render={
<Button
aria-label="Open voice input"
className="size-7 transition-transform active:scale-[0.97]"
size="icon"
type="button"
variant="secondary"
>
<MicrophoneIcon className="size-3.5" />
</Button>
}
/>
</Tooltip>

<Dialog.Content className="max-w-sm shadow-none">
<Dialog.Close />
<Dialog.Header className="border-border/50 border-b text-center">
<Dialog.Title className="text-sm">Voice input</Dialog.Title>
</Dialog.Header>

<Dialog.Body className="flex flex-col items-center gap-5 py-8">
<motion.div
animate={{ opacity: 1, scale: 1 }}
className="grid size-56 place-items-center"
initial={reduceMotion ? false : { opacity: 0, scale: 0.95 }}
transition={{ duration: 0.25, ease: "easeOut" }}
>
<div
className={cn(
"transition-[opacity,filter] duration-300 will-change-transform",
status === "error" && "opacity-40 saturate-50"
)}
ref={orbRef}
>
<motion.div
animate={reduceMotion ? { scale: 1 } : { scale: [1, 1.015, 1] }}
transition={{
duration: 4,
ease: "easeInOut",
repeat: Number.POSITIVE_INFINITY,
}}
>
<FluidOrb aria-hidden="true" color={orbColor} size={208} />
</motion.div>
</div>
</motion.div>

<div
aria-live="polite"
className="flex min-h-20 w-full items-center justify-center px-2 text-center"
>
<AnimatePresence initial={false} mode="wait">
{hasTranscript ? (
<motion.p
animate={{ opacity: 1 }}
className="max-h-24 w-full overflow-y-auto text-balance text-foreground text-sm leading-6"
exit={{ opacity: 0 }}
initial={{ opacity: 0 }}
key="transcript"
ref={transcriptRef}
transition={{
duration: reduceMotion ? 0 : 0.15,
ease: "easeOut",
}}
>
{finalTranscript}
{interimTranscript ? (
<span className="text-muted-foreground">
{finalTranscript ? " " : ""}
{interimTranscript}
</span>
) : null}
</motion.p>
) : (
<motion.div
animate={{ opacity: 1 }}
className="flex flex-col items-center gap-2"
exit={{ opacity: 0 }}
initial={{ opacity: 0 }}
key="status"
transition={{
duration: reduceMotion ? 0 : 0.15,
ease: "easeOut",
}}
>
{status === "error" ? (
<span className="font-medium text-foreground text-sm tracking-tight">
Voice unavailable
</span>
) : (
<Shimmer
as="span"
className="font-medium text-sm tracking-tight"
>
Listening…
</Shimmer>
)}
<span className="max-w-60 text-balance text-muted-foreground text-xs leading-5">
{error ??
(isSupported
? "Start speaking when you're ready"
: "Voice input is not supported in this browser")}
</span>
</motion.div>
)}
</AnimatePresence>
</div>
</Dialog.Body>

<AnimatePresence initial={false}>
{hasTranscript ? (
<motion.div
animate={{ opacity: 1 }}
className="overflow-hidden"
exit={{ opacity: 0 }}
initial={reduceMotion ? false : { opacity: 0 }}
key="footer"
layout
transition={{
duration: reduceMotion ? 0 : 0.2,
ease: "easeOut",
}}
>
<Dialog.Footer className="border-border/50 border-t">
<Button onClick={handleDone}>
Done
<kbd className="ml-1.5 rounded border border-current/30 px-1 font-mono text-[10px] leading-4">
</kbd>
</Button>
</Dialog.Footer>
</motion.div>
) : null}
</AnimatePresence>
</Dialog.Content>
</Dialog>
);
}
Loading
Loading