Ajustando o chat llm
This commit is contained in:
parent
6c8d0d460b
commit
533cc654a8
166
.gitignore
vendored
166
.gitignore
vendored
@ -1,4 +1,164 @@
|
||||
.env
|
||||
/node_modules/
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
venv/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||
.pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
/migrations
|
||||
@ -116,13 +116,20 @@
|
||||
|
||||
<!-- Botão de Treinar -->
|
||||
<v-divider v-if="uploadedFiles.length > 0"></v-divider>
|
||||
<v-card-text v-if="trainingStatus">
|
||||
<v-progress-linear v-if="isTraining" indeterminate color="primary"></v-progress-linear>
|
||||
<v-alert v-if="trainingStatus" :type="trainingStatusType" class="mt-3">
|
||||
{{ trainingStatus }}
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
<v-btn
|
||||
color="#4cc9f0"
|
||||
block
|
||||
@click="trainModel"
|
||||
:disabled="uploadedFiles.length === 0"
|
||||
:disabled="uploadedFiles.length === 0 || isTraining"
|
||||
:loading="isTraining"
|
||||
>
|
||||
Treinar Lucy
|
||||
{{ isTraining ? 'Treinando...' : 'Treinar Lucy' }}
|
||||
</v-btn>
|
||||
</v-card>
|
||||
</v-col>
|
||||
@ -135,39 +142,62 @@
|
||||
</v-toolbar>
|
||||
|
||||
<v-card-text v-if="!modelTrained && !isTraining">
|
||||
<p>Treine Lucy antes de iniciar a conversa.</p>
|
||||
<div class="text-center py-8">
|
||||
<v-icon size="64" color="grey">mdi-robot-off</v-icon>
|
||||
<p class="text-body-1 mt-4">Treine Lucy antes de iniciar a conversa.</p>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-text v-else-if="isTraining">
|
||||
<p>Lucy está sendo treinada... Aguarde!</p>
|
||||
<div class="text-center py-8">
|
||||
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
||||
<p class="text-body-1 mt-4">Lucy está sendo treinada...</p>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-text v-else>
|
||||
<v-list>
|
||||
<v-list-subheader>Conversa com Lucy</v-list-subheader>
|
||||
<v-list-item v-for="(message, index) in chatHistory" :key="index">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title :class="{ 'user-message': message.sender === 'user' }">
|
||||
{{ message.sender }}: {{ message.text }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<v-text-field
|
||||
v-model="userMessage"
|
||||
label="Digite sua mensagem"
|
||||
placeholder="Pergunte algo à Lucy"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
@keyup.enter="sendMessage"
|
||||
:disabled="!modelTrained"
|
||||
></v-text-field>
|
||||
<v-btn
|
||||
color="primary"
|
||||
block
|
||||
@click="sendMessage"
|
||||
:disabled="!modelTrained"
|
||||
>
|
||||
Enviar
|
||||
</v-btn>
|
||||
<v-card-text v-else class="chat-container">
|
||||
<div class="chat-messages" ref="chatMessagesContainer">
|
||||
<v-timeline density="compact" align="start">
|
||||
<v-timeline-item
|
||||
v-for="(message, index) in chatHistory"
|
||||
:key="index"
|
||||
:dot-color="message.sender === 'user' ? 'primary' : 'secondary'"
|
||||
:icon="message.sender === 'user' ? 'mdi-account' : 'mdi-robot'"
|
||||
>
|
||||
<v-card :color="message.sender === 'user' ? 'grey-lighten-4' : 'blue-lighten-5'" class="chat-message">
|
||||
<v-card-title class="text-subtitle-2">{{ message.sender === 'user' ? 'Você' : 'Lucy' }}</v-card-title>
|
||||
<v-card-text v-html="formatMessage(message.text)"></v-card-text>
|
||||
<v-card-subtitle class="text-caption text-right">{{ formatTimestamp(message.timestamp) }}</v-card-subtitle>
|
||||
</v-card>
|
||||
</v-timeline-item>
|
||||
</v-timeline>
|
||||
<div v-if="isTyping" class="typing-indicator">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
<v-divider class="my-2"></v-divider>
|
||||
<div class="message-input">
|
||||
<v-textarea
|
||||
v-model="userMessage"
|
||||
label="Digite sua mensagem"
|
||||
rows="2"
|
||||
auto-grow
|
||||
hide-details
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
@keydown.enter.prevent="sendMessage"
|
||||
:disabled="!modelTrained || isTyping"
|
||||
></v-textarea>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="sendMessage"
|
||||
:disabled="!modelTrained || !userMessage.trim() || isTyping"
|
||||
:loading="isTyping"
|
||||
icon="mdi-send"
|
||||
size="large"
|
||||
class="ml-2 mt-2"
|
||||
></v-btn>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
@ -176,8 +206,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, watch, nextTick, onMounted } from 'vue'
|
||||
import axios from 'axios'
|
||||
|
||||
// Estados principais
|
||||
const tab = ref('1')
|
||||
const files = ref([])
|
||||
const uploadedFiles = ref([])
|
||||
@ -185,10 +217,34 @@ const isDragging = ref(false)
|
||||
const showUrlDialog = ref(false)
|
||||
const url = ref('')
|
||||
const modelTrained = ref(false)
|
||||
const isTraining = ref(false) // Estado para verificar se Lucy está sendo treinada
|
||||
const isTraining = ref(false)
|
||||
const chatHistory = ref([])
|
||||
const userMessage = ref('')
|
||||
const isTyping = ref(false)
|
||||
const trainingStatus = ref('')
|
||||
const trainingStatusType = ref('info')
|
||||
const chatMessagesContainer = ref(null)
|
||||
const filePath = ref('') // Armazena o caminho do arquivo processado
|
||||
const fileContents = ref({}) // Armazena conteúdo dos arquivos por caminho
|
||||
|
||||
// Configuração do endpoint Flask
|
||||
const API_ENDPOINT = 'http://127.0.0.1:5000'
|
||||
|
||||
// Token de autenticação para o backend
|
||||
const authToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTc0MDA5MzU3MSwianRpIjoiYzU5ZDMxODctOTE2ZS00ZmVlLWJiZDAtM2NhODhjZTUzNDhhIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6IjEiLCJuYmYiOjE3NDAwOTM1NzEsImNzcmYiOiI2NzFjMThjZS0xYzI4LTQ4M2ItOTljMC1jOTliZWZkYmYxNjMiLCJleHAiOjE3NDAwOTUzNzF9.lgeUdqIKQEy1yKotmyRBOgV59V_OS3Ycjd05Jv5_vHM'
|
||||
|
||||
// Configurar axios com headers padrão
|
||||
const axiosInstance = axios.create({
|
||||
baseURL: API_ENDPOINT,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${authToken}`
|
||||
}
|
||||
})
|
||||
|
||||
// Extensões de arquivo permitidas
|
||||
const EXTENSOES_PERMITIDAS = ['pdf', 'jpg', 'jpeg', 'png', 'gif', 'xlsx', 'xls', 'txt']
|
||||
|
||||
// Formatar tamanho do arquivo
|
||||
const formatFileSize = (size) => {
|
||||
if (size === 'N/A') return size
|
||||
const units = ['B', 'KB', 'MB', 'GB']
|
||||
@ -203,62 +259,431 @@ const formatFileSize = (size) => {
|
||||
return `${Math.round(formattedSize * 100) / 100} ${units[unitIndex]}`
|
||||
}
|
||||
|
||||
const handleFileUpload = (newFiles, allowedExtensions) => {
|
||||
// Manipular upload de arquivos
|
||||
const handleFileUpload = async (newFiles) => {
|
||||
if (!newFiles) return
|
||||
|
||||
const validFiles = Array.from(newFiles).filter(file => {
|
||||
const ext = file.name.split('.').pop().toLowerCase()
|
||||
return allowedExtensions.includes(ext)
|
||||
return EXTENSOES_PERMITIDAS.includes(ext)
|
||||
})
|
||||
|
||||
// Adiciona metadados para acompanhamento
|
||||
for (const file of validFiles) {
|
||||
file.status = 'pendente'
|
||||
file.metadata = {
|
||||
uploadedAt: new Date().toISOString(),
|
||||
fileType: file.type
|
||||
}
|
||||
}
|
||||
|
||||
uploadedFiles.value.push(...validFiles)
|
||||
files.value = []
|
||||
}
|
||||
|
||||
// Manipular upload de URL
|
||||
const handleUrlUpload = () => {
|
||||
if (url.value.trim()) {
|
||||
uploadedFiles.value.push({
|
||||
name: url.value,
|
||||
type: 'url',
|
||||
size: 'N/A'
|
||||
size: 'N/A',
|
||||
status: 'pendente',
|
||||
metadata: {
|
||||
source: 'web',
|
||||
url: url.value,
|
||||
uploadedAt: new Date().toISOString()
|
||||
}
|
||||
})
|
||||
url.value = ''
|
||||
showUrlDialog.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Remover arquivo
|
||||
const removeFile = (index) => {
|
||||
uploadedFiles.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const onDrop = (event, allowedExtensions) => {
|
||||
// Handlers de drag & drop
|
||||
const onDrop = (event) => {
|
||||
isDragging.value = false
|
||||
const droppedFiles = event.dataTransfer.files
|
||||
|
||||
if (droppedFiles.length) {
|
||||
handleFileUpload(droppedFiles, allowedExtensions)
|
||||
handleFileUpload(droppedFiles)
|
||||
}
|
||||
}
|
||||
|
||||
const onDragOver = () => isDragging.value = true
|
||||
const onDragLeave = () => isDragging.value = false
|
||||
|
||||
const trainModel = () => {
|
||||
isTraining.value = true
|
||||
setTimeout(() => {
|
||||
modelTrained.value = true
|
||||
isTraining.value = false
|
||||
console.log('Lucy foi treinada com os arquivos:', uploadedFiles.value)
|
||||
}, 3000) // Simula o tempo de treinamento
|
||||
}
|
||||
// Processar arquivo com a API Flask
|
||||
const processarArquivoComAPI = async (file) => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const sendMessage = () => {
|
||||
if (userMessage.value.trim()) {
|
||||
chatHistory.value.push({ sender: 'user', text: userMessage.value })
|
||||
chatHistory.value.push({ sender: 'Lucy', text: `Você disse: "${userMessage.value}"` })
|
||||
userMessage.value = ''
|
||||
// Primeiro, use o endpoint /upload para processar o arquivo
|
||||
const uploadResponse = await axiosInstance.post(`/upload`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
'Authorization': `Bearer ${authToken}`
|
||||
}
|
||||
});
|
||||
|
||||
if (uploadResponse.data.file_path) {
|
||||
const filePath = uploadResponse.data.file_path;
|
||||
const content = uploadResponse.data.content;
|
||||
|
||||
// Armazenar conteúdo do arquivo para uso nas consultas
|
||||
fileContents.value[filePath] = content;
|
||||
|
||||
return {
|
||||
text: content,
|
||||
file_path: filePath
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error('Resposta inválida da API ao processar arquivo');
|
||||
} catch (error) {
|
||||
console.error('Erro ao processar arquivo:', error.response?.data || error.message);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Processar URL com a API Flask
|
||||
const processarUrlComAPI = async (urlToProcess) => {
|
||||
try {
|
||||
// Implementar quando a API de processamento de URL estiver disponível
|
||||
const response = await axiosInstance.post('/process-url', {
|
||||
url: urlToProcess
|
||||
}, {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
return {
|
||||
text: response.data.content || `Conteúdo da URL: ${urlToProcess}`,
|
||||
file_path: response.data.file_path,
|
||||
processado: true
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Erro ao processar URL:', error);
|
||||
return {
|
||||
text: `Erro ao processar URL: ${urlToProcess}`,
|
||||
processado: false,
|
||||
mensagem: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Treinar modelo com Flask API
|
||||
const trainModel = async () => {
|
||||
isTraining.value = true;
|
||||
trainingStatus.value = 'Iniciando processamento dos arquivos...';
|
||||
trainingStatusType.value = 'info';
|
||||
|
||||
try {
|
||||
// Inicializar base de conhecimento
|
||||
const baseDeConhecimento = {
|
||||
documentos: [],
|
||||
inicioTreinamento: new Date().toISOString(),
|
||||
statusTreinamento: 'em_andamento',
|
||||
filePaths: [], // Armazenar caminhos dos arquivos processados
|
||||
conteudos: {} // Armazenar conteúdos dos arquivos
|
||||
};
|
||||
|
||||
// Processar cada arquivo
|
||||
for (let i = 0; i < uploadedFiles.value.length; i++) {
|
||||
const file = uploadedFiles.value[i];
|
||||
trainingStatus.value = `Processando arquivo ${i+1}/${uploadedFiles.value.length}: ${file.name}`;
|
||||
|
||||
let dadosDocumento = {};
|
||||
|
||||
try {
|
||||
if (file.type === 'url') {
|
||||
// Processar URL
|
||||
const resultadoUrl = await processarUrlComAPI(file.name);
|
||||
dadosDocumento = {
|
||||
conteudo: resultadoUrl.text || 'Nenhum conteúdo extraído',
|
||||
file_path: resultadoUrl.file_path,
|
||||
metadata: {
|
||||
tipo: 'conteudo-web',
|
||||
url: file.name,
|
||||
processadoEm: new Date().toISOString()
|
||||
}
|
||||
};
|
||||
|
||||
if (resultadoUrl.file_path) {
|
||||
baseDeConhecimento.filePaths.push(resultadoUrl.file_path);
|
||||
baseDeConhecimento.conteudos[resultadoUrl.file_path] = resultadoUrl.text;
|
||||
}
|
||||
} else {
|
||||
// Processar arquivo usando a API atualizada
|
||||
const resultadoArquivo = await processarArquivoComAPI(file);
|
||||
|
||||
dadosDocumento = {
|
||||
conteudo: resultadoArquivo.text || 'Nenhum conteúdo extraído',
|
||||
file_path: resultadoArquivo.file_path,
|
||||
metadata: {
|
||||
tipo: getTipoArquivo(file),
|
||||
nomeArquivo: file.name,
|
||||
processadoEm: new Date().toISOString()
|
||||
}
|
||||
};
|
||||
|
||||
// Armazenar o caminho do arquivo para consultas futuras
|
||||
baseDeConhecimento.filePaths.push(resultadoArquivo.file_path);
|
||||
baseDeConhecimento.conteudos[resultadoArquivo.file_path] = resultadoArquivo.text;
|
||||
}
|
||||
|
||||
// Atualizar status do arquivo
|
||||
file.status = 'processado';
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Erro ao processar ${file.name}:`, error);
|
||||
file.status = 'erro';
|
||||
dadosDocumento = {
|
||||
conteudo: `Erro ao processar arquivo: ${error.message || 'Erro desconhecido'}`,
|
||||
metadata: {
|
||||
tipo: getTipoArquivo(file),
|
||||
nomeArquivo: file.name,
|
||||
erro: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
baseDeConhecimento.documentos.push(dadosDocumento);
|
||||
}
|
||||
|
||||
// Se processamos pelo menos um arquivo com sucesso
|
||||
if (baseDeConhecimento.filePaths.length > 0) {
|
||||
// Usar o caminho do último arquivo processado para as consultas
|
||||
filePath.value = baseDeConhecimento.filePaths[baseDeConhecimento.filePaths.length - 1];
|
||||
// Atualizar fileContents com os conteúdos processados
|
||||
fileContents.value = { ...fileContents.value, ...baseDeConhecimento.conteudos };
|
||||
|
||||
baseDeConhecimento.treinamentoConcluido = true;
|
||||
baseDeConhecimento.statusTreinamento = 'concluido';
|
||||
|
||||
modelTrained.value = true;
|
||||
trainingStatus.value = `Processamento concluído! Processados ${baseDeConhecimento.documentos.length} documentos.`;
|
||||
trainingStatusType.value = 'success';
|
||||
|
||||
// Adicionar mensagem inicial da Lucy
|
||||
chatHistory.value.push({
|
||||
sender: 'Lucy',
|
||||
text: `Olá! Fui treinada com ${baseDeConhecimento.documentos.length} documentos. Como posso ajudar?`,
|
||||
timestamp: new Date()
|
||||
});
|
||||
|
||||
// Armazenar base de conhecimento para uso posterior
|
||||
localStorage.setItem('baseDeConhecimento', JSON.stringify(baseDeConhecimento));
|
||||
} else {
|
||||
trainingStatus.value = 'Nenhum arquivo foi processado com sucesso.';
|
||||
trainingStatusType.value = 'error';
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erro durante o processo de treinamento:', error);
|
||||
trainingStatus.value = `Erro durante o processamento: ${error.message || 'Falha desconhecida'}`;
|
||||
trainingStatusType.value = 'error';
|
||||
} finally {
|
||||
isTraining.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Função auxiliar para determinar o tipo de arquivo
|
||||
const getTipoArquivo = (file) => {
|
||||
const ext = file.name.split('.').pop().toLowerCase();
|
||||
if (ext === 'pdf') return 'pdf';
|
||||
if (['jpg', 'jpeg', 'png', 'gif'].includes(ext)) return 'imagem';
|
||||
if (['xlsx', 'xls'].includes(ext)) return 'excel';
|
||||
if (ext === 'txt') return 'texto';
|
||||
return file.type;
|
||||
};
|
||||
|
||||
// Função personalizada para enviar consulta com conteúdo já armazenado
|
||||
const sendQueryWithStoredContent = async (question, path) => {
|
||||
try {
|
||||
// Verifica se temos o conteúdo do arquivo
|
||||
if (!fileContents.value[path]) {
|
||||
console.warn(`Conteúdo não disponível para ${path}, tentando buscar da API`);
|
||||
// Tentar recuperar o conteúdo primeiro
|
||||
try {
|
||||
const response = await axiosInstance.get(`/get-content`, {
|
||||
params: { file_path: path }
|
||||
});
|
||||
if (response.data.content) {
|
||||
fileContents.value[path] = response.data.content;
|
||||
} else {
|
||||
throw new Error('Conteúdo não disponível');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao recuperar conteúdo:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: 'Não foi possível acessar o conteúdo do documento. Tente processar o arquivo novamente.'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Usar o conteúdo armazenado para construir a requisição
|
||||
const content = fileContents.value[path];
|
||||
|
||||
// Fazendo nossa própria requisição personalizada corrigindo o erro do backend
|
||||
const response = await axiosInstance.post('/query', {
|
||||
question: question,
|
||||
file_path: path,
|
||||
content: content // Enviamos o conteúdo do arquivo também
|
||||
}, {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
response: response.data.response
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Erro ao processar consulta:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: `Erro ao processar consulta: ${error.message}`
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Enviar mensagem
|
||||
const sendMessage = async () => {
|
||||
if (!userMessage.value.trim() || !modelTrained.value || isTyping.value) return;
|
||||
|
||||
// Adiciona mensagem do usuário
|
||||
const userMsg = userMessage.value.trim();
|
||||
chatHistory.value.push({
|
||||
sender: 'user',
|
||||
text: userMsg,
|
||||
timestamp: new Date()
|
||||
});
|
||||
|
||||
const questionText = userMsg; // Guarda a mensagem antes de limpar o input
|
||||
userMessage.value = '';
|
||||
|
||||
// Simula digitação da Lucy
|
||||
isTyping.value = true;
|
||||
|
||||
try {
|
||||
// Obter a base de conhecimento armazenada para verificar o caminho do arquivo
|
||||
const baseDeConhecimento = JSON.parse(localStorage.getItem('baseDeConhecimento') || '{}');
|
||||
const currentFilePath = filePath.value || (baseDeConhecimento.filePaths && baseDeConhecimento.filePaths[0]);
|
||||
|
||||
if (!currentFilePath) {
|
||||
throw new Error('Nenhum arquivo processado disponível para consulta');
|
||||
}
|
||||
|
||||
// Usar nossa função personalizada para enviar a consulta com o conteúdo armazenado
|
||||
const result = await sendQueryWithStoredContent(questionText, currentFilePath);
|
||||
|
||||
if (result.success) {
|
||||
// Adiciona resposta da Lucy
|
||||
chatHistory.value.push({
|
||||
sender: 'Lucy',
|
||||
text: result.response,
|
||||
timestamp: new Date()
|
||||
});
|
||||
} else {
|
||||
// Adiciona mensagem de erro
|
||||
chatHistory.value.push({
|
||||
sender: 'Lucy',
|
||||
text: result.message,
|
||||
timestamp: new Date()
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao obter resposta:', error);
|
||||
chatHistory.value.push({
|
||||
sender: 'Lucy',
|
||||
text: 'Desculpe, tive um problema ao processar sua solicitação. Pode tentar novamente?',
|
||||
timestamp: new Date()
|
||||
});
|
||||
} finally {
|
||||
isTyping.value = false;
|
||||
// Rola para o final da conversa
|
||||
scrollToBottom();
|
||||
}
|
||||
};
|
||||
|
||||
// Formatar mensagem (suporte a markdown básico)
|
||||
const formatMessage = (text) => {
|
||||
if (!text) return '';
|
||||
|
||||
// Converte quebras de linha
|
||||
let formatted = text.replace(/\n/g, '<br>');
|
||||
|
||||
// Formata negrito
|
||||
formatted = formatted.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
||||
|
||||
// Formata itálico
|
||||
formatted = formatted.replace(/\*(.*?)\*/g, '<em>$1</em>');
|
||||
|
||||
// Formata código inline
|
||||
formatted = formatted.replace(/`(.*?)`/g, '<code>$1</code>');
|
||||
|
||||
return formatted;
|
||||
};
|
||||
|
||||
// Formatar timestamp
|
||||
const formatTimestamp = (timestamp) => {
|
||||
if (!timestamp) return '';
|
||||
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
};
|
||||
|
||||
// Rolar para o final da conversa
|
||||
const scrollToBottom = async () => {
|
||||
await nextTick();
|
||||
if (chatMessagesContainer.value) {
|
||||
const container = chatMessagesContainer.value;
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
// Observar mudanças no histórico de chat para rolar para baixo
|
||||
watch(chatHistory, () => {
|
||||
scrollToBottom();
|
||||
}, { deep: true });
|
||||
|
||||
// Montar componente
|
||||
onMounted(() => {
|
||||
// Verificar se temos um modelo previamente treinado
|
||||
const baseDeConhecimentoSalva = localStorage.getItem('baseDeConhecimento');
|
||||
if (baseDeConhecimentoSalva) {
|
||||
const baseDeConhecimento = JSON.parse(baseDeConhecimentoSalva);
|
||||
if (baseDeConhecimento.treinamentoConcluido) {
|
||||
modelTrained.value = true;
|
||||
trainingStatus.value = `Modelo previamente treinado com ${baseDeConhecimento.documentos?.length || 0} documentos.`;
|
||||
trainingStatusType.value = 'success';
|
||||
|
||||
// Restaurar o filePath do último processamento
|
||||
if (baseDeConhecimento.filePaths && baseDeConhecimento.filePaths.length > 0) {
|
||||
filePath.value = baseDeConhecimento.filePaths[baseDeConhecimento.filePaths.length - 1];
|
||||
}
|
||||
|
||||
// Restaurar conteúdos de arquivo
|
||||
if (baseDeConhecimento.conteudos) {
|
||||
fileContents.value = baseDeConhecimento.conteudos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Adiciona mensagem de boas-vindas
|
||||
if (chatHistory.value.length === 0) {
|
||||
chatHistory.value.push({
|
||||
sender: 'Lucy',
|
||||
text: 'Olá! Carregue seus documentos e me treine para que eu possa responder suas perguntas.',
|
||||
timestamp: new Date()
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -270,8 +695,11 @@ const sendMessage = () => {
|
||||
color: #6c00b5;
|
||||
border-radius: 25px;
|
||||
background: linear-gradient(145deg, #eeeded, #ffffff);
|
||||
box-shadow: 20px 20px 60px #d9d9d9,
|
||||
-20px -20px 60px #ffffff;
|
||||
box-shadow: 20px 20px 60px #d9d9d9,
|
||||
-20px -20px 60px #ffffff;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.upload-box {
|
||||
@ -295,8 +723,72 @@ const sendMessage = () => {
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.user-message {
|
||||
font-weight: bold;
|
||||
.chat-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
padding-right: 8px;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.chat-message {
|
||||
margin-bottom: 8px;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.message-input {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.typing-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 5px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.typing-indicator span {
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: #6c00b5;
|
||||
display: block;
|
||||
opacity: 0.4;
|
||||
animation: typing 1s infinite alternate;
|
||||
}
|
||||
|
||||
.typing-indicator span:nth-child(1) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.typing-indicator span:nth-child(2) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
.typing-indicator span:nth-child(3) {
|
||||
animation-delay: 0.6s;
|
||||
}
|
||||
|
||||
@keyframes typing {
|
||||
0% {
|
||||
transform: translateY(0px);
|
||||
opacity: 0.4;
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-5px);
|
||||
opacity: 0.8;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0px);
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
|
||||
@ -5,6 +5,10 @@ import vue from '@vitejs/plugin-vue'
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
proxy: {
|
||||
'/process/': 'http://localhost:8000', // Redireciona as requisições para o back-end FastAPI
|
||||
},
|
||||
historyApiFallback: true, // Fallback para o history mode do Vue Router
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user