Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions apps/desktop2/.cursor/rules/style.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
alwaysApply: true
---

## Conditional Tailwind ClassNames
## Code Commebts

- Use `cn` (import with `import { cn } from "@hypr/ui/lib/utils"`). It is similar to `clsx`. Always pass an array.
- By default, avoid writing comments at all.
- If you write one, it should be about "Why", not "What".

## 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.
4 changes: 4 additions & 0 deletions apps/desktop2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
"@hypr/tiptap": "workspace:^",
"@hypr/ui": "workspace:^",
"@hypr/utils": "workspace:^",
"@orama/highlight": "^0.1.9",
"@orama/orama": "^3.1.15",
"@orama/plugin-qps": "^3.1.15",
"@sentry/react": "^8.55.0",
"@supabase/supabase-js": "^2.75.0",
"@t3-oss/env-core": "^0.13.8",
Expand All @@ -39,6 +42,7 @@
"ai": "^5.0.68",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"dompurify": "^3.2.7",
"lucide-react": "^0.544.0",
"motion": "^11.18.2",
"re-resizable": "^6.11.2",
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop2/src/components/chat/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export function ChatFloatingButton() {
const [chatGroupId, setChatGroupId] = useState<string | undefined>(undefined);

useAutoCloser(() => setIsOpen(false), { esc: isOpen, outside: false });
useHotkeys("meta+j", () => setIsOpen((prev) => !prev));
useHotkeys("mod+j", () => setIsOpen((prev) => !prev));

const handleClickTrigger = useCallback(async () => {
const isExists = await windowsCommands.windowIsExists({ type: "chat" });
Expand Down
84 changes: 77 additions & 7 deletions apps/desktop2/src/components/main/body/search.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,90 @@
import { SearchIcon } from "lucide-react";
import { useState } from "react";
import { Loader2Icon, SearchIcon, XIcon } from "lucide-react";
import { useRef } from "react";
import { useHotkeys } from "react-hotkeys-hook";

import { cn } from "@hypr/ui/lib/utils";
import { useSearch } from "../../../contexts/search";

export function Search() {
const [searchQuery, setSearchQuery] = useState("");
const { query, setQuery, isSearching, isIndexing, onFocus, onBlur } = useSearch();
const inputRef = useRef<HTMLInputElement | null>(null);

const showLoading = isSearching || isIndexing;

useHotkeys("mod+k", (e) => {
e.preventDefault();
inputRef.current?.focus();
});

useHotkeys(
"down",
(event) => {
if (document.activeElement === inputRef.current) {
event.preventDefault();
console.log("down");
}
},
{ enableOnFormTags: true },
);

useHotkeys(
"up",
(event) => {
if (document.activeElement === inputRef.current) {
event.preventDefault();
console.log("up");
}
},
{ enableOnFormTags: true },
);

useHotkeys(
"enter",
(event) => {
if (document.activeElement === inputRef.current) {
event.preventDefault();
console.log("enter");
}
},
{ enableOnFormTags: true },
);

return (
<div className="flex items-center h-full pl-4 flex-[0_1_260px] min-w-[160px] w-full">
<div className="relative flex items-center w-full">
<SearchIcon className="h-4 w-4 absolute left-3 text-gray-400" />
{showLoading
? <Loader2Icon className={cn(["h-4 w-4 absolute left-3 text-gray-400 animate-spin"])} />
: <SearchIcon className={cn(["h-4 w-4 absolute left-3 text-gray-400"])} />}
<input
ref={inputRef}
type="text"
placeholder="Search anything..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-9 pr-4 py-2 text-sm rounded-lg bg-gray-100 focus:outline-none focus:bg-gray-200 border-0"
value={query}
onChange={(e) => setQuery(e.target.value)}
onFocus={onFocus}
onBlur={onBlur}
className={cn([
"text-sm",
"w-full pl-9 py-2",
query ? "pr-9" : "pr-4",
"rounded-lg bg-gray-100 border-0",
"focus:outline-none focus:bg-gray-200",
])}
/>
{query && (
<button
onClick={() => setQuery("")}
className={cn([
"absolute right-3",
"h-4 w-4",
"text-gray-400 hover:text-gray-600",
"transition-colors",
])}
aria-label="Clear search"
>
<XIcon className="h-4 w-4" />
</button>
)}
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ const TEMPLATES = [
"Action Items",
"Decision Log",
"Key Insights",
"Brainstorming",
];

export function FloatingRegenerateButton() {
Expand Down
13 changes: 10 additions & 3 deletions apps/desktop2/src/components/main/sidebar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@ import { clsx } from "clsx";
import { PanelLeftCloseIcon } from "lucide-react";

import { useLeftSidebar } from "@hypr/utils/contexts";
import { useSearch } from "../../../contexts/search";
import { NewNoteButton } from "./new-note-button";
import { ProfileSection } from "./profile";
import { SearchResults } from "./search";
import { TimelineView } from "./timeline";

export function LeftSidebar() {
const { togglePanel: toggleLeftPanel } = useLeftSidebar();
const { query } = useSearch();

const showSearchResults = query.trim() !== "";

return (
<div className="h-full w-full flex flex-col overflow-hidden">
Expand All @@ -29,12 +34,14 @@ export function LeftSidebar() {

<div
className={clsx([
"flex flex-col flex-1 gap-1 overflow-hidden",
"p-1 pr-0",
"flex flex-col flex-1 overflow-hidden",
"p-1 pr-0 gap-1",
])}
>
<NewNoteButton />
<TimelineView />
<div className="flex-1 min-h-0 overflow-hidden">
{showSearchResults ? <SearchResults /> : <TimelineView />}
</div>
<ProfileSection />
</div>
</div>
Expand Down
22 changes: 22 additions & 0 deletions apps/desktop2/src/components/main/sidebar/search/empty.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { SearchXIcon } from "lucide-react";

import { cn } from "@hypr/ui/lib/utils";

export function SearchNoResults() {
return (
<div className={cn(["h-full flex items-center justify-center"])}>
<div className={cn(["text-center px-4 max-w-xs"])}>
<div className={cn(["flex justify-center mb-3"])}>
<SearchXIcon className={cn(["h-10 w-10 text-gray-300"])} />
</div>
<p className={cn(["text-sm font-medium text-gray-700"])}>
No results found
</p>
<p className={cn(["text-xs text-gray-500 mt-2 leading-relaxed"])}>
Try using different keywords or check your spelling. Results are filtered to show only the most relevant
matches.
</p>
</div>
</div>
);
}
89 changes: 89 additions & 0 deletions apps/desktop2/src/components/main/sidebar/search/group.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { ChevronDownIcon } from "lucide-react";
import { useState } from "react";

import { cn } from "@hypr/ui/lib/utils";
import { type SearchGroup } from "../../../../contexts/search";
import { SearchResultItem } from "./item";

const ITEMS_PER_PAGE = 3;
const LOAD_MORE_STEP = 5;

export function SearchResultGroup({
group,
icon: Icon,
rank,
maxScore,
}: {
group: SearchGroup;
icon: React.ComponentType<{ className?: string }>;
rank: number;
maxScore: number;
}) {
const [visibleCount, setVisibleCount] = useState(ITEMS_PER_PAGE);

if (group.totalCount === 0) {
return null;
}

const visibleResults = group.results.slice(0, visibleCount);
const hasMore = group.totalCount > visibleCount;
const isTopRanked = rank === 1;

return (
<div className={cn(["mb-6"])}>
<div
className={cn([
"sticky top-0 z-10",
"px-3 py-2 mb-2",
"flex items-center gap-2",
"bg-gray-50 rounded-lg",
"border-b border-gray-200",
])}
>
<Icon className={cn(["h-4 w-4 text-gray-600"])} />
<h3
className={cn([
"text-xs font-semibold text-gray-700",
"uppercase tracking-wider",
])}
>
{group.title}
</h3>
<span className={cn(["text-xs text-gray-500 font-medium"])}>
({group.totalCount})
</span>
{isTopRanked && (
<span
className={cn([
"ml-auto",
"px-2 py-0.5",
"text-[10px] font-semibold",
"bg-blue-100 text-blue-700",
"rounded-full",
])}
>
Best Match
</span>
)}
</div>
<div className={cn(["space-y-0.5 px-1"])}>
{visibleResults.map((result) => <SearchResultItem key={result.id} result={result} maxScore={maxScore} />)}
</div>
{hasMore && (
<button
onClick={() => setVisibleCount((prev) => prev + LOAD_MORE_STEP)}
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",
"rounded-lg transition-colors",
])}
>
<span>Load 5 more</span>
<ChevronDownIcon className={cn(["h-3 w-3"])} />
</button>
)}
</div>
);
}
46 changes: 46 additions & 0 deletions apps/desktop2/src/components/main/sidebar/search/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Building2Icon, FileTextIcon, UserIcon } from "lucide-react";

import { cn } from "@hypr/ui/lib/utils";
import { useSearch } from "../../../../contexts/search";
import { SearchNoResults } from "./empty";
import { SearchResultGroup } from "./group";

const ICON_MAP = {
session: FileTextIcon,
human: UserIcon,
organization: Building2Icon,
};

export function SearchResults() {
const { results, query } = useSearch();

if (!query || !results) {
return null;
}

if (results.totalResults === 0) {
return <SearchNoResults />;
}

return (
<div className={cn(["h-full overflow-y-auto"])}>
<div className={cn(["px-3 py-3"])}>
<div className={cn(["px-2 py-2 mb-4"])}>
<p className={cn(["text-xs text-gray-500 font-medium"])}>
{results.totalResults} result{results.totalResults !== 1 ? "s" : ""} for "{query}"
</p>
</div>

{results.groups.map((group, index) => (
<SearchResultGroup
key={group.key}
group={group}
icon={ICON_MAP[group.type]}
rank={index + 1}
maxScore={results.maxScore}
/>
))}
</div>
</div>
);
}
Loading
Loading