chore: add feedback survey, pass custom vocav to cloud endpoints

This commit is contained in:
haritabh-z01 2026-01-09 07:19:02 +05:30
parent 3be8306820
commit 423568a127
17 changed files with 205 additions and 17 deletions

View file

@ -133,6 +133,7 @@ jobs:
POSTHOG_HOST: https://app.posthog.com
TELEMETRY_ENABLED: true
POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }}
FEEDBACK_SURVEY_ID: ${{ secrets.FEEDBACK_SURVEY_ID }}
AUTH_CLIENT_ID: ${{ secrets.AUTH_CLIENT_ID }}
AUTHORIZATION_ENDPOINT: ${{ secrets.AUTHORIZATION_ENDPOINT }}
AUTH_TOKEN_ENDPOINT: ${{ secrets.AUTH_TOKEN_ENDPOINT }}
@ -148,6 +149,7 @@ jobs:
POSTHOG_HOST: https://app.posthog.com
TELEMETRY_ENABLED: true
POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }}
FEEDBACK_SURVEY_ID: ${{ secrets.FEEDBACK_SURVEY_ID }}
AUTH_CLIENT_ID: ${{ secrets.AUTH_CLIENT_ID }}
AUTHORIZATION_ENDPOINT: ${{ secrets.AUTHORIZATION_ENDPOINT }}
AUTH_TOKEN_ENDPOINT: ${{ secrets.AUTH_TOKEN_ENDPOINT }}

View file

@ -17,6 +17,7 @@ LOG_LEVEL=info
TELEMETRY_ENABLED=true
POSTHOG_HOST=https://app.posthog.com
POSTHOG_API_KEY=your-posthog-api-key
FEEDBACK_SURVEY_ID=your-posthog-survey-id
# OAuth Configuration
# Required for Amical Cloud transcription

View file

@ -0,0 +1,20 @@
import { IconMessageHeart } from "@tabler/icons-react";
import { SidebarMenuButton, SidebarMenuItem } from "@/components/ui/sidebar";
import { usePostHog } from "@/renderer/main/lib/posthog";
export function FeedbackButton() {
const { enabled, hasSurvey, showFeedbackSurvey } = usePostHog();
if (!enabled || !hasSurvey) {
return null;
}
return (
<SidebarMenuItem>
<SidebarMenuButton onClick={showFeedbackSurvey}>
<IconMessageHeart />
<span>Feedback</span>
</SidebarMenuButton>
</SidebarMenuItem>
);
}

View file

@ -9,6 +9,7 @@ import {
SidebarMenuItem,
} from "@/components/ui/sidebar";
import { AuthButton } from "@/components/auth-button";
import { FeedbackButton } from "@/components/feedback-button";
export function NavSecondary({
items,
@ -39,6 +40,7 @@ export function NavSecondary({
</SidebarMenuButton>
</SidebarMenuItem>
))}
<FeedbackButton />
<AuthButton />
</SidebarMenu>
</SidebarGroupContent>

View file

@ -12,7 +12,8 @@ export interface PipelineContext {
import { GetAccessibilityContextResult } from "@amical/types";
export interface SharedPipelineData {
vocabulary: Map<string, string>;
vocabulary: string[]; // Custom vocab
replacements: Map<string, string>; // Custom replacements
userPreferences: {
language?: string; // Optional - undefined means auto-detect
formattingStyle: "formal" | "casual" | "technical";
@ -31,7 +32,8 @@ export function createDefaultContext(sessionId: string): PipelineContext {
return {
sessionId,
sharedData: {
vocabulary: new Map(),
vocabulary: [],
replacements: new Map(),
userPreferences: {
language: "en",
formattingStyle: "formal",

View file

@ -9,7 +9,7 @@ export { PipelineContext, SharedPipelineData } from "./context";
// Context for transcription operations (shared between transcribe and flush)
export interface TranscribeContext {
vocabulary?: Map<string, string>;
vocabulary?: string[];
accessibilityContext?: GetAccessibilityContextResult | null;
previousChunk?: string;
aggregatedTranscription?: string;
@ -28,7 +28,7 @@ export interface FormatParams {
text: string;
context: {
style?: string;
vocabulary?: Map<string, string>;
vocabulary?: string[];
accessibilityContext?: GetAccessibilityContextResult | null;
previousChunk?: string;
aggregatedTranscription?: string;

View file

@ -33,6 +33,7 @@ export class AmicalCloudProvider implements TranscriptionProvider {
private currentAccessibilityContext: GetAccessibilityContextResult | null =
null;
private currentAggregatedTranscription: string | undefined;
private currentVocabulary: string[] = [];
// Configuration
private readonly FRAME_SIZE = 512; // 32ms at 16kHz
@ -63,6 +64,7 @@ export class AmicalCloudProvider implements TranscriptionProvider {
this.currentLanguage = context.language;
this.currentAccessibilityContext = context?.accessibilityContext ?? null;
this.currentAggregatedTranscription = context?.aggregatedTranscription;
this.currentVocabulary = context?.vocabulary ?? [];
// Check authentication
if (!(await this.authService.isAuthenticated())) {
@ -107,6 +109,7 @@ export class AmicalCloudProvider implements TranscriptionProvider {
this.currentLanguage = context.language;
this.currentAccessibilityContext = context?.accessibilityContext ?? null;
this.currentAggregatedTranscription = context?.aggregatedTranscription;
this.currentVocabulary = context?.vocabulary ?? [];
// Check authentication
if (!(await this.authService.isAuthenticated())) {
@ -222,6 +225,7 @@ export class AmicalCloudProvider implements TranscriptionProvider {
audioData: Array.from(audioData),
vadProbs,
language: this.currentLanguage,
vocabulary: this.currentVocabulary,
previousTranscription: this.currentAggregatedTranscription,
formatting: {
enabled: enableFormatting,

View file

@ -294,17 +294,14 @@ export class WhisperProvider implements TranscriptionProvider {
}
private generateInitialPrompt(
vocabulary?: Map<string, string>,
vocabulary?: string[],
aggregatedTranscription?: string,
): string {
const promptParts: string[] = [];
// Add vocabulary terms if available
if (vocabulary && vocabulary.size > 0) {
// Extract vocabulary keys (the actual terms) and join with commas
const vocabularyTerms = Array.from(vocabulary.keys());
const vocabularyText = vocabularyTerms.join(", ");
promptParts.push(vocabularyText);
if (vocabulary && vocabulary.length > 0) {
promptParts.push(vocabulary.join(", "));
}
// Add last 8 words from aggregated transcription if available

View file

@ -0,0 +1,66 @@
import { useEffect } from "react";
import posthog from "posthog-js";
import { api } from "@/trpc/react";
let initialized = false;
function initPostHog(apiKey: string, host: string, machineId: string): void {
if (initialized) return;
posthog.init(apiKey, {
api_host: host,
opt_out_capturing_by_default: true,
autocapture: false,
capture_pageview: false,
capture_pageleave: false,
disable_session_recording: true,
persistence: "memory",
bootstrap: {
distinctID: machineId,
},
});
initialized = true;
}
function setTelemetryEnabled(enabled: boolean): void {
if (!initialized) return;
if (enabled) {
posthog.opt_in_capturing();
} else {
posthog.opt_out_capturing();
}
}
export function usePostHog() {
const { data: config } = api.settings.getTelemetryConfig.useQuery();
// Initialize PostHog when config is available
useEffect(() => {
if (config?.apiKey) {
initPostHog(config.apiKey, config.host, config.machineId);
}
}, [config?.apiKey, config?.host, config?.machineId]);
// Sync opt-in/opt-out state when enabled changes
useEffect(() => {
if (config?.enabled !== undefined) {
setTelemetryEnabled(config.enabled);
}
}, [config?.enabled]);
const showFeedbackSurvey = () => {
if (!initialized || !config?.feedbackSurveyId) return;
posthog.onSurveysLoaded(() => {
posthog.displaySurvey(config.feedbackSurveyId);
});
};
return {
enabled: config?.enabled ?? false,
hasSurvey: !!config?.feedbackSurveyId,
showFeedbackSurvey,
};
}
export { posthog };

View file

@ -52,6 +52,7 @@ export default function AdvancedSettingsPage() {
api.settings.updateTelemetrySettings.useMutation({
onSuccess: () => {
utils.settings.getTelemetrySettings.invalidate();
utils.settings.getTelemetryConfig.invalidate();
toast.success("Telemetry settings updated");
},
onError: (error) => {

View file

@ -2,6 +2,7 @@ import { createRootRoute, Outlet } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { api, trpcClient } from "@/trpc/react";
import { usePostHog } from "../lib/posthog";
// Create a client
const queryClient = new QueryClient({
@ -17,14 +18,25 @@ export const Route = createRootRoute({
component: RootComponent,
});
function RootComponent() {
// Inner component that uses hooks requiring provider context
function AppShell() {
usePostHog(); // Initialize and sync telemetry
return (
<api.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
<>
<Outlet />
{process.env.NODE_ENV === "development" && (
<TanStackRouterDevtools position="bottom-right" />
)}
</>
);
}
function RootComponent() {
return (
<api.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
<AppShell />
</QueryClientProvider>
</api.Provider>
);

View file

@ -132,9 +132,11 @@ export class TelemetryService {
},
};
this.enabled = true;
this.enabled = telemetrySettings?.enabled !== false;
this.initialized = true;
logger.main.info("Telemetry service initialized successfully");
logger.main.info("Telemetry service initialized successfully", {
enabled: this.enabled,
});
}
private async collectSystemInfo(): Promise<SystemInfo> {
@ -232,6 +234,7 @@ export class TelemetryService {
async optIn(): Promise<void> {
await this.settingsService.setTelemetrySettings({ enabled: true });
this.enabled = true;
if (!this.posthog) {
return;
}
@ -243,6 +246,7 @@ export class TelemetryService {
async optOut(): Promise<void> {
await this.settingsService.setTelemetrySettings({ enabled: false });
this.enabled = false;
if (!this.posthog) {
return;
}

View file

@ -16,6 +16,7 @@ import { TelemetryService } from "../services/telemetry-service";
import type { NativeBridge } from "./platform/native-bridge-service";
import type { OnboardingService } from "./onboarding-service";
import { createTranscription } from "../db/transcriptions";
import { getVocabulary } from "../db/vocabulary";
import { logger } from "../main/logger";
import { v4 as uuid } from "uuid";
import { VADService } from "./vad-service";
@ -609,7 +610,19 @@ export class TranscriptionService {
: dictationSettings.selectedLanguage || "en";
}
// TODO: Load actual vocabulary
// Load vocabulary and replacements
const vocabEntries = await getVocabulary({ limit: 50 });
for (const entry of vocabEntries) {
if (entry.isReplacement) {
context.sharedData.replacements.set(
entry.word,
entry.replacementWord || "",
);
} else {
context.sharedData.vocabulary.push(entry.word);
}
}
// TODO: Load formatter config from settings
return context;

View file

@ -495,6 +495,19 @@ export const settingsRouter = createRouter({
return telemetryService?.getMachineId() ?? "";
}),
// Get telemetry config for renderer (PostHog surveys)
getTelemetryConfig: procedure.query(async ({ ctx }) => {
const telemetryService = ctx.serviceManager.getService("telemetryService");
return {
apiKey: process.env.POSTHOG_API_KEY || __BUNDLED_POSTHOG_API_KEY,
host: process.env.POSTHOG_HOST || __BUNDLED_POSTHOG_HOST,
machineId: telemetryService?.getMachineId() ?? "",
enabled: telemetryService?.isEnabled() ?? false,
feedbackSurveyId:
process.env.FEEDBACK_SURVEY_ID || __BUNDLED_FEEDBACK_SURVEY_ID,
};
}),
// Download log file via save dialog
downloadLogFile: procedure.mutation(async () => {
const { dialog, BrowserWindow } = await import("electron");

View file

@ -5,3 +5,4 @@ declare const __BUNDLED_AUTH_CLIENT_ID: string;
declare const __BUNDLED_AUTH_AUTHORIZATION_ENDPOINT: string;
declare const __BUNDLED_AUTH_TOKEN_ENDPOINT: string;
declare const __BUNDLED_API_ENDPOINT: string;
declare const __BUNDLED_FEEDBACK_SURVEY_ID: string;

View file

@ -19,6 +19,9 @@ export default defineConfig({
process.env.AUTH_TOKEN_ENDPOINT || "",
),
__BUNDLED_API_ENDPOINT: JSON.stringify(process.env.API_ENDPOINT || ""),
__BUNDLED_FEEDBACK_SURVEY_ID: JSON.stringify(
process.env.FEEDBACK_SURVEY_ID || "",
),
},
build: {
rollupOptions: {

47
pnpm-lock.yaml generated
View file

@ -250,6 +250,9 @@ importers:
openai:
specifier: ^4.98.0
version: 4.104.0(encoding@0.1.13)(ws@8.18.3)(zod@3.25.76)
posthog-js:
specifier: ^1.315.1
version: 1.315.1
posthog-node:
specifier: ^5.8.1
version: 5.8.1
@ -1537,6 +1540,12 @@ packages:
'@posthog/core@1.0.2':
resolution: {integrity: sha512-hWk3rUtJl2crQK0WNmwg13n82hnTwB99BT99/XI5gZSvIlYZ1TPmMZE8H2dhJJ98J/rm9vYJ/UXNzw3RV5HTpQ==}
'@posthog/core@1.9.0':
resolution: {integrity: sha512-j7KSWxJTUtNyKynLt/p0hfip/3I46dWU2dk+pt7dKRoz2l5CYueHuHK4EO7Wlgno5yo1HO4sc4s30MXMTICHJw==}
'@posthog/types@1.315.1':
resolution: {integrity: sha512-m2NggfJRYby3AkAES6yHMLURvTeK+rxN+5nmkuaCbOXQPdtWacSFIG5ZwN8d3crSx+WpiFauCDdr1sc3ZFkTHg==}
'@radix-ui/number@1.1.1':
resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
@ -3311,6 +3320,9 @@ packages:
core-js-pure@3.45.1:
resolution: {integrity: sha512-OHnWFKgTUshEU8MK+lOs1H8kC8GkTi9Z1tvNkxrCcw9wl3MJIO7q2ld77wjWn4/xuGrVu2X+nME1iIIPBSdyEQ==}
core-js@3.47.0:
resolution: {integrity: sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==}
create-require@1.1.1:
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
@ -4038,6 +4050,9 @@ packages:
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
engines: {node: ^12.20 || >= 14.13}
fflate@0.4.8:
resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==}
fflate@0.8.2:
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
@ -5457,6 +5472,9 @@ packages:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
posthog-js@1.315.1:
resolution: {integrity: sha512-ambT1azidu4hKhSmB95KdLY6yHfj9vvz1XNn68syh8DtkQ0uSdjpRY6tjMp96EQtPqCrDKr+8QpcusT1KQEZSA==}
posthog-node@5.8.1:
resolution: {integrity: sha512-YJYlYnlpItVjHqM9IhvZx8TzK8gnx2nU+0uhiog4RN47NnV0Z0K1AdC4ul+O8VuvS/jHqKCQvL8iAONRA37+0A==}
engines: {node: '>=20'}
@ -5466,6 +5484,9 @@ packages:
engines: {node: '>=14.0.0'}
hasBin: true
preact@10.28.2:
resolution: {integrity: sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==}
prebuild-install@7.1.3:
resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
engines: {node: '>=10'}
@ -6596,6 +6617,9 @@ packages:
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
engines: {node: '>= 14'}
web-vitals@4.2.4:
resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==}
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
@ -7935,6 +7959,12 @@ snapshots:
'@posthog/core@1.0.2': {}
'@posthog/core@1.9.0':
dependencies:
cross-spawn: 7.0.6
'@posthog/types@1.315.1': {}
'@radix-ui/number@1.1.1': {}
'@radix-ui/primitive@1.1.3': {}
@ -9888,6 +9918,8 @@ snapshots:
core-js-pure@3.45.1: {}
core-js@3.47.0: {}
create-require@1.1.1: {}
cross-dirname@0.1.0: {}
@ -10715,6 +10747,8 @@ snapshots:
node-domexception: 1.0.0
web-streams-polyfill: 3.3.3
fflate@0.4.8: {}
fflate@0.8.2: {}
figures@3.2.0:
@ -12216,6 +12250,15 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
posthog-js@1.315.1:
dependencies:
'@posthog/core': 1.9.0
'@posthog/types': 1.315.1
core-js: 3.47.0
fflate: 0.4.8
preact: 10.28.2
web-vitals: 4.2.4
posthog-node@5.8.1:
dependencies:
'@posthog/core': 1.0.2
@ -12224,6 +12267,8 @@ snapshots:
dependencies:
commander: 9.5.0
preact@10.28.2: {}
prebuild-install@7.1.3:
dependencies:
detect-libc: 2.0.4
@ -13503,6 +13548,8 @@ snapshots:
web-streams-polyfill@4.0.0-beta.3: {}
web-vitals@4.2.4: {}
webidl-conversions@3.0.1: {}
webpack-virtual-modules@0.6.2: {}