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:
Haritabh 2025-09-10 16:13:33 +05:30 committed by GitHub
parent 674092f1b7
commit a128ec7972
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 2980 additions and 10 deletions

View 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;

View file

@ -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,