Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion caido.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,11 @@ export default defineConfig({
plugins: [vue()],
build: {
rollupOptions: {
external: ["@caido/frontend-sdk"],
external: ["@caido/frontend-sdk", "vue"],
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had some problems with the state of components not being updated and after changing this, it started working but this was suggested by LLM so I'm not entirely sure what it does.

},
},
resolve: {
dedupe: ["vue"],
alias: [
{
find: "@",
Expand Down
4 changes: 2 additions & 2 deletions packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@caido/sdk-backend": "^0.48.0",
"@caido/sdk-frontend": "0.48.0",
"@caido/sdk-backend": "^0.55.2",
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I needed to update this in order to get the ability to create a Settings page to config the TTS.

"@caido/sdk-frontend": "0.55.2",
"shared": "workspace:*"
},
"dependencies": {
Expand Down
4 changes: 2 additions & 2 deletions packages/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@
"vue": "3.5.13"
},
"devDependencies": {
"@caido/sdk-backend": "^0.48.0",
"@caido/sdk-frontend": "^0.48.0",
"@caido/sdk-backend": "^0.55.2",
"@caido/sdk-frontend": "0.55.2",
"backend": "workspace:*",
"shared": "workspace:*",
"vue-tsc": "2.2.10"
Expand Down
50 changes: 50 additions & 0 deletions packages/frontend/src/actions/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,56 @@ export const sendSelectedTextToNote = async (sdk: FrontendSDK) => {
}
};

/**
* Sends raw text to the currently open note
*/
export const sendTextToNote = async (sdk: FrontendSDK, text: string) => {
const notesStore = useNotesStore();
const textToSend = text;
if (!textToSend) {
sdk.window.showToast("No text to send", { variant: "warning" });
return;
}

if (!notesStore.currentNotePath) {
sdk.window.showToast(
"No note is currently open. Please open a note first.",
{ variant: "warning" },
);
return;
}

try {
await notesStore.loadNote(notesStore.currentNotePath);

if (notesStore.currentNote) {
const paragraph = createTextParagraph(textToSend);
const updatedContent = addParagraphToContent(
notesStore.currentNote.content,
paragraph,
);

await notesStore.updateNoteContent(
notesStore.currentNotePath,
updatedContent,
);

sdk.window.showToast(
`Text added to note ${notesStore.currentNotePath}`,
{
variant: "success",
},
);

await notesStore.refreshTree();
}
} catch (error) {
sdk.window.showToast(`Error adding text to note: ${error}`, {
variant: "error",
});
}
};

/**
* Sends the replay session to the currently open note
*/
Expand Down
3 changes: 3 additions & 0 deletions packages/frontend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from "@/actions/actions";
import { emitter } from "@/utils/eventBus";
import { convertMarkdownToTipTap } from "@/utils/markdownToJSON";
import { registerVoiceNotes } from "./register-tts";

export const init = (sdk: FrontendSDK) => {
const pinia = createPinia();
Expand Down Expand Up @@ -95,6 +96,8 @@ export const init = (sdk: FrontendSDK) => {
icon: "fas fa-file-alt",
});

registerVoiceNotes(sdk);

emitter.on("confirmMigration", (notes) => {
migrateNotes(sdk, notes);
});
Expand Down
124 changes: 124 additions & 0 deletions packages/frontend/src/register-tts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { sendTextToNote } from "./actions/actions";
import type { FrontendSDK } from "./types";
import Settings from "./views/Settings.vue";

export const registerVoiceNotes = (sdk: FrontendSDK) => {
const COMMAND_KEY = "notesplusplus:tts-toggle-recording";
const MAX_RECORDING_TIME = 60000; // 1 minute

sdk.settings.addToSlot("plugins-section", {
type: "Custom",
name: "TTS",
definition: { component: Settings, props: { sdk } },
});

let isRecording = false;
let mediaRecorder: any = null;
let audioChunks: any[] = [];
let autoStopTimer: ReturnType<typeof setTimeout> | null = null;

const stopRecordingAction = () => {
if (autoStopTimer) {
clearTimeout(autoStopTimer);
autoStopTimer = null;
}

if (mediaRecorder && mediaRecorder.state !== "inactive") {
mediaRecorder.stop();
}
isRecording = false;
};

sdk.commands.register(COMMAND_KEY, {
name: "Toggle Voice Recording",
run: async () => {
if (isRecording) {
stopRecordingAction();
sdk.window.showToast("Recording stopped. Transcribing...", {
variant: "info",
});
return;
}

try {
const stream = await window.navigator.mediaDevices.getUserMedia({
audio: true,
});
audioChunks = [];
mediaRecorder = new MediaRecorder(stream);

mediaRecorder.ondataavailable = (event: any) => {
if (event.data.size > 0) audioChunks.push(event.data);
};

mediaRecorder.onstop = async () => {
// STOP HARDWARE
if (mediaRecorder.stream) {
mediaRecorder.stream
.getTracks()
.forEach((track: { stop: () => void }) => track.stop());
}

const audioBlob = new Blob(audioChunks, { type: "audio/webm" });

const storage = sdk.storage.get() as Record<string, string>;
const apiKey = storage?.["tts-plugin-api-key"];

if (!apiKey) {
sdk.window.showToast("API Key missing! Check settings.", {
variant: "error",
});
return;
}

const formData = new FormData();
formData.append("file", audioBlob, "recording.webm");
formData.append("model", "whisper-1");

try {
const response = await fetch(
"https://api.openai.com/v1/audio/transcriptions",
{
method: "POST",
headers: { Authorization: `Bearer ${apiKey}` },
body: formData,
},
);

const data = await response.json();
if (data.text) {
sendTextToNote(sdk, data.text);
} else {
throw new Error(data.error?.message || "Unknown error");
}
} catch (err: any) {
console.error("OpenAI Whisper Error:", err);
sdk.window.showToast(`Transcription failed: ${err.message}`, {
variant: "error",
});
}
};

mediaRecorder.start();
isRecording = true;
sdk.window.showToast("Recording started...", { variant: "info" });

// AUTO-STOP AFTER 1 MINUTE
autoStopTimer = setTimeout(() => {
if (isRecording) {
sdk.window.showToast("Time limit reached (1 min).", {
variant: "info",
});
stopRecordingAction();
}
}, MAX_RECORDING_TIME);
} catch (err) {
console.error("Microphone Access Error:", err);
sdk.window.showToast("Microphone access denied.", { variant: "error" });
}
},
});

sdk.commandPalette.register(COMMAND_KEY);
sdk.shortcuts.register(COMMAND_KEY, ["alt", "shift", "V"]);
};
Loading