diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a16afa6..49fca72 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 }} diff --git a/apps/desktop/.env.example b/apps/desktop/.env.example index dd4bd1b..64469c9 100644 --- a/apps/desktop/.env.example +++ b/apps/desktop/.env.example @@ -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 diff --git a/apps/desktop/src/components/feedback-button.tsx b/apps/desktop/src/components/feedback-button.tsx new file mode 100644 index 0000000..3469a28 --- /dev/null +++ b/apps/desktop/src/components/feedback-button.tsx @@ -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 ( + + + + Feedback + + + ); +} diff --git a/apps/desktop/src/components/nav-secondary.tsx b/apps/desktop/src/components/nav-secondary.tsx index 37942c8..e5f6db9 100644 --- a/apps/desktop/src/components/nav-secondary.tsx +++ b/apps/desktop/src/components/nav-secondary.tsx @@ -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({ ))} + diff --git a/apps/desktop/src/pipeline/core/context.ts b/apps/desktop/src/pipeline/core/context.ts index c809545..a5d6e13 100644 --- a/apps/desktop/src/pipeline/core/context.ts +++ b/apps/desktop/src/pipeline/core/context.ts @@ -12,7 +12,8 @@ export interface PipelineContext { import { GetAccessibilityContextResult } from "@amical/types"; export interface SharedPipelineData { - vocabulary: Map; + vocabulary: string[]; // Custom vocab + replacements: Map; // 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", diff --git a/apps/desktop/src/pipeline/core/pipeline-types.ts b/apps/desktop/src/pipeline/core/pipeline-types.ts index 30c17d4..afeaaf2 100644 --- a/apps/desktop/src/pipeline/core/pipeline-types.ts +++ b/apps/desktop/src/pipeline/core/pipeline-types.ts @@ -9,7 +9,7 @@ export { PipelineContext, SharedPipelineData } from "./context"; // Context for transcription operations (shared between transcribe and flush) export interface TranscribeContext { - vocabulary?: Map; + vocabulary?: string[]; accessibilityContext?: GetAccessibilityContextResult | null; previousChunk?: string; aggregatedTranscription?: string; @@ -28,7 +28,7 @@ export interface FormatParams { text: string; context: { style?: string; - vocabulary?: Map; + vocabulary?: string[]; accessibilityContext?: GetAccessibilityContextResult | null; previousChunk?: string; aggregatedTranscription?: string; diff --git a/apps/desktop/src/pipeline/providers/transcription/amical-cloud-provider.ts b/apps/desktop/src/pipeline/providers/transcription/amical-cloud-provider.ts index 6b5f93f..1137d8b 100644 --- a/apps/desktop/src/pipeline/providers/transcription/amical-cloud-provider.ts +++ b/apps/desktop/src/pipeline/providers/transcription/amical-cloud-provider.ts @@ -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, diff --git a/apps/desktop/src/pipeline/providers/transcription/whisper-provider.ts b/apps/desktop/src/pipeline/providers/transcription/whisper-provider.ts index e2c9ec2..9bbbe47 100644 --- a/apps/desktop/src/pipeline/providers/transcription/whisper-provider.ts +++ b/apps/desktop/src/pipeline/providers/transcription/whisper-provider.ts @@ -294,17 +294,14 @@ export class WhisperProvider implements TranscriptionProvider { } private generateInitialPrompt( - vocabulary?: Map, + 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 diff --git a/apps/desktop/src/renderer/main/lib/posthog.ts b/apps/desktop/src/renderer/main/lib/posthog.ts new file mode 100644 index 0000000..66bdea1 --- /dev/null +++ b/apps/desktop/src/renderer/main/lib/posthog.ts @@ -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 }; diff --git a/apps/desktop/src/renderer/main/pages/settings/advanced/index.tsx b/apps/desktop/src/renderer/main/pages/settings/advanced/index.tsx index ac2ad84..86bea85 100644 --- a/apps/desktop/src/renderer/main/pages/settings/advanced/index.tsx +++ b/apps/desktop/src/renderer/main/pages/settings/advanced/index.tsx @@ -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) => { diff --git a/apps/desktop/src/renderer/main/routes/__root.tsx b/apps/desktop/src/renderer/main/routes/__root.tsx index 97a20fd..909e5e2 100644 --- a/apps/desktop/src/renderer/main/routes/__root.tsx +++ b/apps/desktop/src/renderer/main/routes/__root.tsx @@ -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, }); +// Inner component that uses hooks requiring provider context +function AppShell() { + usePostHog(); // Initialize and sync telemetry + + return ( + <> + + {process.env.NODE_ENV === "development" && ( + + )} + + ); +} + function RootComponent() { return ( - - {process.env.NODE_ENV === "development" && ( - - )} + ); diff --git a/apps/desktop/src/services/telemetry-service.ts b/apps/desktop/src/services/telemetry-service.ts index 6293816..77fa8e9 100644 --- a/apps/desktop/src/services/telemetry-service.ts +++ b/apps/desktop/src/services/telemetry-service.ts @@ -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 { @@ -232,6 +234,7 @@ export class TelemetryService { async optIn(): Promise { await this.settingsService.setTelemetrySettings({ enabled: true }); + this.enabled = true; if (!this.posthog) { return; } @@ -243,6 +246,7 @@ export class TelemetryService { async optOut(): Promise { await this.settingsService.setTelemetrySettings({ enabled: false }); + this.enabled = false; if (!this.posthog) { return; } diff --git a/apps/desktop/src/services/transcription-service.ts b/apps/desktop/src/services/transcription-service.ts index 77ec690..acad703 100644 --- a/apps/desktop/src/services/transcription-service.ts +++ b/apps/desktop/src/services/transcription-service.ts @@ -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; diff --git a/apps/desktop/src/trpc/routers/settings.ts b/apps/desktop/src/trpc/routers/settings.ts index 27335e8..f612075 100644 --- a/apps/desktop/src/trpc/routers/settings.ts +++ b/apps/desktop/src/trpc/routers/settings.ts @@ -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"); diff --git a/apps/desktop/src/types/bundled-env.d.ts b/apps/desktop/src/types/bundled-env.d.ts index 7cf2e3b..c2e9094 100644 --- a/apps/desktop/src/types/bundled-env.d.ts +++ b/apps/desktop/src/types/bundled-env.d.ts @@ -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; diff --git a/apps/desktop/vite.main.config.mts b/apps/desktop/vite.main.config.mts index 5f5d014..d8f5e9c 100644 --- a/apps/desktop/vite.main.config.mts +++ b/apps/desktop/vite.main.config.mts @@ -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: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 528188a..4bc89e0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: {}