diff --git a/apps/desktop/.cursor/rules/style.mdc b/apps/desktop/.cursor/rules/style.mdc index 80ce531c1a..6b7a4f7afb 100644 --- a/apps/desktop/.cursor/rules/style.mdc +++ b/apps/desktop/.cursor/rules/style.mdc @@ -9,4 +9,4 @@ alwaysApply: true ## Tailwind ClassNames -- If there are many classNames and they have conditional logic, use `cn` (import it with `import { cn } from "@hypr/ui/lib/utils"`). It is similar to `clsx`. Always pass an array. Split by logical grouping. +- If there are many classNames and they have conditional logic, use `cn` (import it with `import { cn } from "@hypr/utils"`). It is similar to `clsx`. Always pass an array. Split by logical grouping. diff --git a/apps/desktop/public/assets/meet.png b/apps/desktop/public/assets/conferencing-platforms/meet.png similarity index 100% rename from apps/desktop/public/assets/meet.png rename to apps/desktop/public/assets/conferencing-platforms/meet.png diff --git a/apps/desktop/public/assets/teams.png b/apps/desktop/public/assets/conferencing-platforms/teams.png similarity index 100% rename from apps/desktop/public/assets/teams.png rename to apps/desktop/public/assets/conferencing-platforms/teams.png diff --git a/apps/desktop/public/assets/webex.png b/apps/desktop/public/assets/conferencing-platforms/webex.png similarity index 100% rename from apps/desktop/public/assets/webex.png rename to apps/desktop/public/assets/conferencing-platforms/webex.png diff --git a/apps/desktop/public/assets/zoom.png b/apps/desktop/public/assets/conferencing-platforms/zoom.png similarity index 100% rename from apps/desktop/public/assets/zoom.png rename to apps/desktop/public/assets/conferencing-platforms/zoom.png diff --git a/apps/desktop/src/components/chat/input.tsx b/apps/desktop/src/components/chat/input.tsx index 1e3a5c553f..edad0c38ef 100644 --- a/apps/desktop/src/components/chat/input.tsx +++ b/apps/desktop/src/components/chat/input.tsx @@ -111,6 +111,7 @@ export function ChatMessageInput({ }); } }} + defaultValue="auto" > diff --git a/apps/desktop/src/components/chat/message/normal.tsx b/apps/desktop/src/components/chat/message/normal.tsx index 92da98d49e..d883119dae 100644 --- a/apps/desktop/src/components/chat/message/normal.tsx +++ b/apps/desktop/src/components/chat/message/normal.tsx @@ -38,7 +38,7 @@ export function NormalMessage({ message, handleReload }: { message: HyprUIMessag )} {shouldShowTimestamp && message.metadata?.createdAt && ( -
+
{formatDistanceToNow(message.metadata.createdAt, { addSuffix: true })}
)} @@ -81,7 +81,7 @@ function Reasoning({ part }: { part: Extract }) { title={title} disabled={streaming} > -
+
{part.text}
diff --git a/apps/desktop/src/components/chat/message/shared.tsx b/apps/desktop/src/components/chat/message/shared.tsx index 05fca0b6d3..9274b28986 100644 --- a/apps/desktop/src/components/chat/message/shared.tsx +++ b/apps/desktop/src/components/chat/message/shared.tsx @@ -35,9 +35,9 @@ export function MessageBubble({
-
+
{children}
diff --git a/apps/desktop/src/components/chat/message/tool/search.tsx b/apps/desktop/src/components/chat/message/tool/search.tsx index cf940f54a6..a859c82067 100644 --- a/apps/desktop/src/components/chat/message/tool/search.tsx +++ b/apps/desktop/src/components/chat/message/tool/search.tsx @@ -68,7 +68,7 @@ function RenderContent({ part }: { part: Part }) { {results.map((result: any, index: number) => ( - + @@ -76,8 +76,8 @@ function RenderContent({ part }: { part: Part }) { ))} - - + +
); diff --git a/apps/desktop/src/components/main/body/calendars.tsx b/apps/desktop/src/components/main/body/calendars.tsx index 6997d30f69..2fb1bdce7d 100644 --- a/apps/desktop/src/components/main/body/calendars.tsx +++ b/apps/desktop/src/components/main/body/calendars.tsx @@ -1,10 +1,23 @@ -import { addMonths, eachDayOfInterval, endOfMonth, format, getDay, isSameMonth, startOfMonth } from "@hypr/utils"; -import { clsx } from "clsx"; -import { CalendarIcon, ChevronLeftIcon, ChevronRightIcon, FileTextIcon, Pen } from "lucide-react"; -import { useState } from "react"; - import { Button } from "@hypr/ui/components/ui/button"; +import { ButtonGroup } from "@hypr/ui/components/ui/button-group"; +import { Checkbox } from "@hypr/ui/components/ui/checkbox"; import { Popover, PopoverContent, PopoverTrigger } from "@hypr/ui/components/ui/popover"; +import { + addDays, + addMonths, + cn, + eachDayOfInterval, + format, + getDay, + isSameDay, + isSameMonth, + startOfMonth, + subDays, +} from "@hypr/utils"; + +import { Calendar, CalendarDays, ChevronLeft, ChevronRight, FileText, Pen } from "lucide-react"; +import { useState } from "react"; + import * as persisted from "../../../store/tinybase/persisted"; import { type Tab, useTabs } from "../../../store/zustand/tabs"; import { StandardTabWrapper } from "./index"; @@ -21,7 +34,7 @@ export const TabItemCalendar: TabItem = ( ) => { return ( } + icon={} title={"Calendar"} active={tab.active} handleCloseThis={() => handleCloseThis(tab)} @@ -37,14 +50,36 @@ export function TabContentCalendar({ tab }: { tab: Tab }) { return null; } + const [sidebarOpen, setSidebarOpen] = useState(false); + const { openCurrent } = useTabs(); + + const calendarIds = persisted.UI.useRowIds("calendars", persisted.STORE_ID); + + const [selectedCalendars, setSelectedCalendars] = useState>(() => new Set(calendarIds)); + + const toggleCalendar = (calendarId: string) => { + setSelectedCalendars((prev) => { + const next = new Set(prev); + if (next.has(calendarId)) { + next.delete(calendarId); + } else { + next.add(calendarId); + } + return next; + }); + }; const monthStart = startOfMonth(tab.month); - const monthEnd = endOfMonth(tab.month); - const days = eachDayOfInterval({ start: monthStart, end: monthEnd }).map((day) => format(day, "yyyy-MM-dd")); const startDayOfWeek = getDay(monthStart); const weekDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; const monthLabel = format(tab.month, "MMMM yyyy"); + // Calculate all days to display including previous and next month + const calendarStart = subDays(monthStart, startDayOfWeek); + const totalCells = 42; // 6 rows * 7 days + const calendarEnd = addDays(calendarStart, totalCells - 1); + const allDays = eachDayOfInterval({ start: calendarStart, end: calendarEnd }).map((day) => format(day, "yyyy-MM-dd")); + const handlePreviousMonth = () => { openCurrent({ ...tab, month: addMonths(tab.month, -1) }); }; @@ -59,67 +94,145 @@ export function TabContentCalendar({ tab }: { tab: Tab }) { return ( -
-
-
{monthLabel}
-
- - - +
+ {sidebarOpen && ( + + )} + +
+
+
+ {!sidebarOpen && ( + + )} +
{monthLabel}
+ + + + + + + +
- -
-
-
-
- {weekDays.map((day) => ( -
- {day} +
+ {weekDays.map((day) => ( +
+ {day} +
+ ))} +
+ + +
+ {Array.from({ length: 6 }).map((_, weekIndex) => ( +
+ {allDays.slice(weekIndex * 7, (weekIndex + 1) * 7).map((day, dayIndex) => ( + + ))}
))}
-
- {Array.from({ length: startDayOfWeek }).map((_, i) => ( -
- ))} - {days.map((day) => ( - - ))} -
); } -function TabContentCalendarDay({ day, isCurrentMonth }: { day: string; isCurrentMonth: boolean }) { - const eventIds = persisted.UI.useSliceRowIds( +function CalendarCheckboxRow( + { id, checked, onToggle }: { id: string; checked: boolean; onToggle: () => void }, +) { + const calendar = persisted.UI.useRow("calendars", id, persisted.STORE_ID); + return ( +
+ + +
+ ); +} + +function TabContentCalendarDay({ + day, + isCurrentMonth, + isFirstColumn, + isLastRow, + selectedCalendars, +}: { + day: string; + isCurrentMonth: boolean; + isFirstColumn: boolean; + isLastColumn: boolean; + isLastRow: boolean; + selectedCalendars: Set; +}) { + const allEventIds = persisted.UI.useSliceRowIds( persisted.INDEXES.eventsByDate, day, persisted.STORE_ID, ); + const store = persisted.UI.useStore(persisted.STORE_ID); + + const eventIds = allEventIds.filter((eventId) => { + const event = store?.getRow("events", eventId); + return event?.calendar_id && selectedCalendars.has(event.calendar_id as string); + }); + const sessionIds = persisted.UI.useSliceRowIds( persisted.INDEXES.sessionByDateWithoutEvent, day, @@ -160,52 +273,49 @@ function TabContentCalendarDay({ day, isCurrentMonth }: { day: string; isCurrent return (
-
-
-
- - {dayNumber} - -
-
-
- -
-
- {visibleItems.map((item) => - item.type === "event" - ? - : +
+ + {dayNumber} + +
- {hiddenCount > 0 && ( - - )} -
+
+ {visibleItems.map((item) => + item.type === "event" + ? + : + )} + + {hiddenCount > 0 && ( + + )}
); @@ -241,31 +351,21 @@ function TabContentCalendarDayEvents({ eventId }: { eventId: string }) { const start = new Date(event.started_at); const end = new Date(event.ended_at); - const formatTime = (date: Date) => { - const hours = date.getHours(); - const minutes = date.getMinutes(); - const ampm = hours >= 12 ? "PM" : "AM"; - const displayHours = hours % 12 || 12; - return `${displayHours}:${minutes.toString().padStart(2, "0")} ${ampm}`; - }; - - const formatDate = (date: Date) => { - const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; - return `${months[date.getMonth()]} ${date.getDate()}`; - }; - - const isSameDay = start.toDateString() === end.toDateString(); - if (isSameDay) { - return `${formatDate(start)}, ${formatTime(start)} - ${formatTime(end)}`; + if (isNaN(start.getTime()) || isNaN(end.getTime())) { + return ""; + } + + if (isSameDay(start, end)) { + return `${format(start, "MMM d")}, ${format(start, "h:mm a")} - ${format(end, "h:mm a")}`; } - return `${formatDate(start)}, ${formatTime(start)} - ${formatDate(end)}, ${formatTime(end)}`; + return `${format(start, "MMM d")}, ${format(start, "h:mm a")} - ${format(end, "MMM d")}, ${format(end, "h:mm a")}`; }; return (
- +
{event.title}
@@ -286,7 +386,7 @@ function TabContentCalendarDayEvents({ eventId }: { eventId: string }) { className="flex items-center gap-2 px-2 py-1 bg-neutral-50 border border-neutral-200 rounded-md cursor-pointer hover:bg-neutral-100 transition-colors" onClick={handleClick} > - +
{linkedSession?.title || "Untitled Note"}
@@ -321,25 +421,23 @@ function TabContentCalendarDaySessions({ sessionId }: { sessionId: string }) { }; const formatSessionTime = () => { - const created = new Date(session.created_at || ""); - const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; - const hours = created.getHours(); - const minutes = created.getMinutes(); - const ampm = hours >= 12 ? "PM" : "AM"; - const displayHours = hours % 12 || 12; - - return `Created: ${months[created.getMonth()]} ${created.getDate()}, ${displayHours}:${ - minutes.toString().padStart(2, "0") - } ${ampm}`; + if (!session.created_at) { + return "Created: —"; + } + const created = new Date(session.created_at); + if (isNaN(created.getTime())) { + return "Created: —"; + } + return `Created: ${format(created, "MMM d, h:mm a")}`; }; return (
- +
- {session.title} + {event && session.event_id ? event.title : session.title || "Untitled"}
@@ -356,7 +454,7 @@ function TabContentCalendarDaySessions({ sessionId }: { sessionId: string }) { className="flex items-center gap-2 px-2 py-1 bg-neutral-50 border border-neutral-200 rounded-md cursor-pointer hover:bg-neutral-100 transition-colors" onClick={handleClick} > - +
{event && session.event_id ? event.title : session.title || "Untitled"}
diff --git a/apps/desktop/src/components/main/body/contacts/details.tsx b/apps/desktop/src/components/main/body/contacts/details.tsx index ab769415fc..cdb902119b 100644 --- a/apps/desktop/src/components/main/body/contacts/details.tsx +++ b/apps/desktop/src/components/main/body/contacts/details.tsx @@ -192,7 +192,7 @@ function EditPersonForm({ return (
-
+

Edit Contact

@@ -201,7 +201,7 @@ function EditPersonForm({ variant="ghost" size="sm" onClick={onCancel} - className="hover:bg-gray-100 text-gray-700" + className="hover:bg-neutral-100 text-neutral-700" > Cancel @@ -209,7 +209,7 @@ function EditPersonForm({ onClick={onSave} variant="ghost" size="sm" - className="bg-gray-100 hover:bg-gray-200 text-gray-700" + className="bg-neutral-100 hover:bg-neutral-200 text-neutral-700" > Save @@ -219,19 +219,19 @@ function EditPersonForm({
-
- +
+ {getInitials(personData.name as string || "?")}
-
+
-
-
Company
+
+
Company
@@ -258,8 +258,8 @@ function EditPersonNameField({ personId }: { personId: string }) { ); return ( -
-
Name
+
+
Name
-
Job Title
+
+
Job Title
-
Email
+
+
Email
-
LinkedIn
+
+
LinkedIn
{organization.name} - + { e.stopPropagation(); handleRemoveOrganization(); @@ -391,7 +391,7 @@ function EditPersonOrganizationSelector({ personId }: { personId: string }) {
) - : Select organization} + : Select organization}
@@ -439,12 +439,12 @@ function OrganizationControl({ return (
-
Organization
+
Organization
-
- +
+ setSearchTerm(e.target.value)} onKeyDown={handleKeyDown} placeholder="Search or add company" - className="w-full bg-transparent text-sm focus:outline-none placeholder:text-gray-400 focus-visible:ring-0 focus-visible:ring-offset-0" + className="w-full bg-transparent text-sm focus:outline-none placeholder:text-neutral-400 focus-visible:ring-0 focus-visible:ring-offset-0" />
{searchTerm.trim() && ( -
+
{organizations.map((org: any) => ( + +
+
+
+
+
+ +
+

People

+
+
+ {peopleInOrg.length > 0 + ? ( + peopleInOrg.map((humanId: string) => { + const human = allHumans[humanId]; + if (!human) { + return null; + } + + return ( +
+
+
+ + {getInitials(human.name as string || human.email as string)} + +
+
+
+ {human.name || human.email || "Unnamed"} +
+ {human.email && human.name && ( +
{human.email as string}
+ )} +
+
+
+ ); + }) + ) + :

No people in this organization

} +
+
+
+ + ) + ) + : ( +
+

Select an organization to view details

+
+ )} +
+ ); +} + +function EditOrganizationForm({ + organizationId, + onSave, + onCancel, +}: { + organizationId: string; + onSave: () => void; + onCancel: () => void; +}) { + const orgData = persisted.UI.useRow("organizations", organizationId, persisted.STORE_ID); + + if (!orgData) { + return null; + } + + return ( +
+
+
+

Edit Organization

+
+ + +
+
+
+ +
+
+
+ +
+
+ +
+ +
+
+
+ ); +} + +function EditOrganizationNameField({ organizationId }: { organizationId: string }) { + const value = persisted.UI.useCell("organizations", organizationId, "name", persisted.STORE_ID); + + const handleChange = persisted.UI.useSetCellCallback( + "organizations", + organizationId, + "name", + (e: React.ChangeEvent) => e.target.value, + [], + persisted.STORE_ID, + ); + + return ( +
+
Name
+
+ +
+
+ ); +} diff --git a/apps/desktop/src/components/main/body/contacts/organizations.tsx b/apps/desktop/src/components/main/body/contacts/organizations.tsx index 4ddb275df0..1ee6083c9a 100644 --- a/apps/desktop/src/components/main/body/contacts/organizations.tsx +++ b/apps/desktop/src/components/main/body/contacts/organizations.tsx @@ -1,28 +1,47 @@ -import { Building2, CornerDownLeft, Pencil, User } from "lucide-react"; +import { cn } from "@hypr/utils"; + +import { Building2, CornerDownLeft, User } from "lucide-react"; import React, { useState } from "react"; -import { cn } from "@hypr/utils"; import * as persisted from "../../../../store/tinybase/persisted"; import { ColumnHeader, type SortOption } from "./shared"; export function OrganizationsColumn({ selectedOrganization, setSelectedOrganization, + isViewingOrgDetails, }: { selectedOrganization: string | null; setSelectedOrganization: (id: string | null) => void; + isViewingOrgDetails: boolean; }) { - const [editingOrg, setEditingOrg] = useState(null); const [showNewOrg, setShowNewOrg] = useState(false); + const [searchValue, setSearchValue] = useState(""); const { organizationIds, sortOption, setSortOption } = useSortedOrganizationIds(); + const allOrgs = persisted.UI.useTable("organizations", persisted.STORE_ID); + + const filteredOrganizationIds = React.useMemo(() => { + if (!searchValue.trim()) { + return organizationIds; + } + + return organizationIds.filter((id) => { + const org = allOrgs[id]; + const nameLower = (org?.name ?? "").toLowerCase(); + return nameLower.includes(searchValue.toLowerCase()); + }); + }, [organizationIds, searchValue, allOrgs]); + return ( -
+
setShowNewOrg(true)} + searchValue={searchValue} + onSearchChange={setSearchValue} />
@@ -42,26 +61,15 @@ export function OrganizationsColumn({ onCancel={() => setShowNewOrg(false)} /> )} - {organizationIds.map((orgId) => - editingOrg === orgId - ? ( - setEditingOrg(null)} - onCancel={() => setEditingOrg(null)} - /> - ) - : ( - setEditingOrg(orgId)} - /> - ) - )} + {filteredOrganizationIds.map((orgId) => ( + + ))}
@@ -79,6 +87,14 @@ function useSortedOrganizationIds() { undefined, persisted.STORE_ID, ); + const reverseAlphabeticalIds = persisted.UI.useResultSortedRowIds( + persisted.QUERIES.visibleOrganizations, + "name", + true, + 0, + undefined, + persisted.STORE_ID, + ); const newestIds = persisted.UI.useResultSortedRowIds( persisted.QUERIES.visibleOrganizations, "created_at", @@ -98,6 +114,8 @@ function useSortedOrganizationIds() { const organizationIds = sortOption === "alphabetical" ? alphabeticalIds + : sortOption === "reverse-alphabetical" + ? reverseAlphabeticalIds : sortOption === "newest" ? newestIds : oldestIds; @@ -108,13 +126,13 @@ function useSortedOrganizationIds() { function OrganizationItem({ organizationId, isSelected, + isViewingDetails, setSelectedOrganization, - handleEditOrganization, }: { organizationId: string; isSelected: boolean; + isViewingDetails: boolean; setSelectedOrganization: (id: string | null) => void; - handleEditOrganization: () => void; }) { const organization = persisted.UI.useRow("organizations", organizationId, persisted.STORE_ID); if (!organization) { @@ -124,97 +142,22 @@ function OrganizationItem({ return (
-
); } -function EditOrganizationForm({ - organizationId, - onSave, - onCancel, -}: { - organizationId: string; - onSave: () => void; - onCancel: () => void; -}) { - const name = persisted.UI.useCell("organizations", organizationId, "name", persisted.STORE_ID); - - const handleChange = persisted.UI.useSetCellCallback( - "organizations", - organizationId, - "name", - (e: React.ChangeEvent) => e.target.value, - [], - persisted.STORE_ID, - ); - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (name?.trim()) { - onSave(); - } - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - e.preventDefault(); - if (name?.trim()) { - onSave(); - } - } - if (e.key === "Escape") { - onCancel(); - } - }; - - return ( -
- -
- - {name?.trim() && ( - - )} -
- -
- ); -} - function NewOrganizationForm({ onSave, onCancel, diff --git a/apps/desktop/src/components/main/body/contacts/people.tsx b/apps/desktop/src/components/main/body/contacts/people.tsx index b074dff91d..468e1131e4 100644 --- a/apps/desktop/src/components/main/body/contacts/people.tsx +++ b/apps/desktop/src/components/main/body/contacts/people.tsx @@ -1,6 +1,7 @@ -import { useState } from "react"; - import { cn } from "@hypr/utils"; + +import { useMemo, useState } from "react"; + import * as persisted from "../../../../store/tinybase/persisted"; import { ColumnHeader, getInitials, type SortOption } from "./shared"; @@ -13,20 +14,38 @@ export function PeopleColumn({ currentHumanId?: string | null; setSelectedPerson: (id: string | null) => void; }) { + const [searchValue, setSearchValue] = useState(""); const { humanIds, sortOption, setSortOption } = useSortedHumanIds(currentOrgId); + const allHumans = persisted.UI.useTable("humans", persisted.STORE_ID); + + const filteredHumanIds = useMemo(() => { + if (!searchValue.trim()) { + return humanIds; + } + + return humanIds.filter((id) => { + const human = allHumans[id]; + const q = searchValue.toLowerCase(); + const name = (human?.name ?? "").toLowerCase(); + const email = (human?.email ?? "").toLowerCase(); + return name.includes(q) || email.includes(q); + }); + }, [humanIds, searchValue, allHumans]); + return ( -
+
{ - }} + onAdd={() => {}} + searchValue={searchValue} + onSearchChange={setSearchValue} />
- {humanIds.map((humanId) => ( + {filteredHumanIds.map((humanId) => ( ("alphabetical"); const allAlphabeticalIds = persisted.UI.useResultSortedRowIds( @@ -51,6 +70,14 @@ function useSortedHumanIds(currentOrgId?: string | null) { undefined, persisted.STORE_ID, ); + const allReverseAlphabeticalIds = persisted.UI.useResultSortedRowIds( + persisted.QUERIES.visibleHumans, + "name", + true, + 0, + undefined, + persisted.STORE_ID, + ); const allNewestIds = persisted.UI.useResultSortedRowIds( persisted.QUERIES.visibleHumans, "created_at", @@ -77,11 +104,15 @@ function useSortedHumanIds(currentOrgId?: string | null) { const humanIds = currentOrgId ? (sortOption === "alphabetical" ? allAlphabeticalIds + : sortOption === "reverse-alphabetical" + ? allReverseAlphabeticalIds : sortOption === "newest" ? allNewestIds : allOldestIds).filter((id) => thisOrgHumanIds.includes(id)) : (sortOption === "alphabetical" ? allAlphabeticalIds + : sortOption === "reverse-alphabetical" + ? allReverseAlphabeticalIds : sortOption === "newest" ? allNewestIds : allOldestIds); @@ -104,8 +135,8 @@ function PersonItem({ +
+
+

{title}

+
+ {onSearchChange && ( + + )} + {sortOption && setSortOption && ( +
+ +
+ )} + +
+ {showSearch && onSearchChange && ( +
+ + onSearchChange(e.target.value)} + onKeyDown={handleSearchKeyDown} + placeholder="Search..." + className="w-full bg-transparent text-sm focus:outline-none placeholder:text-neutral-400" + autoFocus + /> + {searchValue && ( + + )} +
+ )}
); } diff --git a/apps/desktop/src/components/main/body/index.tsx b/apps/desktop/src/components/main/body/index.tsx index 8ae75e23ee..e94f283de4 100644 --- a/apps/desktop/src/components/main/body/index.tsx +++ b/apps/desktop/src/components/main/body/index.tsx @@ -279,7 +279,7 @@ export function StandardTabWrapper( ) { return (
-
+
{children}
diff --git a/apps/desktop/src/components/main/body/search.tsx b/apps/desktop/src/components/main/body/search.tsx index a842107e14..7d4e0438b0 100644 --- a/apps/desktop/src/components/main/body/search.tsx +++ b/apps/desktop/src/components/main/body/search.tsx @@ -69,8 +69,8 @@ export function Search() { >
{showLoading - ? - : } + ? + : } {query && ( @@ -93,7 +93,7 @@ export function Search() { className={cn([ "absolute right-3", "h-4 w-4", - "text-gray-400 hover:text-gray-600", + "text-neutral-400 hover:text-neutral-600", "transition-colors", ])} aria-label="Clear search" diff --git a/apps/desktop/src/components/main/body/sessions/floating/listen.tsx b/apps/desktop/src/components/main/body/sessions/floating/listen.tsx index 7f55eaf4b3..55df9c3ba6 100644 --- a/apps/desktop/src/components/main/body/sessions/floating/listen.tsx +++ b/apps/desktop/src/components/main/body/sessions/floating/listen.tsx @@ -15,7 +15,8 @@ import { FloatingButton, formatTime } from "./shared"; type RemoteMeeting = | { type: "zoom"; url: string | null } | { type: "google-meet"; url: string | null } - | { type: "webex"; url: string | null }; + | { type: "webex"; url: string | null } + | { type: "teams"; url: string | null }; export function ListenButton({ tab }: { tab: Extract }) { const { status, loading, stop } = useListener((state) => ({ @@ -52,7 +53,7 @@ function BeforeMeeingButton({ tab }: { tab: Extract } return ( } + icon={Zoom} > {isNarrow ? "Join & Listen" : "Join Zoom & Start listening"} @@ -63,7 +64,7 @@ function BeforeMeeingButton({ tab }: { tab: Extract } return ( } + icon={Google Meet} > {isNarrow ? "Join & Listen" : "Join Google Meet & Start listening"} @@ -74,13 +75,24 @@ function BeforeMeeingButton({ tab }: { tab: Extract } return ( } + icon={Webex} > {isNarrow ? "Join & Listen" : "Join Webex & Start listening"} ); } + if (remote?.type === "teams") { + return ( + } + > + {isNarrow ? "Join & Listen" : "Join Teams & Start listening"} + + ); + } + return ( - {hovered - ? Stop listening - : ( -
- {formatTime(seconds)} - -
- )} + + {hovered + ? "Stop listening" + : ( +
+ {formatTime(seconds)} + +
+ )} +
); } diff --git a/apps/desktop/src/components/main/body/sessions/index.tsx b/apps/desktop/src/components/main/body/sessions/index.tsx index 38152dbde9..931a50b7df 100644 --- a/apps/desktop/src/components/main/body/sessions/index.tsx +++ b/apps/desktop/src/components/main/body/sessions/index.tsx @@ -38,13 +38,15 @@ export function TabContentNote({ tab }: { tab: Extract }> - -
- -
- +
+ +
+ +
+ +
+
-
diff --git a/apps/desktop/src/components/main/body/sessions/note-input/raw.tsx b/apps/desktop/src/components/main/body/sessions/note-input/raw.tsx index 5a930bc25c..c87b1bfea5 100644 --- a/apps/desktop/src/components/main/body/sessions/note-input/raw.tsx +++ b/apps/desktop/src/components/main/body/sessions/note-input/raw.tsx @@ -100,7 +100,7 @@ const PlaceHolderInner = () => { You can also upload/drop an -
-
+
+
+ {attended + ? + : } +
+ +
+
+ + +
+
+ + + {participant.full_name ? getInitials(participant.full_name) : "?"} + + +
+
{displayName}
+ {participant.job_title &&
{participant.job_title}
} + {participant.organization?.name && ( +
{participant.organization.name}
+ )} +
+
+ {(participant.email || participant.linkedin_username) && ( +
+ {participant.email && ( + + + {participant.email} + + )} + {participant.linkedin_username && ( + + + linkedin.com/in/{participant.linkedin_username} + + )} +
+ )} +
+
+ ); } @@ -136,7 +164,20 @@ function ParticipantsSection({ const [isFocused, setIsFocused] = useState(false); const [selectedIndex, setSelectedIndex] = useState(-1); + // TODO: sort participants based on attendance + const sortedParticipants = useMemo(() => { + return [...participants].sort((_, __) => { + return 0; + }); + }, [participants]); + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Backspace" && !searchQuery && sortedParticipants.length > 0) { + e.preventDefault(); + onParticipantRemove?.(sortedParticipants[sortedParticipants.length - 1].id); + return; + } + if (!searchQuery.trim() || searchResults.length === 0) { return; } @@ -169,9 +210,15 @@ function ParticipantsSection({ return (
Participants
- {participants.length > 0 && ( -
- {participants.map((participant) => ( +
+
document.getElementById("participant-search-input")?.focus()} + > + {sortedParticipants.map((participant) => ( onParticipantRemove?.(participant.id)} /> ))} -
- )} - -
-
- + {sortedParticipants.length === 0 && } { @@ -198,21 +241,9 @@ function ParticipantsSection({ onBlur={() => { setTimeout(() => setIsFocused(false), 200); }} - placeholder="Add participant" - className="w-full bg-transparent text-sm focus:outline-none placeholder:text-neutral-500" + placeholder={sortedParticipants.length === 0 ? "Add participant" : ""} + className="flex-1 min-w-[120px] bg-transparent text-sm focus:outline-none placeholder:text-neutral-500" /> - {searchQuery.trim() && ( - - )}
{isFocused && searchQuery.trim() && ( @@ -402,7 +433,11 @@ export function SessionMetadata({ }); const handleJoinMeeting = useCallback((meetingLink: string) => { - window.open(meetingLink, "_blank"); + openUrl(meetingLink); + }, []); + + const handleCopyLink = useCallback((meetingLink: string) => { + navigator.clipboard.writeText(meetingLink); }, []); const handleParticipantClick = useCallback((participant: MeetingParticipant) => { @@ -476,13 +511,13 @@ export function SessionMetadata({ }} title={meetingMetadata.title} > - -

{meetingMetadata.title}

+ +

{meetingMetadata.title}

- -
+ +
{meetingMetadata.title}
@@ -502,19 +537,31 @@ export function SessionMetadata({ {meetingMetadata.meeting_link && ( <>
-
- - - {getMeetingDomain(meetingMetadata.meeting_link)} - -
+ + + + + + handleJoinMeeting(meetingMetadata.meeting_link!)}> + + Open link + + handleCopyLink(meetingMetadata.meeting_link!)}> + + Copy link + + +
@@ -541,7 +588,7 @@ export function SessionMetadata({ {meetingMetadata.description && ( <>
-
+
{meetingMetadata.description}
diff --git a/apps/desktop/src/components/main/body/sessions/outer-header/other.tsx b/apps/desktop/src/components/main/body/sessions/outer-header/other.tsx index affdb81065..9b8387c06d 100644 --- a/apps/desktop/src/components/main/body/sessions/outer-header/other.tsx +++ b/apps/desktop/src/components/main/body/sessions/outer-header/other.tsx @@ -85,59 +85,65 @@ export function OthersButton(_: { sessionId: string }) { - + - Copy link + Copy link - + Move to {folders.map((folder) => ( - + - {folder.name} + {folder.name} ))} - e.preventDefault()}> + e.preventDefault()}> - Lock note - + Lock note + - + Export to PDF - + {isListening ? : } {isListening ? "Stop listening" : "Start listening"} - + Delete note - + Delete only recording - + History diff --git a/apps/desktop/src/components/main/body/sessions/outer-header/share.tsx b/apps/desktop/src/components/main/body/sessions/outer-header/share.tsx index 8001a0c87c..b70fa4ba3c 100644 --- a/apps/desktop/src/components/main/body/sessions/outer-header/share.tsx +++ b/apps/desktop/src/components/main/body/sessions/outer-header/share.tsx @@ -4,7 +4,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@hypr/ui/components/ui/ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@hypr/ui/components/ui/select"; import { Separator } from "@hypr/ui/components/ui/separator"; -import { CircleMinus, Link2Icon, SearchIcon } from "lucide-react"; +import { CircleMinus, SearchIcon } from "lucide-react"; import { useState } from "react"; import { getInitials } from "../../contacts/shared"; @@ -75,8 +75,8 @@ export function ShareButton(_: { sessionId: string }) { - -
+ +
@@ -158,7 +158,7 @@ export function ShareButton(_: { sessionId: string }) { )} {selectedPeople.length > 0 && ( - )} @@ -170,7 +170,7 @@ export function ShareButton(_: { sessionId: string }) {
People with access
-
+
{invitedPeople.map((person) => (
- Viewer - Editor + Viewer + Editor
@@ -225,11 +225,9 @@ export function ShareButton(_: { sessionId: string }) { )}
diff --git a/apps/desktop/src/components/main/body/shared.tsx b/apps/desktop/src/components/main/body/shared.tsx index 3117124355..646c45ffae 100644 --- a/apps/desktop/src/components/main/body/shared.tsx +++ b/apps/desktop/src/components/main/body/shared.tsx @@ -73,7 +73,7 @@ export function TabItemBase( handleCloseThis(); }} className={clsx([ - "flex-shrink-0 transition-opacity", + "flex-shrink-0 transition-opacity size-6", active ? "opacity-100 text-neutral-700" : "opacity-0 group-hover:opacity-100 text-neutral-500", diff --git a/apps/desktop/src/components/main/sidebar/index.tsx b/apps/desktop/src/components/main/sidebar/index.tsx index 53748b512c..66da4ad2e5 100644 --- a/apps/desktop/src/components/main/sidebar/index.tsx +++ b/apps/desktop/src/components/main/sidebar/index.tsx @@ -37,7 +37,9 @@ export function LeftSidebar() {
{showSearchResults ? : }
- +
+ +
); diff --git a/apps/desktop/src/components/main/sidebar/profile/banner.tsx b/apps/desktop/src/components/main/sidebar/profile/banner.tsx index 75bdb1d3cb..d11ba32a71 100644 --- a/apps/desktop/src/components/main/sidebar/profile/banner.tsx +++ b/apps/desktop/src/components/main/sidebar/profile/banner.tsx @@ -21,7 +21,6 @@ export function TryProBanner({ isDismissed, onDismiss }: { isDismissed: boolean; opacity: 0, height: 0, y: 20, - scale: 0.95, transition: { duration: 0.3, ease: "easeInOut" }, }} className={cn([ @@ -33,7 +32,7 @@ export function TryProBanner({ isDismissed, onDismiss }: { isDismissed: boolean; className={cn([ "relative group overflow-hidden rounded-lg", "flex flex-col gap-3", - "bg-white border border-gray-200 shadow-sm p-4", + "bg-white border border-neutral-200 shadow-sm p-4", ])} >
diff --git a/apps/desktop/src/components/main/sidebar/profile/index.tsx b/apps/desktop/src/components/main/sidebar/profile/index.tsx index de6e046a74..26e2996c8a 100644 --- a/apps/desktop/src/components/main/sidebar/profile/index.tsx +++ b/apps/desktop/src/components/main/sidebar/profile/index.tsx @@ -120,60 +120,59 @@ export function ProfileSection() { ]; return ( -
-
-
+
+ + {isExpanded && ( - - {currentView === "main" - ? ( - - - - -
- - {menuItems.map((item) => )} - - - - ) - : ( - - - - )} - +
+
+ + {currentView === "main" + ? ( + + + + +
+ + {menuItems.map((item) => )} + + + + ) + : ( + + + + )} + +
+
-
+ )} + +
setIsExpanded(!isExpanded)} />
@@ -188,7 +187,7 @@ function ProfileButton({ isExpanded, onClick }: { isExpanded: boolean; onClick: "px-4 py-2", "text-left", "transition-all duration-300", - "hover:bg-gray-100", + "hover:bg-neutral-100", isExpanded && "bg-neutral-50 border-t border-neutral-100", )} onClick={onClick} @@ -197,7 +196,7 @@ function ProfileButton({ isExpanded, onClick }: { isExpanded: boolean; onClick: className={clsx( "flex size-8 flex-shrink-0 items-center justify-center", "overflow-hidden rounded-full", - "border border-white/60 border-t border-gray-400", + "border border-white/60 border-t border-neutral-400", "bg-gradient-to-br from-indigo-400 to-purple-500", "shadow-sm", "transition-transform duration-300", diff --git a/apps/desktop/src/components/main/sidebar/search/group.tsx b/apps/desktop/src/components/main/sidebar/search/group.tsx index 3758d3afa0..5822084372 100644 --- a/apps/desktop/src/components/main/sidebar/search/group.tsx +++ b/apps/desktop/src/components/main/sidebar/search/group.tsx @@ -21,7 +21,7 @@ export function SearchResultGroup({ group }: { group: SearchGroup }) { return (
-

+

{group.title}

@@ -34,8 +34,8 @@ export function SearchResultGroup({ group }: { group: SearchGroup }) { className={cn([ "w-full mt-2 px-3 py-2", "flex items-center justify-center gap-2", - "text-xs font-medium text-gray-600", - "hover:bg-gray-50 active:bg-gray-100", + "text-xs font-medium text-neutral-600", + "hover:bg-neutral-50 active:bg-neutral-100", "rounded-lg transition-colors", ])} > diff --git a/apps/desktop/src/components/main/sidebar/search/index.tsx b/apps/desktop/src/components/main/sidebar/search/index.tsx index 312735f83a..c6e4f18308 100644 --- a/apps/desktop/src/components/main/sidebar/search/index.tsx +++ b/apps/desktop/src/components/main/sidebar/search/index.tsx @@ -23,7 +23,7 @@ function SearchYesResults({ results, query }: { results: GroupedSearchResults; q
-

+

{results.totalResults} result{results.totalResults !== 1 ? "s" : ""} for "{query}"

@@ -39,12 +39,12 @@ function SearchNoResults({ query }: { query: string; setQuery: (query: string) =
- +
-

+

No results found for "{query}"

-

+

Help us improve search

diff --git a/apps/desktop/src/components/main/sidebar/search/item.tsx b/apps/desktop/src/components/main/sidebar/search/item.tsx index 5ef9bdd4c6..e51f9047a2 100644 --- a/apps/desktop/src/components/main/sidebar/search/item.tsx +++ b/apps/desktop/src/components/main/sidebar/search/item.tsx @@ -44,7 +44,7 @@ function HumanSearchResultItem({ result, onClick }: { result: SearchResult; onCl className={cn([ "w-full px-3 py-2.5", "flex items-start gap-3", - "hover:bg-gray-50 active:bg-gray-100", + "hover:bg-neutral-50 active:bg-neutral-100", "rounded-lg transition-colors", "text-left", ])} @@ -56,7 +56,9 @@ function HumanSearchResultItem({ result, onClick }: { result: SearchResult; onCl
@@ -85,17 +87,19 @@ function OrganizationSearchResultItem({ result, onClick }: { result: SearchResul className={cn([ "w-full px-3 py-2.5", "flex items-start gap-3", - "hover:bg-gray-50 active:bg-gray-100", + "hover:bg-neutral-50 active:bg-neutral-100", "rounded-lg transition-colors", "text-left", ])} >
-
+
{memberText}
@@ -143,7 +147,7 @@ function SessionSearchResultItem({ result, onClick }: { result: SearchResult; on className={cn([ "w-full px-3 py-2.5", "flex flex-col gap-1", - "hover:bg-gray-50 active:bg-gray-100", + "hover:bg-neutral-50 active:bg-neutral-100", "rounded-lg transition-colors", "text-left", "min-w-0", @@ -151,18 +155,18 @@ function SessionSearchResultItem({ result, onClick }: { result: SearchResult; on >
-
+
{timeAgo}
{result.content && (
diff --git a/apps/desktop/src/components/main/sidebar/timeline/index.tsx b/apps/desktop/src/components/main/sidebar/timeline/index.tsx index 14cd0f1c7a..11278cb864 100644 --- a/apps/desktop/src/components/main/sidebar/timeline/index.tsx +++ b/apps/desktop/src/components/main/sidebar/timeline/index.tsx @@ -47,7 +47,7 @@ export function TimelineView() { ref={containerRef} className={cn([ "flex flex-col h-full overflow-y-auto", - "bg-gray-50 rounded-lg", + "bg-neutral-50 rounded-lg", ])} > {buckets.map((bucket) => { @@ -57,11 +57,11 @@ export function TimelineView() {
-
{bucket.label}
+
{bucket.label}
{isToday ? ( @@ -91,9 +91,9 @@ export function TimelineView() { size="sm" className={clsx([ "absolute left-1/2 transform -translate-x-1/2", - "rounded-full bg-white hover:bg-gray-50", - "text-gray-700 border border-gray-200", - "z-40 flex items-center gap-1", + "rounded-full bg-white hover:bg-neutral-50", + "text-neutral-700 border border-neutral-200", + "z-20 flex items-center gap-1", "shadow-[inset_0_-4px_6px_-1px_rgba(255,0,0,0.1),inset_0_-2px_4px_-2px_rgba(255,0,0,0.1)]", isScrolledPastToday ? "top-2" : "bottom-2", ])} @@ -139,7 +139,7 @@ function TodayBucket({ return ( <> -
+
No items today
diff --git a/apps/desktop/src/components/main/sidebar/timeline/item.tsx b/apps/desktop/src/components/main/sidebar/timeline/item.tsx index 90e5155eeb..4596cb13fd 100644 --- a/apps/desktop/src/components/main/sidebar/timeline/item.tsx +++ b/apps/desktop/src/components/main/sidebar/timeline/item.tsx @@ -1,8 +1,8 @@ -import { ExternalLink, SquareArrowOutUpRight, Trash2 } from "lucide-react"; +import { ContextMenuItem } from "@hypr/ui/components/ui/context-menu"; +import { cn } from "@hypr/utils"; + import { useCallback, useMemo } from "react"; -import { ContextMenuItem, ContextMenuShortcut } from "@hypr/ui/components/ui/context-menu"; -import { cn } from "@hypr/utils"; import * as persisted from "../../../../store/tinybase/persisted"; import { Tab, useTabs } from "../../../../store/zustand/tabs"; import { id } from "../../../../utils"; @@ -97,24 +97,10 @@ export function TimelineItemComponent({ item, precision }: { item: TimelineItem; const contextMenu = ( <> - handleClick()} - > - - Current Tab - Click - - handleCmdClick()} - > - - New Tab - ⌘ + Click + handleCmdClick()}> + Open in New Tab - - + Delete Completely @@ -149,13 +135,13 @@ export function TimelineItemComponent({ item, precision }: { item: TimelineItem; contextMenu={contextMenu} className={cn([ "w-full text-left px-3 py-2 rounded-lg", - active && "bg-gray-200", - !active && "hover:bg-gray-100", + active && "bg-neutral-200", + !active && "hover:bg-neutral-100", ])} >
{title}
- {displayTime &&
{displayTime}
} + {displayTime &&
{displayTime}
}
); diff --git a/apps/desktop/src/components/settings/ai/llm/configure.tsx b/apps/desktop/src/components/settings/ai/llm/configure.tsx index e16d754483..6f85a860d5 100644 --- a/apps/desktop/src/components/settings/ai/llm/configure.tsx +++ b/apps/desktop/src/components/settings/ai/llm/configure.tsx @@ -66,14 +66,14 @@ function ProviderCard({ return (
{icon} {providerName} {providerConfig.badge && ( - + {providerConfig.badge} )} diff --git a/apps/desktop/src/components/settings/ai/llm/select.tsx b/apps/desktop/src/components/settings/ai/llm/select.tsx index a666f84693..0d7491042c 100644 --- a/apps/desktop/src/components/settings/ai/llm/select.tsx +++ b/apps/desktop/src/components/settings/ai/llm/select.tsx @@ -51,7 +51,7 @@ export function SelectProviderAndModel() {
@@ -87,7 +87,7 @@ export function SelectProviderAndModel() { )} - / + / {(field) => { diff --git a/apps/desktop/src/components/settings/ai/stt/configure.tsx b/apps/desktop/src/components/settings/ai/stt/configure.tsx index 77d79a0c6a..7bcf793f51 100644 --- a/apps/desktop/src/components/settings/ai/stt/configure.tsx +++ b/apps/desktop/src/components/settings/ai/stt/configure.tsx @@ -74,7 +74,7 @@ function NonHyprProviderCard({ config }: { config: typeof PROVIDERS[number] }) {
{icon} {providerName} - + Recommended
@@ -179,7 +179,7 @@ function HyprProviderCloudRow() {
Hyprnote Cloud (Beta) - + Use the Hyprnote Cloud API to transcribe your audio.
@@ -202,7 +202,7 @@ function LocalModelInfo({ displayName }: { return (
{displayName} - + On-device model. No audio leaves your device.
@@ -369,7 +369,7 @@ function ProviderContext({ providerId }: { providerId: ProviderId }) { const content = providerId === "hyprnote" ? "Hyprnote curates list of on-device models and also cloud models with high-availability and performance." : providerId === "deepgram" - ? `Use [Deepgram](https://deepgram.com) for transcriptions. \ + ? `Use [Deepgram](https://deepgram.com) for transcriptions. \ You can choose to use the [EU Endpoint](https://developers.deepgram.com/reference/custom-endpoints#eu-endpoints) if you prefer.` : providerId === "deepgram-custom" ? `If you're using a [Dedicated endpoint](https://developers.deepgram.com/reference/custom-endpoints#deepgram-dedicated-endpoints), or other Deepgram-compatible endpoint, you can configure it here.` diff --git a/apps/desktop/src/components/settings/ai/stt/select.tsx b/apps/desktop/src/components/settings/ai/stt/select.tsx index 4650f49cac..2efaefcd68 100644 --- a/apps/desktop/src/components/settings/ai/stt/select.tsx +++ b/apps/desktop/src/components/settings/ai/stt/select.tsx @@ -51,7 +51,7 @@ export function SelectProviderAndModel() {
@@ -87,7 +87,7 @@ export function SelectProviderAndModel() { )} - / + / {(field) => { diff --git a/apps/desktop/src/components/settings/general.tsx b/apps/desktop/src/components/settings/general.tsx index 05a656c1b2..5c33da68b2 100644 --- a/apps/desktop/src/components/settings/general.tsx +++ b/apps/desktop/src/components/settings/general.tsx @@ -103,7 +103,7 @@ export function SettingsGeneral() {

Main language

-

Language for summaries, chats, and AI-generated responses

+

Language for summaries, chats, and AI-generated responses