Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion apps/api/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ infisical export \
--format=dotenv \
--output-file="$REPO/apps/api/.env" \
--projectId=87dad7b5-72a6-4791-9228-b3b86b169db1 \
--path="/api"
--path="/ai"
```
16 changes: 2 additions & 14 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,13 +242,7 @@ pub async fn main() {

{
let app_handle = app.handle().clone();
if app.get_onboarding_needed().unwrap_or(true) {
AppWindow::Main.hide(&app_handle).unwrap();
AppWindow::Onboarding.show(&app_handle).unwrap();
} else {
AppWindow::Onboarding.destroy(&app_handle).unwrap();
AppWindow::Main.show(&app_handle).unwrap();
}
AppWindow::Main.show(&app_handle).unwrap();
}

#[cfg(target_os = "macos")]
Expand All @@ -269,13 +263,7 @@ pub async fn main() {
app.run(move |app, event| match event {
#[cfg(target_os = "macos")]
tauri::RunEvent::Reopen { .. } => {
if app.get_onboarding_needed().unwrap_or(true) {
AppWindow::Main.hide(&app).unwrap();
AppWindow::Onboarding.show(&app).unwrap();
} else {
AppWindow::Onboarding.destroy(&app).unwrap();
AppWindow::Main.show(&app).unwrap();
}
AppWindow::Main.show(&app).unwrap();
}
#[cfg(target_os = "macos")]
tauri::RunEvent::ExitRequested { api, .. } => {
Expand Down
82 changes: 56 additions & 26 deletions apps/desktop/src/components/main/body/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
import { loadExtensionPanels } from "./extensions/registry";
import { TabContentFolder, TabItemFolder } from "./folders";
import { TabContentHuman, TabItemHuman } from "./humans";
import { TabContentOnboarding, TabItemOnboarding } from "./onboarding";
import { Search } from "./search";
import { TabContentNote, TabItemNote } from "./sessions";
import { useCaretPosition } from "./sessions/caret-position-context";
Expand Down Expand Up @@ -89,6 +90,9 @@ function Header({ tabs }: { tabs: Tab[] }) {
const { leftsidebar } = useShell();
const isLinux = platform() === "linux";
const notifications = useNotifications();
const currentTab = useTabs((state) => state.currentTab);
const isOnboarding = currentTab?.type === "onboarding";
const isSidebarHidden = isOnboarding || !leftsidebar.expanded;
const {
select,
close,
Expand Down Expand Up @@ -168,11 +172,11 @@ function Header({ tabs }: { tabs: Tab[] }) {
data-tauri-drag-region
className={cn([
"w-full h-9 flex items-center",
!leftsidebar.expanded && (isLinux ? "pl-3" : "pl-18"),
isSidebarHidden && (isLinux ? "pl-3" : "pl-20"),
])}
>
{!leftsidebar.expanded && isLinux && <TrafficLights className="mr-2" />}
{!leftsidebar.expanded && (
{isSidebarHidden && isLinux && <TrafficLights className="mr-2" />}
{!leftsidebar.expanded && !isOnboarding && (
<div className="relative">
<Tooltip>
<TooltipTrigger asChild>
Expand All @@ -197,24 +201,26 @@ function Header({ tabs }: { tabs: Tab[] }) {
</div>
)}

<div className="flex items-center h-full shrink-0">
<Button
onClick={goBack}
disabled={!canGoBack}
variant="ghost"
size="icon"
>
<ArrowLeftIcon size={16} />
</Button>
<Button
onClick={goNext}
disabled={!canGoNext}
variant="ghost"
size="icon"
>
<ArrowRightIcon size={16} />
</Button>
</div>
{!isOnboarding && (
<div className="flex items-center h-full shrink-0">
<Button
onClick={goBack}
disabled={!canGoBack}
variant="ghost"
size="icon"
>
<ArrowLeftIcon size={16} />
</Button>
<Button
onClick={goNext}
disabled={!canGoNext}
variant="ghost"
size="icon"
>
<ArrowRightIcon size={16} />
</Button>
</div>
)}

{listeningTab && (
<div className="flex items-center h-full shrink-0 mr-1">
Expand Down Expand Up @@ -307,19 +313,25 @@ function Header({ tabs }: { tabs: Tab[] }) {
>
{!isSearchManuallyExpanded && (
<Button
onClick={handleNewEmptyTab}
onContextMenu={showNewTabMenu}
onClick={isOnboarding ? undefined : handleNewEmptyTab}
onContextMenu={isOnboarding ? undefined : showNewTabMenu}
disabled={isOnboarding}
variant="ghost"
size="icon"
className="text-neutral-600"
className={cn([
"text-neutral-600",
isOnboarding && "opacity-40 cursor-not-allowed",
])}
>
<PlusIcon size={16} />
</Button>
)}

<div className="flex items-center gap-1 h-full ml-auto">
<Update />
<Search onManualExpandChange={setIsSearchManuallyExpanded} />
{!isOnboarding && (
<Search onManualExpandChange={setIsSearchManuallyExpanded} />
)}
</div>
</div>
</div>
Expand Down Expand Up @@ -566,6 +578,20 @@ function TabItem({
/>
);
}
if (tab.type === "onboarding") {
return (
<TabItemOnboarding
tab={tab}
tabIndex={tabIndex}
handleCloseThis={handleClose}
handleSelectThis={handleSelect}
handleCloseOthers={handleCloseOthers}
handleCloseAll={handleCloseAll}
handlePinThis={handlePinThis}
handleUnpinThis={handleUnpinThis}
/>
);
}
return null;
}

Expand Down Expand Up @@ -616,6 +642,9 @@ function ContentWrapper({ tab }: { tab: Tab }) {
if (tab.type === "chat") {
return <TabContentChat tab={tab} />;
}
if (tab.type === "onboarding") {
return <TabContentOnboarding tab={tab} />;
}
return null;
}

Expand Down Expand Up @@ -652,7 +681,8 @@ function TabChatButton({
if (
currentTab?.type === "ai" ||
currentTab?.type === "settings" ||
currentTab?.type === "chat"
currentTab?.type === "chat" ||
currentTab?.type === "onboarding"
) {
return null;
}
Expand Down
195 changes: 195 additions & 0 deletions apps/desktop/src/components/main/body/onboarding/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import { useQueryClient } from "@tanstack/react-query";
import { platform } from "@tauri-apps/plugin-os";
import { Volume2Icon, VolumeXIcon } from "lucide-react";
import { useCallback, useEffect, useState } from "react";

import { commands as analyticsCommands } from "@hypr/plugin-analytics";
import { commands as sfxCommands } from "@hypr/plugin-sfx";

import { usePermissions } from "../../../../hooks/usePermissions";
import { type Tab, useTabs } from "../../../../store/zustand/tabs";
import { CalendarSection } from "../../../onboarding/calendar";
import {
getInitialStep,
getNextStep,
getPrevStep,
getStepStatus,
} from "../../../onboarding/config";
import { FinalSection, finishOnboarding } from "../../../onboarding/final";
import { FolderLocationSection } from "../../../onboarding/folder-location";
import { LoginSection } from "../../../onboarding/login";
import { PermissionsSection } from "../../../onboarding/permissions";
import { OnboardingSection } from "../../../onboarding/shared";
import { StandardTabWrapper } from "../index";
import { type TabItem, TabItemBase } from "../shared";

export const TabItemOnboarding: TabItem<
Extract<Tab, { type: "onboarding" }>
> = ({
tab,
tabIndex,
handleCloseThis,
handleSelectThis,
handleCloseOthers,
handleCloseAll,
handlePinThis,
handleUnpinThis,
}) => {
return (
<TabItemBase
icon={<span className="text-sm">👋</span>}
title="Welcome"
selected={tab.active}
allowPin={false}
allowClose={false}
tabIndex={tabIndex}
handleCloseThis={() => handleCloseThis(tab)}
handleSelectThis={() => handleSelectThis(tab)}
handleCloseOthers={handleCloseOthers}
handleCloseAll={handleCloseAll}
handlePinThis={() => handlePinThis(tab)}
handleUnpinThis={() => handleUnpinThis(tab)}
/>
);
};

export function TabContentOnboarding({
tab: _tab,
}: {
tab: Extract<Tab, { type: "onboarding" }>;
}) {
const queryClient = useQueryClient();
const close = useTabs((state) => state.close);
const currentTab = useTabs((state) => state.currentTab);
const [isMuted, setIsMuted] = useState(false);
const [currentStep, setCurrentStep] = useState(getInitialStep);

const {
micPermissionStatus,
systemAudioPermissionStatus,
accessibilityPermissionStatus,
} = usePermissions();

const allPermissionsGranted =
micPermissionStatus.data === "authorized" &&
systemAudioPermissionStatus.data === "authorized" &&
accessibilityPermissionStatus.data === "authorized";

useEffect(() => {
if (currentStep === "permissions" && allPermissionsGranted) {
setCurrentStep("login");
}
}, [currentStep, allPermissionsGranted]);

const goNext = useCallback(() => {
const next = getNextStep(currentStep);
if (next) setCurrentStep(next);
}, [currentStep]);

const goBack = useCallback(() => {
const prev = getPrevStep(currentStep);
if (prev) setCurrentStep(prev);
}, [currentStep]);

useEffect(() => {
void analyticsCommands.event({
event: "onboarding_step_viewed",
step: currentStep,
platform: platform(),
});
}, [currentStep]);

useEffect(() => {
sfxCommands
.play("BGM")
.then(() => sfxCommands.setVolume("BGM", 0.2))
.catch(console.error);
return () => {
sfxCommands.stop("BGM").catch(console.error);
};
}, []);

useEffect(() => {
sfxCommands.setVolume("BGM", isMuted ? 0 : 0.2).catch(console.error);
}, [isMuted]);

const handleFinish = useCallback(() => {
void queryClient.invalidateQueries({ queryKey: ["onboarding-needed"] });
if (currentTab) {
close(currentTab);
}
}, [close, currentTab, queryClient]);

return (
<StandardTabWrapper>
<div className="relative h-full overflow-y-auto">
<button
onClick={() => setIsMuted((prev) => !prev)}
className="sticky top-2 float-right mr-2 p-1.5 rounded-full hover:bg-neutral-100 transition-colors z-10"
aria-label={isMuted ? "Unmute" : "Mute"}
>
{isMuted ? (
<VolumeXIcon size={16} className="text-neutral-600" />
) : (
<Volume2Icon size={16} className="text-neutral-600" />
)}
</button>

<div className="flex flex-col px-6 pt-4 pb-16 gap-8">
<h1 className="text-2xl font-semibold font-serif text-neutral-900">
Welcome to Hyprnote
</h1>

<OnboardingSection
title="Permissions"
description="Required for best experience"
status={getStepStatus("permissions", currentStep)}
onBack={goBack}
onNext={goNext}
>
<PermissionsSection />
</OnboardingSection>

<OnboardingSection
title="Account"
description="Sign in to sync and unlock Pro features"
status={getStepStatus("login", currentStep)}
onBack={goBack}
onNext={goNext}
>
<LoginSection onContinue={goNext} />
</OnboardingSection>

<OnboardingSection
title="Calendar"
description="Select calendars to sync"
status={getStepStatus("calendar", currentStep)}
onBack={goBack}
onNext={goNext}
>
<CalendarSection onContinue={goNext} />
</OnboardingSection>

<OnboardingSection
title="Storage"
description="Where your notes and recordings are stored"
status={getStepStatus("folder-location", currentStep)}
onBack={goBack}
onNext={goNext}
>
<FolderLocationSection onContinue={goNext} />
</OnboardingSection>

<OnboardingSection
title="Ready to go"
status={getStepStatus("final", currentStep)}
onBack={goBack}
onNext={() => void finishOnboarding(handleFinish)}
>
<FinalSection onContinue={handleFinish} />
</OnboardingSection>
</div>
</div>
</StandardTabWrapper>
);
}
Loading
Loading