Added Internal Audio Recorder Modal

Ability to pause, resume, and reset recording
Saves as Wav file for iOS support
This commit is contained in:
Mossy426 2024-07-02 15:18:47 -04:00
parent fcb2096ef2
commit d9d8fbeeac
7 changed files with 685 additions and 97 deletions

View file

@ -0,0 +1,245 @@
import { Modal, setIcon } from 'obsidian';
export class SmartMemosAudioRecordModal extends Modal {
private mediaRecorder: MediaRecorder | null = null;
private chunks: BlobPart[] = [];
private resolve: (value: Blob | PromiseLike<Blob>) => void;
private reject: (reason?: any) => void;
private handleAudioRecording: (audioFile: Blob | null, transcribe: boolean) => void;
private isRecording: boolean = false;
private timer: HTMLElement;
private intervalId: number | null = null;
private startTime: number = 0;
private elapsedTime: number = 0; // To keep track of the elapsed time
private redDot: HTMLElement;
private isResetting: boolean = false; // Flag to track reset state
constructor(app: any, handleAudioRecording: (audioFile: Blob | null, transcribe: boolean) => void) {
super(app);
this.handleAudioRecording = handleAudioRecording;
}
onOpen() {
const { contentEl, modalEl } = this;
if (!contentEl || !modalEl) {
console.error('contentEl or modalEl is null');
return;
}
// Apply initial recording state
modalEl.addClass('smart-memo-recording');
// Header and timer container
const headerTimerContainer = contentEl.createDiv({ cls: 'smart-memo-header-timer-container' });
const header = headerTimerContainer.createEl('h2', { text: 'Recording...', cls: 'smart-memo-recording-header' });
this.timer = headerTimerContainer.createEl('div', { cls: 'smart-memo-timer', text: '00:00' });
// Add specific class to modal-content
contentEl.addClass('smart-memo-audio-record-modal-content');
// Red dot animation container
const redDotContainer = contentEl.createDiv({ cls: 'smart-memo-red-dot-container' });
this.redDot = redDotContainer.createDiv({ cls: 'smart-memo-red-dot' });
// Control buttons group
const controlGroupWrapper = contentEl.createDiv({ cls: 'smart-memo-control-group-wrapper' });
const controlGroup = controlGroupWrapper.createDiv({ cls: 'smart-memo-modal-button-group' });
const playPauseButton = controlGroup.createEl('button', { cls: 'smart-memo-modal-button smart-memo-flex' });
const stopButton = controlGroup.createEl('button', { cls: 'smart-memo-modal-button smart-memo-flex' });
setIcon(playPauseButton, 'pause'); // Initially set to pause
setIcon(stopButton, 'square'); // Stop icon
stopButton.addEventListener('click', async () => {
const audioFile = await this.stopRecording();
this.handleAudioRecording(audioFile, false);
});
playPauseButton.addEventListener('click', () => {
if (this.isRecording) {
this.pauseRecording();
setIcon(playPauseButton, 'circle');
header.textContent = 'Paused';
modalEl.addClass('smart-memo-paused');
modalEl.removeClass('smart-memo-recording');
} else {
this.resumeOrStartRecording();
setIcon(playPauseButton, 'pause');
header.textContent = 'Recording...';
modalEl.removeClass('smart-memo-paused');
modalEl.addClass('smart-memo-recording');
}
this.isRecording = !this.isRecording;
});
const transcribeButton = controlGroupWrapper.createEl('button', { cls: 'smart-memo-modal-button smart-memo-full-width-button smart-memo-transcribe-button' });
transcribeButton.addEventListener('click', async () => {
const audioFile = await this.stopRecording();
this.handleAudioRecording(audioFile, true);
});
setIcon(transcribeButton, 'file-text'); // Initially set to bulb
// Append text to the button
const buttonText = document.createTextNode(' Smart Transcribe');
transcribeButton.appendChild(buttonText);
// Add margin-right to the SVG element
const svgElement = transcribeButton.querySelector('svg');
if (svgElement) {
svgElement.style.marginRight = '10px';
}
const resetButton = contentEl.createEl('button', { cls: 'smart-memo-modal-button smart-memo-full-width-button smart-memo-reset-button', text: 'Restart' });
resetButton.addEventListener('click', () => {
this.hardReset();
setIcon(playPauseButton, 'circle');
header.textContent = 'Ready to Record';
this.isRecording = false;
modalEl.addClass('smart-memo-paused');
modalEl.removeClass('smart-memo-recording');
// Ensure red dot stops pulsing
this.redDot.classList.remove('smart-memo-pulse-animation');
});
// Start recording immediately upon opening the modal
this.startRecording();
this.isRecording = true;
this.redDot.classList.add('smart-memo-pulse-animation');
// Blur any focused element
const activeElement = document.activeElement as HTMLElement;
if (activeElement) {
activeElement.blur();
}
}
startRecording() {
navigator.mediaDevices.getUserMedia({ audio: true })
.then(stream => {
this.mediaRecorder = new MediaRecorder(stream);
this.setupMediaRecorder();
this.mediaRecorder.start(1000);
this.startTime = Date.now();
this.startTimer();
this.mediaRecorder.addEventListener('dataavailable', this.onDataAvailable.bind(this));
})
.catch(error => {
console.error('Error accessing microphone:', error);
this.reject(error);
});
}
setupMediaRecorder() {
if (this.mediaRecorder) {
this.mediaRecorder.addEventListener('stop', this.onStop.bind(this));
}
}
onDataAvailable(event: BlobEvent) {
if (this.isResetting) {
return;
}
this.chunks.push(event.data);
}
onStop() {
if (this.isResetting) {
this.isResetting = false; // Reset the flag after reset
return;
}
const blob = new Blob(this.chunks, { type: 'audio/wav' });
if (this.resolve) {
this.resolve(blob);
this.close();
} else {
console.error('Resolve function is not defined');
}
}
pauseRecording() {
if (this.mediaRecorder && this.mediaRecorder.state === 'recording') {
this.mediaRecorder.pause();
this.stopTimer();
this.elapsedTime += Date.now() - this.startTime; // Accumulate elapsed time
// Ensure red dot stops pulsing
this.redDot.classList.remove('smart-memo-pulse-animation');
}
}
resumeOrStartRecording() {
if (this.mediaRecorder && this.mediaRecorder.state === 'paused') {
this.mediaRecorder.resume();
} else {
this.startRecording();
}
this.startTime = Date.now(); // Reset start time to now
this.startTimer();
// Ensure red dot starts pulsing
this.redDot.classList.add('smart-memo-pulse-animation');
}
hardReset() {
if (this.mediaRecorder) {
this.mediaRecorder.stop();
this.mediaRecorder.onstop = null;
this.mediaRecorder.ondataavailable = null;
this.mediaRecorder = null;
}
this.isResetting = true; // Set the reset flag
this.chunks = []; // Clear the chunks
this.elapsedTime = 0; // Reset elapsed time
this.stopTimer();
this.timer.textContent = '00:00';
// Ensure red dot stops pulsing
this.redDot.classList.remove('smart-memo-pulse-animation'); // Ensure it's removed on reset
}
stopRecording() {
return new Promise<Blob | null>((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
if (this.mediaRecorder) {
this.mediaRecorder.addEventListener('stop', this.onStop.bind(this));
this.mediaRecorder.stop();
this.stopTimer();
} else {
resolve(null);
}
});
}
startTimer() {
this.stopTimer(); // Clear any existing timer
this.intervalId = window.setInterval(() => {
const elapsedTimeInSeconds = Math.floor(this.elapsedTime / 1000) + Math.floor((Date.now() - this.startTime) / 1000);
const minutes = Math.floor(elapsedTimeInSeconds / 60);
const seconds = elapsedTimeInSeconds % 60;
this.timer.textContent = `${this.padNumber(minutes)}:${this.padNumber(seconds)}`;
}, 1000);
}
stopTimer() {
if (this.intervalId !== null) {
window.clearInterval(this.intervalId);
this.intervalId = null;
}
}
padNumber(num: number): string {
return num.toString().padStart(2, '0');
}
open() {
super.open();
return new Promise<Blob>((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
}
}

43
Utils.ts Normal file
View file

@ -0,0 +1,43 @@
import { App, TFile, TFolder } from 'obsidian';
export async function saveFile(app: App, audioBlob: Blob, fileName: string, path: string): Promise<TFile> {
try {
const filePath = `${path}/${fileName}`;
await directoryExistsAtPath(app, path);
const arrayBuffer = await audioBlob.arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
const file = await app.vault.createBinary(filePath, uint8Array);
if (!file) {
throw new Error('File creation failed and returned null');
}
return file;
} catch (error) {
console.error('Error saving audio file:', error);
throw error;
}
}
async function directoryExistsAtPath(app: App, folderPath: string) {
const parts = folderPath.split('/');
let currentPath = '';
for (const part of parts) {
currentPath = currentPath ? `${currentPath}/${part}` : part;
try {
const folder = app.vault.getAbstractFileByPath(currentPath);
if (!folder) {
await app.vault.createFolder(currentPath);
} else if (folder instanceof TFolder) {
console.log(`Folder already exists: ${currentPath}`);
} else {
throw new Error(`${currentPath} is not a folder`);
}
} catch (error) {
console.error(`Error ensuring directory exists: ${error.message}`);
throw error;
}
}
}

210
main.ts
View file

@ -1,7 +1,10 @@
import { App, Editor, MarkdownView, normalizePath, Notice, Plugin, PluginSettingTab, requestUrl, RequestUrlParam, Setting, TAbstractFile, TFile } from 'obsidian'; import { App, Editor, MarkdownView, normalizePath, Notice, Plugin, PluginSettingTab, requestUrl, RequestUrlParam, Setting, TAbstractFile, TFile, MarkdownPostProcessorContext } from 'obsidian';
const {SmartChatModel} = require('smart-chat-model'); const {SmartChatModel} = require('smart-chat-model');
import { SmartMemosAudioRecordModal } from './SmartMemosAudioRecordModal'; // Update with the correct path
import { saveFile } from 'Utils';
interface AudioPluginSettings { interface AudioPluginSettings {
model: string; model: string;
apiKey: string; apiKey: string;
@ -16,23 +19,6 @@ let DEFAULT_SETTINGS: AudioPluginSettings = {
includeTranscript: true includeTranscript: true
} }
interface TokenLimits {
[key: string]: number;
}
const TOKEN_LIMITS: TokenLimits = {
'gpt-3.5-turbo-16k': 16000,
'gpt-3.5-turbo-0613':4096,
'text-davinci-003': 4097,
'text-davinci-002': 4097,
'code-davinci-002': 8001,
'code-davinci-001': 8001,
'gpt-4-0613': 8192,
'gpt-4-32k-0613': 32768,
'gpt-4o': 32768
}
const MODELS: string[] = [ const MODELS: string[] = [
'gpt-3.5-turbo-16k', 'gpt-3.5-turbo-16k',
'gpt-3.5-turbo-0613', 'gpt-3.5-turbo-0613',
@ -56,6 +42,9 @@ export default class SmartMemosPlugin extends Plugin {
appJsonObj : any; appJsonObj : any;
// Add a new property to store the audio file
audioFile: Blob;
async onload() { async onload() {
await this.loadSettings(); await this.loadSettings();
@ -72,10 +61,126 @@ export default class SmartMemosPlugin extends Plugin {
} }
}); });
this.registerMarkdownPostProcessor((el: HTMLElement, ctx: MarkdownPostProcessorContext) => {
const audioLinks = el.querySelectorAll('a.internal-link[data-href$=".wav"]');
console.log('audio links: ', audioLinks);
audioLinks.forEach(link => {
console.log('linksss');
const href = link.getAttribute('data-href');
if (href === null) {
console.error('Failed to get the href attribute from the link element.');
return; // Skip this iteration because there's no href
}
const abstractFile = this.app.vault.getAbstractFileByPath(href);
if (!(abstractFile instanceof TFile)) {
console.error('The path does not point to a valid file in the vault.');
return; // Skip this iteration because there's no file
}
const audio = document.createElement('audio');
audio.src = this.app.vault.getResourcePath(abstractFile);
audio.controls = true;
audio.addEventListener('loadedmetadata', () => {
if (audio.parentNode) {
const durationDisplay = document.createElement('span');
durationDisplay.textContent = `Duration: ${audio.duration.toFixed(2)} seconds`;
audio.parentNode.insertBefore(durationDisplay, audio.nextSibling);
}
});
audio.load(); // Trigger metadata loading
link.replaceWith(audio); // Replace the link with the audio player
});
});
// Add the audio recorder ribbon
// Update the callback for the audio recorder ribbon
this.addRibbonIcon('microphone', 'Record smart memo', async (evt: MouseEvent) => {
// Open the audio recorder and store the recorded audio
this.audioFile = await new SmartMemosAudioRecordModal(this.app, this.handleAudioRecording.bind(this)).open();
});
this.addSettingTab(new SmartMemosSettingTab(this.app, this)); this.addSettingTab(new SmartMemosSettingTab(this.app, this));
} }
// Add a new method to handle the audio recording and processing
async handleAudioRecording(audioFile: Blob, transcribe: boolean) {
try {
console.log('Handling audio recording:', audioFile);
if (!audioFile) {
console.log('No audio was recorded.');
return;
}
this.audioFile = audioFile;
// Save the audio recording as a .wav file
const fileName = `recording-${Date.now()}.wav`;
const file = await saveFile(this.app, this.audioFile, fileName, this.appJsonObj.attachmentFolderPath);
// Insert a link to the audio file in the current note
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (activeView) {
const editor = activeView.editor;
const cursor = editor.getCursor();
const link = `![[${file.path}]]`;
editor.replaceRange(link, cursor);
// Trigger a change in the editor to force Obsidian to re-render the note
editor.replaceRange('', { line: cursor.line, ch: cursor.ch }, { line: cursor.line, ch: cursor.ch });
}
// Transcribe the audio file if the transcribe parameter is true
if (transcribe) {
this.transcribeRecording(file);
}
// Handle the saved audio file
// You can replace this with your own handling logic
console.log(file);
} catch (error) {
console.error('Error handling audio recording:', error);
new Notice('Failed to handle audio recording');
}
}
// Add a new method to transcribe the audio file and generate text
async transcribeRecording(audioFile: TFile) {
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (!activeView) {
console.error('No active Markdown view found.');
return;
}
const editor = activeView.editor;
this.app.vault.readBinary(audioFile).then((audioBuffer) => {
if (this.writing) {
new Notice('Generator is already in progress.');
return;
}
this.writing = true;
new Notice("Generating transcript...");
const fileType = audioFile.extension;
this.generateTranscript(audioBuffer, fileType).then((result) => {
this.transcript = result;
const prompt = this.settings.prompt + result;
new Notice('Transcript generated...');
this.generateText(prompt, editor , editor.getCursor('to').line);
}).catch(error => {
console.warn(error.message);
new Notice(error.message);
this.writing = false;
});
});
}
writeText(editor: Editor, LnToWrite: number, text: string) { writeText(editor: Editor, LnToWrite: number, text: string) {
const newLine = this.getNextNewLine(editor, LnToWrite); const newLine = this.getNextNewLine(editor, LnToWrite);
editor.setLine(newLine, '\n' + text.trim() + '\n'); editor.setLine(newLine, '\n' + text.trim() + '\n');
@ -203,9 +308,17 @@ export default class SmartMemosPlugin extends Plugin {
if (filename == '') throw new Error('No file found in the text.'); if (filename == '') throw new Error('No file found in the text.');
// Use the attachment folder path from the plugin settings // Use the attachment folder path from the plugin settings
const fullPath = this.appJsonObj.attachmentFolderPath + '/' + filename; // Check if the filename already contains the attachment folder path
let fullPath;
if (filename.includes(this.appJsonObj.attachmentFolderPath)) {
fullPath = filename;
} else {
// Use the attachment folder path from the plugin settings
fullPath = this.appJsonObj.attachmentFolderPath + '/' + filename;
}
const file = this.app.vault.getAbstractFileByPath(fullPath); const file = this.app.vault.getAbstractFileByPath(fullPath);
if (file instanceof TFile) { if (file instanceof TFile) {
return file; return file;
@ -258,60 +371,6 @@ export default class SmartMemosPlugin extends Plugin {
} }
); );
const resp = await smart_chat_model.complete({messages: messages}); const resp = await smart_chat_model.complete({messages: messages});
console.log('resp: ', resp);
// editor.setLine(LnToWrite, editor.getLine(LnToWrite) + resp);
// const response = await requestUrl(options);
// if (response.status !== 200) {
// const errorResponse = JSON.parse(response.text);
// const errorMessage = errorResponse && errorResponse.error.message ? errorResponse.error.message : "Error";
// new Notice(`Error. ${errorMessage}`);
// throw new Error(`Error. ${errorMessage}`);
// } else {
// new Notice(`Superhuman analysis complete!`);
// }
// // Assuming the responseBody is an array of data
// const data = response.text.split('\n');
// let LnToWrite = this.getNextNewLine(editor, currentLn);
// editor.setLine(LnToWrite++, '\n');
// let end = false;
// let buffer = '';
// for (const datum of data) {
// if (datum.trim() === 'data: [DONE]') {
// end = true;
// break;
// }
// if (datum.startsWith('data:')) {
// const json = JSON.parse(datum.substring(6));
// if ('error' in json) throw new Error('Error: ' + json.error.message);
// if (!('choices' in json)) throw new Error('Error: ' + JSON.stringify(json));
// if ('content' in json.choices[0].delta) {
// const text = json.choices[0].delta.content;
// if (buffer.length < 1) buffer += text.trim();
// if (buffer.length > 0) {
// const lines = text.split('\n');
// if (lines.length > 1) {
// for (const word of lines) {
// editor.setLine(LnToWrite, editor.getLine(LnToWrite++) + word + '\n');
// }
// } else {
// editor.setLine(LnToWrite, editor.getLine(LnToWrite) + text);
// }
// }
// }
// }
// }
// editor.setLine(LnToWrite, editor.getLine(LnToWrite) + '\n');
// // Add the raw transcript at the end
// if (this.transcript) {
// editor.setLine(LnToWrite++, '# Transcript');
// editor.setLine(LnToWrite++, this.transcript);
// }
this.writing = false; this.writing = false;
} }
@ -325,7 +384,6 @@ export default class SmartMemosPlugin extends Plugin {
} }
} }
class SmartMemosSettingTab extends PluginSettingTab { class SmartMemosSettingTab extends PluginSettingTab {
plugin: SmartMemosPlugin; plugin: SmartMemosPlugin;
@ -372,7 +430,7 @@ class SmartMemosSettingTab extends PluginSettingTab {
.setDesc('Prompt that will be sent to Chatpgt right before adding your transcribed audio') .setDesc('Prompt that will be sent to Chatpgt right before adding your transcribed audio')
.addTextArea(text => { .addTextArea(text => {
if (text.inputEl) { if (text.inputEl) {
text.inputEl.classList.add('text-box'); text.inputEl.classList.add('smart-memo-text-box');
} }
text.setPlaceholder( text.setPlaceholder(
'Act as my personal secretary and worlds greatest entreprenuer and know I will put these notes in my personal obsidian where I have all my notes linked by categories, tags, etc. The following is a transcription of recording of someone talking aloud or people in a conversation. May be a lot of random things that are said given fluidity of conversation and the microphone ability to pick up all audio. Make outline of all topics and points within a structured hierarchy. Make sure to include any quantifiable information said such as the cost of headphones being $400. Then go into to detail with summaries that explain things more eloquently. Finally, Create a mermaid chart code that complements the outline.\n\n') 'Act as my personal secretary and worlds greatest entreprenuer and know I will put these notes in my personal obsidian where I have all my notes linked by categories, tags, etc. The following is a transcription of recording of someone talking aloud or people in a conversation. May be a lot of random things that are said given fluidity of conversation and the microphone ability to pick up all audio. Make outline of all topics and points within a structured hierarchy. Make sure to include any quantifiable information said such as the cost of headphones being $400. Then go into to detail with summaries that explain things more eloquently. Finally, Create a mermaid chart code that complements the outline.\n\n')

View file

@ -1,7 +1,7 @@
{ {
"id": "smart-memos", "id": "smart-memos",
"name": "Smart Memos", "name": "Smart Memos",
"version": "1.0.10", "version": "1.1.0",
"minAppVersion": "0.15.0", "minAppVersion": "0.15.0",
"description": "Create personalized and intelligent analysis, summaries, and more for audio recordings that can be imported or spoken directly into a note", "description": "Create personalized and intelligent analysis, summaries, and more for audio recordings that can be imported or spoken directly into a note",
"author": "Evan Moscoso", "author": "Evan Moscoso",

12
package-lock.json generated
View file

@ -1,16 +1,17 @@
{ {
"name": "smart-memos", "name": "smart-memos",
"version": "1.0.7", "version": "1.0.10",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "smart-memos", "name": "smart-memos",
"version": "1.0.7", "version": "1.0.10",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"smart-chat-model": "^1.0.5" "smart-chat-model": "^1.0.5",
"wavesurfer.js": "^7.8.0"
}, },
"devDependencies": { "devDependencies": {
"@types/d3": "^7.4.3", "@types/d3": "^7.4.3",
@ -2473,6 +2474,11 @@
"dev": true, "dev": true,
"peer": true "peer": true
}, },
"node_modules/wavesurfer.js": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/wavesurfer.js/-/wavesurfer.js-7.8.0.tgz",
"integrity": "sha512-V9SIfE08VtSIl1KYHi+i+52gytEIxk0nDKlV98fjrK0UW+z37ojImgsYINEV015syLB9sZVAXDdGI8F4xmU7KQ=="
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View file

@ -1,6 +1,6 @@
{ {
"name": "smart-memos", "name": "smart-memos",
"version": "1.0.10", "version": "1.1.0",
"description": "Create personalized and intelligent analysis, summaries, and more for audio recordings that can be imported or spoken directly into a note", "description": "Create personalized and intelligent analysis, summaries, and more for audio recordings that can be imported or spoken directly into a note",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
@ -36,6 +36,7 @@
}, },
"dependencies": { "dependencies": {
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"smart-chat-model": "^1.0.5" "smart-chat-model": "^1.0.5",
"wavesurfer.js": "^7.8.0"
} }
} }

View file

@ -1,17 +1,252 @@
/* .smart-memo-text-box {
This CSS file will be included with your plugin, and
available in the app when your plugin is enabled.
If your plugin does not need CSS, delete this file.
*/
.mermaid svg {
width: 100%;
height: auto;
}
.text-box {
width: 350px; width: 350px;
height: 400px; height: 400px;
} }
body .modal-container .modal.smart-memo-paused {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 90%; /* Adjust width if needed */
max-width: 400px; /* Set a max width */
height: 80%; /* Adjust height if needed */
max-height: 600px; /* Set a max height */
border-radius: 20px;
padding: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
overflow: hidden;
box-sizing: border-box;
animation: none;
background-size: 200% 200% !important;
background: linear-gradient(270deg, var(--background-primary), var(--background-secondary));
/* background: linear-gradient(270deg, #D08770, #babc8d, #B48EAD, #D08770); */
}
body .modal-container .modal.smart-memo-recording {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 90%; /* Adjust width if needed */
max-width: 400px; /* Set a max width */
height: 80%; /* Adjust height if needed */
max-height: 600px; /* Set a max height */
border-radius: 20px;
padding: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
overflow: hidden;
box-sizing: border-box;
background: var(--background-secondary);
background: linear-gradient(270deg, var(--background-primary), var(--background-secondary));
background-size: 200% 200% !important;
/* background-size: 200% 200%; */
animation: gradientAnimation 5s ease infinite;
}
.smart-memo-audio-record-modal-content {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 20px;
background: none; /* Make modal content transparent */
}
@keyframes gradientAnimation {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
.smart-memo-modal-button {
background-color:var(--interactive-accent) !important;
color: var(--text-on-accent);
border: none;
padding: 12px 24px;
font-size: 16px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: background-color 0.3s, transform 0.2s;
height: 40px;
border-radius: 5px !important;
}
.smart-memo-modal-button:hover {
background-color: rgba(68, 68, 68, 0.8);
/* transform: translateY(-2px); */
}
.smart-memo-modal-button-group {
display: flex;
justify-content: space-between;
width: 100%;
box-sizing: border-box; /* Include padding and border in the element's total width and height */
gap: 10px; /* Consistent spacing between buttons */
}
.smart-memo-full-width-button {
width: 100%;
}
.smart-memo-control-group-wrapper {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px; /* Consistent spacing between elements */
}
.smart-memo-header-timer-container {
display: flex;
flex-direction: column;
align-items: center;
}
.smart-memo-header-timer-container h2 {
font-size: 24px;
margin-bottom: 5px; /* Reduce the bottom margin */
/* color: var(--text-normal); */
color: var(--text-on-accent);
}
.smart-memo-transcribe-button {
width: 100%;
box-sizing: border-box; /* Include padding and border in the element's total width and height */
}
.smart-memo-flex {
flex: auto;
}
.smart-memo-timer {
/* color: var(--text-normal); */
color: var(--text-on-accent);
font-size: 20px; /* Adjust the font size if needed */
margin-bottom: 10px; /* Adjust the spacing as needed */
}
.smart-memo-red-dot-container {
height: 24px; /* Fixed height */
display: flex;
align-items: center;
justify-content: center;
}
.smart-memo-red-dot {
width: 25px;
height: 25px;
background-color: #e03a3a;
border-radius: 50%;
border: 3px solid #ffffff; /* Increased border for better visibility */
visibility: visible; /* Default to visible */
}
/* Define the animation in a separate class */
.smart-memo-pulse-animation {
animation: pulse 1s infinite;
}
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
}
.smart-memo-reset-button {
width: 100%;
margin-top: 20px; /* Add space between the middle buttons and the reset button */
height: 40px; /* Ensure it has the same height as other buttons */
box-sizing: border-box; /* Include padding and border in the element's total width and height */
}
.smart-memo-paused {
animation-play-state: paused;
}
/* Add this CSS to your existing stylesheet */
body .modal-container .modal .smart-memo-modal-button:focus,
body .modal-container .modal .smart-memo-modal-button:focus-visible {
outline: none;
box-shadow: none;
}
/* uncomment later when updating to new animations and modal container
/* .smart-memo-wave-animation-active {
animation: waveAnimation 2s linear infinite;
} */
/* @media (max-width: 600px) {
body .modal-container .modal.smart-memo-paused,
body .modal-container .modal.smart-memo-recording {
background-size: 1200% 1200% !important;
background: linear-gradient(270deg, #e67e80, #e69875, #dbbc7f, #a7c080, #83c092, #7fbbb3, #d3c6aa);
}
body .modal-container .modal.smart-memo-recording {
animation: gradientAnimation 15s ease infinite;
}
} */
/* .wave-container {
display: flex;
justify-content: center;
align-items: center;
margin-top: 20px; /* Adjust as needed
overflow: hidden; /* Hide overflow to create seamless effect
width: 100%; /* Full width of the container
height: 100px; /* Height of the wave
/* } */
/* .wave-animation {
width: 200%; /* Full width of the container
} */
/* @keyframes waveAnimation {
0% {
d: path("M 0,50 Q 20,30 40,50 T 80,50 T 120,50 T 160,50 T 200,50");
}
50% {
d: path("M 0,50 Q 20,70 40,50 T 80,50 T 120,50 T 160,50 T 200,50");
}
100% {
d: path("M 0,50 Q 20,30 40,50 T 80,50 T 120,50 T 160,50 T 200,50");
}
}
.wave-animation-active path {
animation: waveAnimation 2s linear infinite;
}
.straight-line path {
animation: none;
} */