feat: adiciona integração com edge function para processamento de áudio

- Cria serviço audioService para upload e processamento
- Implementa componente AudioUploader com feedback visual
- Adiciona componente Button reutilizável
- Integra processamento de áudio na página de histórias
This commit is contained in:
Lucas Santana 2024-12-21 16:12:02 -03:00
parent 6f8e890e86
commit 797967ca5b
16 changed files with 815 additions and 0 deletions

5
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"recommendations": [
"denoland.vscode-deno"
]
}

79
n8n.js Normal file
View File

@ -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}}"
}
}
}
}
]

309
package-lock.json generated
View File

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

View File

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

View File

@ -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<string>();
const [error, setError] = React.useState<string>();
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
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 (
<div className="space-y-4">
<div>
<input
type="file"
accept="audio/*"
onChange={handleFileUpload}
disabled={isProcessing}
className="hidden"
id="audio-upload"
/>
<label htmlFor="audio-upload">
<Button
as="span"
disabled={isProcessing}
className="cursor-pointer"
>
{isProcessing ? 'Processando...' : 'Enviar Áudio'}
</Button>
</label>
</div>
{error && (
<p className="text-red-500 text-sm">{error}</p>
)}
{transcription && (
<div className="p-4 bg-gray-50 rounded-lg">
<h3 className="font-medium mb-2">Transcrição:</h3>
<p>{transcription}</p>
</div>
)}
</div>
);
}

View File

@ -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 (
<>
<button
onClick={() => setSidebarOpen(!sidebarOpen)}
type="button"
className="inline-flex items-center p-2 mt-2 ms-3 text-sm text-gray-500 rounded-lg sm:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600"
>
<span className="sr-only">Abrir menu</span>
<svg className="w-6 h-6" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20">
<path clipRule="evenodd" fillRule="evenodd" d="M2 4.75A.75.75 0 012.75 4h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 4.75zm0 10.5a.75.75 0 01.75-.75h7.5a.75.75 0 010 1.5h-7.5a.75.75 0 01-.75-.75zM2 10a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 10z" />
</svg>
</button>
<aside
className={`fixed top-0 left-0 z-40 w-64 h-screen transition-transform ${
sidebarOpen ? 'translate-x-0' : '-translate-x-full'
} sm:translate-x-0`}
>
<div className="h-full px-3 py-4 overflow-y-auto bg-white border-r border-gray-200 dark:bg-gray-800 dark:border-gray-700">
<div className="flex items-center pb-4 mb-4 border-b border-gray-200 dark:border-gray-700">
<img src="/logo.svg" className="h-8 me-3" alt="Logo" />
<span className="self-center text-xl font-semibold whitespace-nowrap dark:text-white">
Histórias Mágicas
</span>
</div>
<DashboardSidebar />
</div>
</aside>
<div className="p-4 sm:ml-64">
<div className="p-4 border-2 border-gray-200 border-dashed rounded-lg dark:border-gray-700">
{children}
</div>
</div>
</>
);
}

View File

@ -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 (
<div className="min-h-screen bg-gray-50 flex">
<DashboardSidebar />
<main className="flex-1 min-h-screen transition-all duration-300">
<div className="w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="mb-6 sm:mb-8">
<h1 className="text-2xl sm:text-3xl font-bold text-primary mb-2">
{title}
</h1>
{description && (
<p className="text-sm sm:text-base text-gray-600">
{description}
</p>
)}
</div>
<div className="space-y-6">
{children}
</div>
</div>
</main>
</div>
);
}

View File

@ -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: <LayoutDashboard className="w-5 h-5" />, label: 'Visão Geral', href: '/dashboard' },
{ icon: <Users className="w-5 h-5" />, label: 'Turmas', href: '/dashboard/turmas' },
{ icon: <GraduationCap className="w-5 h-5" />, label: 'Professores', href: '/dashboard/professores' },
{ icon: <UserCircle className="w-5 h-5" />, label: 'Alunos', href: '/dashboard/alunos' },
{ icon: <BookOpen className="w-5 h-5" />, label: 'Histórias', href: '/dashboard/historias' },
{ icon: <Settings className="w-5 h-5" />, label: 'Configurações', href: '/dashboard/configuracoes' },
{ icon: <LogOut className="w-5 h-5" />, label: 'Sair', href: '/logout' },
];
export function DashboardSidebar(): JSX.Element {
return (
<ul className="space-y-2 font-medium">
{links.map((link) => (
<li key={link.href}>
<NavLink
to={link.href}
className={({ isActive }) => `
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' : ''}
`}
>
<div className="flex-shrink-0 w-5 h-5 text-gray-500 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white">
{link.icon}
</div>
<span className="flex-1 ms-3 whitespace-nowrap">{link.label}</span>
</NavLink>
</li>
))}
</ul>
);
}

View File

@ -0,0 +1,30 @@
import React from 'react';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
as?: 'button' | 'span';
children: React.ReactNode;
}
export function Button({
as: Component = 'button',
className = '',
children,
...props
}: ButtonProps): JSX.Element {
return (
<Component
className={`
inline-flex items-center justify-center px-4 py-2
text-sm font-medium text-white
bg-purple-600 hover:bg-purple-700
rounded-md shadow-sm
transition-colors duration-200
disabled:opacity-50 disabled:cursor-not-allowed
${className}
`}
{...props}
>
{children}
</Component>
);
}

View File

@ -0,0 +1,10 @@
import { DashboardLayout } from "../../components/layouts/DashboardLayout";
import React from "react";
export function DashboardPage(): JSX.Element {
return (
<DashboardLayout>
{/* Conteúdo da página */}
</DashboardLayout>
);
}

View File

@ -0,0 +1,38 @@
import { supabase } from '../lib/supabase';
interface ProcessAudioResponse {
transcription?: string;
error?: string;
}
export async function processAudio(audioFile: File): Promise<ProcessAudioResponse> {
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<ProcessAudioResponse>('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.'
};
}
}

4
supabase/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
# Supabase
.branches
.temp
.env

22
supabase/config.toml Normal file
View File

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

View File

@ -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' } }
)
}
})

0
supabase/seed.sql Normal file
View File