Feat/notes init (#40)
* feat: notes init * Feat/notes page (#39) * wip: notes page ui added * notes page basic ui done * wip: page UI * minor cleanup * todo comments added for easy ref for backend wiring --------- Co-authored-by: amadeus-x1 <45001978+amadeus-x1@users.noreply.github.com> * feat: wire up notes --------- Co-authored-by: amadeus-x1 <45001978+amadeus-x1@users.noreply.github.com>
This commit is contained in:
parent
674092f1b7
commit
a128ec7972
39 changed files with 2980 additions and 10 deletions
193
apps/desktop/src/services/notes-service.ts
Normal file
193
apps/desktop/src/services/notes-service.ts
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
import * as cron from "node-cron";
|
||||
import {
|
||||
createNote,
|
||||
getNotes,
|
||||
getNoteById,
|
||||
updateNote,
|
||||
deleteNote,
|
||||
saveYjsUpdate as saveYjsUpdateToDB,
|
||||
loadYjsUpdates as loadYjsUpdatesFromDB,
|
||||
getUniqueNoteIds,
|
||||
getYjsUpdatesByNoteId,
|
||||
replaceYjsUpdates,
|
||||
} from "../db/notes";
|
||||
import * as Y from "yjs";
|
||||
import { logger } from "../main/logger";
|
||||
|
||||
export interface NoteCreateOptions {
|
||||
title: string;
|
||||
initialContent?: string;
|
||||
icon?: string | null;
|
||||
}
|
||||
|
||||
export interface NoteUpdateOptions {
|
||||
title?: string;
|
||||
transcriptionId?: number | null;
|
||||
icon?: string | null;
|
||||
}
|
||||
|
||||
class NotesService {
|
||||
private static instance: NotesService;
|
||||
private compactionTask: cron.ScheduledTask | null = null;
|
||||
|
||||
private constructor() {
|
||||
// Set up cron job for daily compaction
|
||||
this.setupCompactionCron();
|
||||
}
|
||||
|
||||
public static getInstance(): NotesService {
|
||||
if (!NotesService.instance) {
|
||||
NotesService.instance = new NotesService();
|
||||
}
|
||||
return NotesService.instance;
|
||||
}
|
||||
|
||||
async createNote(options: NoteCreateOptions) {
|
||||
// Create the note in the database
|
||||
const note = await createNote({
|
||||
title: options.title,
|
||||
icon: options.icon,
|
||||
});
|
||||
|
||||
// Initialize yjs document with initial content if provided
|
||||
if (options.initialContent) {
|
||||
const ydoc = new Y.Doc();
|
||||
const text = ydoc.getText("content");
|
||||
text.insert(0, options.initialContent);
|
||||
|
||||
// Save initial content as a YJS update
|
||||
const initialUpdate = Y.encodeStateAsUpdate(ydoc);
|
||||
await saveYjsUpdateToDB(note.id, initialUpdate);
|
||||
}
|
||||
|
||||
return note;
|
||||
}
|
||||
|
||||
async getNote(id: number) {
|
||||
const note = await getNoteById(id);
|
||||
return note;
|
||||
}
|
||||
|
||||
async listNotes(options?: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
sortBy?: "title" | "updatedAt" | "createdAt";
|
||||
sortOrder?: "asc" | "desc";
|
||||
search?: string;
|
||||
transcriptionId?: number | null;
|
||||
}) {
|
||||
return await getNotes(options);
|
||||
}
|
||||
|
||||
async updateNote(id: number, options: NoteUpdateOptions) {
|
||||
return await updateNote(id, options);
|
||||
}
|
||||
|
||||
async deleteNote(id: number) {
|
||||
const note = await getNoteById(id);
|
||||
if (!note) return null;
|
||||
|
||||
return await deleteNote(id);
|
||||
}
|
||||
|
||||
// Save yjs update to database
|
||||
async saveYjsUpdate(noteId: number, update: Uint8Array) {
|
||||
await saveYjsUpdateToDB(noteId, update);
|
||||
}
|
||||
|
||||
// Load all yjs updates for a note
|
||||
async loadYjsUpdates(noteId: number): Promise<Uint8Array[]> {
|
||||
return await loadYjsUpdatesFromDB(noteId);
|
||||
}
|
||||
|
||||
// Compact all note documents
|
||||
async compactAllNotes(): Promise<void> {
|
||||
const startTime = Date.now();
|
||||
logger.main.info("Starting yjs compaction for all notes");
|
||||
|
||||
try {
|
||||
// Get all unique note IDs that have updates
|
||||
const noteIds = await getUniqueNoteIds();
|
||||
logger.main.info(`Found ${noteIds.length} notes to compact`);
|
||||
|
||||
let totalUpdatesBefore = 0;
|
||||
let totalUpdatesAfter = 0;
|
||||
|
||||
for (const noteId of noteIds) {
|
||||
const compactResult = await this.compactNote(noteId);
|
||||
totalUpdatesBefore += compactResult.updatesBefore;
|
||||
totalUpdatesAfter += compactResult.updatesAfter;
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
logger.main.info(`Compaction completed in ${duration}ms`, {
|
||||
notesCompacted: noteIds.length,
|
||||
totalUpdatesBefore,
|
||||
totalUpdatesAfter,
|
||||
updatesReduced: totalUpdatesBefore - totalUpdatesAfter,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.main.error("Failed to compact notes:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Compact a specific note
|
||||
async compactNote(
|
||||
noteId: number,
|
||||
): Promise<{ updatesBefore: number; updatesAfter: number }> {
|
||||
// Get all updates for this note
|
||||
const updates = await getYjsUpdatesByNoteId(noteId);
|
||||
const updatesBefore = updates.length;
|
||||
|
||||
if (updatesBefore <= 1) {
|
||||
// No need to compact if there's only one update or none
|
||||
return { updatesBefore, updatesAfter: updatesBefore };
|
||||
}
|
||||
|
||||
// Create a new Y.Doc and apply all updates
|
||||
const ydoc = new Y.Doc();
|
||||
for (const update of updates) {
|
||||
const updateArray = new Uint8Array(update.updateData as Buffer);
|
||||
Y.applyUpdate(ydoc, updateArray);
|
||||
}
|
||||
|
||||
// Encode the current state as a single update
|
||||
const stateUpdate = Y.encodeStateAsUpdate(ydoc);
|
||||
|
||||
// Replace all updates with the compacted one
|
||||
await replaceYjsUpdates(noteId, stateUpdate);
|
||||
|
||||
logger.main.debug(
|
||||
`Compacted note ${noteId}: ${updatesBefore} updates -> 1 update`,
|
||||
);
|
||||
|
||||
return { updatesBefore, updatesAfter: 1 };
|
||||
}
|
||||
|
||||
// Set up cron job for scheduled compaction
|
||||
private setupCompactionCron() {
|
||||
// Schedule for daily at 2 AM in production, every 5 minutes in development
|
||||
const schedule =
|
||||
process.env.NODE_ENV === "development" ? "*/5 * * * *" : "0 2 * * *";
|
||||
|
||||
this.compactionTask = cron.schedule(schedule, async () => {
|
||||
logger.main.info(
|
||||
`Running scheduled yjs compaction (schedule: ${schedule})`,
|
||||
);
|
||||
await this.compactAllNotes();
|
||||
});
|
||||
|
||||
logger.main.info(`Yjs compaction cron job scheduled: ${schedule}`);
|
||||
}
|
||||
|
||||
// Clean up resources
|
||||
cleanup() {
|
||||
// Stop the cron job
|
||||
if (this.compactionTask) {
|
||||
this.compactionTask.stop();
|
||||
this.compactionTask = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default NotesService;
|
||||
|
|
@ -413,14 +413,13 @@ export class TranscriptionService {
|
|||
? completionTime - session.recordingStartedAt
|
||||
: undefined;
|
||||
|
||||
const selectedModel =
|
||||
this.modelManagerService.getSelectedModel() || "unknown";
|
||||
const selectedModel = await this.modelManagerService.getSelectedModel();
|
||||
const audioDurationSeconds =
|
||||
session.context.sharedData.audioMetadata?.duration;
|
||||
|
||||
this.telemetryService.trackTranscriptionCompleted({
|
||||
session_id: sessionId,
|
||||
model_id: selectedModel,
|
||||
model_id: selectedModel!,
|
||||
model_preloaded: this.modelWasPreloaded,
|
||||
total_duration_ms: totalDuration || 0,
|
||||
recording_duration_ms: recordingDuration,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue