Browse Source

arruma visual chat e reply

pull/2436/head
yasminddias 3 months ago
parent
commit
ac67571748
6 changed files with 1035 additions and 167 deletions
  1. 16
      frontend/components/discrepancy/DiscrepancyList.vue
  2. 1013
      frontend/components/discussions/ChatDiscussion.vue
  3. 1
      frontend/i18n/en/generic.js
  4. 2
      frontend/i18n/en/projects/perspectives.js
  5. 75
      frontend/pages/projects/_id/discussions/_discussionId.vue
  6. 95
      frontend/pages/projects/_id/perspectives/index.vue

16
frontend/components/discrepancy/DiscrepancyList.vue

@ -118,11 +118,11 @@
<div>
<div class="caption">
<v-icon small color="success">mdi-check</v-icon>
{{ item.discrepancyData.agreement }}% acordo
{{ item.discrepancyData.agreement }}% agreement
</div>
<div class="caption">
<v-icon small :color="getDiscrepancyColor(item.discrepancyPercentage)">mdi-alert</v-icon>
{{ item.discrepancyData.disagreement }}% discrepância
{{ item.discrepancyData.disagreement }}% discrepancy
</div>
</div>
</div>
@ -569,6 +569,18 @@ loadLabelsIfNeeded() {
return 'success'
},
showTooltip(event: MouseEvent, item: any) {
const rect = (event.target as HTMLElement).getBoundingClientRect()
this.tooltip.x = rect.left + rect.width / 2
this.tooltip.y = rect.top - 10
this.tooltip.content = `${item.discrepancyData.agreement}% agreement, ${item.discrepancyData.disagreement}% discrepancy`
this.tooltip.show = true
},
hideTooltip() {
this.tooltip.show = false
},
getRowClass(item: any): string {
const discrepancyPercentage = item.discrepancyPercentage || 0
if (discrepancyPercentage >= 70) return 'high-discrepancy-row'

1013
frontend/components/discussions/ChatDiscussion.vue
File diff suppressed because it is too large
View File

1
frontend/i18n/en/generic.js

@ -19,4 +19,5 @@ export default {
type: 'Type',
loading: 'Loading... Please wait',
more: 'More',
ok: 'OK',
}

2
frontend/i18n/en/projects/perspectives.js

@ -44,6 +44,8 @@ export default {
failedToSaveQuestion: 'Failed to save question',
failedToDeleteQuestion: 'Failed to delete question',
failedToSubmitAnswer: 'Failed to submit answer',
databaseConnectionError: 'Unable to connect to database. Cannot delete question.',
databaseConnectionErrorTitle: 'Database Connection Error',
// Validation messages
questionTextRequired: 'Question text is required',

75
frontend/pages/projects/_id/discussions/_discussionId.vue

@ -48,8 +48,10 @@
<chat-discussion
v-if="!isDiscussionClosed || isProjectAdmin"
:current-user-id="currentUserId"
:current-username="currentUsername"
:messages="messages"
:read-only="isDiscussionClosed"
:is-online="isOnline"
class="mt-4"
@send-message="handleSendMessage"
@retry-message="handleRetryMessage"
@ -191,13 +193,33 @@ export default defineComponent({
async loadMessages() {
try {
const res = await this.$axios.get(`/v1/discussions/${this.discussionId}/chat/`)
this.messages = res.data.map((msg: any) => ({
id: msg.id,
userId: msg.userId,
username: msg.username || 'Usuário',
text: msg.text,
timestamp: new Date(msg.timestamp)
}))
this.messages = res.data.map((msg: any) => {
// Processar mensagens com formato antigo de reply
let replyTo = null;
let cleanText = msg.text;
if (msg.text && msg.text.startsWith('↪')) {
const replyPattern = /^\s+([^:]+):\s+([^\n]*)\n(.*)$/s;
const match = msg.text.match(replyPattern);
if (match) {
replyTo = {
username: match[1].trim(),
text: match[2].trim()
};
cleanText = match[3].trim();
}
}
return {
id: msg.id,
userId: msg.userId,
username: msg.username || 'Usuário',
text: cleanText,
replyTo,
timestamp: new Date(msg.timestamp)
};
})
} catch (e) {
console.error("Erro ao carregar mensagens:", e);
this.isOnline = false;
@ -235,21 +257,40 @@ export default defineComponent({
}
},
async handleSendMessage(text: string) {
async handleSendMessage(messageData: any) {
// Processar tanto string (formato antigo) quanto objeto (formato novo)
let text: string;
let replyTo: any = null;
if (typeof messageData === 'string') {
text = messageData;
} else {
text = messageData.text;
replyTo = messageData.replyTo;
}
const optimisticMessage: Message = {
id: Date.now(),
userId: this.currentUserId,
username: this.currentUsername,
text,
replyTo,
timestamp: new Date(),
status: 'sending'
status: this.isOnline ? 'sending' : 'failed'
};
this.messages.push(optimisticMessage);
if (this.isOnline) {
try {
// Enviar apenas o texto para o backend (por enquanto)
const savedMessage = await this.$axios.$post(`/v1/discussions/${this.discussionId}/chat/`, { text });
const index = this.messages.findIndex(m => m.id === optimisticMessage.id);
if (index !== -1) this.$set(this.messages, index, savedMessage);
if (index !== -1) {
// Manter a estrutura de reply no frontend
savedMessage.replyTo = replyTo;
this.$set(this.messages, index, savedMessage);
}
} catch (error) {
this.handleSendFailure(optimisticMessage);
}
@ -284,9 +325,19 @@ export default defineComponent({
this.messages[index].status = 'sending';
try {
// Preparar dados para reenvio
const postData: any = { text: messageToRetry.text };
// Adicionar dados de reply se existirem
if (messageToRetry.replyTo) {
postData.reply_to_id = messageToRetry.replyTo.id;
postData.reply_to_username = messageToRetry.replyTo.username;
postData.reply_to_text = messageToRetry.replyTo.text;
}
const savedMessage = await this.$axios.$post(
`/v1/discussions/${this.discussionId}/chat/`,
{ text: messageToRetry.text }
`/v1/discussions/${this.discussionId}/chat/`,
postData
);
this.$set(this.messages, index, savedMessage);
this.removeMessageFromQueue(messageToRetry.id);

95
frontend/pages/projects/_id/perspectives/index.vue

@ -85,6 +85,27 @@
</v-card-actions>
</v-card>
</v-dialog>
<!-- Database Connection Error Dialog -->
<v-dialog v-model="showDatabaseErrorDialog" max-width="450px" persistent>
<v-card>
<v-card-title class="headline error--text">
<v-icon color="error" class="mr-2">mdi-database-alert</v-icon>
{{ $t('perspectives.databaseConnectionErrorTitle') }}
</v-card-title>
<v-card-text class="pt-4">
<v-alert type="error" outlined class="mb-3">
{{ $t('perspectives.databaseConnectionError') }}
</v-alert>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn color="primary" @click="showDatabaseErrorDialog = false">
{{ $t('generic.ok') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-container>
</template>
@ -123,6 +144,7 @@ export default {
formLoading: false,
showCreateDialog: false,
showDeleteDialog: false,
showDatabaseErrorDialog: false,
editingQuestion: null,
questionToDelete: null,
isProjectAdmin: false,
@ -262,17 +284,75 @@ export default {
await this.loadStats()
}
} catch (error) {
this.$store.dispatch('notification/setNotification', {
color: 'error',
text: this.$t('perspectives.failedToDeleteQuestion')
})
console.error(error)
console.error('Delete error:', error)
// Check if it's a database connection error
const isDatabaseError = this.isDatabaseConnectionError(error)
if (isDatabaseError) {
// Show database connection error dialog
this.showDatabaseErrorDialog = true
} else {
// Show generic error notification
this.$store.dispatch('notification/setNotification', {
color: 'error',
text: this.$t('perspectives.failedToDeleteQuestion')
})
}
} finally {
this.showDeleteDialog = false
this.questionToDelete = null
}
},
isDatabaseConnectionError(error) {
// Check various indicators of database connection issues
if (!error) return false
// Check for network errors (no response from server)
if (error.code === 'NETWORK_ERROR' || error.message === 'Network Error') {
return true
}
// Check for connection refused or timeout errors
if (error.message && (
error.message.includes('ECONNREFUSED') ||
error.message.includes('timeout') ||
error.message.includes('Connection refused') ||
error.message.includes('ERR_NETWORK')
)) {
return true
}
// Check HTTP status codes that indicate server/database issues
if (error.response) {
const status = error.response.status
// 500: Internal Server Error (could be database)
// 502: Bad Gateway (server down)
// 503: Service Unavailable (database down)
// 504: Gateway Timeout (database timeout)
if (status === 500 || status === 502 || status === 503 || status === 504) {
return true
}
// Check response data for database-specific error messages
if (error.response.data) {
const errorData = error.response.data
const errorText = typeof errorData === 'string' ? errorData : JSON.stringify(errorData)
if (errorText.includes('database') ||
errorText.includes('connection') ||
errorText.includes('DatabaseError') ||
errorText.includes('OperationalError') ||
errorText.includes('InterfaceError')) {
return true
}
}
}
return false
},
async saveQuestion(questionData) {
this.formLoading = true
try {
@ -351,6 +431,11 @@ export default {
closeDialog() {
this.showCreateDialog = false
this.editingQuestion = null
},
// Method for testing database connection error (can be removed in production)
testDatabaseError() {
this.showDatabaseErrorDialog = true
}
}
}

Loading…
Cancel
Save