diff --git a/.prettierignore b/.prettierignore index efb4589..d3ebe5e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -7,6 +7,7 @@ out/ .vite/ dist/ build/ +.next/ # Dependencies node_modules/ diff --git a/.vscode/launch.json b/.vscode/launch.json index e1abde9..9b43d80 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -37,4 +37,4 @@ "preLaunchTask": "swift: Build Release SwiftHelper (packages/native-helpers/swift-helper)" } ] -} \ No newline at end of file +} diff --git a/README.md b/README.md index 6f376c2..9146783 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,6 @@ -

PRs Welcome @@ -42,6 +41,7 @@ Open source AI Dictation and Note-taking\ Dictate hands-free, transcribe meetings, and capture notes effortlessly - powered by Gen AI ## ✨ Features + > ✔︎ - Done, ◑ - In Progress, ◯ - Planned ### 📱 Apps @@ -121,8 +121,9 @@ Contributions are welcome! Please read the [Contributing Guide][contributing] to Released under [MIT][license]. + [contributing]: https://github.com/amicalhq/amical/blob/main/CONTRIBUTING.md [license]: https://github.com/amicalhq/amical/blob/main/LICENSE [discussions]: https://discuss.amical.ai [issues]: https://github.com/amicalhq/amical/issues -[pulls]: https://github.com/amicalhq/amical/pulls "submit a pull request" \ No newline at end of file +[pulls]: https://github.com/amicalhq/amical/pulls "submit a pull request" diff --git a/apps/desktop/drizzle.config.ts b/apps/desktop/drizzle.config.ts index e424d94..f5d85e8 100644 --- a/apps/desktop/drizzle.config.ts +++ b/apps/desktop/drizzle.config.ts @@ -1,10 +1,10 @@ -import type { Config } from 'drizzle-kit'; +import type { Config } from "drizzle-kit"; export default { - schema: './src/db/schema.ts', - out: './src/db/migrations', - dialect: 'sqlite', + schema: "./src/db/schema.ts", + out: "./src/db/migrations", + dialect: "sqlite", dbCredentials: { - url: 'file:./amical.db', + url: "file:./amical.db", }, } satisfies Config; diff --git a/apps/desktop/forge.config.ts b/apps/desktop/forge.config.ts index d6a083c..67ea994 100644 --- a/apps/desktop/forge.config.ts +++ b/apps/desktop/forge.config.ts @@ -1,46 +1,53 @@ -import type { ForgeConfig } from '@electron-forge/shared-types'; -import { MakerSquirrel } from '@electron-forge/maker-squirrel'; -import { MakerDMG } from '@electron-forge/maker-dmg'; -import { MakerDeb } from '@electron-forge/maker-deb'; -import { MakerRpm } from '@electron-forge/maker-rpm'; -import { VitePlugin } from '@electron-forge/plugin-vite'; -import { FusesPlugin } from '@electron-forge/plugin-fuses'; -import { FuseV1Options, FuseVersion } from '@electron/fuses'; -import { PublisherGithub } from '@electron-forge/publisher-github'; -import { readdirSync, rmdirSync, statSync, existsSync, mkdirSync, cpSync } from 'node:fs'; -import { join, normalize } from 'node:path'; +import type { ForgeConfig } from "@electron-forge/shared-types"; +import { MakerSquirrel } from "@electron-forge/maker-squirrel"; +import { MakerDMG } from "@electron-forge/maker-dmg"; +import { MakerDeb } from "@electron-forge/maker-deb"; +import { MakerRpm } from "@electron-forge/maker-rpm"; +import { VitePlugin } from "@electron-forge/plugin-vite"; +import { FusesPlugin } from "@electron-forge/plugin-fuses"; +import { FuseV1Options, FuseVersion } from "@electron/fuses"; +import { PublisherGithub } from "@electron-forge/publisher-github"; +import { + readdirSync, + rmdirSync, + statSync, + existsSync, + mkdirSync, + cpSync, +} from "node:fs"; +import { join, normalize } from "node:path"; // Use flora-colossus for finding all dependencies of EXTERNAL_DEPENDENCIES // flora-colossus is maintained by MarshallOfSound (a top electron-forge contributor) // already included as a dependency of electron-packager/galactus (so we do NOT have to add it to package.json) // grabs nested dependencies from tree -import { Walker, DepType, type Module } from 'flora-colossus'; +import { Walker, DepType, type Module } from "flora-colossus"; let nativeModuleDependenciesToPackage: string[] = []; export const EXTERNAL_DEPENDENCIES = [ - 'electron-squirrel-startup', - 'smart-whisper', - '@libsql/client', - '@libsql/darwin-arm64', - '@libsql/darwin-x64', - '@libsql/linux-x64-gnu', - '@libsql/linux-x64-musl', - '@libsql/win32-x64-msvc', - 'libsql', + "electron-squirrel-startup", + "smart-whisper", + "@libsql/client", + "@libsql/darwin-arm64", + "@libsql/darwin-x64", + "@libsql/linux-x64-gnu", + "@libsql/linux-x64-musl", + "@libsql/win32-x64-msvc", + "libsql", // Add any other native modules you need here ]; const config: ForgeConfig = { hooks: { prePackage: async () => { - console.error('prePackage'); + console.error("prePackage"); const projectRoot = normalize(__dirname); // In a monorepo, node_modules are typically at the root level - const monorepoRoot = join(projectRoot, '../../'); // Go up to monorepo root - + const monorepoRoot = join(projectRoot, "../../"); // Go up to monorepo root + const getExternalNestedDependencies = async ( nodeModuleNames: string[], - includeNestedDeps = true + includeNestedDeps = true, ) => { const foundModules = new Set(nodeModuleNames); if (includeNestedDeps) { @@ -52,17 +59,21 @@ const config: ForgeConfig = { modules: Module[]; walkDependenciesForModule: ( moduleRoot: string, - depType: DepType + depType: DepType, ) => Promise; }; - const moduleRoot = join(monorepoRoot, 'node_modules', external); - console.log('moduleRoot', moduleRoot); + const moduleRoot = join(monorepoRoot, "node_modules", external); + console.log("moduleRoot", moduleRoot); // Initialize Walker with monorepo root as base path - const walker = new Walker(monorepoRoot) as unknown as MyPublicWalker; + const walker = new Walker( + monorepoRoot, + ) as unknown as MyPublicWalker; walker.modules = []; await walker.walkDependenciesForModule(moduleRoot, DepType.PROD); walker.modules - .filter((dep) => (dep.nativeModuleType as number) === DepType.PROD) + .filter( + (dep) => (dep.nativeModuleType as number) === DepType.PROD, + ) // Remove the problematic name splitting that breaks scoped packages .map((dep) => dep.name) .forEach((name) => foundModules.add(name)); @@ -70,21 +81,25 @@ const config: ForgeConfig = { } return foundModules; }; - - const nativeModuleDependencies = await getExternalNestedDependencies(EXTERNAL_DEPENDENCIES); + + const nativeModuleDependencies = await getExternalNestedDependencies( + EXTERNAL_DEPENDENCIES, + ); nativeModuleDependenciesToPackage = Array.from(nativeModuleDependencies); - + // Copy external dependencies to local node_modules - console.error('Copying external dependencies to local node_modules'); - const localNodeModules = join(projectRoot, 'node_modules'); - const rootNodeModules = join(monorepoRoot, 'node_modules'); + console.error("Copying external dependencies to local node_modules"); + const localNodeModules = join(projectRoot, "node_modules"); + const rootNodeModules = join(monorepoRoot, "node_modules"); // Ensure local node_modules directory exists if (!existsSync(localNodeModules)) { mkdirSync(localNodeModules, { recursive: true }); } - console.log(`Found ${nativeModuleDependenciesToPackage.length} dependencies to copy`); + console.log( + `Found ${nativeModuleDependenciesToPackage.length} dependencies to copy`, + ); // Copy all required dependencies for (const dep of nativeModuleDependenciesToPackage) { @@ -108,7 +123,6 @@ const config: ForgeConfig = { console.log(`Copying ${dep}...`); cpSync(rootDepPath, localDepPath, { recursive: true }); console.log(`✓ Successfully copied ${dep}`); - } catch (error) { console.error(`Failed to copy ${dep}:`, error); } @@ -116,82 +130,82 @@ const config: ForgeConfig = { }, packageAfterPrune: async (_forgeConfig, buildPath) => { try { - function getItemsFromFolder( - path: string, - totalCollection: { - path: string; - type: 'directory' | 'file'; - empty: boolean; - }[] = [] - ) { - try { - const normalizedPath = normalize(path); - const childItems = readdirSync(normalizedPath); - const getItemStats = statSync(normalizedPath); - if (getItemStats.isDirectory()) { - totalCollection.push({ - path: normalizedPath, - type: 'directory', - empty: childItems.length === 0, - }); - } - childItems.forEach((childItem) => { - const childItemNormalizedPath = join(normalizedPath, childItem); - const childItemStats = statSync(childItemNormalizedPath); - if (childItemStats.isDirectory()) { - getItemsFromFolder(childItemNormalizedPath, totalCollection); - } else { + function getItemsFromFolder( + path: string, + totalCollection: { + path: string; + type: "directory" | "file"; + empty: boolean; + }[] = [], + ) { + try { + const normalizedPath = normalize(path); + const childItems = readdirSync(normalizedPath); + const getItemStats = statSync(normalizedPath); + if (getItemStats.isDirectory()) { totalCollection.push({ - path: childItemNormalizedPath, - type: 'file', - empty: false, + path: normalizedPath, + type: "directory", + empty: childItems.length === 0, }); } - }); - } catch { - return; + childItems.forEach((childItem) => { + const childItemNormalizedPath = join(normalizedPath, childItem); + const childItemStats = statSync(childItemNormalizedPath); + if (childItemStats.isDirectory()) { + getItemsFromFolder(childItemNormalizedPath, totalCollection); + } else { + totalCollection.push({ + path: childItemNormalizedPath, + type: "file", + empty: false, + }); + } + }); + } catch { + return; + } + return totalCollection; } - return totalCollection; - } - const getItems = getItemsFromFolder(buildPath) ?? []; - for (const item of getItems) { - const DELETE_EMPTY_DIRECTORIES = true; - if (item.empty === true) { - if (DELETE_EMPTY_DIRECTORIES) { - const pathToDelete = normalize(item.path); - // one last check to make sure it is a directory and is empty - const stats = statSync(pathToDelete); - if (!stats.isDirectory()) { - // SKIPPING DELETION: pathToDelete is not a directory - return; + const getItems = getItemsFromFolder(buildPath) ?? []; + for (const item of getItems) { + const DELETE_EMPTY_DIRECTORIES = true; + if (item.empty === true) { + if (DELETE_EMPTY_DIRECTORIES) { + const pathToDelete = normalize(item.path); + // one last check to make sure it is a directory and is empty + const stats = statSync(pathToDelete); + if (!stats.isDirectory()) { + // SKIPPING DELETION: pathToDelete is not a directory + return; + } + const childItems = readdirSync(pathToDelete); + if (childItems.length !== 0) { + // SKIPPING DELETION: pathToDelete is not empty + return; + } + rmdirSync(pathToDelete); } - const childItems = readdirSync(pathToDelete); - if (childItems.length !== 0) { - // SKIPPING DELETION: pathToDelete is not empty - return; - } - rmdirSync(pathToDelete); } } - } } catch (error) { - console.error('Error in packageAfterPrune:', error); + console.error("Error in packageAfterPrune:", error); throw error; } }, }, packagerConfig: { asar: true, - name: 'Amical', - executableName: 'Amical', - icon: './assets/logo', // Path to your icon file (without extension) + name: "Amical", + executableName: "Amical", + icon: "./assets/logo", // Path to your icon file (without extension) extraResource: [ - '../../packages/native-helpers/swift-helper/bin', - './src/db/migrations', + "../../packages/native-helpers/swift-helper/bin", + "./src/db/migrations", ], extendInfo: { NSMicrophoneUsageDescription: - 'This app needs access to your microphone to record audio for transcription.', + "This app needs access to your microphone to record audio for transcription.", }, // Code signing configuration for macOS (configure when ready to sign) // osxSign: { @@ -211,19 +225,21 @@ const config: ForgeConfig = { prune: false, ignore: (file: string) => { try { - const filePath = file.toLowerCase(); const KEEP_FILE = { keep: false, log: true, }; // NOTE: must return false for empty string or nothing will be packaged - if (filePath === '') KEEP_FILE.keep = true; - if (!KEEP_FILE.keep && filePath === '/package.json') KEEP_FILE.keep = true; - if (!KEEP_FILE.keep && filePath === '/node_modules') KEEP_FILE.keep = true; - if (!KEEP_FILE.keep && filePath === '/.vite') KEEP_FILE.keep = true; - if (!KEEP_FILE.keep && filePath.startsWith('/.vite/')) KEEP_FILE.keep = true; - if (!KEEP_FILE.keep && filePath.startsWith('/node_modules/')) { + if (filePath === "") KEEP_FILE.keep = true; + if (!KEEP_FILE.keep && filePath === "/package.json") + KEEP_FILE.keep = true; + if (!KEEP_FILE.keep && filePath === "/node_modules") + KEEP_FILE.keep = true; + if (!KEEP_FILE.keep && filePath === "/.vite") KEEP_FILE.keep = true; + if (!KEEP_FILE.keep && filePath.startsWith("/.vite/")) + KEEP_FILE.keep = true; + if (!KEEP_FILE.keep && filePath.startsWith("/node_modules/")) { // check if matches any of the external dependencies for (const dep of nativeModuleDependenciesToPackage) { if ( @@ -242,29 +258,31 @@ const config: ForgeConfig = { KEEP_FILE.log = false; break; } - + // Handle scoped packages: if dep is @scope/package, also keep @scope/ directory - if (dep.includes('/') && dep.startsWith('@')) { - const scopeDir = dep.split('/')[0]; // @libsql/client -> @libsql + if (dep.includes("/") && dep.startsWith("@")) { + const scopeDir = dep.split("/")[0]; // @libsql/client -> @libsql if ( filePath === `/node_modules/${scopeDir}/` || filePath === `/node_modules/${scopeDir}` || filePath.startsWith(`/node_modules/${scopeDir}/`) ) { KEEP_FILE.keep = true; - KEEP_FILE.log = filePath === `/node_modules/${scopeDir}/` || filePath === `/node_modules/${scopeDir}`; + KEEP_FILE.log = + filePath === `/node_modules/${scopeDir}/` || + filePath === `/node_modules/${scopeDir}`; break; } } } } if (KEEP_FILE.keep) { - if (KEEP_FILE.log) console.log('Keeping:', file); + if (KEEP_FILE.log) console.log("Keeping:", file); return false; } return true; } catch (error) { - console.error('Error in ignore:', error); + console.error("Error in ignore:", error); throw error; } }, @@ -272,12 +290,15 @@ const config: ForgeConfig = { rebuildConfig: {}, makers: [ new MakerSquirrel({}), - new MakerDMG({ - name: 'Amical', - icon: './assets/logo.svg' - }, ['darwin']), - new MakerRpm({}), - new MakerDeb({}) + new MakerDMG( + { + name: "Amical", + icon: "./assets/logo.svg", + }, + ["darwin"], + ), + new MakerRpm({}), + new MakerDeb({}), ], plugins: [ new VitePlugin({ @@ -286,24 +307,24 @@ const config: ForgeConfig = { build: [ { // `entry` is just an alias for `build.lib.entry` in the corresponding file of `config`. - entry: 'src/main/main.ts', - config: 'vite.main.config.mts', - target: 'main', + entry: "src/main/main.ts", + config: "vite.main.config.mts", + target: "main", }, { - entry: 'src/main/preload.ts', - config: 'vite.preload.config.mts', - target: 'preload', + entry: "src/main/preload.ts", + config: "vite.preload.config.mts", + target: "preload", }, ], renderer: [ { - name: 'main_window', - config: 'vite.renderer.config.mts', + name: "main_window", + config: "vite.renderer.config.mts", }, { - name: 'widget_window', - config: 'vite.widget.config.mts', + name: "widget_window", + config: "vite.widget.config.mts", }, ], }), @@ -322,8 +343,8 @@ const config: ForgeConfig = { publishers: [ new PublisherGithub({ repository: { - owner: 'amicalhq', - name: 'amical', + owner: "amicalhq", + name: "amical", }, prerelease: true, draft: true, // Create draft releases first for review diff --git a/apps/desktop/src/components/ShortcutIndicator.tsx b/apps/desktop/src/components/ShortcutIndicator.tsx index 2c97a41..24ba8ef 100644 --- a/apps/desktop/src/components/ShortcutIndicator.tsx +++ b/apps/desktop/src/components/ShortcutIndicator.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState } from "react"; const ShortcutIndicator: React.FC = () => { const [isPressed, setIsPressed] = useState(false); @@ -20,9 +20,9 @@ const ShortcutIndicator: React.FC = () => { return (

- {isPressed ? 'Pressed!' : 'Alt+Space'} + {isPressed ? "Pressed!" : "Alt+Space"}
); }; diff --git a/apps/desktop/src/components/Waveform.tsx b/apps/desktop/src/components/Waveform.tsx index 0619506..dc2e55f 100644 --- a/apps/desktop/src/components/Waveform.tsx +++ b/apps/desktop/src/components/Waveform.tsx @@ -1,5 +1,5 @@ -import React from 'react'; -import { motion } from 'framer-motion'; +import React from "react"; +import { motion } from "framer-motion"; interface WaveformProps { index: number; @@ -42,11 +42,11 @@ export function Waveform({ }} transition={{ duration: voiceDetected ? 0.8 : 0.3, - ease: 'easeInOut', + ease: "easeInOut", repeat: voiceDetected ? Number.POSITIVE_INFINITY : 0, - repeatType: 'loop', + repeatType: "loop", delay: index * 0.06, - type: 'tween', + type: "tween", }} /> ); diff --git a/apps/desktop/src/components/app-sidebar.tsx b/apps/desktop/src/components/app-sidebar.tsx index 658b29f..0fddfe4 100644 --- a/apps/desktop/src/components/app-sidebar.tsx +++ b/apps/desktop/src/components/app-sidebar.tsx @@ -1,4 +1,4 @@ -import * as React from "react" +import * as React from "react"; import { IconDatabase, IconFileDescription, @@ -6,10 +6,10 @@ import { IconReport, IconSettings, IconBookFilled, -} from "@tabler/icons-react" +} from "@tabler/icons-react"; -import { NavMain } from "@/components/nav-main" -import { NavSecondary } from "@/components/nav-secondary" +import { NavMain } from "@/components/nav-main"; +import { NavSecondary } from "@/components/nav-secondary"; import { Sidebar, SidebarContent, @@ -18,16 +18,16 @@ import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, -} from "@/components/ui/sidebar" +} from "@/components/ui/sidebar"; // Custom Discord icon component const DiscordIcon = ({ className }: { className?: string }) => ( - Discord -) +); const data = { user: { @@ -88,14 +88,18 @@ const data = { icon: IconFileWord, }, ], -} +}; interface AppSidebarProps extends React.ComponentProps { onNavigate?: (item: { title: string }) => void; currentView?: string; } -export function AppSidebar({ onNavigate, currentView, ...props }: AppSidebarProps) { +export function AppSidebar({ + onNavigate, + currentView, + ...props +}: AppSidebarProps) { return (
@@ -106,8 +110,15 @@ export function AppSidebar({ onNavigate, currentView, ...props }: AppSidebarProp asChild className="data-[slot=sidebar-menu-button]:!p-1.5" > -
- Amical Logo + + Amical Logo Amical @@ -115,12 +126,19 @@ export function AppSidebar({ onNavigate, currentView, ...props }: AppSidebarProp - - + + - - {/* */} - + {/* */} - ) + ); } diff --git a/apps/desktop/src/components/data-table.tsx b/apps/desktop/src/components/data-table.tsx index 0af1fc3..d87782d 100644 --- a/apps/desktop/src/components/data-table.tsx +++ b/apps/desktop/src/components/data-table.tsx @@ -1,4 +1,4 @@ -import * as React from "react" +import * as React from "react"; import { closestCenter, DndContext, @@ -9,15 +9,15 @@ import { useSensors, type DragEndEvent, type UniqueIdentifier, -} from "@dnd-kit/core" -import { restrictToVerticalAxis } from "@dnd-kit/modifiers" +} from "@dnd-kit/core"; +import { restrictToVerticalAxis } from "@dnd-kit/modifiers"; import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy, -} from "@dnd-kit/sortable" -import { CSS } from "@dnd-kit/utilities" +} from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; import { IconChevronDown, IconChevronLeft, @@ -31,7 +31,7 @@ import { IconLoader, IconPlus, IconTrendingUp, -} from "@tabler/icons-react" +} from "@tabler/icons-react"; import { ColumnDef, ColumnFiltersState, @@ -46,21 +46,21 @@ import { SortingState, useReactTable, VisibilityState, -} from "@tanstack/react-table" -import { Area, AreaChart, CartesianGrid, XAxis } from "recharts" -import { toast } from "sonner" -import { z } from "zod" +} from "@tanstack/react-table"; +import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"; +import { toast } from "sonner"; +import { z } from "zod"; -import { useIsMobile } from "@/hooks/use-mobile" -import { Badge } from "@/components/ui/badge" -import { Button } from "@/components/ui/button" +import { useIsMobile } from "@/hooks/use-mobile"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, -} from "@/components/ui/chart" -import { Checkbox } from "@/components/ui/checkbox" +} from "@/components/ui/chart"; +import { Checkbox } from "@/components/ui/checkbox"; import { Drawer, DrawerClose, @@ -70,7 +70,7 @@ import { DrawerHeader, DrawerTitle, DrawerTrigger, -} from "@/components/ui/drawer" +} from "@/components/ui/drawer"; import { DropdownMenu, DropdownMenuCheckboxItem, @@ -78,17 +78,17 @@ import { DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" +} from "@/components/ui/dropdown-menu"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from "@/components/ui/select" -import { Separator } from "@/components/ui/separator" +} from "@/components/ui/select"; +import { Separator } from "@/components/ui/separator"; import { Table, TableBody, @@ -96,13 +96,8 @@ import { TableHead, TableHeader, TableRow, -} from "@/components/ui/table" -import { - Tabs, - TabsContent, - TabsList, - TabsTrigger, -} from "@/components/ui/tabs" +} from "@/components/ui/table"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; export const schema = z.object({ id: z.number(), @@ -112,13 +107,13 @@ export const schema = z.object({ target: z.string(), limit: z.string(), reviewer: z.string(), -}) +}); // Create a separate component for the drag handle function DragHandle({ id }: { id: number }) { const { attributes, listeners } = useSortable({ id, - }) + }); return ( - ) + ); } const columns: ColumnDef>[] = [ @@ -170,7 +165,7 @@ const columns: ColumnDef>[] = [ accessorKey: "header", header: "Header", cell: ({ row }) => { - return + return ; }, enableHiding: false, }, @@ -205,12 +200,12 @@ const columns: ColumnDef>[] = [ cell: ({ row }) => (
{ - e.preventDefault() + e.preventDefault(); toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), { loading: `Saving ${row.original.header}`, success: "Done", error: "Error", - }) + }); }} >