diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..09cf720 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "denoland.vscode-deno" + ] +} diff --git a/n8n.js b/n8n.js new file mode 100644 index 0000000..8341986 --- /dev/null +++ b/n8n.js @@ -0,0 +1,79 @@ +// Estrutura do fluxo +[ + { + // 1. Webhook Trigger + "name": "Webhook", + "type": "n8n-nodes-base.webhook", + "parameters": { + "path": "audio-processing", + "responseMode": "lastNode" + } + }, + { + // 2. Download do Áudio do Supabase + "name": "Supabase", + "type": "n8n-nodes-base.supabase", + "parameters": { + "operation": "download", + "bucket": "audios", + "filePath": "={{$json.file_path}}" + } + }, + { + // 3. Pré-processamento do Áudio (usando FFmpeg) + "name": "FFmpeg", + "type": "n8n-nodes-base.executeCommand", + "parameters": { + "command": "ffmpeg -i input.wav -af 'anlmdn,highpass=f=200,lowpass=f=3000,silenceremove=1:0:-50dB' output.wav" + } + }, + { + // 4. Transcrição (usando OpenAI Whisper) + "name": "Whisper", + "type": "n8n-nodes-base.httpRequest", + "parameters": { + "url": "https://api.openai.com/v1/audio/transcriptions", + "method": "POST", + "headers": { + "Authorization": "Bearer {{$env.OPENAI_API_KEY}}" + } + } + }, + { + // 5. Análise do Texto (usando GPT-4) + "name": "GPT4Analysis", + "type": "n8n-nodes-base.openAi", + "parameters": { + "model": "gpt-4", + "prompt": `Analise a seguinte transcrição considerando: + 1. Fluência (velocidade, pausas, prosódia) + 2. Pronúncia (precisão fonética, clareza) + 3. Erros (substituições, omissões) + 4. Compreensão (coerência, autocorreção) + + Transcrição: {{$node.Whisper.data.text}} + + Forneça uma análise detalhada seguindo as métricas especificadas.` + } + }, + { + // 6. Salvar Resultados no Supabase + "name": "SaveResults", + "type": "n8n-nodes-base.supabase", + "parameters": { + "operation": "insert", + "table": "audio_analysis", + "data": { + "audio_path": "={{$json.file_path}}", + "transcription": "={{$node.Whisper.data.text}}", + "analysis": "={{$node.GPT4Analysis.data.choices[0].text}}", + "metrics": { + "fluency": "={{$node.GPT4Analysis.data.metrics.fluency}}", + "pronunciation": "={{$node.GPT4Analysis.data.metrics.pronunciation}}", + "errors": "={{$node.GPT4Analysis.data.metrics.errors}}", + "comprehension": "={{$node.GPT4Analysis.data.metrics.comprehension}}" + } + } + } + } + ] \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 618e034..b948088 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "eslint-plugin-react-refresh": "^0.4.11", "globals": "^15.9.0", "postcss": "^8.4.49", + "supabase": "^2.1.1", "tailwindcss": "^3.4.17", "typescript": "^5.5.3", "typescript-eslint": "^8.3.0", @@ -907,6 +908,19 @@ "node": ">=12" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", @@ -1979,6 +1993,16 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2092,6 +2116,23 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/bin-links": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bin-links/-/bin-links-5.0.0.tgz", + "integrity": "sha512-sdleLVfCjBtgO5cNjA2HVRvWBJAHs4zwenaCPMNJAJU0yNxpzj80IpjOIimkpkr+mhlA+how5poQtt53PygbHA==", + "dev": true, + "license": "ISC", + "dependencies": { + "cmd-shim": "^7.0.0", + "npm-normalize-package-bin": "^4.0.0", + "proc-log": "^5.0.0", + "read-cmd-shim": "^5.0.0", + "write-file-atomic": "^6.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -2246,6 +2287,16 @@ "node": ">= 6" } }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -2255,6 +2306,16 @@ "node": ">=6" } }, + "node_modules/cmd-shim": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-7.0.0.tgz", + "integrity": "sha512-rtpaCbr164TPPh+zFdkWpCyZuKkjpAzODfaZCf/SVJZzJN+4bHQb/LP3Jzq5/+84um3XXY8r548XiWKSborwVw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -2332,6 +2393,16 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "devOptional": true }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", @@ -2883,6 +2954,30 @@ "reusify": "^1.0.4" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -2957,6 +3052,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -3129,6 +3237,20 @@ "entities": "^4.4.0" } }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3493,6 +3615,36 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/minizlib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.1.tgz", + "integrity": "sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.4", + "rimraf": "^5.0.5" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3534,6 +3686,45 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/node-releases": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", @@ -3573,6 +3764,16 @@ "node": ">=0.10.0" } }, + "node_modules/npm-normalize-package-bin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", + "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -3910,6 +4111,16 @@ "node": ">= 0.8.0" } }, + "node_modules/proc-log": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -4035,6 +4246,16 @@ "pify": "^2.3.0" } }, + "node_modules/read-cmd-shim": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-5.0.0.tgz", + "integrity": "sha512-SEbJV7tohp3DAAILbEMPXavBjAnMN0tVnh4+9G8ihV4Pq3HYF9h8QNez9zkJ1ILkv9G2BjdzwctznGZXgu/HGw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -4095,6 +4316,22 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/rollup": { "version": "4.24.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", @@ -4343,6 +4580,26 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/supabase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/supabase/-/supabase-2.1.1.tgz", + "integrity": "sha512-KPP4LrvMmu6IWIcqSOcPbepTGT2Fv05TMiYwbcEaKgCVUznDNtlyXQTImJhjP+8FhNuJ+tF9SLoQgMQqDei+jA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bin-links": "^5.0.0", + "https-proxy-agent": "^7.0.2", + "node-fetch": "^3.3.2", + "tar": "7.4.3" + }, + "bin": { + "supabase": "bin/supabase" + }, + "engines": { + "npm": ">=8" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -4415,6 +4672,34 @@ "node": ">=14.0.0" } }, + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -4645,6 +4930,16 @@ } } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -4795,6 +5090,20 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/write-file-atomic": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-6.0.0.tgz", + "integrity": "sha512-GmqrO8WJ1NuzJ2DrziEI2o57jKAVIQNf8a18W3nCYU3H7PNWqCCVTeH6/NQE93CIllIgQS98rrmVkYgTX9fFJQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/ws": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", diff --git a/package.json b/package.json index 6c226a1..d61037e 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "eslint-plugin-react-refresh": "^0.4.11", "globals": "^15.9.0", "postcss": "^8.4.49", + "supabase": "^2.1.1", "tailwindcss": "^3.4.17", "typescript": "^5.5.3", "typescript-eslint": "^8.3.0", diff --git a/src/components/audio/AudioUploader.tsx b/src/components/audio/AudioUploader.tsx new file mode 100644 index 0000000..fcd458a --- /dev/null +++ b/src/components/audio/AudioUploader.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { processAudio } from '../../services/audioService'; +import { Button } from '../ui/button'; + +export function AudioUploader(): JSX.Element { + const [isProcessing, setIsProcessing] = React.useState(false); + const [transcription, setTranscription] = React.useState(); + const [error, setError] = React.useState(); + + const handleFileUpload = async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + try { + setIsProcessing(true); + setError(undefined); + + const response = await processAudio(file); + + if (response.error) { + setError(response.error); + } else { + setTranscription(response.transcription); + } + } catch (err) { + setError('Erro ao processar áudio. Tente novamente.'); + console.error(err); + } finally { + setIsProcessing(false); + } + }; + + return ( +
+
+ + +
+ + {error && ( +

{error}

+ )} + + {transcription && ( +
+

Transcrição:

+

{transcription}

+
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/components/layouts/DashboardLayout.tsx b/src/components/layouts/DashboardLayout.tsx new file mode 100644 index 0000000..56f2e3d --- /dev/null +++ b/src/components/layouts/DashboardLayout.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { DashboardSidebar } from './DashboardSidebar'; + +interface DashboardLayoutProps { + children: React.ReactNode; +} + +export function DashboardLayout({ children }: DashboardLayoutProps): JSX.Element { + const [sidebarOpen, setSidebarOpen] = React.useState(false); + + return ( + <> + + + + +
+
+ {children} +
+
+ + ); +} \ No newline at end of file diff --git a/src/components/layouts/DashboardPageLayout.tsx b/src/components/layouts/DashboardPageLayout.tsx new file mode 100644 index 0000000..ba4dfc4 --- /dev/null +++ b/src/components/layouts/DashboardPageLayout.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { DashboardSidebar } from './DashboardSidebar'; + +interface DashboardPageLayoutProps { + children: React.ReactNode; + title: string; + description?: string; +} + +export function DashboardPageLayout({ + children, + title, + description +}: DashboardPageLayoutProps): JSX.Element { + return ( +
+ + +
+
+
+

+ {title} +

+ {description && ( +

+ {description} +

+ )} +
+ +
+ {children} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/layouts/DashboardSidebar.tsx b/src/components/layouts/DashboardSidebar.tsx new file mode 100644 index 0000000..88487e5 --- /dev/null +++ b/src/components/layouts/DashboardSidebar.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { NavLink } from 'react-router-dom'; +import { LayoutDashboard, Users, GraduationCap, UserCircle, BookOpen, Settings, LogOut } from 'lucide-react'; + +const links = [ + { icon: , label: 'Visão Geral', href: '/dashboard' }, + { icon: , label: 'Turmas', href: '/dashboard/turmas' }, + { icon: , label: 'Professores', href: '/dashboard/professores' }, + { icon: , label: 'Alunos', href: '/dashboard/alunos' }, + { icon: , label: 'Histórias', href: '/dashboard/historias' }, + { icon: , label: 'Configurações', href: '/dashboard/configuracoes' }, + { icon: , label: 'Sair', href: '/logout' }, +]; + +export function DashboardSidebar(): JSX.Element { + return ( +
    + {links.map((link) => ( +
  • + ` + flex items-center p-2 text-gray-900 rounded-lg dark:text-white + hover:bg-gray-100 dark:hover:bg-gray-700 group + ${isActive ? 'bg-gray-100 dark:bg-gray-700' : ''} + `} + > +
    + {link.icon} +
    + {link.label} +
    +
  • + ))} +
+ ); +} \ No newline at end of file diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..82b482e --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,30 @@ +import React from 'react'; + +interface ButtonProps extends React.ButtonHTMLAttributes { + as?: 'button' | 'span'; + children: React.ReactNode; +} + +export function Button({ + as: Component = 'button', + className = '', + children, + ...props +}: ButtonProps): JSX.Element { + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/src/pages/dashboard/DashboardPage.tsx b/src/pages/dashboard/DashboardPage.tsx new file mode 100644 index 0000000..c010184 --- /dev/null +++ b/src/pages/dashboard/DashboardPage.tsx @@ -0,0 +1,10 @@ +import { DashboardLayout } from "../../components/layouts/DashboardLayout"; +import React from "react"; + +export function DashboardPage(): JSX.Element { + return ( + + {/* Conteúdo da página */} + + ); +} \ No newline at end of file diff --git a/src/services/audioService.ts b/src/services/audioService.ts new file mode 100644 index 0000000..9094735 --- /dev/null +++ b/src/services/audioService.ts @@ -0,0 +1,38 @@ +import { supabase } from '../lib/supabase'; + +interface ProcessAudioResponse { + transcription?: string; + error?: string; +} + +export async function processAudio(audioFile: File): Promise { + try { + // 1. Upload do arquivo para o bucket do Supabase + const fileName = `audio-${Date.now()}-${audioFile.name}`; + const { data: uploadData, error: uploadError } = await supabase.storage + .from('audio-uploads') + .upload(fileName, audioFile); + + if (uploadError) throw new uploadError; + + // 2. Chama a Edge Function para processar o áudio + const { data, error } = await supabase.functions.invoke('process-audio', { + body: { + fileName: uploadData.path, + bucket: 'audio-uploads' + } + }); + + if (error) throw error; + + return { + transcription: data?.transcription + }; + + } catch (error) { + console.error('Erro ao processar áudio:', error); + return { + error: 'Falha ao processar o áudio. Tente novamente.' + }; + } +} \ No newline at end of file diff --git a/supabase/.gitignore b/supabase/.gitignore new file mode 100644 index 0000000..a3ad880 --- /dev/null +++ b/supabase/.gitignore @@ -0,0 +1,4 @@ +# Supabase +.branches +.temp +.env diff --git a/supabase/config.toml b/supabase/config.toml new file mode 100644 index 0000000..2dc7896 --- /dev/null +++ b/supabase/config.toml @@ -0,0 +1,22 @@ +project_id = "bsjlbnyslxzsdwxvkaap" + +[auth] +enabled = true +site_url = "https://historiasmagicas.netlify.app" +additional_redirect_urls = ["https://historiasmagicas.netlify.app", "https://*.historiasmagicas.netlify.app"] +jwt_expiry = 3600 +enable_refresh_token_rotation = true +refresh_token_reuse_interval = 10 + +[auth.mfa.totp] +enroll_enabled = true +verify_enabled = true + +[auth.email] +enable_signup = true +double_confirm_changes = true +enable_confirmations = true +secure_password_change = false +max_frequency = "1m0s" +otp_length = 6 +otp_expiry = 86400 diff --git a/supabase/functions/process-audio/index.ts b/supabase/functions/process-audio/index.ts new file mode 100644 index 0000000..7b9b06d --- /dev/null +++ b/supabase/functions/process-audio/index.ts @@ -0,0 +1,127 @@ +import { serve } from 'https://deno.land/std@0.168.0/http/server.ts' +import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' +import { Configuration, OpenAIApi } from 'https://esm.sh/openai@3.1.0' + +// Configurar OpenAI +const openaiConfig = new Configuration({ + apiKey: Deno.env.get('OPENAI_API_KEY') +}) +const openai = new OpenAIApi(openaiConfig) + +// Configurar Supabase +const supabaseUrl = Deno.env.get('SUPABASE_URL') +const supabaseAnonKey = Deno.env.get('SUPABASE_ANON_KEY') +const supabase = createClient(supabaseUrl, supabaseAnonKey) + +serve(async (req) => { + try { + // Extrair dados do webhook + const { record } = await req.json() + const { id, audio_url, story_id } = record + + // Atualizar status para processing + await supabase + .from('story_recordings') + .update({ status: 'processing' }) + .eq('id', id) + + // Buscar texto original da história + const { data: storyData } = await supabase + .from('stories') + .select('content') + .eq('id', story_id) + .single() + + const originalText = storyData.content.pages[0].text + + // 1. Transcrever áudio com Whisper + const audioResponse = await fetch(audio_url) + const audioBlob = await audioResponse.blob() + + const transcription = await openai.createTranscription( + audioBlob, + 'whisper-1', + 'pt', + 'verbose_json' + ) + + // 2. Analisar com GPT-4 + const analysis = await openai.createChatCompletion({ + model: "gpt-4", + messages: [ + { + role: "system", + content: `Você é um especialista em análise de leitura infantil. + Analise a transcrição comparando com o texto original. + Forneça uma análise detalhada em formato JSON com métricas de 0-100.` + }, + { + role: "user", + content: ` + Texto Original: "${originalText}" + Transcrição: "${transcription.data.text}" + + Analise e retorne um JSON com: + { + "metrics": { + "fluency": number, + "pronunciation": number, + "accuracy": number, + "comprehension": number + }, + "feedback": { + "strengths": string[], + "improvements": string[], + "suggestions": string + }, + "details": { + "wordsPerMinute": number, + "pauseCount": number, + "errorCount": number, + "selfCorrections": number + } + }` + } + ] + }) + + const analysisResult = JSON.parse(analysis.data.choices[0].message.content) + + // 3. Atualizar registro com resultados + await supabase + .from('story_recordings') + .update({ + transcription: transcription.data.text, + metrics: analysisResult.metrics, + feedback: analysisResult.feedback, + details: analysisResult.details, + status: 'analyzed', + processed_at: new Date().toISOString() + }) + .eq('id', id) + + return new Response( + JSON.stringify({ success: true }), + { headers: { 'Content-Type': 'application/json' } } + ) + + } catch (error) { + console.error('Erro:', error) + + // Atualizar registro com erro + if (error.record?.id) { + await supabase + .from('story_recordings') + .update({ + status: 'error', + error_message: error.message + }) + .eq('id', error.record.id) + } + + return new Response( + JSON.stringify({ error: error.message }), + { status: 500, headers: { 'Content-Type': 'application/json' } } + ) + } +}) \ No newline at end of file diff --git a/supabase/migrations/20241221175449_remote_schema.sql b/supabase/migrations/20241221175449_remote_schema.sql new file mode 100644 index 0000000..e69de29 diff --git a/supabase/seed.sql b/supabase/seed.sql new file mode 100644 index 0000000..e69de29