Skip to content

Commit

Permalink
feat: add meetings section with line-by-line streaming (#310)
Browse files Browse the repository at this point in the history
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: ben <ben@prologe.io>
  • Loading branch information
1 parent 48419c9 commit 963fe77
Show file tree
Hide file tree
Showing 4 changed files with 6,880 additions and 8,280 deletions.
57 changes: 57 additions & 0 deletions packages/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,63 @@ 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 formattedContent = "";
let lastLineCount = 0;

const updateCallback = async (chunk: string) => {
if (chunkMode === 'line') {
// Split chunk into lines and only append new lines
const lines = chunk.split("\n");
const newLines = lines.slice(lastLineCount);
if (newLines.length > 0) {
formattedContent = lines.join("\n");
lastLineCount = lines.length;
await this.app.vault.modify(file, formattedContent);
}
} else {
// For partial mode, just append the new chunk
formattedContent = chunk;
await this.app.vault.modify(file, formattedContent);
}
};

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
171 changes: 171 additions & 0 deletions packages/plugin/views/assistant/organizer/meetings/meetings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
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";

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 [isScreenpipeAvailable, setIsScreenpipeAvailable] = React.useState<boolean>(false);

// Check Screenpipe availability on component mount
React.useEffect(() => {
checkScreenpipeHealth();
}, []);

const checkScreenpipeHealth = async () => {
try {
const response = await fetch('http://localhost:3030/health');
if (response.ok) {
setIsScreenpipeAvailable(true);
setError(null);
} else {
throw new Error('Screenpipe service is not responding');
}
} catch (err) {
setIsScreenpipeAvailable(false);
setError(
'Screenpipe is not running. Please install it from https://screenpi.pe and ensure it is running locally.'
);
}
};

const enhanceMeetingNotes = async () => {
if (!file) return;
if (!isScreenpipeAvailable) {
new Notice('Please install and start Screenpipe first');
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();

// Fetch transcripts from Screenpipe
let transcriptions = '';
let hasContent = false;

const queryUrl = `http://localhost:3030/search?content_type=audio&start_time=${startTime}&end_time=${endTime}`;
const response = await fetch(queryUrl);

if (!response.ok) {
throw new Error(`HTTP error: status ${response.status}`);
}

const data = await response.json();
// Combine all transcriptions from the results
transcriptions = data.data
.map((item: any) => item.content.transcription)
.join("\n");

hasContent = transcriptions.trim().length > 0;

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>
{!isScreenpipeAvailable && (
<div className="mt-2">
<p className="text-sm">
To use this feature, you need to:
</p>
<ol className="list-decimal ml-4 text-sm">
<li>Visit <a href="https://screenpi.pe" className="text-[--text-accent] hover:underline">screenpi.pe</a></li>
<li>Download and install Screenpipe</li>
<li>Start the Screenpipe application</li>
</ol>
<button onClick={checkScreenpipeHealth} className="mt-2 mod-cta">
Check Again
</button>
</div>
)}
{isScreenpipeAvailable && (
<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"
disabled={!isScreenpipeAvailable}
>
Enhance Meeting Notes
</button>
{!isScreenpipeAvailable && (
<p className="text-sm mt-2 text-[--text-muted]">
Please install and start Screenpipe to use this feature
</p>
)}
</>
)}
</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
Loading

0 comments on commit 963fe77

Please sign in to comment.