From 03a73af4c917d4fd7dd6327bb592f4d6ecd03052 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Mon, 20 Oct 2025 20:07:27 +0900 Subject: [PATCH 1/7] update tests --- apps/desktop/src/utils/transcript.test.ts | 39 +++++++++++++++++++---- apps/desktop/src/utils/transcript.ts | 8 ++++- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src/utils/transcript.test.ts b/apps/desktop/src/utils/transcript.test.ts index 7a905b5cc9..337243a5ab 100644 --- a/apps/desktop/src/utils/transcript.test.ts +++ b/apps/desktop/src/utils/transcript.test.ts @@ -25,14 +25,14 @@ describe("buildSegments", () => { end_ms, }); - it.each([ + const testcases = [ { description: "returns empty array for empty input", words: [], segments: [], }, { - description: "creates single segment from single word", + description: "simple single segment from single word", words: [ WORD({ text: "hello", channel: 0, start_ms: 0, end_ms: 100 }), ], @@ -40,11 +40,36 @@ describe("buildSegments", () => { { speaker: "Speaker 0", text: "hello" }, ], }, - ])("$description", ({ words, segments }) => { - const result = buildSegments({ - words, - speakerFromChannel: (channel) => `Speaker ${channel}`, - }); + { + description: "merge adjacent words from same channel", + words: [ + WORD({ text: "hello", channel: 0, start_ms: 0, end_ms: 100 }), + WORD({ text: "world", channel: 0, start_ms: 120, end_ms: 220 }), + ], + segments: [ + { speaker: "Speaker 0", text: "hello world" }, + ], + }, + { + description: "different channels are different speakers", + words: [ + WORD({ text: "hello", channel: 0, start_ms: 0, end_ms: 100 }), + WORD({ text: "world", channel: 1, start_ms: 120, end_ms: 220 }), + ], + segments: [ + { speaker: "Speaker 0", text: "hello" }, + { speaker: "Speaker 1", text: "world" }, + ], + }, + ]; + + it.each(testcases)("$description", ({ words, segments }) => { + const result = buildSegments( + { + words, + speakerFromChannel: (channel) => `Speaker ${channel}`, + }, + ); expect(result).toEqual(segments); }); diff --git a/apps/desktop/src/utils/transcript.ts b/apps/desktop/src/utils/transcript.ts index 4159d0de86..d6d38852e2 100644 --- a/apps/desktop/src/utils/transcript.ts +++ b/apps/desktop/src/utils/transcript.ts @@ -18,7 +18,13 @@ export const buildSegments = ( for (const word of words) { const speaker = speakerFromChannel(word.channel); - segments.push({ text: word.text, speaker }); + const lastSegment = segments[segments.length - 1]; + + if (lastSegment && lastSegment.speaker === speaker) { + lastSegment.text += ` ${word.text}`; + } else { + segments.push({ text: word.text, speaker }); + } } return segments; From 721572c30617e02f88976b3edecb1bcaff59b912 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Mon, 20 Oct 2025 20:14:37 +0900 Subject: [PATCH 2/7] refactor --- .../components/main/sidebar/profile/ota.tsx | 59 +++++++++---------- .../main/sidebar/profile/shared.tsx | 24 +++++--- 2 files changed, 43 insertions(+), 40 deletions(-) diff --git a/apps/desktop/src/components/main/sidebar/profile/ota.tsx b/apps/desktop/src/components/main/sidebar/profile/ota.tsx index 537c33658d..b40618f8b4 100644 --- a/apps/desktop/src/components/main/sidebar/profile/ota.tsx +++ b/apps/desktop/src/components/main/sidebar/profile/ota.tsx @@ -1,5 +1,3 @@ -import { Spinner } from "@hypr/ui/components/ui/spinner"; - import { relaunch } from "@tauri-apps/plugin-process"; import { check, type Update } from "@tauri-apps/plugin-updater"; import { createStore } from "@xstate/store"; @@ -7,6 +5,8 @@ import { useSelector } from "@xstate/store/react"; import { clsx } from "clsx"; import { AlertCircle, CheckCircle, Download, RefreshCw, X } from "lucide-react"; +import { Spinner } from "@hypr/ui/components/ui/spinner"; +import { cn } from "@hypr/utils"; import { MenuItem } from "./shared"; type State = @@ -203,31 +203,19 @@ export function UpdateChecker() { if (state === "checking") { return ( -
+ - Checking for updates... -
+ Checking for updates... + ); } if (state === "noUpdate") { return ( -
- - You're up to date -
+ + + You're up to date + ); } @@ -271,7 +259,7 @@ export function UpdateChecker() { if (state === "downloading") { return ( -
+
@@ -296,7 +284,7 @@ export function UpdateChecker() { style={{ width: `${downloadProgress.percentage}%` }} />
-
+ ); } @@ -313,16 +301,10 @@ export function UpdateChecker() { if (state === "installing") { return ( -
+ Installing... -
+ ); } @@ -334,3 +316,18 @@ export function UpdateChecker() { /> ); } + +function MenuItemLikeContainer({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ); +} diff --git a/apps/desktop/src/components/main/sidebar/profile/shared.tsx b/apps/desktop/src/components/main/sidebar/profile/shared.tsx index 3e279886d1..2952ed9f64 100644 --- a/apps/desktop/src/components/main/sidebar/profile/shared.tsx +++ b/apps/desktop/src/components/main/sidebar/profile/shared.tsx @@ -1,17 +1,23 @@ -import { clsx } from "clsx"; +import { cn } from "@hypr/utils"; export function MenuItem( - { icon: Icon, label, badge, suffixIcon: SuffixIcon, onClick }: { - icon: any; + { + icon: Icon, + label, + badge, + suffixIcon: SuffixIcon, + onClick, + }: { + icon: React.ComponentType<{ className?: string }>; label: string; badge?: number | React.ReactNode; - suffixIcon?: any; + suffixIcon?: React.ComponentType<{ className?: string }>; onClick: () => void; }, ) { return ( ); } From 456e472d0b43202dc5352ccd095c32cfff2cb2d8 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Mon, 20 Oct 2025 20:16:20 +0900 Subject: [PATCH 3/7] mv --- .../components/main/sidebar/profile/{ota.tsx => ota/index.tsx} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename apps/desktop/src/components/main/sidebar/profile/{ota.tsx => ota/index.tsx} (99%) diff --git a/apps/desktop/src/components/main/sidebar/profile/ota.tsx b/apps/desktop/src/components/main/sidebar/profile/ota/index.tsx similarity index 99% rename from apps/desktop/src/components/main/sidebar/profile/ota.tsx rename to apps/desktop/src/components/main/sidebar/profile/ota/index.tsx index b40618f8b4..10542ea59a 100644 --- a/apps/desktop/src/components/main/sidebar/profile/ota.tsx +++ b/apps/desktop/src/components/main/sidebar/profile/ota/index.tsx @@ -7,7 +7,7 @@ import { AlertCircle, CheckCircle, Download, RefreshCw, X } from "lucide-react"; import { Spinner } from "@hypr/ui/components/ui/spinner"; import { cn } from "@hypr/utils"; -import { MenuItem } from "./shared"; +import { MenuItem } from "../shared"; type State = | "idle" From 05bdf11d13011729a0108b9b51acaa58f9196ef1 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Mon, 20 Oct 2025 20:20:08 +0900 Subject: [PATCH 4/7] splits --- .../main/sidebar/profile/ota/index.tsx | 129 +----------------- .../main/sidebar/profile/ota/store.ts | 106 ++++++++++++++ .../main/sidebar/profile/ota/task.ts | 24 ++++ apps/desktop/src/components/task-manager.tsx | 2 +- 4 files changed, 133 insertions(+), 128 deletions(-) create mode 100644 apps/desktop/src/components/main/sidebar/profile/ota/store.ts create mode 100644 apps/desktop/src/components/main/sidebar/profile/ota/task.ts diff --git a/apps/desktop/src/components/main/sidebar/profile/ota/index.tsx b/apps/desktop/src/components/main/sidebar/profile/ota/index.tsx index 10542ea59a..f6a1cde55f 100644 --- a/apps/desktop/src/components/main/sidebar/profile/ota/index.tsx +++ b/apps/desktop/src/components/main/sidebar/profile/ota/index.tsx @@ -1,6 +1,4 @@ import { relaunch } from "@tauri-apps/plugin-process"; -import { check, type Update } from "@tauri-apps/plugin-updater"; -import { createStore } from "@xstate/store"; import { useSelector } from "@xstate/store/react"; import { clsx } from "clsx"; import { AlertCircle, CheckCircle, Download, RefreshCw, X } from "lucide-react"; @@ -8,131 +6,8 @@ import { AlertCircle, CheckCircle, Download, RefreshCw, X } from "lucide-react"; import { Spinner } from "@hypr/ui/components/ui/spinner"; import { cn } from "@hypr/utils"; import { MenuItem } from "../shared"; - -type State = - | "idle" - | "checking" - | "error" - | "noUpdate" - | "available" - | "downloading" - | "ready" - | "installing"; - -interface Context { - update: Update | null; - error: string | null; - downloadProgress: { - downloaded: number; - total: number | null; - percentage: number; - }; - state: State; -} - -const updateStore = createStore({ - context: { - update: null, - error: null, - downloadProgress: { - downloaded: 0, - total: null, - percentage: 0, - }, - state: "idle" as State, - } as Context, - on: { - setState: (context, event: { state: State }) => ({ - ...context, - state: event.state, - }), - checkSuccess: (context, event: { update: Update | null }) => ({ - ...context, - update: event.update, - error: null, - state: event.update ? ("available" as State) : ("noUpdate" as State), - }), - checkError: (context, event: { error: string }) => ({ - ...context, - error: event.error, - update: null, - state: "error" as State, - }), - startDownload: (context) => ({ - ...context, - downloadProgress: { - downloaded: 0, - total: null, - percentage: 0, - }, - state: "downloading" as State, - }), - downloadProgress: (context, event: { chunkLength: number; contentLength?: number }) => ({ - ...context, - downloadProgress: { - downloaded: context.downloadProgress.downloaded + event.chunkLength, - total: event.contentLength ?? context.downloadProgress.total, - percentage: event.contentLength || context.downloadProgress.total - ? Math.round( - ((context.downloadProgress.downloaded + event.chunkLength) - / (event.contentLength ?? context.downloadProgress.total ?? 1)) - * 100, - ) - : 0, - }, - }), - downloadFinished: (context) => ({ - ...context, - state: "ready" as State, - }), - cancelDownload: (context) => ({ - ...context, - update: null, - downloadProgress: { - downloaded: 0, - total: null, - percentage: 0, - }, - state: "idle" as State, - }), - setInstalling: (context) => ({ - ...context, - state: "installing" as State, - }), - reset: (context) => ({ - ...context, - update: null, - error: null, - downloadProgress: { - downloaded: 0, - total: null, - percentage: 0, - }, - state: "idle" as State, - }), - }, -}); - -export const checkForUpdate = async () => { - updateStore.trigger.setState({ state: "checking" }); - - try { - const update = await check(); - updateStore.trigger.checkSuccess({ update }); - - if (!update) { - setTimeout(() => { - const currentState = updateStore.getSnapshot().context.state; - if (currentState === "noUpdate") { - updateStore.trigger.reset(); - } - }, 2000); - } - } catch (err) { - const errorMessage = err instanceof Error ? err.message : "Failed to check for updates"; - updateStore.trigger.checkError({ error: errorMessage }); - } -}; +import { updateStore } from "./store"; +import { checkForUpdate } from "./task"; export function UpdateChecker() { const snapshot = useSelector(updateStore, (state) => state.context); diff --git a/apps/desktop/src/components/main/sidebar/profile/ota/store.ts b/apps/desktop/src/components/main/sidebar/profile/ota/store.ts new file mode 100644 index 0000000000..b70ebfa0e4 --- /dev/null +++ b/apps/desktop/src/components/main/sidebar/profile/ota/store.ts @@ -0,0 +1,106 @@ +import { type Update } from "@tauri-apps/plugin-updater"; +import { createStore } from "@xstate/store"; + +type State = + | "idle" + | "checking" + | "error" + | "noUpdate" + | "available" + | "downloading" + | "ready" + | "installing"; + +type Context = { + state: State; + update: Update | null; + error: string | null; + downloadProgress: { + downloaded: number; + total: number | null; + percentage: number; + }; +}; + +export const updateStore = createStore({ + context: { + state: "idle" as State, + update: null, + error: null, + downloadProgress: { + downloaded: 0, + total: null, + percentage: 0, + }, + } as Context, + on: { + setState: (context, event: { state: State }) => ({ + ...context, + state: event.state, + }), + checkSuccess: (context, event: { update: Update | null }) => ({ + ...context, + update: event.update, + error: null, + state: event.update ? ("available" as State) : ("noUpdate" as State), + }), + checkError: (context, event: { error: string }) => ({ + ...context, + error: event.error, + update: null, + state: "error" as State, + }), + startDownload: (context) => ({ + ...context, + downloadProgress: { + downloaded: 0, + total: null, + percentage: 0, + }, + state: "downloading" as State, + }), + downloadProgress: (context, event: { chunkLength: number; contentLength?: number }) => ({ + ...context, + downloadProgress: { + downloaded: context.downloadProgress.downloaded + event.chunkLength, + total: event.contentLength ?? context.downloadProgress.total, + percentage: event.contentLength || context.downloadProgress.total + ? Math.round( + ((context.downloadProgress.downloaded + event.chunkLength) + / (event.contentLength ?? context.downloadProgress.total ?? 1)) + * 100, + ) + : 0, + }, + }), + downloadFinished: (context) => ({ + ...context, + state: "ready" as State, + }), + cancelDownload: (context) => ({ + ...context, + update: null, + downloadProgress: { + downloaded: 0, + total: null, + percentage: 0, + }, + state: "idle" as State, + }), + setInstalling: (context) => ({ + ...context, + state: "installing" as State, + }), + reset: (context) => ({ + ...context, + update: null, + error: null, + downloadProgress: { + downloaded: 0, + total: null, + percentage: 0, + }, + state: "idle" as State, + }), + }, +}); diff --git a/apps/desktop/src/components/main/sidebar/profile/ota/task.ts b/apps/desktop/src/components/main/sidebar/profile/ota/task.ts new file mode 100644 index 0000000000..5c8d8343cd --- /dev/null +++ b/apps/desktop/src/components/main/sidebar/profile/ota/task.ts @@ -0,0 +1,24 @@ +import { check } from "@tauri-apps/plugin-updater"; + +import { updateStore } from "./store"; + +export const checkForUpdate = async () => { + updateStore.trigger.setState({ state: "checking" }); + + try { + const update = await check(); + updateStore.trigger.checkSuccess({ update }); + + if (!update) { + setTimeout(() => { + const currentState = updateStore.getSnapshot().context.state; + if (currentState === "noUpdate") { + updateStore.trigger.reset(); + } + }, 2000); + } + } catch (err) { + const errorMessage = err instanceof Error ? err.message : "Failed to check for updates"; + updateStore.trigger.checkError({ error: errorMessage }); + } +}; diff --git a/apps/desktop/src/components/task-manager.tsx b/apps/desktop/src/components/task-manager.tsx index a4605c1853..d542d61120 100644 --- a/apps/desktop/src/components/task-manager.tsx +++ b/apps/desktop/src/components/task-manager.tsx @@ -3,7 +3,7 @@ import { useScheduleTaskRun, useSetTask } from "tinytick/ui-react"; import { commands as localSttCommands } from "@hypr/plugin-local-stt"; import type { SupportedSttModel } from "@hypr/plugin-local-stt"; -import { checkForUpdate } from "./main/sidebar/profile/ota"; +import { checkForUpdate } from "./main/sidebar/profile/ota/task"; const UPDATE_CHECK_TASK_ID = "checkForUpdate"; const UPDATE_CHECK_INTERVAL = 30 * 1000; From fa5703dba0fb1e11a8e78785728234baf0228691 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Mon, 20 Oct 2025 20:31:52 +0900 Subject: [PATCH 5/7] use ota --- .../main/sidebar/profile/ota/index.tsx | 110 +++++------------- .../main/sidebar/profile/ota/task.ts | 77 ++++++++++++ 2 files changed, 107 insertions(+), 80 deletions(-) diff --git a/apps/desktop/src/components/main/sidebar/profile/ota/index.tsx b/apps/desktop/src/components/main/sidebar/profile/ota/index.tsx index f6a1cde55f..6c1c923411 100644 --- a/apps/desktop/src/components/main/sidebar/profile/ota/index.tsx +++ b/apps/desktop/src/components/main/sidebar/profile/ota/index.tsx @@ -1,86 +1,30 @@ -import { relaunch } from "@tauri-apps/plugin-process"; -import { useSelector } from "@xstate/store/react"; import { clsx } from "clsx"; import { AlertCircle, CheckCircle, Download, RefreshCw, X } from "lucide-react"; import { Spinner } from "@hypr/ui/components/ui/spinner"; import { cn } from "@hypr/utils"; import { MenuItem } from "../shared"; -import { updateStore } from "./store"; -import { checkForUpdate } from "./task"; +import { useOTA } from "./task"; export function UpdateChecker() { - const snapshot = useSelector(updateStore, (state) => state.context); - const { state, update, error, downloadProgress } = snapshot; - - const handleCheckForUpdate = () => checkForUpdate(); - - const handleStartDownload = async () => { - if (!update) { - return; - } - - updateStore.trigger.startDownload(); - - try { - await update.download((event) => { - if (event.event === "Started") { - updateStore.trigger.downloadProgress({ - chunkLength: 0, - contentLength: event.data.contentLength, - }); - } else if (event.event === "Progress") { - updateStore.trigger.downloadProgress({ - chunkLength: event.data.chunkLength, - }); - } else if (event.event === "Finished") { - updateStore.trigger.downloadFinished(); - } - }); - } catch (err) { - const errorMessage = err instanceof Error ? err.message : "Download failed"; - updateStore.trigger.checkError({ error: errorMessage }); - } - }; - - const handleCancelDownload = async () => { - if (update) { - try { - await update.close(); - } catch (err) { - console.error("Failed to close update:", err); - } - } - updateStore.trigger.cancelDownload(); - }; - - const handleInstall = async () => { - if (!update) { - return; - } - - updateStore.trigger.setInstalling(); - - try { - if (process.env.NODE_ENV !== "development") { - await update.install(); - await relaunch(); - } - } catch (err) { - const errorMessage = err instanceof Error ? err.message : "Installation failed"; - updateStore.trigger.checkError({ error: errorMessage }); - } - }; - - const handleRetry = () => { - handleCheckForUpdate(); - }; + const { + state, + update, + error, + downloadProgress, + handleCheckForUpdate, + handleStartDownload, + handleCancelDownload, + handleInstall, + } = useOTA(); if (state === "checking") { return ( - Checking for updates... + + Checking for updates... + ); } @@ -89,7 +33,9 @@ export function UpdateChecker() { return ( - You're up to date + + You're up to date + ); } @@ -103,7 +49,7 @@ export function UpdateChecker() { -
+
@@ -114,7 +112,7 @@ export function UpdateChecker() { } + badge={
} onClick={handleInstall} /> ); @@ -122,12 +120,11 @@ export function UpdateChecker() { if (state === "installing") { return ( - - - - Installing... - - + {}} + /> ); } diff --git a/apps/desktop/src/mocks/updater.ts b/apps/desktop/src/mocks/updater.ts index 400001ab99..c9de989f55 100644 --- a/apps/desktop/src/mocks/updater.ts +++ b/apps/desktop/src/mocks/updater.ts @@ -16,7 +16,7 @@ interface Update { close: () => Promise; } -export const check = check_1; +export const check = check_2; export async function check_1(): Promise { await new Promise((resolve) => setTimeout(resolve, 1000)); From 805e4e252f2a90a0d3f239fcee9313a76d5eb935 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Tue, 21 Oct 2025 09:06:17 +0900 Subject: [PATCH 7/7] chores --- .../src/components/main/sidebar/profile/ota/store.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/components/main/sidebar/profile/ota/store.ts b/apps/desktop/src/components/main/sidebar/profile/ota/store.ts index b70ebfa0e4..7ea06f6b4c 100644 --- a/apps/desktop/src/components/main/sidebar/profile/ota/store.ts +++ b/apps/desktop/src/components/main/sidebar/profile/ota/store.ts @@ -65,10 +65,13 @@ export const updateStore = createStore({ downloaded: context.downloadProgress.downloaded + event.chunkLength, total: event.contentLength ?? context.downloadProgress.total, percentage: event.contentLength || context.downloadProgress.total - ? Math.round( - ((context.downloadProgress.downloaded + event.chunkLength) - / (event.contentLength ?? context.downloadProgress.total ?? 1)) - * 100, + ? Math.min( + 100, + Math.round( + ((context.downloadProgress.downloaded + event.chunkLength) + / (event.contentLength ?? context.downloadProgress.total ?? 1)) + * 100, + ), ) : 0, },