diff --git a/SmartMemosAudioRecordModal.ts b/SmartMemosAudioRecordModal.ts new file mode 100644 index 0000000..503fa56 --- /dev/null +++ b/SmartMemosAudioRecordModal.ts @@ -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) => 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((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((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }); + } +} diff --git a/Utils.ts b/Utils.ts new file mode 100644 index 0000000..627edd0 --- /dev/null +++ b/Utils.ts @@ -0,0 +1,43 @@ +import { App, TFile, TFolder } from 'obsidian'; + +export async function saveFile(app: App, audioBlob: Blob, fileName: string, path: string): Promise { + 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; + } + } +} \ No newline at end of file diff --git a/main.ts b/main.ts index 75c2f39..2063154 100644 --- a/main.ts +++ b/main.ts @@ -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'); +import { SmartMemosAudioRecordModal } from './SmartMemosAudioRecordModal'; // Update with the correct path +import { saveFile } from 'Utils'; + interface AudioPluginSettings { model: string; apiKey: string; @@ -16,23 +19,6 @@ let DEFAULT_SETTINGS: AudioPluginSettings = { 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[] = [ 'gpt-3.5-turbo-16k', 'gpt-3.5-turbo-0613', @@ -56,6 +42,9 @@ export default class SmartMemosPlugin extends Plugin { appJsonObj : any; + // Add a new property to store the audio file + audioFile: Blob; + async onload() { 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)); } + // 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) { const newLine = this.getNextNewLine(editor, LnToWrite); 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.'); + // 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); if (file instanceof TFile) { return file; @@ -258,60 +371,6 @@ export default class SmartMemosPlugin extends Plugin { } ); 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; } @@ -325,7 +384,6 @@ export default class SmartMemosPlugin extends Plugin { } } - class SmartMemosSettingTab extends PluginSettingTab { plugin: SmartMemosPlugin; @@ -372,7 +430,7 @@ class SmartMemosSettingTab extends PluginSettingTab { .setDesc('Prompt that will be sent to Chatpgt right before adding your transcribed audio') .addTextArea(text => { if (text.inputEl) { - text.inputEl.classList.add('text-box'); + text.inputEl.classList.add('smart-memo-text-box'); } 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') diff --git a/manifest.json b/manifest.json index f777648..cb683c0 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "smart-memos", "name": "Smart Memos", - "version": "1.0.10", + "version": "1.1.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", "author": "Evan Moscoso", diff --git a/package-lock.json b/package-lock.json index 414410b..1279599 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,17 @@ { "name": "smart-memos", - "version": "1.0.7", + "version": "1.0.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "smart-memos", - "version": "1.0.7", + "version": "1.0.10", "license": "MIT", "dependencies": { "bootstrap": "^5.3.3", - "smart-chat-model": "^1.0.5" + "smart-chat-model": "^1.0.5", + "wavesurfer.js": "^7.8.0" }, "devDependencies": { "@types/d3": "^7.4.3", @@ -2473,6 +2474,11 @@ "dev": 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": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index c517d20..8bedc95 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "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", "main": "main.js", "scripts": { @@ -36,6 +36,7 @@ }, "dependencies": { "bootstrap": "^5.3.3", - "smart-chat-model": "^1.0.5" + "smart-chat-model": "^1.0.5", + "wavesurfer.js": "^7.8.0" } } diff --git a/styles.css b/styles.css index 4248c4f..fb28cc8 100644 --- a/styles.css +++ b/styles.css @@ -1,17 +1,252 @@ -/* - -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 { +.smart-memo-text-box { width: 350px; height: 400px; -} \ No newline at end of file +} + + +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; +} */