Skip to content

Commit 727d500

Browse files
note enhancing change (#2716)
* feat(note-input): enhance generating note tab with cancel and progress * feat(ui): conditionally show spinner on non-active items * refactor(ui): adjust note input and overlay styling * chore(deps): add benchmark and test utility dependencies * fmt * fix(ui): replace span with button for better accessibility
1 parent 795030b commit 727d500

File tree

8 files changed

+88
-140
lines changed

8 files changed

+88
-140
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
1.16 KB
Loading

apps/desktop/src/components/main/body/sessions/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export const TabItemNote: TabItem<Extract<Tab, { type: "sessions" }>> = ({
5050
const isEnhancing = useIsSessionEnhancing(tab.id);
5151
const isActive = sessionMode === "active" || sessionMode === "finalizing";
5252
const isFinalizing = sessionMode === "finalizing";
53-
const showSpinner = isFinalizing || isEnhancing;
53+
const showSpinner = !tab.active && (isFinalizing || isEnhancing);
5454

5555
return (
5656
<TabItemBase
Lines changed: 31 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,46 @@
1-
import { Loader2Icon } from "lucide-react";
2-
import { LayoutGroup, motion } from "motion/react";
3-
import { useCallback, useEffect, useRef } from "react";
41
import { Streamdown } from "streamdown";
52

63
import { cn } from "@hypr/utils";
74

85
import { useAITaskTask } from "../../../../../../hooks/useAITaskTask";
9-
import {
10-
createTaskId,
11-
type TaskId,
12-
} from "../../../../../../store/zustand/ai-task/task-configs";
6+
import { createTaskId } from "../../../../../../store/zustand/ai-task/task-configs";
137
import { type TaskStepInfo } from "../../../../../../store/zustand/ai-task/tasks";
148

159
export function StreamingView({ enhancedNoteId }: { enhancedNoteId: string }) {
1610
const taskId = createTaskId(enhancedNoteId, "enhance");
17-
const { streamedText, isGenerating } = useAITaskTask(taskId, "enhance");
11+
const { streamedText, currentStep, isGenerating } = useAITaskTask(
12+
taskId,
13+
"enhance",
14+
);
1815

19-
const containerRef = useAutoScrollToBottom(streamedText);
16+
const step = currentStep as TaskStepInfo<"enhance"> | undefined;
17+
const hasContent = streamedText.length > 0;
18+
19+
let statusText: string | null = null;
20+
if (isGenerating && !hasContent) {
21+
if (step?.type === "analyzing") {
22+
statusText = "Analyzing structure...";
23+
} else if (step?.type === "generating") {
24+
statusText = "Generating...";
25+
} else if (step?.type === "retrying") {
26+
statusText = `Retrying (attempt ${step.attempt})...`;
27+
} else {
28+
statusText = "Loading...";
29+
}
30+
}
2031

2132
return (
22-
<div ref={containerRef} className="flex flex-col pb-2 space-y-1">
23-
<LayoutGroup>
24-
<motion.div layout>
25-
<Streamdown
26-
components={streamdownComponents}
27-
className={cn(["space-y-2"])}
28-
>
29-
{streamedText}
30-
</Streamdown>
31-
</motion.div>
32-
33-
{isGenerating && (
34-
<motion.div className="sticky bottom-0 pt-1">
35-
<Status taskId={taskId} />
36-
</motion.div>
37-
)}
38-
</LayoutGroup>
33+
<div className="pb-2">
34+
{statusText ? (
35+
<p className="text-sm text-neutral-500">{statusText}</p>
36+
) : (
37+
<Streamdown
38+
components={streamdownComponents}
39+
className={cn(["space-y-2"])}
40+
>
41+
{streamedText}
42+
</Streamdown>
43+
)}
3944
</div>
4045
);
4146
}
@@ -106,81 +111,3 @@ const streamdownComponents = {
106111
return <p className="py-2">{props.children as React.ReactNode}</p>;
107112
},
108113
} as const;
109-
110-
function Status({ taskId }: { taskId: TaskId<"enhance"> }) {
111-
const { currentStep, cancel, isGenerating } = useAITaskTask(
112-
taskId,
113-
"enhance",
114-
);
115-
if (!isGenerating) {
116-
return null;
117-
}
118-
const step = currentStep as TaskStepInfo<"enhance"> | undefined;
119-
120-
const handleClick = useCallback(
121-
(event: React.MouseEvent<HTMLButtonElement>) => {
122-
event.preventDefault();
123-
event.stopPropagation();
124-
cancel();
125-
},
126-
[cancel],
127-
);
128-
129-
let statusText = "Loading";
130-
if (step?.type === "analyzing") {
131-
statusText = "Analyzing structure...";
132-
} else if (step?.type === "generating") {
133-
statusText = "Generating";
134-
} else if (step?.type === "retrying") {
135-
statusText = `Retrying (attempt ${step.attempt})...`;
136-
}
137-
138-
return (
139-
<button
140-
className={cn([
141-
"group flex items-center justify-center w-[calc(100%-24px)] gap-3",
142-
"border border-neutral-200 bg-neutral-800 rounded-lg py-3 transition-colors",
143-
"cursor-pointer hover:bg-neutral-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-100/60",
144-
])}
145-
type="button"
146-
aria-label="Cancel enhance task"
147-
onClick={handleClick}
148-
>
149-
<Loader2Icon
150-
aria-hidden="true"
151-
className="w-4 h-4 animate-spin text-neutral-50"
152-
/>
153-
<span className="text-xs text-neutral-50 group-hover:hidden group-focus-visible:hidden">
154-
{statusText}
155-
</span>
156-
<span className="hidden text-xs text-neutral-50 group-hover:inline group-focus-visible:inline">
157-
Press to cancel
158-
</span>
159-
</button>
160-
);
161-
}
162-
163-
function useAutoScrollToBottom(text: string) {
164-
const containerRef = useRef<HTMLDivElement | null>(null);
165-
166-
useEffect(() => {
167-
const container = containerRef.current;
168-
if (!container) {
169-
return;
170-
}
171-
172-
const scrollableParent = container.parentElement;
173-
if (!scrollableParent) {
174-
return;
175-
}
176-
177-
const { scrollTop, scrollHeight, clientHeight } = scrollableParent;
178-
const isNearBottom = scrollHeight - scrollTop - clientHeight < 100;
179-
180-
if (isNearBottom) {
181-
scrollableParent.scrollTop = scrollHeight;
182-
}
183-
}, [text]);
184-
185-
return containerRef;
186-
}

apps/desktop/src/components/main/body/sessions/note-input/header.tsx

Lines changed: 50 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AlertCircleIcon, PlusIcon, RefreshCcwIcon } from "lucide-react";
1+
import { AlertCircleIcon, PlusIcon, RefreshCcwIcon, XIcon } from "lucide-react";
22
import { useCallback, useEffect, useRef, useState } from "react";
33

44
import { commands as analyticsCommands } from "@hypr/plugin-analytics";
@@ -8,11 +8,7 @@ import {
88
PopoverContent,
99
PopoverTrigger,
1010
} from "@hypr/ui/components/ui/popover";
11-
import {
12-
Tooltip,
13-
TooltipContent,
14-
TooltipTrigger,
15-
} from "@hypr/ui/components/ui/tooltip";
11+
import { Spinner } from "@hypr/ui/components/ui/spinner";
1612
import { cn } from "@hypr/utils";
1713

1814
import { useListener } from "../../../../../contexts/listener";
@@ -27,6 +23,7 @@ import {
2723
} from "../../../../../hooks/useLLMConnection";
2824
import * as main from "../../../../../store/tinybase/store/main";
2925
import { createTaskId } from "../../../../../store/zustand/ai-task/task-configs";
26+
import { type TaskStepInfo } from "../../../../../store/zustand/ai-task/tasks";
3027
import { useTabs } from "../../../../../store/zustand/tabs";
3128
import { type EditorView } from "../../../../../store/zustand/tabs/schema";
3229
import { useHasTranscript } from "../shared";
@@ -88,10 +85,8 @@ function HeaderTabEnhanced({
8885
sessionId: string;
8986
enhancedNoteId: string;
9087
}) {
91-
const { isGenerating, isError, error, onRegenerate } = useEnhanceLogic(
92-
sessionId,
93-
enhancedNoteId,
94-
);
88+
const { isGenerating, isError, onRegenerate, onCancel, currentStep } =
89+
useEnhanceLogic(sessionId, enhancedNoteId);
9590

9691
const title =
9792
main.UI.useCell("enhanced_notes", enhancedNoteId, "title", main.STORE_ID) ||
@@ -106,12 +101,51 @@ function HeaderTabEnhanced({
106101
);
107102

108103
if (isGenerating) {
104+
const step = currentStep as TaskStepInfo<"enhance"> | undefined;
105+
106+
const handleCancelClick = (e: React.MouseEvent) => {
107+
e.stopPropagation();
108+
onCancel();
109+
};
110+
109111
return (
110-
<HeaderTab isActive={isActive} onClick={onClick}>
111-
<span className="flex items-center gap-1">
112+
<button
113+
onClick={onClick}
114+
className={cn([
115+
"group/tab relative my-2 py-0.5 px-1 text-xs font-medium transition-all duration-200 border-b-2",
116+
isActive
117+
? ["text-neutral-900", "border-neutral-900"]
118+
: [
119+
"text-neutral-600",
120+
"border-transparent",
121+
"hover:text-neutral-800",
122+
],
123+
])}
124+
>
125+
<span className="flex items-center gap-1 h-5">
112126
<TruncatedTitle title={title} isActive={isActive} />
127+
<button
128+
type="button"
129+
onClick={handleCancelClick}
130+
className="inline-flex h-5 w-5 items-center justify-center rounded cursor-pointer hover:bg-neutral-200"
131+
aria-label="Cancel enhancement"
132+
>
133+
<span className="group-hover/tab:hidden flex items-center justify-center">
134+
{step?.type === "generating" ? (
135+
<img
136+
src="/assets/write-animation.gif"
137+
alt=""
138+
aria-hidden="true"
139+
className="size-3"
140+
/>
141+
) : (
142+
<Spinner size={14} />
143+
)}
144+
</span>
145+
<XIcon className="hidden group-hover/tab:flex items-center justify-center size-4" />
146+
</button>
113147
</span>
114-
</HeaderTab>
148+
</button>
115149
);
116150
}
117151

@@ -161,24 +195,7 @@ function HeaderTabEnhanced({
161195
>
162196
<span className="flex items-center gap-1 h-5">
163197
<TruncatedTitle title={title} isActive={isActive} />
164-
{isActive && (
165-
<div className="flex items-center gap-1">
166-
{isError ? (
167-
<Tooltip delayDuration={0}>
168-
<TooltipTrigger asChild>{regenerateIcon}</TooltipTrigger>
169-
{error && (
170-
<TooltipContent side="bottom">
171-
<p className="text-xs max-w-xs">
172-
{error instanceof Error ? error.message : String(error)}
173-
</p>
174-
</TooltipContent>
175-
)}
176-
</Tooltip>
177-
) : (
178-
regenerateIcon
179-
)}
180-
</div>
181-
)}
198+
{isActive && regenerateIcon}
182199
</span>
183200
</button>
184201
);
@@ -519,6 +536,8 @@ function useEnhanceLogic(sessionId: string, enhancedNoteId: string) {
519536
isError,
520537
error,
521538
onRegenerate,
539+
onCancel: enhanceTask.cancel,
540+
currentStep: enhanceTask.currentStep,
522541
};
523542
}
524543

apps/desktop/src/components/main/body/sessions/note-input/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ export const NoteInput = forwardRef<
222222
/>
223223
</div>
224224

225-
<div className="relative flex-1 mt-2 overflow-hidden">
225+
<div className="relative flex-1 overflow-hidden">
226226
<div
227227
ref={(node) => {
228228
fadeRef.current = node;
@@ -238,7 +238,7 @@ export const NoteInput = forwardRef<
238238
"h-full px-3",
239239
currentTab.type === "transcript"
240240
? "overflow-hidden"
241-
: ["overflow-auto", "pb-6"],
241+
: ["overflow-auto", "pt-2", "pb-6"],
242242
])}
243243
>
244244
{currentTab.type === "enhanced" && (

apps/desktop/src/components/main/body/sessions/title-input.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ export const TitleInput = forwardRef<
202202
}}
203203
onMouseDown={(e) => e.preventDefault()}
204204
className={cn([
205-
"shrink-0 p-1",
205+
"shrink-0",
206206
"text-muted-foreground hover:text-foreground",
207207
"opacity-50 hover:opacity-100 transition-opacity",
208208
])}

apps/desktop/src/components/main/sidebar/timeline/item.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ const SessionItem = memo(
214214
const sessionMode = useListener((state) => state.getSessionMode(sessionId));
215215
const isEnhancing = useIsSessionEnhancing(sessionId);
216216
const isFinalizing = sessionMode === "finalizing";
217-
const showSpinner = isFinalizing || isEnhancing;
217+
const showSpinner = !selected && (isFinalizing || isEnhancing);
218218

219219
const calendarId = useMemo(() => {
220220
if (!store || !item.data.event_id) {

0 commit comments

Comments
 (0)