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
8 changes: 5 additions & 3 deletions apps/desktop/src/components/chat/interactive.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Resizable } from "re-resizable";
import { type ReactNode, useState } from "react";
import { createPortal } from "react-dom";

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

Expand All @@ -16,9 +17,9 @@ export function InteractiveContainer(
) {
const [isResizing, setIsResizing] = useState(false);

return (
return createPortal(
<div
className="absolute z-10"
className="fixed z-[100]"
style={{ right: 16, bottom: 16 }}
>
<Resizable
Expand Down Expand Up @@ -63,6 +64,7 @@ export function InteractiveContainer(
>
{children}
</Resizable>
</div>
</div>,
document.body,
);
}
9 changes: 6 additions & 3 deletions apps/desktop/src/components/chat/trigger.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { createPortal } from "react-dom";

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

export function ChatTrigger({ onClick }: { onClick: () => void }) {
return (
return createPortal(
<button
onClick={onClick}
className={cn([
"absolute bottom-4 right-4 z-10",
"fixed bottom-4 right-4 z-[100]",
"w-14 h-14 rounded-full",
"bg-white shadow-lg hover:shadow-xl",
"border border-neutral-200",
Expand All @@ -19,6 +21,7 @@ export function ChatTrigger({ onClick }: { onClick: () => void }) {
alt="Chat Assistant"
className="w-12 h-12 object-contain"
/>
</button>
</button>,
document.body,
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ export function GenerateButton({ sessionId }: { sessionId: string }) {
onRegenerate(null);
}}
disabled={!model}
tooltip={!model
? {
content: "Language model not configured",
side: "top",
}
: undefined}
>
<span>Regenerate</span>
</FloatingButton>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export function FloatingActionButton({ tab }: { tab: Extract<Tab, { type: "sessi

function FloatingButtonContainer({ children }: { children: ReactNode }) {
return (
<div className="absolute bottom-6 left-1/2 -translate-x-1/2">
<div className="absolute bottom-6 left-1/2 -translate-x-1/2 z-50">
{children}
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function FloatingButton({
const button = (
<Button
size="lg"
className="rounded-lg"
className="rounded-lg disabled:opacity-100 disabled:bg-neutral-500"
onClick={onClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,14 @@ export function NoteInput({ tab }: { tab: Extract<Tab, { type: "sessions" }> })
return (
<div className="flex flex-col h-full">
<Header editorTabs={editorTabs} currentTab={currentTab} handleTabChange={handleTabChange} />
<div className="flex-1 overflow-auto mt-3" onClick={handleContainerClick}>
<div
className={cn([
"flex-1",
"mt-3",
currentTab === "transcript" ? "overflow-hidden" : "overflow-auto",
])}
onClick={handleContainerClick}
>
{currentTab === "enhanced" && <Enhanced ref={editorRef} sessionId={sessionId} />}
{currentTab === "raw" && <RawEditor ref={editorRef} sessionId={sessionId} />}
{currentTab === "transcript" && <Transcript sessionId={sessionId} />}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,54 +1,103 @@
import { cn } from "@hypr/utils";
import { type MaybePartialWord, useFinalWords, useMergedWordsByChannel, usePartialWords } from "./segment";
import { DependencyList, useLayoutEffect, useRef } from "react";
import { useSegments } from "./segment";

export function TranscriptViewer({ sessionId }: { sessionId: string }) {
const finalWords = useFinalWords(sessionId);
const partialWords = usePartialWords();
const wordsByChannel = useMergedWordsByChannel(finalWords, partialWords);

return <Renderer wordsByChannel={wordsByChannel} />;
const segments = useSegments(sessionId);
return <Renderer segments={segments} />;
}

function Renderer({ wordsByChannel }: { wordsByChannel: Map<number, MaybePartialWord[]> }) {
const channelIds = Array.from(wordsByChannel.keys()).sort((a, b) => a - b);
function Renderer({ segments }: { segments: ReturnType<typeof useSegments> }) {
const containerRef = useAutoScroll<HTMLDivElement>([segments]);

if (channelIds.length === 0) {
if (segments.length === 0) {
return null;
}

return (
<div className="flex flex-col overflow-y-auto overflow-x-hidden max-h-[calc(100vh-250px)]">
{channelIds.map((channelId) => {
const words = wordsByChannel.get(channelId) ?? [];
return (
<div key={channelId} className="flex flex-col">
<div
className={cn([
"sticky top-0 z-10",
"py-2 px-3 -mx-3",
"bg-background",
"border-b border-border",
"text-sm font-semibold",
])}
>
Channel {channelId}
</div>
<div className="text-sm leading-relaxed py-4 break-words overflow-wrap-anywhere">
{words.map((word, idx) => (
<span
key={`${word.start_ms}-${idx}`}
className={cn([
!word.isFinal && ["opacity-60", "italic"],
])}
>
{word.text}
{" "}
</span>
))}
</div>
</div>
);
})}
<div
ref={containerRef}
className={cn([
"space-y-8 h-full overflow-y-auto overflow-x-hidden",
"px-0.5 pb-32 scroll-pb-[8rem]",
])}
>
{segments.map(
(segment, i) => <Segment key={i} segment={segment} />,
)}
</div>
);
}

function Segment({ segment }: { segment: ReturnType<typeof useSegments>[number] }) {
const timestamp = segment.words.length > 0
? `${formatTimestamp(segment.words[0].start_ms)} - ${
formatTimestamp(segment.words[segment.words.length - 1].end_ms)
}`
: "00:00 - 00:00";

return (
<section>
<p
className={cn([
"sticky top-0 z-20",
"-mx-3 px-3 py-1",
"bg-background",
"border-b border-neutral-200",
"text-neutral-500 text-xs font-light",
"flex items-center justify-between",
])}
>
<span>Channel {segment.channel}</span>
<span className="font-mono">{timestamp}</span>
</p>

<div className="mt-1.5 text-sm leading-relaxed break-words overflow-wrap-anywhere">
{segment.words.map((word, idx) => (
<span
key={`${word.start_ms}-${idx}`}
className={cn([
!word.isFinal && ["opacity-60", "italic"],
])}
>
{word.text}
{" "}
</span>
))}
</div>
</section>
);
}

function useAutoScroll<T extends HTMLElement>(deps: DependencyList) {
const ref = useRef<T | null>(null);

useLayoutEffect(() => {
const element = ref.current;
if (!element) {
return;
}

const isAtTop = element.scrollTop === 0;
const isNearBottom = element.scrollHeight - element.scrollTop - element.clientHeight < 100;

if (isAtTop || isNearBottom) {
element.scrollTop = element.scrollHeight;
}
}, deps);

return ref;
}

function formatTimestamp(ms: number): string {
const totalSeconds = Math.floor(ms / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;

if (hours > 0) {
return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
}

return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
}
Loading
Loading