Seguidores

quarta-feira, 25 de junho de 2025

Dashboard de Produtividade Pessoal T1E20: BugCT1-7 Não abriu Dashboard

INTRODUÇÃO

Post anterior: Dashboard de Produtividade Pessoal T1E19: BugCT1-7 Não abriu Dashboard - corrigido - teste falhou

O objetivo deste post é apresentar um prompt para o Gemini 2.5 Pro (Prévia), no qual é feito um pedido de outra correção, pois a anterior não funcionou, do bug identificado por "BugCT1-7 Não abriu Dashboard", no App Dashboard de Produtividade Pessoal (D2P).


Prompt para o Gemini 2.5 Pro (Prévia)

Olá, Gemini. Seguem as informações para correção:

1. ERROS DO CONSOLE DO NAVEGADOR:

Live reload enabled.

button:1 

            

           Failed to load resource: the server responded with a status of 403 ()Entenda o erro

m=credential_button_library:74 [GSI_LOGGER]: The given origin is not allowed for the given client ID.

_.G @ m=credential_button_library:74Entenda o erro

client:339 Cross-Origin-Opener-Policy policy would block the window.postMessage call.

Ts @ client:339Entenda o erro

client:339 Cross-Origin-Opener-Policy policy would block the window.postMessage call.

Ts @ client:339Entenda o erro

index.html:58 Uncaught ReferenceError: jwt_decode is not defined

    at Ir.handleCredentialResponse [as callback] (index.html:58:37)

    at Rs.Fa (client:340:422)

    at Ir.Fa (client:351:45)

    at Ts.c.port1.onmessage (client:339:72)Entenda o erro


                

         Chrome is moving towards a new experience that allows users to choose to browse without third-party cookies.

2. CÓDIGO DO ARQUIVO d2p.html:

Aqui foi colado o arquivo d2p.html, para entregar ao Gemini.

3. CÓDIGO DO ARQUIVO dashboard.js:

Aqui foi colado o arquivo dashboard.js, para entregar ao Gemini.

4. CÓDIGO DO ARQUIVO index.html:

Aqui foi colado o arquivo dashboard.js, para entregar ao Gemini.


Resposta do Gemini 2.5 Pro (Prévia)

Olá\! Vamos analisar esses novos erros. A boa notícia é que estamos chegando perto da solução. Os erros atuais são muito específicos e nos dão um caminho claro para a correção.

Basicamente, temos dois problemas principais acontecendo:

1.  **Erro de Configuração no Google Cloud:** O erro `The given origin is not allowed for the given client ID` persiste. Isso é 100% um problema de configuração na sua conta Google e é o bloqueio principal do login.
2.  **Erro de Código no `index.html`:** O erro `Uncaught ReferenceError: jwt_decode is not defined` indica que a biblioteca para decodificar o token do Google não está sendo carregada corretamente na sua página de login.

Vamos resolver isso e, em seguida, organizar a estrutura do seu código para que fique mais limpa e fácil de manter.

-----

### Análise e Diagnóstico Detalhado

#### 1\. ERRO CRÍTICO: `The given origin is not allowed for the given client ID` (Erro 403)

Como mencionei antes, este erro não pode ser corrigido com código. Ele significa que o Google está rejeitando a autenticação porque o endereço de onde ela parte (`http://127.0.0.1:xxxx` ou `http://localhost:xxxx`) não está na "lista de permissões" do seu projeto.

**Ação Obrigatória (Verifique Novamente):**

1.  Abra sua aplicação no navegador e copie **exatamente** o endereço que aparece na barra de URL. Por exemplo: `http://127.0.0.1:5500`.
2.  Vá para o [Google Cloud Console](https://console.cloud.google.com/).
3.  Navegue até **APIs e Serviços \> Credenciais**.
4.  Clique no nome do seu **ID do Cliente OAuth 2.0**.
5.  Na seção **"Origens JavaScript autorizadas"**, confirme que o endereço que você copiou no passo 1 está na lista. Se não estiver, clique em **"ADICIONAR URI"** e adicione-o.
6.  Clique em **"Salvar"**. **Aguarde 2 a 5 minutos** para que a alteração seja propagada pelos servidores do Google.

#### 2\. ERRO CRÍTICO: `Uncaught ReferenceError: jwt_decode is not defined`

Este erro acontece no seu arquivo `index.html` (a página de login). A função `handleCredentialResponse` é chamada, mas quando ela tenta executar `jwt_decode(...)`, o navegador diz "eu não conheço essa função".

  * **Causa:** A biblioteca `jwt-decode` não foi carregada antes da função ser chamada. Isso acontece porque a tag `<script>` que importa essa biblioteca está faltando no seu arquivo `index.html`.

-----

### Solução Corrigida e Código Refatorado

Para resolver o erro de código e também melhorar a organização, vamos seguir a arquitetura de duas páginas que discutimos, mas com o código corrigido e uma separação mais limpa entre HTML e JavaScript.

#### Passo 1: Corrija o arquivo `index.html` (Página de Login)

Substitua o conteúdo do seu `index.html` por este. Ele agora inclui a tag `<script>` que estava faltando para a biblioteca `jwt-decode`.

```html
<!DOCTYPE html>
<html lang="pt-br">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Login - Dashboard de Produtividade (D2P)</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            margin: 0;
            background-color: #f0f2f5;
            text-align: center;
        }
        .container {
            padding: 40px;
            background-color: white;
            border-radius: 8px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.1);
        }
        h1 { color: #333; }
        p { color: #555; }
    </style>

    <script src="https://unpkg.com/jwt-decode/build/jwt-decode.js"></script>

</head>
<body>

    <div class="container">
        <h1>Dashboard de Produtividade Pessoal (D2P)</h1>
        <p>Faça o login com sua conta Google para continuar.</p>
        
        <div id="g_id_onload"
             data-client_id="756336753138-q6bta7q070rrfmdqp92tqk0jdd0ilpct.apps.googleusercontent.com"
             data-callback="handleCredentialResponse"
             data-auto_prompt="false">
        </div>
        <div class="g_id_signin"
             data-type="standard"
             data-size="large"
             data-theme="outline"
             data-text="sign_in_with"
             data-shape="rectangular"
             data-logo_alignment="left">
        </div>
    </div>
    
    <script src="https://accounts.google.com/gsi/client" async defer></script>

    <script>
        function handleCredentialResponse(response) {
            // Agora a função jwt_decode será encontrada!
            const responsePayload = jwt_decode(response.credential);

            console.log("Login bem-sucedido. Redirecionando...");

            localStorage.setItem('usuarioNome', responsePayload.name);
            localStorage.setItem('usuarioEmail', responsePayload.email);
            
            window.location.href = 'd2p.html'; 
        }
    </script>
</body>
</html>
```

#### Passo 2: Limpe o arquivo `d2p.html` (Estrutura do Dashboard)

Vamos transformar seu `d2p.html` em um arquivo "casca", contendo apenas o HTML. Toda a lógica de JavaScript será movida para o arquivo `dashboard.js`. Isso torna o código muito mais organizado.

Substitua o conteúdo do seu `d2p.html` por este:

```html
<!DOCTYPE html>
<html lang="pt-BR">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Dashboard de Produtividade Pessoal</title>
    
    <script src="https://cdn.tailwindcss.com"></script>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns"></script>
    
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
    
    <script src="https://accounts.google.com/gsi/client" async defer></script>

    <style>
        body { font-family: 'Inter', sans-serif; background-color: #f0f2f5; }
        .btn { transition: all 0.2s ease-in-out; }
        .btn:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
        .task-active { box-shadow: 0 0 0 3px #3b82f6; transform: scale(1.02); }
    </style>
</head>
<body class="bg-gray-100 text-gray-800 p-4 md:p-8">

    <div class="max-w-7xl mx-auto">
        <header class="mb-8 flex justify-between items-center">
            <div>
                <h1 class="text-3xl md:text-4xl font-bold text-gray-900">Dashboard de Produtividade</h1>
                <p id="welcome-message" class="text-gray-600 mt-1">Carregando dados do usuário...</p>
            </div>
            <button id="logoutButton" class="btn bg-gray-600 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded-lg">Sair</button>
        </header>

        <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
            <div class="bg-white p-6 rounded-xl shadow-md">
                <h2 class="text-xl font-semibold mb-4">Jornada do Dia</h2>
                <div class="flex items-center justify-between mb-4">
                    <button id="btnStartWorkday" class="btn bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded-lg w-full mr-2">Iniciar Jornada</button>
                    <button id="btnEndWorkday" class="btn bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded-lg w-full ml-2" disabled>Finalizar Jornada</button>
                </div>
                <div class="text-sm text-gray-500">
                    <p>Início: <span id="workdayStartTime" class="font-medium">--:--:--</span></p>
                    <p>Duração Total: <span id="workdayDuration" class="font-medium">0h 0m 0s</span></p>
                </div>
            </div>
             <div class="bg-white p-6 rounded-xl shadow-md text-center">
                <h2 class="text-xl font-semibold mb-2">Produtividade</h2>
                <p class="text-4xl font-bold text-blue-600"><span id="productivityRate">0.00</span></p>
                <p class="text-gray-500">Pontos / Hora</p>
            </div>
            <div class="bg-white p-6 rounded-xl shadow-md text-center">
                <h2 class="text-xl font-semibold mb-2">Pontos Concluídos</h2>
                <p class="text-4xl font-bold text-green-600"><span id="totalPointsCompleted">0</span></p>
                <p class="text-gray-500">Total de pontos de tarefas finalizadas</p>
            </div>
        </div>
        
        <div class="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-8">
            <div class="lg:col-span-2 bg-white p-6 rounded-xl shadow-md">
                <h2 class="text-xl font-semibold mb-4">Plano do Dia</h2>
                <form id="formAddTask" class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6 items-end">
                    <div class="md:col-span-2">
                        <label for="taskName" class="block text-sm font-medium text-gray-700">Nome da Tarefa</label>
                        <input type="text" id="taskName" required class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
                    </div>
                    <div>
                        <label for="taskCode" class="block text-sm font-medium text-gray-700">Código</label>
                        <input type="text" id="taskCode" class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
                    </div>
                    <div>
                        <label for="taskPoints" class="block text-sm font-medium text-gray-700">Pontos</label>
                        <input type="number" id="taskPoints" required min="1" class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
                    </div>
                    <button type="submit" class="btn md:col-start-4 bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-lg">Adicionar Tarefa</button>
                </form>

                <div id="taskList" class="space-y-3">
                    <p class="text-gray-500 text-center py-4">Nenhuma tarefa adicionada ainda.</p>
                </div>
            </div>
            <div class="space-y-8">
                <div class="bg-white p-6 rounded-xl shadow-md">
                    <h2 class="text-xl font-semibold mb-4">Tarefa em Execução</h2>
                    <div id="currentTaskDisplay" class="bg-gray-50 p-4 rounded-lg mb-4 text-center">
                        <h3 id="currentTaskName" class="text-lg font-medium text-gray-800">Nenhuma tarefa iniciada</h3>
                        <p id="currentTaskTimer" class="text-5xl font-bold tracking-tight my-2">00:00:00</p>
                        <p class="text-sm text-gray-500">Duração Efetiva</p>
                    </div>
                    <div class="grid grid-cols-2 gap-4">
                        <button id="btnPauseResumeTask" class="btn bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 rounded-lg" disabled>Pausar</button>
                        <button id="btnStopTask" class="finalizar-btn bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded-lg" disabled>Finalizar</button>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script src="dashboard.js" defer></script>
</body>
</html>
```

#### Passo 3: Crie o arquivo `dashboard.js` (Lógica do Dashboard)

Agora, crie um arquivo chamado `dashboard.js` e coloque **toda a lógica** nele. Este código unifica tudo: a verificação de login, a função de logout e toda a lógica do seu dashboard que estava antes no `d2p.html`.

```javascript
document.addEventListener('DOMContentLoaded', function() {
    // ---- VERIFICAÇÃO DE LOGIN E AUTENTICAÇÃO ----
    const nomeUsuario = localStorage.getItem('usuarioNome');
    const emailUsuario = localStorage.getItem('usuarioEmail');

    if (!nomeUsuario || !emailUsuario) {
        alert("Acesso negado. Por favor, faça o login.");
        window.location.href = 'index.html';
        return;
    }

    // Popula a mensagem de boas-vindas
    document.getElementById('welcome-message').textContent = `Bem-vindo(a), ${nomeUsuario}!`;

    // Configura o botão de logout
    document.getElementById('logoutButton').addEventListener('click', () => {
        if (typeof google !== 'undefined') {
            google.accounts.id.disableAutoSelect();
        }
        localStorage.clear();
        window.location.href = 'index.html';
    });

    // ---- A PARTIR DAQUI, COMEÇA A LÓGICA DO DASHBOARD ----
    
    // --- ELEMENTOS DO DOM ---
    const formAddTask = document.getElementById('formAddTask');
    const taskListEl = document.getElementById('taskList');
    const btnStartWorkday = document.getElementById('btnStartWorkday');
    const btnEndWorkday = document.getElementById('btnEndWorkday');
    const workdayStartTimeEl = document.getElementById('workdayStartTime');
    const workdayDurationEl = document.getElementById('workdayDuration');
    const productivityRateEl = document.getElementById('productivityRate');
    const totalPointsCompletedEl = document.getElementById('totalPointsCompleted');
    const currentTaskNameEl = document.getElementById('currentTaskName');
    const currentTaskTimerEl = document.getElementById('currentTaskTimer');
    const btnPauseResumeTask = document.getElementById('btnPauseResumeTask');
    const btnStopTask = document.getElementById('btnStopTask');

    // --- ESTADO DA APLICAÇÃO ---
    let state = {
        tasks: [],
        workday: {
            startTime: null,
            endTime: null,
            durationTimer: null,
            totalSeconds: 0,
            totalEffectiveSeconds: 0,
        },
        currentTask: {
            id: null,
            timer: null,
            isPaused: false,
            effectiveSeconds: 0,
        },
        nextTaskId: 1,
    };

    // --- FUNÇÕES DE UTILIDADE ---
    const formatTime = (totalSeconds) => {
        if (isNaN(totalSeconds) || totalSeconds < 0) return "00:00:00";
        const hours = Math.floor(totalSeconds / 3600);
        const minutes = Math.floor((totalSeconds % 3600) / 60);
        const seconds = Math.floor(totalSeconds % 60);
        return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
    };

    const formatTimeHours = (totalSeconds) => {
        if (isNaN(totalSeconds) || totalSeconds <= 0) return 0;
        return totalSeconds / 3600;
    };

    // --- RENDERIZAÇÃO ---
    const renderTasks = () => {
        if (state.tasks.length === 0) {
            taskListEl.innerHTML = '<p class="text-gray-500 text-center py-4">Nenhuma tarefa adicionada ainda.</p>';
            return;
        }
        taskListEl.innerHTML = '';
        state.tasks.forEach(task => {
            const taskEl = document.createElement('div');
            taskEl.className = `p-4 border rounded-lg flex items-center justify-between transition-all duration-300 ${task.status === 'completed' ? 'bg-green-50 border-green-200' : 'bg-white'} ${state.currentTask.id === task.id ? 'task-active' : ''}`;
            taskEl.id = `task-${task.id}`;
            let statusIndicator = '';
            if (task.status === 'completed') {
                statusIndicator = '<span class="text-xs font-medium bg-green-100 text-green-800 py-1 px-2 rounded-full">Concluída</span>';
            } else if (task.status === 'in-progress') {
                statusIndicator = '<span class="text-xs font-medium bg-blue-100 text-blue-800 py-1 px-2 rounded-full">Em Progresso</span>';
            }
            taskEl.innerHTML = `
                <div>
                    <p class="font-semibold">${task.name} <span class="text-sm font-normal text-gray-500">(${task.code || 'N/C'})</span></p>
                    <p class="text-sm text-gray-600">Pontos: ${task.points} | Duração Efetiva: ${formatTime(task.effectiveDuration)}</p>
                </div>
                <div class="flex items-center space-x-2">
                    ${statusIndicator}
                    <button data-task-id="${task.id}" class="btn-start-task bg-blue-500 hover:bg-blue-600 text-white p-2 rounded-full disabled:bg-gray-300 disabled:cursor-not-allowed" ${task.status !== 'pending' || !state.workday.startTime || state.currentTask.id ? 'disabled' : ''} title="Iniciar Tarefa">
                        <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
                    </button>
                </div>
            `;
            taskListEl.appendChild(taskEl);
        });
        document.querySelectorAll('.btn-start-task').forEach(btn => {
            btn.addEventListener('click', () => handleStartTask(parseInt(btn.dataset.taskId)));
        });
    };
    
    const updateUI = () => {
        renderTasks();
        btnStartWorkday.disabled = !!state.workday.startTime;
        btnEndWorkday.disabled = !state.workday.startTime || !!state.workday.endTime || !!state.currentTask.id;
        const hasActiveTask = !!state.currentTask.id;
        btnPauseResumeTask.disabled = !hasActiveTask;
        btnStopTask.disabled = !hasActiveTask;
        btnPauseResumeTask.textContent = state.currentTask.isPaused ? 'Retomar' : 'Pausar';
        btnPauseResumeTask.className = `btn font-bold py-2 px-4 rounded-lg ${state.currentTask.isPaused ? 'bg-green-500 hover:bg-green-600' : 'bg-yellow-500 hover:bg-yellow-600'} text-white`;
        const totalCompletedPoints = state.tasks.filter(t => t.status === 'completed').reduce((sum, t) => sum + t.points, 0);
        totalPointsCompletedEl.textContent = totalCompletedPoints;
        const hoursWorked = formatTimeHours(state.workday.totalEffectiveSeconds);
        const productivity = hoursWorked > 0 ? (totalCompletedPoints / hoursWorked).toFixed(2) : '0.00';
        productivityRateEl.textContent = productivity;
    };

    // --- LÓGICA DE NEGÓCIO ---
    const handleStartWorkday = () => {
        state.workday.startTime = new Date();
        workdayStartTimeEl.textContent = state.workday.startTime.toLocaleTimeString();
        state.workday.durationTimer = setInterval(() => {
            state.workday.totalSeconds = Math.floor((new Date() - state.workday.startTime) / 1000);
            workdayDurationEl.textContent = formatTime(state.workday.totalSeconds);
            if (!state.currentTask.isPaused && state.currentTask.id) {
                state.workday.totalEffectiveSeconds++;
                updateUI();
            }
        }, 1000);
        updateUI();
    };
    
    const handleAddTask = (e) => {
        e.preventDefault();
        const taskName = document.getElementById('taskName').value;
        const taskCode = document.getElementById('taskCode').value;
        const taskPoints = parseInt(document.getElementById('taskPoints').value);
        if (taskName && taskPoints) {
            const newTask = {
                id: state.nextTaskId++,
                name: taskName,
                code: taskCode,
                points: taskPoints,
                status: 'pending',
                effectiveDuration: 0,
            };
            state.tasks.push(newTask);
            formAddTask.reset();
            updateUI();
        }
    };
    
    const handleStartTask = (taskId) => {
        if (state.currentTask.id) {
            alert('Já existe uma tarefa em andamento. Finalize-a primeiro.');
            return;
        }
        const task = state.tasks.find(t => t.id === taskId);
        if (task) {
            task.status = 'in-progress';
            state.currentTask.id = taskId;
            state.currentTask.effectiveSeconds = task.effectiveDuration;
            state.currentTask.isPaused = false;
            currentTaskNameEl.textContent = task.name;
            currentTaskTimerEl.textContent = formatTime(state.currentTask.effectiveSeconds);
            state.currentTask.timer = setInterval(() => {
                if (!state.currentTask.isPaused) {
                    state.currentTask.effectiveSeconds++;
                    currentTaskTimerEl.textContent = formatTime(state.currentTask.effectiveSeconds);
                }
            }, 1000);
            updateUI();
        }
    };

    const handlePauseResumeTask = () => {
        state.currentTask.isPaused = !state.currentTask.isPaused;
        updateUI();
    };

    const handleStopTask = () => {
        clearInterval(state.currentTask.timer);
        const task = state.tasks.find(t => t.id === state.currentTask.id);
        if(task) {
            task.status = 'completed';
            task.effectiveDuration = state.currentTask.effectiveSeconds;
        }
        state.currentTask = { id: null, timer: null, isPaused: false, effectiveSeconds: 0 };
        currentTaskNameEl.textContent = 'Nenhuma tarefa iniciada';
        currentTaskTimerEl.textContent = '00:00:00';
        updateUI();
    };

    // --- INICIALIZAÇÃO ---
    const init = () => {
        formAddTask.addEventListener('submit', handleAddTask);
        btnStartWorkday.addEventListener('click', handleStartWorkday);
        btnPauseResumeTask.addEventListener('click', handlePauseResumeTask);
        btnStopTask.addEventListener('click', handleStopTask);
        updateUI();
    };

    init();
});
```

### Resumo das Ações Finais

1.  **Corrija a Configuração do Google Cloud:** Este é o passo mais importante. Certifique-se de que a origem (`http://127.0.0.1:xxxx`) está autorizada.
2.  **Substitua `index.html`:** Use o novo código fornecido, que agora inclui a biblioteca `jwt-decode`.
3.  **Substitua `d2p.html`:** Use a nova versão "limpa" que apenas estrutura o HTML e chama o `dashboard.js`.
4.  **Crie/Substitua `dashboard.js`:** Use o código completo que unifica toda a lógica do seu aplicativo.
5.  **Estrutura de Arquivos:** Garanta que `index.html`, `d2p.html`, e `dashboard.js` estejam todos na mesma pasta no seu projeto.

Depois de fazer essas alterações, o fluxo de login deve funcionar sem erros e a sua aplicação ficará muito mais organizada.

CONCLUSÃO

As correções foram realizadas conforme o passo a passo acima, mas o teste falhou, mais uma vez, porque não consegui configurar corretamente o login.

Considerando que, por falta de conhecimento, não fui capaz de configurar o login no Google, decidi mudar a arquitetura do d2p, para não usar o login, usando apenas planilha Google junto com o Dashboard, conectando os dois com o Apps Script.

Portanto, a decisão é encerrar a temporada 1 desta série e iniciar a temporada 2, sem login.



Nenhum comentário:

Postar um comentário

Entendendo a Metáfora "Home Office é Moto Boy"

UCEM L-pI.184 Exercícios Parte I Lição 184 - Caso de uso da IA para estudar

  Lição anterior Escolher outra lição Próxima lição Lição 184   O Nome de Deus é a minha herança. Texto e audio da  Lição 184  no site ofici...