Skip to content

Commit 2d54995

Browse files
committed
update ui
1 parent 9939fba commit 2d54995

File tree

6 files changed

+379
-91
lines changed

6 files changed

+379
-91
lines changed

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { TooltipProvider } from "@hypr/ui/components/ui/tooltip";
44
import type { Tab } from "../../../../../store/zustand/tabs/schema";
55
import { useCurrentNoteTab, useHasTranscript } from "../shared";
66

7-
import { GenerateButton } from "./generate";
87
import { ListenButton } from "./listen";
98

109
export function FloatingActionButton({ tab }: { tab: Extract<Tab, { type: "sessions" }> }) {
@@ -15,8 +14,6 @@ export function FloatingActionButton({ tab }: { tab: Extract<Tab, { type: "sessi
1514

1615
if (currentTab === "raw" && !hasTranscript) {
1716
button = <ListenButton tab={tab} />;
18-
} else if (currentTab === "enhanced") {
19-
button = <GenerateButton sessionId={tab.id} />;
2017
}
2118

2219
if (!button) {
Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
import { useCallback, useState } from "react";
2+
3+
import { commands as windowsCommands } from "@hypr/plugin-windows";
4+
import { Popover, PopoverContent, PopoverTrigger } from "@hypr/ui/components/ui/popover";
5+
import { cn } from "@hypr/utils";
6+
import { AlertCircleIcon, RefreshCcwIcon, SparklesIcon } from "lucide-react";
7+
import { useAITask } from "../../../../../contexts/ai-task";
8+
import { useListener } from "../../../../../contexts/listener";
9+
import { useLanguageModel } from "../../../../../hooks/useLLMConnection";
10+
import { useTaskStatus } from "../../../../../hooks/useTaskStatus";
11+
import * as main from "../../../../../store/tinybase/main";
12+
import { createTaskId } from "../../../../../store/zustand/ai-task/task-configs";
13+
import { getTaskState } from "../../../../../store/zustand/ai-task/tasks";
14+
import { type EditorView } from "../../../../../store/zustand/tabs/schema";
15+
import { useHasTranscript } from "../shared";
16+
17+
function HeaderTab({
18+
isActive,
19+
onClick,
20+
children,
21+
}: {
22+
isActive: boolean;
23+
onClick: () => void;
24+
children: React.ReactNode;
25+
}) {
26+
return (
27+
<button
28+
onClick={onClick}
29+
className={cn([
30+
"relative my-2 py-0.5 px-1 text-xs font-medium transition-all duration-200 border-b-2",
31+
isActive
32+
? ["text-neutral-900", "border-neutral-900"]
33+
: ["text-neutral-600", "border-transparent", "hover:text-neutral-800"],
34+
])}
35+
>
36+
{children}
37+
</button>
38+
);
39+
}
40+
41+
function HeaderTabEnhanced(
42+
{ isActive, onClick, sessionId }: { isActive: boolean; onClick: () => void; sessionId: string },
43+
) {
44+
const [open, setOpen] = useState(false);
45+
const { model, templates, isGenerating, isError, onRegenerate } = useEnhanceLogic(sessionId);
46+
47+
const handleTabClick = useCallback(() => {
48+
if (!isActive) {
49+
onClick();
50+
}
51+
}, [isActive, onClick]);
52+
53+
const handleIconClick = useCallback((e: React.MouseEvent) => {
54+
e.stopPropagation();
55+
if (!model) {
56+
handleConfigureModel();
57+
return;
58+
}
59+
if (isError) {
60+
onRegenerate(null);
61+
return;
62+
}
63+
setOpen(!open);
64+
}, [model, isError, open, onRegenerate]);
65+
66+
const handleTemplateClick = useCallback((templateId: string | null) => {
67+
setOpen(false);
68+
onRegenerate(templateId);
69+
}, [onRegenerate]);
70+
71+
if (isGenerating) {
72+
return (
73+
<HeaderTab isActive={isActive} onClick={handleTabClick}>
74+
<span className="flex items-center gap-1">
75+
<span>Summary</span>
76+
</span>
77+
</HeaderTab>
78+
);
79+
}
80+
81+
return (
82+
<Popover open={open} onOpenChange={setOpen}>
83+
<PopoverTrigger asChild>
84+
<button
85+
onClick={handleTabClick}
86+
className={cn([
87+
"relative my-2 py-0.5 px-1 text-xs font-medium transition-all duration-200 border-b-2",
88+
isActive
89+
? ["text-neutral-900", "border-neutral-900"]
90+
: ["text-neutral-600", "border-transparent", "hover:text-neutral-800"],
91+
])}
92+
>
93+
<span className="flex items-center gap-1">
94+
<span>Summary</span>
95+
{isActive && (
96+
<button
97+
onClick={handleIconClick}
98+
className={cn([
99+
"p-0.5 rounded hover:bg-neutral-200 transition-colors",
100+
isError && "text-red-600 hover:bg-red-50",
101+
])}
102+
>
103+
{isError ? <AlertCircleIcon size={12} /> : <RefreshCcwIcon size={12} />}
104+
</button>
105+
)}
106+
</span>
107+
</button>
108+
</PopoverTrigger>
109+
<PopoverContent className="w-64" align="start">
110+
<div className="flex flex-col gap-2">
111+
{Object.entries(templates).length > 0
112+
? (
113+
Object.entries(templates).map(([templateId, template]) => (
114+
<TemplateButton
115+
key={templateId}
116+
onClick={() => handleTemplateClick(templateId)}
117+
>
118+
{template.title}
119+
</TemplateButton>
120+
))
121+
)
122+
: (
123+
<TemplateButton
124+
className="italic text-neutral-500 hover:text-neutral-700 hover:bg-neutral-50"
125+
onClick={() => {
126+
setOpen(false);
127+
handleGoToTemplates();
128+
}}
129+
>
130+
Create templates
131+
</TemplateButton>
132+
)}
133+
134+
<div className="flex items-center gap-3 text-neutral-400 text-sm my-1">
135+
<div className="flex-1 h-px bg-neutral-300"></div>
136+
<span>or</span>
137+
<div className="flex-1 h-px bg-neutral-300"></div>
138+
</div>
139+
140+
<TemplateButton
141+
className={cn([
142+
"flex items-center justify-center gap-2",
143+
"text-neutral-100 bg-neutral-800 hover:bg-neutral-700",
144+
])}
145+
onClick={() => handleTemplateClick(null)}
146+
>
147+
<SparklesIcon className="w-4 h-4" />
148+
<span className="text-sm">Auto</span>
149+
</TemplateButton>
150+
</div>
151+
</PopoverContent>
152+
</Popover>
153+
);
154+
}
155+
156+
export function Header(
157+
{
158+
sessionId,
159+
editorTabs,
160+
currentTab,
161+
handleTabChange,
162+
}: {
163+
sessionId: string;
164+
editorTabs: EditorView[];
165+
currentTab: EditorView;
166+
handleTabChange: (view: EditorView) => void;
167+
},
168+
) {
169+
if (editorTabs.length === 1 && editorTabs[0] === "raw") {
170+
return null;
171+
}
172+
173+
return (
174+
<div className="flex gap-1">
175+
{editorTabs.map((view) => {
176+
if (view === "enhanced") {
177+
return (
178+
<HeaderTabEnhanced
179+
key={view}
180+
sessionId={sessionId}
181+
isActive={currentTab === view}
182+
onClick={() => handleTabChange(view)}
183+
/>
184+
);
185+
}
186+
187+
return (
188+
<HeaderTab
189+
key={view}
190+
isActive={currentTab === view}
191+
onClick={() => handleTabChange(view)}
192+
>
193+
{labelForEditorView(view)}
194+
</HeaderTab>
195+
);
196+
})}
197+
</div>
198+
);
199+
}
200+
201+
export function useEditorTabs({ sessionId }: { sessionId: string }): EditorView[] {
202+
const { status, sessionId: activeSessionId } = useListener((state) => ({
203+
status: state.status,
204+
sessionId: state.sessionId,
205+
}));
206+
const hasTranscript = useHasTranscript(sessionId);
207+
208+
if (status !== "inactive" && activeSessionId === sessionId) {
209+
return ["raw", "transcript"];
210+
}
211+
212+
if (hasTranscript) {
213+
return ["enhanced", "raw", "transcript"];
214+
}
215+
216+
return ["raw"];
217+
}
218+
219+
function labelForEditorView(view: EditorView): string {
220+
if (view === "enhanced") {
221+
return "Summary";
222+
}
223+
if (view === "raw") {
224+
return "Memos";
225+
}
226+
if (view === "transcript") {
227+
return "Transcript";
228+
}
229+
return "";
230+
}
231+
232+
function useEnhanceLogic(sessionId: string) {
233+
const model = useLanguageModel();
234+
const taskId = createTaskId(sessionId, "enhance");
235+
236+
const enhancedMd = main.UI.useCell("sessions", sessionId, "enhanced_md", main.STORE_ID);
237+
238+
const updateEnhancedMd = main.UI.useSetPartialRowCallback(
239+
"sessions",
240+
sessionId,
241+
(input: string) => ({ enhanced_md: input }),
242+
[],
243+
main.STORE_ID,
244+
);
245+
246+
const { generate, rawStatus, streamedText, error } = useAITask((state) => {
247+
const taskState = getTaskState(state.tasks, taskId);
248+
return {
249+
generate: state.generate,
250+
rawStatus: taskState?.status ?? "idle",
251+
streamedText: taskState?.streamedText ?? "",
252+
error: taskState?.error,
253+
};
254+
});
255+
256+
const { isGenerating, isError } = useTaskStatus(rawStatus, {
257+
onSuccess: () => {
258+
if (streamedText) {
259+
updateEnhancedMd(streamedText);
260+
}
261+
},
262+
});
263+
264+
const templates = main.UI.useResultTable(main.QUERIES.visibleTemplates, main.STORE_ID);
265+
266+
const onRegenerate = useCallback(async (templateId: string | null) => {
267+
if (!model) {
268+
return;
269+
}
270+
271+
await generate(taskId, {
272+
model,
273+
taskType: "enhance",
274+
args: { sessionId, templateId: templateId ?? undefined },
275+
});
276+
}, [model, generate, taskId, sessionId]);
277+
278+
const hasContent = !!enhancedMd && enhancedMd.trim().length > 0;
279+
280+
return {
281+
model,
282+
templates,
283+
isGenerating,
284+
isError,
285+
error,
286+
onRegenerate,
287+
hasContent,
288+
};
289+
}
290+
291+
function handleConfigureModel() {
292+
windowsCommands.windowShow({ type: "settings" })
293+
.then(() => new Promise((resolve) => setTimeout(resolve, 1000)))
294+
.then(() =>
295+
windowsCommands.windowEmitNavigate({ type: "settings" }, {
296+
path: "/app/settings",
297+
search: { tab: "intelligence" },
298+
})
299+
);
300+
}
301+
302+
function handleGoToTemplates() {
303+
windowsCommands.windowShow({ type: "settings" })
304+
.then(() => new Promise((resolve) => setTimeout(resolve, 1000)))
305+
.then(() =>
306+
windowsCommands.windowEmitNavigate({ type: "settings" }, {
307+
path: "/app/settings",
308+
search: { tab: "templates" },
309+
})
310+
);
311+
}
312+
313+
function TemplateButton({
314+
children,
315+
onClick,
316+
className,
317+
}: {
318+
children: React.ReactNode;
319+
onClick: () => void;
320+
className?: string;
321+
}) {
322+
return (
323+
<button
324+
className={cn([
325+
"text-center text-sm py-2 px-3 rounded-md transition-colors hover:bg-neutral-100",
326+
className,
327+
])}
328+
onClick={onClick}
329+
>
330+
{children}
331+
</button>
332+
);
333+
}

0 commit comments

Comments
 (0)