From 40a0f7d822be37bdbb567b66841a260ebf9c6128 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Thu, 16 Oct 2025 13:23:10 +0900 Subject: [PATCH 1/7] refactior --- .../components/main/body/sessions/index.tsx | 60 +++--- .../main/body/sessions/inner-header.tsx | 107 ----------- .../main/body/sessions/note-input.tsx | 181 ++++++++++++++++++ .../main/body/sessions/title-input.tsx | 22 +-- packages/tiptap/src/transcript/utils.ts | 2 +- 5 files changed, 226 insertions(+), 146 deletions(-) delete mode 100644 apps/desktop2/src/components/main/body/sessions/inner-header.tsx create mode 100644 apps/desktop2/src/components/main/body/sessions/note-input.tsx diff --git a/apps/desktop2/src/components/main/body/sessions/index.tsx b/apps/desktop2/src/components/main/body/sessions/index.tsx index 2d03a9952..1e3e1ea40 100644 --- a/apps/desktop2/src/components/main/body/sessions/index.tsx +++ b/apps/desktop2/src/components/main/body/sessions/index.tsx @@ -1,19 +1,24 @@ import { StickyNoteIcon } from "lucide-react"; -import { useMemo, useState } from "react"; +import { useState } from "react"; -import NoteEditor from "@hypr/tiptap/editor"; import { AudioPlayerProvider } from "../../../../contexts/audio-player"; import * as persisted from "../../../../store/tinybase/persisted"; import { rowIdfromTab, type Tab } from "../../../../store/zustand/tabs"; import { type TabItem, TabItemBase } from "../shared"; import { FloatingActionButtonn } from "./floating-action"; -import { InnerHeader } from "./inner-header"; +import { NoteInput } from "./note-input"; import { OuterHeader } from "./outer-header"; import { AudioPlayer } from "./player"; import { TitleInput } from "./title-input"; export const TabItemNote: TabItem = ( - { tab, handleCloseThis, handleSelectThis, handleCloseOthers, handleCloseAll }, + { + tab, + handleCloseThis, + handleSelectThis, + handleCloseOthers, + handleCloseAll, + }, ) => { const title = persisted.UI.useCell("sessions", rowIdfromTab(tab), "title", persisted.STORE_ID); @@ -35,11 +40,6 @@ export function TabContentNote({ tab }: { tab: Tab }) { const sessionRow = persisted.UI.useRow("sessions", sessionId, persisted.STORE_ID); const [showAudioPlayer, setShowAudioPlayer] = useState(false); - const editorKey = useMemo( - () => `session-${sessionId}-raw`, - [sessionId], - ); - const handleEditTitle = persisted.UI.useSetRowCallback( "sessions", sessionId, @@ -56,6 +56,22 @@ export function TabContentNote({ tab }: { tab: Tab }) { persisted.STORE_ID, ); + const handleEditEnhancedMd = persisted.UI.useSetRowCallback( + "sessions", + sessionId, + (input: string, _store) => ({ ...sessionRow, enhanced_md: input }), + [sessionRow], + persisted.STORE_ID, + ); + + const handleEditTranscript = persisted.UI.useSetRowCallback( + "sessions", + sessionId, + (input: string, _store) => ({ ...sessionRow, transcript: input }), + [sessionRow], + persisted.STORE_ID, + ); + const handleRegenerate = (templateId: string | null) => { console.log("Regenerate clicked:", templateId); }; @@ -76,29 +92,21 @@ export function TabContentNote({ tab }: { tab: Tab }) { handleEditTitle(e.target.value)} + onChange={handleEditTitle} /> - {}} + sessionId={sessionId} + rawValue={sessionRow.raw_md ?? ""} + enhancedValue={sessionRow.enhanced_md ?? ""} + transcriptValue={sessionRow.transcript ?? ""} + onRawChange={handleEditRawMd} + onEnhancedChange={handleEditEnhancedMd} + onTranscriptChange={handleEditTranscript} isCurrentlyRecording={false} shouldShowTab={true} shouldShowEnhancedTab={false} /> -
-
- handleEditRawMd(e)} - mentionConfig={{ - trigger: "@", - handleSearch: async () => { - return []; - }, - }} - /> -
{showAudioPlayer && } diff --git a/apps/desktop2/src/components/main/body/sessions/inner-header.tsx b/apps/desktop2/src/components/main/body/sessions/inner-header.tsx deleted file mode 100644 index 177e96359..000000000 --- a/apps/desktop2/src/components/main/body/sessions/inner-header.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { useEffect } from "react"; - -import { cn } from "@hypr/ui/lib/utils"; -import { type Tab, useTabs } from "../../../../store/zustand/tabs"; - -interface TabHeaderProps { - tab: Tab; - onVisibilityChange?: (isVisible: boolean) => void; - isCurrentlyRecording: boolean; - shouldShowTab: boolean; - shouldShowEnhancedTab: boolean; -} - -export const InnerHeader = ({ - tab, - onVisibilityChange, - isCurrentlyRecording, - shouldShowTab, - shouldShowEnhancedTab, -}: TabHeaderProps) => { - const { updateSessionTabState } = useTabs(); - - const currentTab = tab.type === "sessions" ? (tab.state.editor ?? "raw") : "raw"; - - const handleTabChange = (view: "raw" | "enhanced" | "transcript") => { - updateSessionTabState(tab, { editor: view }); - }; - - // set default tab to 'raw' for blank notes (no meeting session) - useEffect(() => { - if (!shouldShowTab && tab.type === "sessions") { - updateSessionTabState(tab, { editor: "raw" }); - } - }, [shouldShowTab, tab, updateSessionTabState]); - - // notify parent when visibility changes - useEffect(() => { - if (onVisibilityChange) { - onVisibilityChange(shouldShowTab ?? false); - } - }, [shouldShowTab, onVisibilityChange]); - - // don't render tabs at all for blank notes (no meeting session) - if (!shouldShowTab) { - return null; - } - - return ( -
- {/* Tab container */} -
-
-
- {/* Raw Note Tab */} - - {/* Enhanced Note Tab - show when session ended OR transcript exists OR enhanced memo exists */} - {shouldShowEnhancedTab && ( - - )} - - - - {/* Transcript Tab - always show */} - -
-
-
-
- ); -}; diff --git a/apps/desktop2/src/components/main/body/sessions/note-input.tsx b/apps/desktop2/src/components/main/body/sessions/note-input.tsx new file mode 100644 index 000000000..bb113e1e0 --- /dev/null +++ b/apps/desktop2/src/components/main/body/sessions/note-input.tsx @@ -0,0 +1,181 @@ +import { useEffect, useMemo } from "react"; + +import { type Word2 } from "@hypr/plugin-listener"; +import NoteEditor from "@hypr/tiptap/editor"; +import TranscriptEditor, { type SpeakerViewInnerProps } from "@hypr/tiptap/transcript"; +import { cn } from "@hypr/ui/lib/utils"; +import { type Tab, useTabs } from "../../../../store/zustand/tabs"; + +type EditorView = "raw" | "enhanced" | "transcript"; + +export function NoteInput({ + tab, + sessionId, + rawValue, + enhancedValue, + transcriptValue, + onRawChange, + onEnhancedChange, + onTranscriptChange, + isCurrentlyRecording, + shouldShowTab, + shouldShowEnhancedTab, +}: { + tab: Tab; + sessionId: string; + rawValue: string; + enhancedValue: string; + transcriptValue: string; + onRawChange: (value: string) => void; + onEnhancedChange: (value: string) => void; + onTranscriptChange: (value: string) => void; + isCurrentlyRecording: boolean; + shouldShowTab: boolean; + shouldShowEnhancedTab: boolean; +}) { + const { updateSessionTabState } = useTabs(); + + const currentTab = tab.type === "sessions" ? (tab.state.editor ?? "raw") : "raw"; + + const handleTabChange = (view: EditorView) => { + updateSessionTabState(tab, { editor: view }); + }; + + const editorKey = useMemo( + () => `session-${sessionId}-${currentTab}`, + [sessionId, currentTab], + ); + + useEffect(() => { + if (!shouldShowTab && tab.type === "sessions") { + updateSessionTabState(tab, { editor: "raw" }); + } + }, [shouldShowTab, tab, updateSessionTabState]); + + const parseTranscript = (value: string): Word2[] | null => { + if (!value) { + return null; + } + try { + const parsed = JSON.parse(value); + return parsed.words ?? null; + } catch { + return null; + } + }; + + const renderEditor = () => { + switch (currentTab) { + case "enhanced": + return ( + { + return []; + }, + }} + /> + ); + case "transcript": + return ( + { + onTranscriptChange(JSON.stringify({ words })); + }} + c={SpeakerSelector} + /> + ); + case "raw": + default: + return ( + { + return []; + }, + }} + /> + ); + } + }; + + return ( +
+ {shouldShowTab && ( +
+
+
+
+ {shouldShowEnhancedTab && ( + + )} + + + + +
+
+
+
+ )} + +
+
+ {renderEditor()} +
+
+ ); +} + +function SpeakerSelector({ speakerLabel, speakerIndex }: SpeakerViewInnerProps) { + const displayLabel = speakerLabel || `Speaker ${speakerIndex ?? 0}`; + return {displayLabel}; +} diff --git a/apps/desktop2/src/components/main/body/sessions/title-input.tsx b/apps/desktop2/src/components/main/body/sessions/title-input.tsx index cfa27c316..f0994f2a0 100644 --- a/apps/desktop2/src/components/main/body/sessions/title-input.tsx +++ b/apps/desktop2/src/components/main/body/sessions/title-input.tsx @@ -1,14 +1,5 @@ import { cn } from "@hypr/ui/lib/utils"; -import { type ChangeEvent, type KeyboardEvent, useEffect, useRef } from "react"; - -interface TitleInputProps { - value: string; - onChange: (e: ChangeEvent) => void; - onNavigateToEditor?: () => void; - editable?: boolean; - isGenerating?: boolean; - autoFocus?: boolean; -} +import { type KeyboardEvent, useEffect, useRef } from "react"; export function TitleInput({ value, @@ -17,7 +8,14 @@ export function TitleInput({ editable, isGenerating = false, autoFocus = false, -}: TitleInputProps) { +}: { + value: string; + onChange: (value: string) => void; + onNavigateToEditor?: () => void; + editable?: boolean; + isGenerating?: boolean; + autoFocus?: boolean; +}) { const inputRef = useRef(null); const handleKeyDown = (e: KeyboardEvent) => { @@ -50,7 +48,7 @@ export function TitleInput({ disabled={!editable || isGenerating} id="note-title-input" type="text" - onChange={onChange} + onChange={(e) => onChange(e.target.value)} value={value} placeholder={getPlaceholder()} className={cn( diff --git a/packages/tiptap/src/transcript/utils.ts b/packages/tiptap/src/transcript/utils.ts index 988f2acde..b9a2da91f 100644 --- a/packages/tiptap/src/transcript/utils.ts +++ b/packages/tiptap/src/transcript/utils.ts @@ -37,7 +37,7 @@ type SpeakerContent = { export const wordsToSpeakerChunks = (words: Word2[]): { words: Word2[]; speaker: SpeakerIdentity | null }[] => { return words.reduce<{ cur: SpeakerIdentity | null; acc: { words: Word2[]; speaker: SpeakerIdentity | null }[] }>( - (state, word, index) => { + (state, word, _index) => { const isFirst = state.acc.length === 0; const isSameSpeaker = (!state.cur && !word.speaker) From 3899195d84ae2bac640fd50add2baeb12e5b1449 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Thu, 16 Oct 2025 13:28:25 +0900 Subject: [PATCH 2/7] split into 3 files --- .../body/sessions/note-input/enhanced.tsx | 25 +++++++ .../{note-input.tsx => note-input/index.tsx} | 65 +++++-------------- .../main/body/sessions/note-input/raw.tsx | 25 +++++++ .../body/sessions/note-input/transcript.tsx | 41 ++++++++++++ 4 files changed, 107 insertions(+), 49 deletions(-) create mode 100644 apps/desktop2/src/components/main/body/sessions/note-input/enhanced.tsx rename apps/desktop2/src/components/main/body/sessions/{note-input.tsx => note-input/index.tsx} (72%) create mode 100644 apps/desktop2/src/components/main/body/sessions/note-input/raw.tsx create mode 100644 apps/desktop2/src/components/main/body/sessions/note-input/transcript.tsx diff --git a/apps/desktop2/src/components/main/body/sessions/note-input/enhanced.tsx b/apps/desktop2/src/components/main/body/sessions/note-input/enhanced.tsx new file mode 100644 index 000000000..50bd45c10 --- /dev/null +++ b/apps/desktop2/src/components/main/body/sessions/note-input/enhanced.tsx @@ -0,0 +1,25 @@ +import NoteEditor from "@hypr/tiptap/editor"; + +export function EnhancedEditor({ + editorKey, + value, + onChange, +}: { + editorKey: string; + value: string; + onChange: (value: string) => void; +}) { + return ( + { + return []; + }, + }} + /> + ); +} diff --git a/apps/desktop2/src/components/main/body/sessions/note-input.tsx b/apps/desktop2/src/components/main/body/sessions/note-input/index.tsx similarity index 72% rename from apps/desktop2/src/components/main/body/sessions/note-input.tsx rename to apps/desktop2/src/components/main/body/sessions/note-input/index.tsx index bb113e1e0..f584b6391 100644 --- a/apps/desktop2/src/components/main/body/sessions/note-input.tsx +++ b/apps/desktop2/src/components/main/body/sessions/note-input/index.tsx @@ -1,10 +1,10 @@ import { useEffect, useMemo } from "react"; -import { type Word2 } from "@hypr/plugin-listener"; -import NoteEditor from "@hypr/tiptap/editor"; -import TranscriptEditor, { type SpeakerViewInnerProps } from "@hypr/tiptap/transcript"; import { cn } from "@hypr/ui/lib/utils"; -import { type Tab, useTabs } from "../../../../store/zustand/tabs"; +import { type Tab, useTabs } from "../../../../../store/zustand/tabs"; +import { EnhancedEditor } from "./enhanced"; +import { RawEditor } from "./raw"; +import { TranscriptEditorWrapper } from "./transcript"; type EditorView = "raw" | "enhanced" | "transcript"; @@ -52,59 +52,31 @@ export function NoteInput({ } }, [shouldShowTab, tab, updateSessionTabState]); - const parseTranscript = (value: string): Word2[] | null => { - if (!value) { - return null; - } - try { - const parsed = JSON.parse(value); - return parsed.words ?? null; - } catch { - return null; - } - }; - const renderEditor = () => { switch (currentTab) { case "enhanced": return ( - { - return []; - }, - }} + ); case "transcript": return ( - { - onTranscriptChange(JSON.stringify({ words })); - }} - c={SpeakerSelector} + ); case "raw": default: return ( - { - return []; - }, - }} + ); } @@ -174,8 +146,3 @@ export function NoteInput({ ); } - -function SpeakerSelector({ speakerLabel, speakerIndex }: SpeakerViewInnerProps) { - const displayLabel = speakerLabel || `Speaker ${speakerIndex ?? 0}`; - return {displayLabel}; -} diff --git a/apps/desktop2/src/components/main/body/sessions/note-input/raw.tsx b/apps/desktop2/src/components/main/body/sessions/note-input/raw.tsx new file mode 100644 index 000000000..7b654aa12 --- /dev/null +++ b/apps/desktop2/src/components/main/body/sessions/note-input/raw.tsx @@ -0,0 +1,25 @@ +import NoteEditor from "@hypr/tiptap/editor"; + +export function RawEditor({ + editorKey, + value, + onChange, +}: { + editorKey: string; + value: string; + onChange: (value: string) => void; +}) { + return ( + { + return []; + }, + }} + /> + ); +} diff --git a/apps/desktop2/src/components/main/body/sessions/note-input/transcript.tsx b/apps/desktop2/src/components/main/body/sessions/note-input/transcript.tsx new file mode 100644 index 000000000..f91436ce0 --- /dev/null +++ b/apps/desktop2/src/components/main/body/sessions/note-input/transcript.tsx @@ -0,0 +1,41 @@ +import { type Word2 } from "@hypr/plugin-listener"; +import TranscriptEditor, { type SpeakerViewInnerProps } from "@hypr/tiptap/transcript"; + +export function TranscriptEditorWrapper({ + editorKey, + value, + onChange, +}: { + editorKey: string; + value: string; + onChange: (value: string) => void; +}) { + const parseTranscript = (value: string): Word2[] | null => { + if (!value) { + return null; + } + try { + const parsed = JSON.parse(value); + return parsed.words ?? null; + } catch { + return null; + } + }; + + return ( + { + onChange(JSON.stringify({ words })); + }} + c={SpeakerSelector} + /> + ); +} + +function SpeakerSelector({ speakerLabel, speakerIndex }: SpeakerViewInnerProps) { + const displayLabel = speakerLabel || `Speaker ${speakerIndex ?? 0}`; + return {displayLabel}; +} From b344d850885477888a32793453f8ff1995a5f504 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Thu, 16 Oct 2025 13:37:54 +0900 Subject: [PATCH 3/7] wip --- .../components/main/body/sessions/index.tsx | 38 +---- .../body/sessions/note-input/enhanced.tsx | 28 +-- .../main/body/sessions/note-input/index.tsx | 159 +++++------------- .../main/body/sessions/note-input/raw.tsx | 28 +-- .../body/sessions/note-input/transcript.tsx | 28 +-- 5 files changed, 96 insertions(+), 185 deletions(-) diff --git a/apps/desktop2/src/components/main/body/sessions/index.tsx b/apps/desktop2/src/components/main/body/sessions/index.tsx index 1e3e1ea40..80187c237 100644 --- a/apps/desktop2/src/components/main/body/sessions/index.tsx +++ b/apps/desktop2/src/components/main/body/sessions/index.tsx @@ -48,30 +48,6 @@ export function TabContentNote({ tab }: { tab: Tab }) { persisted.STORE_ID, ); - const handleEditRawMd = persisted.UI.useSetRowCallback( - "sessions", - sessionId, - (input: string, _store) => ({ ...sessionRow, raw_md: input }), - [sessionRow], - persisted.STORE_ID, - ); - - const handleEditEnhancedMd = persisted.UI.useSetRowCallback( - "sessions", - sessionId, - (input: string, _store) => ({ ...sessionRow, enhanced_md: input }), - [sessionRow], - persisted.STORE_ID, - ); - - const handleEditTranscript = persisted.UI.useSetRowCallback( - "sessions", - sessionId, - (input: string, _store) => ({ ...sessionRow, transcript: input }), - [sessionRow], - persisted.STORE_ID, - ); - const handleRegenerate = (templateId: string | null) => { console.log("Regenerate clicked:", templateId); }; @@ -94,19 +70,7 @@ export function TabContentNote({ tab }: { tab: Tab }) { value={sessionRow.title ?? ""} onChange={handleEditTitle} /> - + {showAudioPlayer && } diff --git a/apps/desktop2/src/components/main/body/sessions/note-input/enhanced.tsx b/apps/desktop2/src/components/main/body/sessions/note-input/enhanced.tsx index 50bd45c10..c2d01df34 100644 --- a/apps/desktop2/src/components/main/body/sessions/note-input/enhanced.tsx +++ b/apps/desktop2/src/components/main/body/sessions/note-input/enhanced.tsx @@ -1,19 +1,23 @@ import NoteEditor from "@hypr/tiptap/editor"; -export function EnhancedEditor({ - editorKey, - value, - onChange, -}: { - editorKey: string; - value: string; - onChange: (value: string) => void; -}) { +import * as persisted from "../../../../../store/tinybase/persisted"; + +export function EnhancedEditor({ sessionId }: { sessionId: string }) { + const value = persisted.UI.useCell("sessions", sessionId, "enhanced_md", persisted.STORE_ID); + + const handleEnhancedChange = persisted.UI.useSetPartialRowCallback( + "sessions", + sessionId, + (input: string) => ({ enhanced_md: input }), + [], + persisted.STORE_ID, + ); + return ( { diff --git a/apps/desktop2/src/components/main/body/sessions/note-input/index.tsx b/apps/desktop2/src/components/main/body/sessions/note-input/index.tsx index f584b6391..7b06f4314 100644 --- a/apps/desktop2/src/components/main/body/sessions/note-input/index.tsx +++ b/apps/desktop2/src/components/main/body/sessions/note-input/index.tsx @@ -1,5 +1,3 @@ -import { useEffect, useMemo } from "react"; - import { cn } from "@hypr/ui/lib/utils"; import { type Tab, useTabs } from "../../../../../store/zustand/tabs"; import { EnhancedEditor } from "./enhanced"; @@ -10,138 +8,73 @@ type EditorView = "raw" | "enhanced" | "transcript"; export function NoteInput({ tab, - sessionId, - rawValue, - enhancedValue, - transcriptValue, - onRawChange, - onEnhancedChange, - onTranscriptChange, - isCurrentlyRecording, - shouldShowTab, - shouldShowEnhancedTab, }: { tab: Tab; - sessionId: string; - rawValue: string; - enhancedValue: string; - transcriptValue: string; - onRawChange: (value: string) => void; - onEnhancedChange: (value: string) => void; - onTranscriptChange: (value: string) => void; - isCurrentlyRecording: boolean; - shouldShowTab: boolean; - shouldShowEnhancedTab: boolean; }) { const { updateSessionTabState } = useTabs(); - const currentTab = tab.type === "sessions" ? (tab.state.editor ?? "raw") : "raw"; - const handleTabChange = (view: EditorView) => { updateSessionTabState(tab, { editor: view }); }; - const editorKey = useMemo( - () => `session-${sessionId}-${currentTab}`, - [sessionId, currentTab], - ); - - useEffect(() => { - if (!shouldShowTab && tab.type === "sessions") { - updateSessionTabState(tab, { editor: "raw" }); - } - }, [shouldShowTab, tab, updateSessionTabState]); + if (tab.type !== "sessions") { + return null; + } - const renderEditor = () => { - switch (currentTab) { - case "enhanced": - return ( - - ); - case "transcript": - return ( - - ); - case "raw": - default: - return ( - - ); - } - }; + const sessionId = tab.id; + const currentTab = tab.state.editor ?? "raw"; return (
- {shouldShowTab && ( -
-
-
-
- {shouldShowEnhancedTab && ( - - )} +
+
+
+
+ - + - -
+
- )} +
- {renderEditor()} + {currentTab === "enhanced" && } + {currentTab === "raw" && } + {currentTab === "transcript" && }
); diff --git a/apps/desktop2/src/components/main/body/sessions/note-input/raw.tsx b/apps/desktop2/src/components/main/body/sessions/note-input/raw.tsx index 7b654aa12..230db10f6 100644 --- a/apps/desktop2/src/components/main/body/sessions/note-input/raw.tsx +++ b/apps/desktop2/src/components/main/body/sessions/note-input/raw.tsx @@ -1,19 +1,23 @@ import NoteEditor from "@hypr/tiptap/editor"; -export function RawEditor({ - editorKey, - value, - onChange, -}: { - editorKey: string; - value: string; - onChange: (value: string) => void; -}) { +import * as persisted from "../../../../../store/tinybase/persisted"; + +export function RawEditor({ sessionId }: { sessionId: string }) { + const value = persisted.UI.useCell("sessions", sessionId, "raw_md", persisted.STORE_ID); + + const handleRawChange = persisted.UI.useSetPartialRowCallback( + "sessions", + sessionId, + (input: string) => ({ raw_md: input }), + [], + persisted.STORE_ID, + ); + return ( { diff --git a/apps/desktop2/src/components/main/body/sessions/note-input/transcript.tsx b/apps/desktop2/src/components/main/body/sessions/note-input/transcript.tsx index f91436ce0..e80c161fd 100644 --- a/apps/desktop2/src/components/main/body/sessions/note-input/transcript.tsx +++ b/apps/desktop2/src/components/main/body/sessions/note-input/transcript.tsx @@ -1,15 +1,23 @@ import { type Word2 } from "@hypr/plugin-listener"; import TranscriptEditor, { type SpeakerViewInnerProps } from "@hypr/tiptap/transcript"; +import * as persisted from "../../../../../store/tinybase/persisted"; + export function TranscriptEditorWrapper({ - editorKey, - value, - onChange, + sessionId, }: { - editorKey: string; - value: string; - onChange: (value: string) => void; + sessionId: string; }) { + const value = persisted.UI.useCell("sessions", sessionId, "transcript", persisted.STORE_ID); + + const handleTranscriptChange = persisted.UI.useSetPartialRowCallback( + "sessions", + sessionId, + (input: Word2[]) => ({ transcript: JSON.stringify(input) }), + [], + persisted.STORE_ID, + ); + const parseTranscript = (value: string): Word2[] | null => { if (!value) { return null; @@ -24,12 +32,10 @@ export function TranscriptEditorWrapper({ return ( { - onChange(JSON.stringify({ words })); - }} + onUpdate={handleTranscriptChange} c={SpeakerSelector} /> ); From 66d2f9f12a254e0e3106c7dfc94f9736ac85e86f Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Thu, 16 Oct 2025 13:47:50 +0900 Subject: [PATCH 4/7] remove daily note --- .../src/components/main/body/daily.tsx | 77 ------------------- .../src/components/main/body/index.tsx | 15 ---- .../components/main/sidebar/profile/index.tsx | 8 +- .../desktop2/src/store/zustand/tabs/schema.ts | 7 -- 4 files changed, 1 insertion(+), 106 deletions(-) delete mode 100644 apps/desktop2/src/components/main/body/daily.tsx diff --git a/apps/desktop2/src/components/main/body/daily.tsx b/apps/desktop2/src/components/main/body/daily.tsx deleted file mode 100644 index caec209c3..000000000 --- a/apps/desktop2/src/components/main/body/daily.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { format } from "date-fns"; -import { CalendarIcon, CheckSquare, Mail, Sun } from "lucide-react"; - -import { type Tab } from "../../../store/zustand/tabs"; -import { type TabItem, TabItemBase } from "./shared"; - -export const TabItemDaily: TabItem = ( - { - tab, - handleCloseThis, - handleSelectThis, - handleCloseOthers, - handleCloseAll, - }, -) => { - return ( - } - title={tab.type === "daily" ? format(tab.date, "MMM d, yyyy") : "Daily Note"} - active={tab.active} - handleCloseThis={() => handleCloseThis(tab)} - handleSelectThis={() => handleSelectThis(tab)} - handleCloseOthers={handleCloseOthers} - handleCloseAll={handleCloseAll} - /> - ); -}; - -export function TabContentDaily({ tab }: { tab: Tab }) { - if (tab.type !== "daily") { - return null; - } - - return ( -
-

{format(tab.date, "MMM d, yyyy")}

- -
-
-

Task

-
- {[1, 2, 3].map((i) => ( -
- - task {i} -
- ))} -
-
- -
-

Email

-
- {[1, 2, 3].map((i) => ( -
- - email {i} -
- ))} -
-
- -
-

Event

-
- {[1, 2, 3].map((i) => ( -
- - event {i} -
- ))} -
-
-
-
- ); -} diff --git a/apps/desktop2/src/components/main/body/index.tsx b/apps/desktop2/src/components/main/body/index.tsx index bde9c5ad4..63138ce6c 100644 --- a/apps/desktop2/src/components/main/body/index.tsx +++ b/apps/desktop2/src/components/main/body/index.tsx @@ -12,7 +12,6 @@ import { id } from "../../../utils"; import { ChatFloatingButton } from "../../chat"; import { TabContentCalendar, TabItemCalendar } from "./calendars"; import { TabContentContact, TabItemContact } from "./contacts"; -import { TabContentDaily, TabItemDaily } from "./daily"; import { TabContentEvent, TabItemEvent } from "./events"; import { TabContentFolder, TabItemFolder } from "./folders"; import { TabContentHuman, TabItemHuman } from "./humans"; @@ -236,17 +235,6 @@ function TabItem( /> ); } - if (tab.type === "daily") { - return ( - - ); - } if (tab.type === "calendars") { return ( @@ -287,9 +275,6 @@ function Content({ tab }: { tab: Tab }) { if (tab.type === "humans") { return ; } - if (tab.type === "daily") { - return ; - } if (tab.type === "calendars") { return ; diff --git a/apps/desktop2/src/components/main/sidebar/profile/index.tsx b/apps/desktop2/src/components/main/sidebar/profile/index.tsx index a3022fb82..2f348e9f7 100644 --- a/apps/desktop2/src/components/main/sidebar/profile/index.tsx +++ b/apps/desktop2/src/components/main/sidebar/profile/index.tsx @@ -1,5 +1,5 @@ import { clsx } from "clsx"; -import { Calendar, ChevronUpIcon, FileText, FolderOpen, Settings, Users } from "lucide-react"; +import { Calendar, ChevronUpIcon, FolderOpen, Settings, Users } from "lucide-react"; import { useCallback, useState } from "react"; import { commands as windowsCommands } from "@hypr/plugin-windows/v1"; @@ -47,16 +47,10 @@ export function ProfileSection() { closeMenu(); }, [openNew, closeMenu]); - const handleClickDailyNote = useCallback(() => { - openNew({ type: "daily", date: new Date(), active: true }); - closeMenu(); - }, [openNew, closeMenu]); - const menuItems = [ { icon: FolderOpen, label: "Folders", onClick: handleClickFolders }, { icon: Users, label: "Contacts", onClick: handleClickContacts }, { icon: Calendar, label: "Calendar", onClick: handleClickCalendar }, - { icon: FileText, label: "Daily note", onClick: handleClickDailyNote }, { icon: Settings, label: "Settings", onClick: handleClickSettings }, ]; diff --git a/apps/desktop2/src/store/zustand/tabs/schema.ts b/apps/desktop2/src/store/zustand/tabs/schema.ts index e726303a2..360604164 100644 --- a/apps/desktop2/src/store/zustand/tabs/schema.ts +++ b/apps/desktop2/src/store/zustand/tabs/schema.ts @@ -45,10 +45,6 @@ export const tabSchema = z.discriminatedUnion("type", [ type: z.literal("calendars"), month: z.coerce.date(), }), - baseTabSchema.extend({ - type: z.literal("daily"), - date: z.coerce.date(), - }), ]); export type Tab = z.infer; @@ -70,7 +66,6 @@ export const rowIdfromTab = (tab: Tab): string => { return tab.id; case "calendars": case "contacts": - case "daily": throw new Error("invalid_resource"); case "folders": if (!tab.id) { @@ -94,8 +89,6 @@ export const uniqueIdfromTab = (tab: Tab): string => { return `calendars-${tab.month.getFullYear()}-${tab.month.getMonth()}`; case "contacts": return `contacts`; - case "daily": - return `daily-${tab.date.getFullYear()}-${tab.date.getMonth()}-${tab.date.getDate()}`; case "folders": return `folders-${tab.id ?? "all"}`; } From 5af1a9edeb2882a39c7b2948e7b26e34392abeeb Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Thu, 16 Oct 2025 14:55:52 +0900 Subject: [PATCH 5/7] add onEmpty handler in tabs and better autoscroll support --- .../main/sidebar/timeline/anchor.ts | 14 ++++++- .../main/sidebar/timeline/index.tsx | 6 +++ apps/desktop2/src/routes/app/main/_layout.tsx | 37 +++++++++---------- apps/desktop2/src/store/zustand/tabs/basic.ts | 11 ++++-- .../src/store/zustand/tabs/lifecycle.ts | 15 ++++++++ apps/desktop2/src/store/zustand/tabs/utils.ts | 12 ++++++ 6 files changed, 70 insertions(+), 25 deletions(-) diff --git a/apps/desktop2/src/components/main/sidebar/timeline/anchor.ts b/apps/desktop2/src/components/main/sidebar/timeline/anchor.ts index 26af66c9a..be6f5df24 100644 --- a/apps/desktop2/src/components/main/sidebar/timeline/anchor.ts +++ b/apps/desktop2/src/components/main/sidebar/timeline/anchor.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState } from "react"; +import { type DependencyList, useCallback, useEffect, useRef, useState } from "react"; export function useAnchor() { const containerRef = useRef(null); @@ -62,10 +62,12 @@ export function useAutoScrollToAnchor({ scrollFn, isVisible, anchorNode, + deps = [], }: { scrollFn: () => void; isVisible: boolean; anchorNode: HTMLDivElement | null; + deps?: DependencyList; }) { const hasMountedRef = useRef(false); const prevAnchorNodeRef = useRef(null); @@ -95,4 +97,14 @@ export function useAutoScrollToAnchor({ } }); }, [anchorNode, isVisible, scrollFn]); + + useEffect(() => { + if (!anchorNode || isVisible) { + return; + } + + requestAnimationFrame(() => { + scrollFn(); + }); + }, deps); } diff --git a/apps/desktop2/src/components/main/sidebar/timeline/index.tsx b/apps/desktop2/src/components/main/sidebar/timeline/index.tsx index b224072f6..e6f4e3d57 100644 --- a/apps/desktop2/src/components/main/sidebar/timeline/index.tsx +++ b/apps/desktop2/src/components/main/sidebar/timeline/index.tsx @@ -24,10 +24,16 @@ export function TimelineView() { anchorNode: todayAnchorNode, } = useAnchor(); + const todayBucketLength = useMemo(() => { + const b = buckets.find(bucket => bucket.label === "Today"); + return b?.items.length ?? 0; + }, [buckets]); + useAutoScrollToAnchor({ scrollFn: scrollToToday, isVisible: isTodayVisible, anchorNode: todayAnchorNode, + deps: [todayBucketLength], }); return ( diff --git a/apps/desktop2/src/routes/app/main/_layout.tsx b/apps/desktop2/src/routes/app/main/_layout.tsx index fff694ca5..45be62687 100644 --- a/apps/desktop2/src/routes/app/main/_layout.tsx +++ b/apps/desktop2/src/routes/app/main/_layout.tsx @@ -16,8 +16,8 @@ export const Route = createFileRoute("/app/main/_layout")({ }); function Component() { - const { persistedStore } = useRouteContext({ from: "__root__" }); - const { registerOnClose } = useTabs(); + const { persistedStore, internalStore } = useRouteContext({ from: "__root__" }); + const { registerOnClose, registerOnEmpty, currentTab, openNew } = useTabs(); useEffect(() => { return registerOnClose((tab) => { @@ -34,6 +34,21 @@ function Component() { }); }, [persistedStore, registerOnClose]); + useEffect(() => { + const createDefaultSession = () => { + const user_id = internalStore?.getValue("user_id"); + const sessionId = id(); + persistedStore?.setRow("sessions", sessionId, { user_id, created_at: new Date().toISOString() }); + openNew({ id: sessionId, type: "sessions", active: true, state: { editor: "raw" } }); + }; + + if (!currentTab) { + createDefaultSession(); + } + + return registerOnEmpty(createDefaultSession); + }, [currentTab, persistedStore, internalStore, registerOnEmpty, openNew]); + return ( @@ -41,7 +56,6 @@ function Component() { - @@ -63,20 +77,3 @@ function ToolRegistration() { return null; } - -// TOOD -function NotSureAboutThis() { - const { persistedStore, internalStore } = useRouteContext({ from: "__root__" }); - const { currentTab, openNew } = useTabs(); - - useEffect(() => { - if (!currentTab) { - const user_id = internalStore?.getValue("user_id"); - const sessionId = id(); - persistedStore?.setRow("sessions", sessionId, { user_id, created_at: new Date().toISOString() }); - openNew({ id: sessionId, type: "sessions", active: true, state: { editor: "raw" } }); - } - }, [currentTab]); - - return null; -} diff --git a/apps/desktop2/src/store/zustand/tabs/basic.ts b/apps/desktop2/src/store/zustand/tabs/basic.ts index 69981bd2d..c1ca827dd 100644 --- a/apps/desktop2/src/store/zustand/tabs/basic.ts +++ b/apps/desktop2/src/store/zustand/tabs/basic.ts @@ -4,7 +4,7 @@ import type { LifecycleState } from "./lifecycle"; import type { NavigationState } from "./navigation"; import type { Tab, TabHistory } from "./schema"; import { isSameTab, tabSchema } from "./schema"; -import { computeHistoryFlags, getSlotId, notifyTabClose, notifyTabsClose, pushHistory } from "./utils"; +import { computeHistoryFlags, getSlotId, notifyEmpty, notifyTabClose, notifyTabsClose, pushHistory } from "./utils"; export type BasicState = { currentTab: Tab | null; @@ -100,19 +100,21 @@ export const createBasicSlice = ); }, close: (tab) => { - const { tabs, history, onCloseHandlers } = get(); + const { tabs, history, onCloseHandlers, onEmptyHandlers } = get(); const remainingTabs = tabs.filter((t) => !isSameTab(t, tab)); notifyTabClose(onCloseHandlers, tab); if (remainingTabs.length === 0) { - return set({ + set({ tabs: [] as Tab[], currentTab: null, history: new Map(), canGoBack: false, canGoNext: false, } as Partial); + notifyEmpty(onEmptyHandlers); + return; } const closedTabIndex = tabs.findIndex((t) => isSameTab(t, tab)); @@ -162,7 +164,7 @@ export const createBasicSlice = ); }, closeAll: () => { - const { tabs, onCloseHandlers } = get(); + const { tabs, onCloseHandlers, onEmptyHandlers } = get(); notifyTabsClose(onCloseHandlers, tabs); set({ tabs: [], @@ -171,5 +173,6 @@ export const createBasicSlice = ); + notifyEmpty(onEmptyHandlers); }, }); diff --git a/apps/desktop2/src/store/zustand/tabs/lifecycle.ts b/apps/desktop2/src/store/zustand/tabs/lifecycle.ts index 186a184e2..15cf692b0 100644 --- a/apps/desktop2/src/store/zustand/tabs/lifecycle.ts +++ b/apps/desktop2/src/store/zustand/tabs/lifecycle.ts @@ -4,10 +4,12 @@ import type { Tab } from "./schema"; export type LifecycleState = { onCloseHandlers: Set<(tab: Tab) => void>; + onEmptyHandlers: Set<() => void>; }; export type LifecycleActions = { registerOnClose: (handler: (tab: Tab) => void) => () => void; + registerOnEmpty: (handler: () => void) => () => void; }; export const createLifecycleSlice = ( @@ -15,6 +17,7 @@ export const createLifecycleSlice = ( get: StoreApi["getState"], ): LifecycleState & LifecycleActions => ({ onCloseHandlers: new Set(), + onEmptyHandlers: new Set(), registerOnClose: (handler) => { const { onCloseHandlers } = get(); const nextHandlers = new Set(onCloseHandlers); @@ -27,4 +30,16 @@ export const createLifecycleSlice = ( set({ onCloseHandlers: nextHandlers } as Partial); }; }, + registerOnEmpty: (handler) => { + const { onEmptyHandlers } = get(); + const nextHandlers = new Set(onEmptyHandlers); + nextHandlers.add(handler); + set({ onEmptyHandlers: nextHandlers } as Partial); + return () => { + const { onEmptyHandlers: currentHandlers } = get(); + const nextHandlers = new Set(currentHandlers); + nextHandlers.delete(handler); + set({ onEmptyHandlers: nextHandlers } as Partial); + }; + }, }); diff --git a/apps/desktop2/src/store/zustand/tabs/utils.ts b/apps/desktop2/src/store/zustand/tabs/utils.ts index 72a4ca50b..4a29620d3 100644 --- a/apps/desktop2/src/store/zustand/tabs/utils.ts +++ b/apps/desktop2/src/store/zustand/tabs/utils.ts @@ -27,6 +27,18 @@ export const notifyTabsClose = ( tabs.forEach((tab) => notifyTabClose(handlers, tab)); }; +export const notifyEmpty = ( + handlers: Set<() => void>, +): void => { + handlers.forEach((handler) => { + try { + handler(); + } catch (error) { + console.error("tab onEmpty handler failed", error); + } + }); +}; + export const computeHistoryFlags = ( history: Map, currentTab: Tab | null, From 0e8c76cdfc0f15d143f3524d3623f8a8925e2aaf Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Thu, 16 Oct 2025 16:53:16 +0900 Subject: [PATCH 6/7] chores --- .../main/body/sessions/note-input/index.tsx | 79 ++++++++----------- apps/desktop2/src/store/tinybase/persisted.ts | 22 +++++- apps/desktop2/src/utils/timeline.ts | 16 ++-- 3 files changed, 61 insertions(+), 56 deletions(-) diff --git a/apps/desktop2/src/components/main/body/sessions/note-input/index.tsx b/apps/desktop2/src/components/main/body/sessions/note-input/index.tsx index 7b06f4314..16f994ab8 100644 --- a/apps/desktop2/src/components/main/body/sessions/note-input/index.tsx +++ b/apps/desktop2/src/components/main/body/sessions/note-input/index.tsx @@ -1,4 +1,5 @@ import { cn } from "@hypr/ui/lib/utils"; +import { motion } from "motion/react"; import { type Tab, useTabs } from "../../../../../store/zustand/tabs"; import { EnhancedEditor } from "./enhanced"; import { RawEditor } from "./raw"; @@ -6,11 +7,13 @@ import { TranscriptEditorWrapper } from "./transcript"; type EditorView = "raw" | "enhanced" | "transcript"; -export function NoteInput({ - tab, -}: { - tab: Tab; -}) { +const tabs = [ + { id: "enhanced", label: "Summary" }, + { id: "raw", label: "Memo" }, + { id: "transcript", label: "Transcript" }, +] as const; + +export function NoteInput({ tab }: { tab: Tab }) { const { updateSessionTabState } = useTabs(); const handleTabChange = (view: EditorView) => { @@ -26,51 +29,33 @@ export function NoteInput({ return (
-
-
-
-
- - - - - -
-
+
+
+ {tabs.map((t) => ( + + ))}
-
+
{currentTab === "enhanced" && } {currentTab === "raw" && } diff --git a/apps/desktop2/src/store/tinybase/persisted.ts b/apps/desktop2/src/store/tinybase/persisted.ts index 028c0016a..fb2a9890a 100644 --- a/apps/desktop2/src/store/tinybase/persisted.ts +++ b/apps/desktop2/src/store/tinybase/persisted.ts @@ -417,7 +417,16 @@ export const StoreComponent = () => { INDEXES.eventsByDate, "events", (getCell) => { - const d = new Date(getCell("started_at")!); + const cell = getCell("started_at"); + if (!cell) { + return ""; + } + + const d = new Date(cell); + if (isNaN(d.getTime())) { + return ""; + } + return format(d, "yyyy-MM-dd"); }, "started_at", @@ -432,7 +441,16 @@ export const StoreComponent = () => { return ""; } - const d = new Date(getCell("created_at")!); + const cell = getCell("created_at"); + if (!cell) { + return ""; + } + + const d = new Date(cell); + if (isNaN(d.getTime())) { + return ""; + } + return format(d, "yyyy-MM-dd"); }, "created_at", diff --git a/apps/desktop2/src/utils/timeline.ts b/apps/desktop2/src/utils/timeline.ts index 831270045..8e4d669ee 100644 --- a/apps/desktop2/src/utils/timeline.ts +++ b/apps/desktop2/src/utils/timeline.ts @@ -128,7 +128,7 @@ export function buildTimelineBuckets({ return; } - if (!isPast(eventStartTime)) { + if (eventStartTime && !isPast(eventStartTime)) { items.push({ type: "event", id: eventId, @@ -153,12 +153,14 @@ export function buildTimelineBuckets({ return; } - items.push({ - type: "session", - id: sessionId, - date: format(date, "yyyy-MM-dd"), - data: row as unknown as persisted.Session, - }); + if (date) { + items.push({ + type: "session", + id: sessionId, + date: format(date, "yyyy-MM-dd"), + data: row as unknown as persisted.Session, + }); + } }); items.sort((a, b) => { From cb6153fda093f35a5f14ce9432b50aa3e0e0711a Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Thu, 16 Oct 2025 18:21:35 +0900 Subject: [PATCH 7/7] editable title breadcrumb, and consistent tab styling --- .../main/body/sessions/note-input/index.tsx | 41 +++++------ .../body/sessions/outer-header/folder.tsx | 72 ++++++++++++++----- 2 files changed, 70 insertions(+), 43 deletions(-) diff --git a/apps/desktop2/src/components/main/body/sessions/note-input/index.tsx b/apps/desktop2/src/components/main/body/sessions/note-input/index.tsx index 16f994ab8..584683c4a 100644 --- a/apps/desktop2/src/components/main/body/sessions/note-input/index.tsx +++ b/apps/desktop2/src/components/main/body/sessions/note-input/index.tsx @@ -1,5 +1,4 @@ import { cn } from "@hypr/ui/lib/utils"; -import { motion } from "motion/react"; import { type Tab, useTabs } from "../../../../../store/zustand/tabs"; import { EnhancedEditor } from "./enhanced"; import { RawEditor } from "./raw"; @@ -7,11 +6,11 @@ import { TranscriptEditorWrapper } from "./transcript"; type EditorView = "raw" | "enhanced" | "transcript"; -const tabs = [ - { id: "enhanced", label: "Summary" }, - { id: "raw", label: "Memo" }, - { id: "transcript", label: "Transcript" }, -] as const; +const EDITOR_TABS = [ + { view: "enhanced" as const, label: "Summary" }, + { view: "raw" as const, label: "Memos" }, + { view: "transcript" as const, label: "Transcript" }, +]; export function NoteInput({ tab }: { tab: Tab }) { const { updateSessionTabState } = useTabs(); @@ -29,34 +28,26 @@ export function NoteInput({ tab }: { tab: Tab }) { return (
-
-
- {tabs.map((t) => ( +
+
+ {EDITOR_TABS.map(({ view, label }) => ( ))}
-
-
+
{currentTab === "enhanced" && } {currentTab === "raw" && } {currentTab === "transcript" && } diff --git a/apps/desktop2/src/components/main/body/sessions/outer-header/folder.tsx b/apps/desktop2/src/components/main/body/sessions/outer-header/folder.tsx index 66aeb8d68..7f98abdee 100644 --- a/apps/desktop2/src/components/main/body/sessions/outer-header/folder.tsx +++ b/apps/desktop2/src/components/main/body/sessions/outer-header/folder.tsx @@ -5,19 +5,38 @@ import { useTabs } from "../../../../../store/zustand/tabs"; export function FolderChain({ sessionId }: { sessionId: string }) { const folderId = persisted.UI.useCell("sessions", sessionId, "folder_id", persisted.STORE_ID); - const title = persisted.UI.useCell("sessions", sessionId, "title", persisted.STORE_ID); + const title = persisted.UI.useCell("sessions", sessionId, "title", persisted.STORE_ID) ?? "Untitled"; + + const handleChangeTitle = persisted.UI.useSetPartialRowCallback( + "sessions", + sessionId, + (title: string) => ({ title }), + [], + persisted.STORE_ID, + ); return (
{!folderId - ? - : } + ? + : }
); } -function RenderIfRootExist({ folderId, title }: { folderId: string; title: string }) { +function RenderIfRootExist( + { + folderId, + title, + handleChangeTitle, + }: { + folderId: string; + title: string; + handleChangeTitle: (title: string) => void; + }, +) { const folderIds = useFolderList(folderId); + return ( <> {folderIds.map((id, index) => ( @@ -27,34 +46,31 @@ function RenderIfRootExist({ folderId, title }: { folderId: string; title: strin
))} / - {title} + ); } -function RenderIfRootNotExist({ sessionId }: { sessionId: string }) { - const title = persisted.UI.useCell("sessions", sessionId, "title", persisted.STORE_ID); - +function RenderIfRootNotExist( + { + title, + handleChangeTitle, + }: { + title: string; + handleChangeTitle: (title: string) => void; + }, +) { return ( <> / - {title ?? "Untitled"} + ); } -function useFolderList(rootFolderId: string) { - const folderIds = persisted.UI.useLinkedRowIds( - "folderToParentFolder", - rootFolderId, - persisted.STORE_ID, - ); - return [...folderIds].reverse(); -} - function FolderItem({ folderId }: { folderId: string }) { const folderName = persisted.UI.useCell("folders", folderId, "name", persisted.STORE_ID); @@ -72,3 +88,23 @@ function FolderItem({ folderId }: { folderId: string }) { ); } + +function useFolderList(rootFolderId: string) { + const folderIds = persisted.UI.useLinkedRowIds( + "folderToParentFolder", + rootFolderId, + persisted.STORE_ID, + ); + return [...folderIds].reverse(); +} + +function TitleInput({ title, handleChangeTitle }: { title: string; handleChangeTitle: (title: string) => void }) { + return ( + handleChangeTitle(e.target.value)} + /> + ); +}