Ajustando o chat llm
This commit is contained in:
parent
6c8d0d460b
commit
533cc654a8
166
.gitignore
vendored
166
.gitignore
vendored
@ -1,4 +1,164 @@
|
|||||||
.env
|
# Byte-compiled / optimized / DLL files
|
||||||
/node_modules/
|
|
||||||
__pycache__/
|
__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 -->
|
<!-- Botão de Treinar -->
|
||||||
<v-divider v-if="uploadedFiles.length > 0"></v-divider>
|
<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
|
<v-btn
|
||||||
color="#4cc9f0"
|
color="#4cc9f0"
|
||||||
block
|
block
|
||||||
@click="trainModel"
|
@click="trainModel"
|
||||||
:disabled="uploadedFiles.length === 0"
|
:disabled="uploadedFiles.length === 0 || isTraining"
|
||||||
|
:loading="isTraining"
|
||||||
>
|
>
|
||||||
Treinar Lucy
|
{{ isTraining ? 'Treinando...' : 'Treinar Lucy' }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
@ -135,39 +142,62 @@
|
|||||||
</v-toolbar>
|
</v-toolbar>
|
||||||
|
|
||||||
<v-card-text v-if="!modelTrained && !isTraining">
|
<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-card-text v-else-if="isTraining">
|
<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-card-text v-else>
|
<v-card-text v-else class="chat-container">
|
||||||
<v-list>
|
<div class="chat-messages" ref="chatMessagesContainer">
|
||||||
<v-list-subheader>Conversa com Lucy</v-list-subheader>
|
<v-timeline density="compact" align="start">
|
||||||
<v-list-item v-for="(message, index) in chatHistory" :key="index">
|
<v-timeline-item
|
||||||
<v-list-item-content>
|
v-for="(message, index) in chatHistory"
|
||||||
<v-list-item-title :class="{ 'user-message': message.sender === 'user' }">
|
:key="index"
|
||||||
{{ message.sender }}: {{ message.text }}
|
:dot-color="message.sender === 'user' ? 'primary' : 'secondary'"
|
||||||
</v-list-item-title>
|
:icon="message.sender === 'user' ? 'mdi-account' : 'mdi-robot'"
|
||||||
</v-list-item-content>
|
>
|
||||||
</v-list-item>
|
<v-card :color="message.sender === 'user' ? 'grey-lighten-4' : 'blue-lighten-5'" class="chat-message">
|
||||||
</v-list>
|
<v-card-title class="text-subtitle-2">{{ message.sender === 'user' ? 'Você' : 'Lucy' }}</v-card-title>
|
||||||
<v-text-field
|
<v-card-text v-html="formatMessage(message.text)"></v-card-text>
|
||||||
v-model="userMessage"
|
<v-card-subtitle class="text-caption text-right">{{ formatTimestamp(message.timestamp) }}</v-card-subtitle>
|
||||||
label="Digite sua mensagem"
|
</v-card>
|
||||||
placeholder="Pergunte algo à Lucy"
|
</v-timeline-item>
|
||||||
variant="outlined"
|
</v-timeline>
|
||||||
density="compact"
|
<div v-if="isTyping" class="typing-indicator">
|
||||||
@keyup.enter="sendMessage"
|
<span></span>
|
||||||
:disabled="!modelTrained"
|
<span></span>
|
||||||
></v-text-field>
|
<span></span>
|
||||||
<v-btn
|
</div>
|
||||||
color="primary"
|
</div>
|
||||||
block
|
<v-divider class="my-2"></v-divider>
|
||||||
@click="sendMessage"
|
<div class="message-input">
|
||||||
:disabled="!modelTrained"
|
<v-textarea
|
||||||
>
|
v-model="userMessage"
|
||||||
Enviar
|
label="Digite sua mensagem"
|
||||||
</v-btn>
|
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-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
@ -176,8 +206,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref, watch, nextTick, onMounted } from 'vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
// Estados principais
|
||||||
const tab = ref('1')
|
const tab = ref('1')
|
||||||
const files = ref([])
|
const files = ref([])
|
||||||
const uploadedFiles = ref([])
|
const uploadedFiles = ref([])
|
||||||
@ -185,10 +217,34 @@ const isDragging = ref(false)
|
|||||||
const showUrlDialog = ref(false)
|
const showUrlDialog = ref(false)
|
||||||
const url = ref('')
|
const url = ref('')
|
||||||
const modelTrained = ref(false)
|
const modelTrained = ref(false)
|
||||||
const isTraining = ref(false) // Estado para verificar se Lucy está sendo treinada
|
const isTraining = ref(false)
|
||||||
const chatHistory = ref([])
|
const chatHistory = ref([])
|
||||||
const userMessage = 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) => {
|
const formatFileSize = (size) => {
|
||||||
if (size === 'N/A') return size
|
if (size === 'N/A') return size
|
||||||
const units = ['B', 'KB', 'MB', 'GB']
|
const units = ['B', 'KB', 'MB', 'GB']
|
||||||
@ -203,62 +259,431 @@ const formatFileSize = (size) => {
|
|||||||
return `${Math.round(formattedSize * 100) / 100} ${units[unitIndex]}`
|
return `${Math.round(formattedSize * 100) / 100} ${units[unitIndex]}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFileUpload = (newFiles, allowedExtensions) => {
|
// Manipular upload de arquivos
|
||||||
|
const handleFileUpload = async (newFiles) => {
|
||||||
if (!newFiles) return
|
if (!newFiles) return
|
||||||
|
|
||||||
const validFiles = Array.from(newFiles).filter(file => {
|
const validFiles = Array.from(newFiles).filter(file => {
|
||||||
const ext = file.name.split('.').pop().toLowerCase()
|
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)
|
uploadedFiles.value.push(...validFiles)
|
||||||
files.value = []
|
files.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Manipular upload de URL
|
||||||
const handleUrlUpload = () => {
|
const handleUrlUpload = () => {
|
||||||
if (url.value.trim()) {
|
if (url.value.trim()) {
|
||||||
uploadedFiles.value.push({
|
uploadedFiles.value.push({
|
||||||
name: url.value,
|
name: url.value,
|
||||||
type: 'url',
|
type: 'url',
|
||||||
size: 'N/A'
|
size: 'N/A',
|
||||||
|
status: 'pendente',
|
||||||
|
metadata: {
|
||||||
|
source: 'web',
|
||||||
|
url: url.value,
|
||||||
|
uploadedAt: new Date().toISOString()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
url.value = ''
|
url.value = ''
|
||||||
showUrlDialog.value = false
|
showUrlDialog.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remover arquivo
|
||||||
const removeFile = (index) => {
|
const removeFile = (index) => {
|
||||||
uploadedFiles.value.splice(index, 1)
|
uploadedFiles.value.splice(index, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onDrop = (event, allowedExtensions) => {
|
// Handlers de drag & drop
|
||||||
|
const onDrop = (event) => {
|
||||||
isDragging.value = false
|
isDragging.value = false
|
||||||
const droppedFiles = event.dataTransfer.files
|
const droppedFiles = event.dataTransfer.files
|
||||||
|
|
||||||
if (droppedFiles.length) {
|
if (droppedFiles.length) {
|
||||||
handleFileUpload(droppedFiles, allowedExtensions)
|
handleFileUpload(droppedFiles)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onDragOver = () => isDragging.value = true
|
const onDragOver = () => isDragging.value = true
|
||||||
const onDragLeave = () => isDragging.value = false
|
const onDragLeave = () => isDragging.value = false
|
||||||
|
|
||||||
const trainModel = () => {
|
// Processar arquivo com a API Flask
|
||||||
isTraining.value = true
|
const processarArquivoComAPI = async (file) => {
|
||||||
setTimeout(() => {
|
try {
|
||||||
modelTrained.value = true
|
const formData = new FormData();
|
||||||
isTraining.value = false
|
formData.append('file', file);
|
||||||
console.log('Lucy foi treinada com os arquivos:', uploadedFiles.value)
|
|
||||||
}, 3000) // Simula o tempo de treinamento
|
|
||||||
}
|
|
||||||
|
|
||||||
const sendMessage = () => {
|
// Primeiro, use o endpoint /upload para processar o arquivo
|
||||||
if (userMessage.value.trim()) {
|
const uploadResponse = await axiosInstance.post(`/upload`, formData, {
|
||||||
chatHistory.value.push({ sender: 'user', text: userMessage.value })
|
headers: {
|
||||||
chatHistory.value.push({ sender: 'Lucy', text: `Você disse: "${userMessage.value}"` })
|
'Content-Type': 'multipart/form-data',
|
||||||
userMessage.value = ''
|
'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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -270,8 +695,11 @@ const sendMessage = () => {
|
|||||||
color: #6c00b5;
|
color: #6c00b5;
|
||||||
border-radius: 25px;
|
border-radius: 25px;
|
||||||
background: linear-gradient(145deg, #eeeded, #ffffff);
|
background: linear-gradient(145deg, #eeeded, #ffffff);
|
||||||
box-shadow: 20px 20px 60px #d9d9d9,
|
box-shadow: 20px 20px 60px #d9d9d9,
|
||||||
-20px -20px 60px #ffffff;
|
-20px -20px 60px #ffffff;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-box {
|
.upload-box {
|
||||||
@ -295,8 +723,72 @@ const sendMessage = () => {
|
|||||||
color: #777;
|
color: #777;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-message {
|
.chat-container {
|
||||||
font-weight: bold;
|
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 {
|
.text {
|
||||||
|
|||||||
@ -5,6 +5,10 @@ import vue from '@vitejs/plugin-vue'
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue()],
|
plugins: [vue()],
|
||||||
server: {
|
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
|
historyApiFallback: true, // Fallback para o history mode do Vue Router
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user