diff --git a/apps/desktop/src/renderer/main/index.tsx b/apps/desktop/src/renderer/main/index.tsx
index b5ca196..7222c9b 100644
--- a/apps/desktop/src/renderer/main/index.tsx
+++ b/apps/desktop/src/renderer/main/index.tsx
@@ -2,6 +2,7 @@ import React, { Suspense } from "react";
import { createRoot } from "react-dom/client";
import "@/styles/globals.css";
import { ThemeProvider } from "@/components/theme-provider";
+import { Toaster } from "@/components/ui/sonner";
// Lazy import the main content
const Content = React.lazy(
@@ -75,6 +76,7 @@ const App: React.FC = () => {
}>
+
);
};
diff --git a/apps/desktop/src/renderer/main/pages/settings/components/AdvancedSettings.tsx b/apps/desktop/src/renderer/main/pages/settings/components/AdvancedSettings.tsx
new file mode 100644
index 0000000..c7f2a6e
--- /dev/null
+++ b/apps/desktop/src/renderer/main/pages/settings/components/AdvancedSettings.tsx
@@ -0,0 +1,57 @@
+import React from "react";
+import {
+ Card,
+ CardHeader,
+ CardTitle,
+ CardDescription,
+ CardContent,
+} from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Label } from "@/components/ui/label";
+import { Switch } from "@/components/ui/switch";
+
+export function AdvancedSettings() {
+ return (
+
+
+ Advanced Settings
+ Advanced configuration options
+
+
+
+
+
+
+ Enable detailed logging
+
+
+
+
+
+
+
+
+
+ Automatically check for updates
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/desktop/src/renderer/main/pages/settings/components/FormatterSettings.tsx b/apps/desktop/src/renderer/main/pages/settings/components/FormatterSettings.tsx
new file mode 100644
index 0000000..cb91a0c
--- /dev/null
+++ b/apps/desktop/src/renderer/main/pages/settings/components/FormatterSettings.tsx
@@ -0,0 +1,182 @@
+import React, { useState, useEffect } from "react";
+import {
+ Card,
+ CardHeader,
+ CardTitle,
+ CardDescription,
+ CardContent,
+} from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Label } from "@/components/ui/label";
+import { Switch } from "@/components/ui/switch";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { Input } from "@/components/ui/input";
+import { FormatterConfig } from "@/types/formatter";
+import { api } from "@/trpc/react";
+import { toast } from "sonner";
+
+// OpenRouter models list
+const OPENROUTER_MODELS = [
+ { value: "google/gemini-2.0-flash-001", label: "Gemini 2.0 Flash" },
+ { value: "anthropic/claude-3.5-sonnet", label: "Claude 3.5 Sonnet" },
+ { value: "anthropic/claude-3-haiku", label: "Claude 3 Haiku" },
+ { value: "openai/gpt-4o", label: "GPT-4o" },
+ { value: "openai/gpt-4o-mini", label: "GPT-4o mini" },
+ { value: "openai/gpt-4-turbo", label: "GPT-4 Turbo" },
+ { value: "meta-llama/llama-3.1-8b-instruct", label: "Llama 3.1 8B" },
+ { value: "meta-llama/llama-3.1-70b-instruct", label: "Llama 3.1 70B" },
+ { value: "google/gemini-pro-1.5", label: "Gemini Pro 1.5" },
+];
+
+export function FormatterSettings() {
+ const [formatterProvider, setFormatterProvider] =
+ useState<"openrouter">("openrouter");
+ const [openrouterModel, setOpenrouterModel] = useState("");
+ const [openrouterApiKey, setOpenrouterApiKey] = useState("");
+ const [formatterEnabled, setFormatterEnabled] = useState(false);
+
+ // tRPC queries and mutations
+ const formatterConfigQuery = api.settings.getFormatterConfig.useQuery();
+ const utils = api.useUtils();
+
+ const setFormatterConfigMutation =
+ api.settings.setFormatterConfig.useMutation({
+ onSuccess: () => {
+ toast.success("Configuration saved successfully!");
+ utils.settings.getFormatterConfig.invalidate();
+ },
+ onError: (error) => {
+ console.error("Failed to save formatter config:", error);
+ toast.error("Failed to save configuration. Please try again.");
+ },
+ });
+
+ // Load configuration when query data is available
+ useEffect(() => {
+ if (formatterConfigQuery.data) {
+ const config = formatterConfigQuery.data;
+ setFormatterProvider(config.provider);
+ setOpenrouterModel(config.model);
+ setOpenrouterApiKey(config.apiKey);
+ setFormatterEnabled(config.enabled);
+ }
+ }, [formatterConfigQuery.data]);
+
+ const saveFormatterConfig = async () => {
+ const config: FormatterConfig = {
+ provider: formatterProvider,
+ model: openrouterModel,
+ apiKey: openrouterApiKey,
+ enabled: formatterEnabled,
+ };
+
+ setFormatterConfigMutation.mutate(config);
+ };
+
+ return (
+
+
+ Text Formatting Configuration
+
+ Configure AI-powered post-processing of transcriptions
+
+
+
+
+
+
+
+
+ {formatterProvider === "openrouter" && (
+ <>
+
+
+
+
+
+
+
+
setOpenrouterApiKey(e.target.value)}
+ />
+
+ Get your API key from{" "}
+
+ openrouter.ai
+
+
+
+ >
+ )}
+
+
+
+
+
+ Apply AI formatting to transcriptions
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/desktop/src/renderer/main/pages/settings/components/GeneralSettings.tsx b/apps/desktop/src/renderer/main/pages/settings/components/GeneralSettings.tsx
new file mode 100644
index 0000000..47251bf
--- /dev/null
+++ b/apps/desktop/src/renderer/main/pages/settings/components/GeneralSettings.tsx
@@ -0,0 +1,53 @@
+import React from "react";
+import {
+ Card,
+ CardHeader,
+ CardTitle,
+ CardDescription,
+ CardContent,
+} from "@/components/ui/card";
+import { Label } from "@/components/ui/label";
+import { Switch } from "@/components/ui/switch";
+import { ThemeToggle } from "@/components/theme-toggle";
+
+export function GeneralSettings() {
+ return (
+
+
+ General Settings
+ Configure your general preferences
+
+
+
+
+
+
+ Start Amical when you log in
+
+
+
+
+
+
+
+
+
+ Keep running in system tray when closed
+
+
+
+
+
+
+
+
+
+ Choose your preferred theme
+
+
+
+
+
+
+ );
+}
diff --git a/apps/desktop/src/renderer/main/pages/settings/components/MicrophoneSettings.tsx b/apps/desktop/src/renderer/main/pages/settings/components/MicrophoneSettings.tsx
new file mode 100644
index 0000000..f6254ac
--- /dev/null
+++ b/apps/desktop/src/renderer/main/pages/settings/components/MicrophoneSettings.tsx
@@ -0,0 +1,50 @@
+import React from "react";
+import {
+ Card,
+ CardHeader,
+ CardTitle,
+ CardDescription,
+ CardContent,
+} from "@/components/ui/card";
+import { Label } from "@/components/ui/label";
+import { Switch } from "@/components/ui/switch";
+
+export function MicrophoneSettings() {
+ return (
+
+
+ Microphone Settings
+ Configure your microphone preferences
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/desktop/src/renderer/main/pages/settings/components/SettingsManager.tsx b/apps/desktop/src/renderer/main/pages/settings/components/SettingsManager.tsx
index efb0cc8..6061d4e 100644
--- a/apps/desktop/src/renderer/main/pages/settings/components/SettingsManager.tsx
+++ b/apps/desktop/src/renderer/main/pages/settings/components/SettingsManager.tsx
@@ -1,86 +1,12 @@
-import React, { useState, useEffect } from "react";
-import {
- Card,
- CardHeader,
- CardTitle,
- CardDescription,
- CardContent,
-} from "@/components/ui/card";
-import { Button } from "@/components/ui/button";
-import { Label } from "@/components/ui/label";
-import { Switch } from "@/components/ui/switch";
+import React from "react";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import { Input } from "@/components/ui/input";
-import { ThemeToggle } from "@/components/theme-toggle";
-import { FormatterConfig } from "@/types/formatter";
-import { api } from "@/trpc/react";
-import { toast } from "sonner";
-
-// OpenRouter models list
-const OPENROUTER_MODELS = [
- { value: "google/gemini-2.0-flash-001", label: "Gemini 2.0 Flash" },
- { value: "anthropic/claude-3.5-sonnet", label: "Claude 3.5 Sonnet" },
- { value: "anthropic/claude-3-haiku", label: "Claude 3 Haiku" },
- { value: "openai/gpt-4o", label: "GPT-4o" },
- { value: "openai/gpt-4o-mini", label: "GPT-4o mini" },
- { value: "openai/gpt-4-turbo", label: "GPT-4 Turbo" },
- { value: "meta-llama/llama-3.1-8b-instruct", label: "Llama 3.1 8B" },
- { value: "meta-llama/llama-3.1-70b-instruct", label: "Llama 3.1 70B" },
- { value: "google/gemini-pro-1.5", label: "Gemini Pro 1.5" },
-];
+import { GeneralSettings } from "./GeneralSettings";
+import { MicrophoneSettings } from "./MicrophoneSettings";
+import { ShortcutsSettings } from "./ShortcutsSettings";
+import { FormatterSettings } from "./FormatterSettings";
+import { AdvancedSettings } from "./AdvancedSettings";
export function SettingsManager() {
- const [formatterProvider, setFormatterProvider] =
- useState<"openrouter">("openrouter");
- const [openrouterModel, setOpenrouterModel] = useState("");
- const [openrouterApiKey, setOpenrouterApiKey] = useState("");
- const [formatterEnabled, setFormatterEnabled] = useState(false);
-
- // tRPC queries and mutations
- const formatterConfigQuery = api.settings.getFormatterConfig.useQuery();
- const utils = api.useUtils();
-
- const setFormatterConfigMutation =
- api.settings.setFormatterConfig.useMutation({
- onSuccess: () => {
- toast.success("Configuration saved successfully!");
- utils.settings.getFormatterConfig.invalidate();
- },
- onError: (error) => {
- console.error("Failed to save formatter config:", error);
- toast.error("Failed to save configuration. Please try again.");
- },
- });
-
- // Load configuration when query data is available
- useEffect(() => {
- if (formatterConfigQuery.data) {
- const config = formatterConfigQuery.data;
- setFormatterProvider(config.provider);
- setOpenrouterModel(config.model);
- setOpenrouterApiKey(config.apiKey);
- setFormatterEnabled(config.enabled);
- }
- }, [formatterConfigQuery.data]);
-
- const saveFormatterConfig = async () => {
- const config: FormatterConfig = {
- provider: formatterProvider,
- model: openrouterModel,
- apiKey: openrouterApiKey,
- enabled: formatterEnabled,
- };
-
- setFormatterConfigMutation.mutate(config);
- };
-
return (
@@ -93,270 +19,23 @@ export function SettingsManager() {
-
-
- General Settings
-
- Configure your general preferences
-
-
-
-
-
-
-
- Start Amical when you log in
-
-
-
-
-
-
-
-
-
- Keep running in system tray when closed
-
-
-
-
-
-
-
-
-
- Choose your preferred theme
-
-
-
-
-
-
+
-
-
- Microphone Settings
-
- Configure your microphone preferences
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
- Keyboard Shortcuts
-
- Customize your keyboard shortcuts
-
-
-
-
-
-
-
- Start/stop recording
-
-
-
- Ctrl+Shift+Space
-
-
-
-
-
-
-
- Show/hide main window
-
-
-
- Ctrl+Shift+A
-
-
-
-
-
-
+
-
-
- Text Formatting Configuration
-
- Configure AI-powered post-processing of transcriptions
-
-
-
-
-
-
-
-
- {formatterProvider === "openrouter" && (
- <>
-
-
-
-
-
-
-
-
setOpenrouterApiKey(e.target.value)}
- />
-
- Get your API key from{" "}
-
- openrouter.ai
-
-
-
- >
- )}
-
-
-
-
-
- Apply AI formatting to transcriptions
-
-
-
-
-
-
-
-
-
-
+
-
-
- Advanced Settings
- Advanced configuration options
-
-
-
-
-
-
- Enable detailed logging
-
-
-
-
-
-
-
-
-
- Automatically check for updates
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/apps/desktop/src/renderer/main/pages/settings/components/ShortcutsSettings.tsx b/apps/desktop/src/renderer/main/pages/settings/components/ShortcutsSettings.tsx
new file mode 100644
index 0000000..9dda326
--- /dev/null
+++ b/apps/desktop/src/renderer/main/pages/settings/components/ShortcutsSettings.tsx
@@ -0,0 +1,104 @@
+import React, { useState, useEffect } from "react";
+import {
+ Card,
+ CardHeader,
+ CardTitle,
+ CardDescription,
+ CardContent,
+} from "@/components/ui/card";
+import { Label } from "@/components/ui/label";
+import { ShortcutInput } from "@/components/shortcut-input";
+import { api } from "@/trpc/react";
+import { toast } from "sonner";
+
+export function ShortcutsSettings() {
+ const [pushToTalkShortcut, setPushToTalkShortcut] = useState("");
+ const [toggleRecordingShortcut, setToggleRecordingShortcut] = useState("");
+ const [recordingShortcut, setRecordingShortcut] = useState<
+ "pushToTalk" | "toggleRecording" | null
+ >(null);
+
+ // tRPC queries and mutations
+ const shortcutsQuery = api.settings.getShortcuts.useQuery();
+ const utils = api.useUtils();
+
+ const setShortcutMutation = api.settings.setShortcut.useMutation({
+ onSuccess: () => {
+ utils.settings.getShortcuts.invalidate();
+ },
+ onError: (error) => {
+ console.error("Failed to save shortcut:", error);
+ toast.error("Failed to save shortcut. Please try again.");
+ },
+ });
+
+ // Load shortcuts when query data is available
+ useEffect(() => {
+ if (shortcutsQuery.data) {
+ setPushToTalkShortcut(shortcutsQuery.data.pushToTalk);
+ setToggleRecordingShortcut(shortcutsQuery.data.toggleRecording);
+ }
+ }, [shortcutsQuery.data]);
+
+ const handlePushToTalkChange = (shortcut: string) => {
+ setPushToTalkShortcut(shortcut);
+ setShortcutMutation.mutate({
+ type: "pushToTalk",
+ shortcut: shortcut,
+ });
+ toast.success("Push to Talk shortcut updated");
+ };
+
+ const handleToggleRecordingChange = (shortcut: string) => {
+ setToggleRecordingShortcut(shortcut);
+ setShortcutMutation.mutate({
+ type: "toggleRecording",
+ shortcut: shortcut,
+ });
+ toast.success("Toggle Recording shortcut updated");
+ };
+
+ return (
+
+
+ Keyboard Shortcuts
+ Customize your keyboard shortcuts
+
+
+
+
+
+
+ Hold to dictate while key is pressed
+
+
+
+ setRecordingShortcut(recording ? "pushToTalk" : null)
+ }
+ />
+
+
+
+
+
+
+ Start/stop dictation
+
+
+
+ setRecordingShortcut(recording ? "toggleRecording" : null)
+ }
+ />
+
+
+
+ );
+}
diff --git a/apps/desktop/src/renderer/main/pages/transcriptions/components/TranscriptionsList.tsx b/apps/desktop/src/renderer/main/pages/transcriptions/components/TranscriptionsList.tsx
index 4521fe4..8590e06 100644
--- a/apps/desktop/src/renderer/main/pages/transcriptions/components/TranscriptionsList.tsx
+++ b/apps/desktop/src/renderer/main/pages/transcriptions/components/TranscriptionsList.tsx
@@ -120,6 +120,9 @@ export const TranscriptionsList: React.FC = () => {
};
const getTitle = (text: string) => {
+ if (!text || text.trim() === "") {
+ return `no words detected`;
+ }
const firstSentence = text.split(".")[0];
return firstSentence.length > 50
? firstSentence.substring(0, 50) + "..."
@@ -127,7 +130,174 @@ export const TranscriptionsList: React.FC = () => {
};
const getWordCount = (text: string) => {
- return text.split(" ").length;
+ const trimmedText = text.trim();
+ if (!trimmedText) return 0;
+ return trimmedText.split(/\s+/).length;
+ };
+
+ const renderLoadingState = () => (
+
+
+
+
+
+ Loading transcriptions...
+
+
+
+
+ );
+
+ const renderEmptyState = () => (
+
+
+
+
+
No transcriptions found
+
+ {searchTerm
+ ? "Try adjusting your search terms."
+ : "Start recording to see your transcriptions here."}
+
+ {!searchTerm &&
}
+
+
+
+ );
+
+ const renderTranscriptionCard = (transcription: Transcription) => (
+
+
+
+
+
+
+ {getTitle(transcription.text)}
+
+
+
+
+
+
+
+ {getWordCount(transcription.text)} words
+
+
+ {format(new Date(transcription.timestamp), "MMM d")}
+
+ {format(new Date(transcription.timestamp), "h:mm a")}
+
+ {transcription.language?.toUpperCase() || "EN"}
+
+
+
+
+
+
+
+ Copy transcription
+
+
+
+ {transcription.audioFile && (
+
+
+
+
+
+ Play audio
+
+
+ )}
+
+
+ setOpenDropdownId(open ? transcription.id : null)
+ }
+ >
+
+
+
+
+ Actions
+ {transcription.audioFile && (
+ <>
+ handleDownloadAudio(transcription.id)}
+ disabled={downloadAudioMutation.isPending}
+ >
+
+ Download Audio
+
+
+ >
+ )}
+ handleDelete(transcription.id)}
+ className="text-destructive"
+ disabled={deleteTranscriptionMutation.isPending}
+ >
+
+ Delete
+
+
+
+
+
+
+
+ );
+
+ const renderTranscriptionsList = () => (
+
+ {transcriptions.map(renderTranscriptionCard)}
+
+ );
+
+ const renderFooter = () => {
+ if (loading || transcriptions.length === 0) return null;
+
+ return (
+
+
+ Showing {transcriptions.length} of {totalCount} transcription
+ {totalCount !== 1 ? "s" : ""}
+
+
+ Total:{" "}
+ {transcriptions.reduce((acc, t) => acc + getWordCount(t.text), 0)}{" "}
+ words
+
+
+ );
+ };
+
+ const renderContent = () => {
+ if (loading) return renderLoadingState();
+ if (transcriptions.length === 0) return renderEmptyState();
+ return renderTranscriptionsList();
};
return (
@@ -145,161 +315,11 @@ export const TranscriptionsList: React.FC = () => {