Skip to content

Commit

Permalink
feat: add meetings section with line-by-line streaming support
Browse files Browse the repository at this point in the history
- 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 <ben@prologe.io>
  • Loading branch information
devin-ai-integration[bot] and benjaminshafii committed Jan 18, 2025
1 parent 48419c9 commit c47089d
Show file tree
Hide file tree
Showing 3 changed files with 196 additions and 0 deletions.
54 changes: 54 additions & 0 deletions packages/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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<void> {
const fileName = `${title}.md`;
const filePath = `${this.settings.pathToWatch}/${fileName}`;
Expand Down
130 changes: 130 additions & 0 deletions packages/plugin/views/assistant/organizer/meetings/meetings.tsx
Original file line number Diff line number Diff line change
@@ -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<MeetingsProps> = ({
plugin,
file,
content,
refreshKey,
}) => {
const [minutes, setMinutes] = React.useState(5);
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState<string | null>(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 (
<div className="bg-[--background-primary-alt] text-[--text-normal] p-4 rounded-lg shadow-md">
{loading ? (
<SkeletonLoader count={1} rows={4} width="100%" />
) : error ? (
<div className="error-container">
<p>Error: {error}</p>
<button onClick={() => setError(null)} className="retry-button">
Retry
</button>
</div>
) : (
<>
<div className="flex items-center gap-2 mb-2">
<label>Last X minutes:</label>
<input
type="number"
value={minutes}
onChange={(e) => setMinutes(Number(e.target.value))}
min={1}
className="input-minutes"
/>
</div>
<button onClick={enhanceMeetingNotes} className="mod-cta">
Enhance Meeting Notes
</button>
</>
)}
</div>
);
};
12 changes: 12 additions & 0 deletions packages/plugin/views/assistant/organizer/organizer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -250,6 +251,17 @@ export const AssistantView: React.FC<AssistantViewProps> = ({
)}
</>
)}

<SectionHeader text="Meetings" icon="📅 " />
{renderSection(
<Meetings
plugin={plugin}
file={activeFile}
content={noteContent}
refreshKey={refreshKey}
/>,
"Error loading meetings section"
)}
</div>
);
};
Expand Down

0 comments on commit c47089d

Please sign in to comment.