From c47089d17ac366cab767f831f35d47da6914f5e4 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 18 Jan 2025 06:36:10 +0000 Subject: [PATCH] feat: add meetings section with line-by-line streaming support - Add Meetings component for enhancing notes with audio transcripts - Implement streamFormatInCurrentNoteLineByLine for granular updates - Add secure API communication with proper error handling Co-Authored-By: ben --- packages/plugin/index.ts | 54 ++++++++ .../assistant/organizer/meetings/meetings.tsx | 130 ++++++++++++++++++ .../views/assistant/organizer/organizer.tsx | 12 ++ 3 files changed, 196 insertions(+) create mode 100644 packages/plugin/views/assistant/organizer/meetings/meetings.tsx diff --git a/packages/plugin/index.ts b/packages/plugin/index.ts index cd8f9fbf..5c63e956 100644 --- a/packages/plugin/index.ts +++ b/packages/plugin/index.ts @@ -303,6 +303,60 @@ export default class FileOrganizer extends Plugin { } } + async streamFormatInCurrentNoteLineByLine({ + file, + formattingInstruction, + content, + chunkMode = 'line', + }: { + file: TFile; + formattingInstruction: string; + content: string; + chunkMode?: 'line' | 'partial'; + }): Promise { + try { + new Notice("Formatting content line by line...", 3000); + + // Backup the file before formatting + const backupFile = await this.backupTheFileAndAddReferenceToCurrentFile(file); + + // Prepare streaming + let partialContent = ""; + + const updateCallback = async (chunk: string) => { + if (chunkMode === 'line') { + // Option 1: break chunk into lines + const lines = chunk.split("\n"); + for (const line of lines) { + partialContent += line + "\n"; + await this.app.vault.modify(file, partialContent); + } + } else { + // Option 2: use partial increments directly + partialContent += chunk; + await this.app.vault.modify(file, partialContent); + } + }; + + await this.formatStream( + content, + formattingInstruction, + this.getServerUrl(), + this.getApiKey(), + updateCallback + ); + + // Insert reference to backup + await this.appendBackupLinkToCurrentFile(file, backupFile); + new Notice("Line-by-line update done!", 3000); + + } catch (error) { + logger.error("Error formatting content line by line:", error); + new Notice("An error occurred while formatting the content.", 6000); + throw error; // Re-throw to allow component to handle error state + } + } + async createFileInInbox(title: string, content: string): Promise { const fileName = `${title}.md`; const filePath = `${this.settings.pathToWatch}/${fileName}`; diff --git a/packages/plugin/views/assistant/organizer/meetings/meetings.tsx b/packages/plugin/views/assistant/organizer/meetings/meetings.tsx new file mode 100644 index 00000000..1548646b --- /dev/null +++ b/packages/plugin/views/assistant/organizer/meetings/meetings.tsx @@ -0,0 +1,130 @@ +import * as React from "react"; +import { Notice, TFile } from "obsidian"; +import FileOrganizer from "../../../../index"; +import { SkeletonLoader } from "../components/skeleton-loader"; +import { logger } from "../../../../services/logger"; +import { makeApiRequest } from "../../../../apiUtils"; + +interface MeetingsProps { + plugin: FileOrganizer; + file: TFile | null; + content: string; + refreshKey: number; +} + +export const Meetings: React.FC = ({ + plugin, + file, + content, + refreshKey, +}) => { + const [minutes, setMinutes] = React.useState(5); + const [loading, setLoading] = React.useState(false); + const [error, setError] = React.useState(null); + + const enhanceMeetingNotes = async () => { + if (!file) return; + + setLoading(true); + setError(null); + + try { + // Calculate the start time based on minutes + const endTime = new Date().toISOString(); + const startTime = new Date(Date.now() - minutes * 60_000).toISOString(); + + // Use plugin's secure API methods for data fetching + let transcriptions = ''; + let hasContent = false; + + const updateCallback = (chunk: string) => { + try { + const data = JSON.parse(chunk); + if (data.transcription) { + transcriptions += data.transcription + '\n'; + hasContent = true; + } + } catch (e) { + // Handle non-JSON chunks (e.g., partial data) + if (chunk.trim()) { + transcriptions += chunk; + hasContent = true; + } + } + }; + + // Use the plugin's secure API request method + // Use plugin's formatStream method for secure API communication + await plugin.formatStream( + JSON.stringify({ startTime, endTime }), + 'fetch_audio_transcripts', + plugin.getServerUrl(), + plugin.getApiKey(), + updateCallback + ); + + if (!hasContent) { + throw new Error("No recent audio data found in the last " + minutes + " minutes"); + } + + // Format the instruction for merging transcripts + const formattingInstruction = ` + You have the following recent transcript from the meeting: + ${transcriptions} + + Merge/improve the current meeting notes below with any details from the new transcript: + ${content} + + Provide an updated version of these meeting notes in a cohesive style. + `; + + // Stream the formatted content into the current note line by line + await plugin.streamFormatInCurrentNoteLineByLine({ + file, + formattingInstruction, + content, + chunkMode: 'line', // Use line-by-line mode for more granular updates + }); + + new Notice("Meeting notes successfully enhanced!"); + } catch (err) { + logger.error("Error enhancing meeting notes:", err); + const errorMessage = err instanceof Error ? err.message : "Unknown error"; + setError(errorMessage); + new Notice(`Failed to enhance meeting notes: ${errorMessage}`); + } finally { + setLoading(false); + } + }; + + return ( +
+ {loading ? ( + + ) : error ? ( +
+

Error: {error}

+ +
+ ) : ( + <> +
+ + setMinutes(Number(e.target.value))} + min={1} + className="input-minutes" + /> +
+ + + )} +
+ ); +}; diff --git a/packages/plugin/views/assistant/organizer/organizer.tsx b/packages/plugin/views/assistant/organizer/organizer.tsx index dc464337..c6752b1d 100644 --- a/packages/plugin/views/assistant/organizer/organizer.tsx +++ b/packages/plugin/views/assistant/organizer/organizer.tsx @@ -15,6 +15,7 @@ import { EmptyState } from "./components/empty-state"; import { logMessage } from "../../../someUtils"; import { LicenseValidator } from "./components/license-validator"; import { VALID_MEDIA_EXTENSIONS } from "../../../constants"; +import { Meetings } from "./meetings/meetings"; import { logger } from "../../../services/logger"; interface AssistantViewProps { @@ -250,6 +251,17 @@ export const AssistantView: React.FC = ({ )} )} + + + {renderSection( + , + "Error loading meetings section" + )} ); };