-
Notifications
You must be signed in to change notification settings - Fork 536
feat(desktop): AI-powered note enhancement with auto-trigger and batch improvements #3903
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1f79bab
d23f20b
371b4c3
9aff6dc
e39ad25
1dd8eb6
39e1998
a49f5fb
cb8c29b
5f096a4
76b9ae8
1edb2fb
cc1267f
2e242bc
89c85c4
cab718f
0a0816f
903b58f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ import { useCallback, useState } from "react"; | |
| import { commands as analyticsCommands } from "@hypr/plugin-analytics"; | ||
| import { commands as fsSyncCommands } from "@hypr/plugin-fs-sync"; | ||
| import { commands as listener2Commands } from "@hypr/plugin-listener2"; | ||
| import { md2json } from "@hypr/tiptap/shared"; | ||
| import { Button } from "@hypr/ui/components/ui/button"; | ||
| import { | ||
| Popover, | ||
|
|
@@ -20,10 +21,15 @@ import { | |
| TooltipTrigger, | ||
| } from "@hypr/ui/components/ui/tooltip"; | ||
|
|
||
| import { useAITask } from "../../../../../contexts/ai-task"; | ||
| import { useListener } from "../../../../../contexts/listener"; | ||
| import { fromResult } from "../../../../../effect"; | ||
| import { getEligibility } from "../../../../../hooks/autoEnhance/eligibility"; | ||
| import { useCreateEnhancedNote } from "../../../../../hooks/useEnhancedNotes"; | ||
| import { useLanguageModel } from "../../../../../hooks/useLLMConnection"; | ||
| import { useRunBatch } from "../../../../../hooks/useRunBatch"; | ||
| import * as main from "../../../../../store/tinybase/store/main"; | ||
| import { createTaskId } from "../../../../../store/zustand/ai-task/task-configs"; | ||
| import { type Tab, useTabs } from "../../../../../store/zustand/tabs"; | ||
| import { ChannelProfile } from "../../../../../utils/segment"; | ||
| import { ActionableTooltipContent } from "./shared"; | ||
|
|
@@ -50,9 +56,13 @@ export function OptionsMenu({ | |
| const handleBatchFailed = useListener((state) => state.handleBatchFailed); | ||
| const clearBatchSession = useListener((state) => state.clearBatchSession); | ||
|
|
||
| const store = main.UI.useStore(main.STORE_ID); | ||
| const store = main.UI.useStore(main.STORE_ID) as main.Store | undefined; | ||
| const indexes = main.UI.useIndexes(main.STORE_ID); | ||
| const { user_id } = main.UI.useValues(main.STORE_ID); | ||
| const updateSessionTabState = useTabs((state) => state.updateSessionTabState); | ||
| const createEnhancedNote = useCreateEnhancedNote(); | ||
| const model = useLanguageModel(); | ||
| const generate = useAITask((state) => state.generate); | ||
| const sessionTab = useTabs((state) => { | ||
| const found = state.tabs.find( | ||
| (tab): tab is Extract<Tab, { type: "sessions" }> => | ||
|
|
@@ -61,6 +71,79 @@ export function OptionsMenu({ | |
| return found ?? null; | ||
| }); | ||
|
|
||
| const triggerEnhance = useCallback(() => { | ||
| if (!store || !indexes || !model) return; | ||
|
|
||
| const transcriptIds = indexes.getSliceRowIds( | ||
| main.INDEXES.transcriptBySession, | ||
| sessionId, | ||
| ); | ||
| const hasTranscript = transcriptIds.length > 0; | ||
| const eligibility = getEligibility(hasTranscript, transcriptIds, store); | ||
|
|
||
| if (!eligibility.eligible) return; | ||
|
|
||
| const enhancedNoteId = createEnhancedNote(sessionId); | ||
| if (!enhancedNoteId) return; | ||
|
|
||
| if (sessionTab) { | ||
| updateSessionTabState(sessionTab, { | ||
| ...sessionTab.state, | ||
| view: { type: "enhanced", id: enhancedNoteId }, | ||
| }); | ||
| } | ||
|
|
||
| const enhanceTaskId = createTaskId(enhancedNoteId, "enhance"); | ||
| void generate(enhanceTaskId, { | ||
|
Comment on lines
+96
to
+97
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 triggerEnhance missing task-already-running guard before calling generate The new const existingTask = getAITaskState(enhanceTaskId);
if (existingTask?.status === "generating" || existingTask?.status === "success") {
return { type: "started", noteId: enhancedNoteId };
}Detailed trigger scenario and impactThis can be triggered in two ways:
Prompt for agentsWas this helpful? React with 👍 or 👎 to provide feedback. |
||
| model, | ||
| taskType: "enhance", | ||
| args: { sessionId, enhancedNoteId }, | ||
| onComplete: (text) => { | ||
| if (!text || !store) return; | ||
| try { | ||
| const jsonContent = md2json(text); | ||
| store.setPartialRow("enhanced_notes", enhancedNoteId, { | ||
| content: JSON.stringify(jsonContent), | ||
| }); | ||
|
|
||
| const currentTitle = store.getCell("sessions", sessionId, "title"); | ||
| const trimmedTitle = | ||
| typeof currentTitle === "string" ? currentTitle.trim() : ""; | ||
|
|
||
| if (!trimmedTitle && model) { | ||
| const titleTaskId = createTaskId(sessionId, "title"); | ||
| void generate(titleTaskId, { | ||
| model, | ||
| taskType: "title", | ||
| args: { sessionId }, | ||
| onComplete: (titleText) => { | ||
| if (titleText && store) { | ||
| const trimmed = titleText.trim(); | ||
| if (trimmed && trimmed !== "<EMPTY>") { | ||
| store.setPartialRow("sessions", sessionId, { | ||
| title: trimmed, | ||
| }); | ||
| } | ||
| } | ||
| }, | ||
| }); | ||
| } | ||
| } catch (error) { | ||
| console.error("Failed to convert markdown to JSON:", error); | ||
| } | ||
| }, | ||
| }); | ||
| }, [ | ||
| store, | ||
| indexes, | ||
| model, | ||
| sessionId, | ||
| createEnhancedNote, | ||
| sessionTab, | ||
| updateSessionTabState, | ||
| generate, | ||
| ]); | ||
|
|
||
| const handleFilePath = useCallback( | ||
| (selection: FileSelection, kind: "audio" | "transcript") => { | ||
| if (!selection) { | ||
|
|
@@ -126,6 +209,8 @@ export function OptionsMenu({ | |
| file_type: "transcript", | ||
| token_count: subtitle.tokens.length, | ||
| }); | ||
|
|
||
| triggerEnhance(); | ||
| }), | ||
| ), | ||
| ); | ||
|
|
@@ -171,6 +256,7 @@ export function OptionsMenu({ | |
| ), | ||
| Effect.tap(() => Effect.sync(() => clearBatchSession(sessionId))), | ||
| Effect.flatMap(() => Effect.promise(() => runBatch(path))), | ||
| Effect.tap(() => Effect.sync(() => triggerEnhance())), | ||
| Effect.catchAll((error: unknown) => | ||
| Effect.sync(() => { | ||
| const msg = error instanceof Error ? error.message : String(error); | ||
|
|
@@ -188,6 +274,7 @@ export function OptionsMenu({ | |
| sessionId, | ||
| sessionTab, | ||
| store, | ||
| triggerEnhance, | ||
| updateSessionTabState, | ||
| user_id, | ||
| ], | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 handleBatchStarted never called with 'importing' phase, making 'Importing audio...' label dead code
In
options-menu.tsx:239,handleBatchStarted(sessionId)is called without aphaseparameter before the audio import step (fsSyncCommands.audioImport). Inbatch.ts:57, the phase defaults to"transcribing"when not provided.Detailed Explanation
The PR adds a new
BatchPhasetype ("importing" | "transcribing") and updatesprogress.tsx:19-22to conditionally show "Importing audio..." only whenphase === "importing":Previously (old code at
progress.tsx:18-19), the label was always "Importing audio..." when percentage was 0. Now it requiresphase === "importing", but no caller ever passes"importing"as the phase:options-menu.tsx:239:handleBatchStarted(sessionId)→ defaults to"transcribing"general.ts:480:get().handleBatchStarted(sessionId)→ defaults to"transcribing"general.ts:505:get().handleBatchStarted(payload.session_id)→ defaults to"transcribing"Impact: The "Importing audio..." message is never displayed. Users always see "Processing..." during audio import, which is a regression from the previous behavior.
(Refers to line 239)
Was this helpful? React with 👍 or 👎 to provide feedback.