This commit is contained in:
mmrbnjd
2025-08-10 20:52:45 +03:30
parent 3942c2a915
commit 0856c8ae59

View File

@@ -6,6 +6,7 @@
@using HushianWebApp.Service
@using HushianWebApp.Services
@using Microsoft.AspNetCore.SignalR.Client;
@using System.Threading;
@inject NavigationManager NavigationManager
@inject ChatService ChatService
@@ -77,11 +78,26 @@
<div class="d-flex mb-2 @(msg.Type!=Common.Enums.ConversationType.UE ? "justify-content-end" : "justify-content-start")">
<div class="chat-bubble @(msg.Type!=Common.Enums.ConversationType.UE ? "chat-mine": "chat-other")" data-id="@msg.ID">
@if (msg.FileContent != null && msg.FileContent.Length > 0 && !string.IsNullOrWhiteSpace(msg.FileType) && msg.FileType.StartsWith("image/"))
@if (msg.FileContent != null && msg.FileContent.Length > 0 && !string.IsNullOrWhiteSpace(msg.FileType))
{
<a href="@GetImageDataUrl(msg.FileType, msg.FileContent)" download="@GetDownloadFileName(msg.FileName, msg.FileType)" title="دانلود تصویر" style="display:inline-block">
<img src="@GetImageDataUrl(msg.FileType, msg.FileContent)" alt="image" style="max-width: 220px; border-radius: 8px; display: block; cursor: pointer;" />
</a>
@if (msg.FileType.StartsWith("image/"))
{
<a href="@GetImageDataUrl(msg.FileType, msg.FileContent)" download="@GetDownloadFileName(msg.FileName, msg.FileType)" title="دانلود تصویر" style="display:inline-block">
<img src="@GetImageDataUrl(msg.FileType, msg.FileContent)" alt="image" style="max-width: 220px; border-radius: 8px; display: block; cursor: pointer;" />
</a>
}
else if (msg.FileType.StartsWith("audio/"))
{
<div class="audio-message">
<audio controls style="max-width: 250px;">
<source src="@GetAudioDataUrl(msg.FileType, msg.FileContent)" type="@msg.FileType">
مرورگر شما از پخش صدا پشتیبانی نمی‌کند.
</audio>
<div class="audio-info">
<small class="text-muted">@GetAudioDuration(msg.FileContent)</small>
</div>
</div>
}
@if (!string.IsNullOrWhiteSpace(msg.text))
{
<div style="margin-top:6px">@msg.text</div>
@@ -159,10 +175,31 @@
<Button Color="ButtonColor.Secondary" Size=ButtonSize.Small Outline="true" @onclick="OpenFileDialog" Class="attach-btn" title="افزودن تصویر">
<Icon Name="IconName.Image" />
</Button>
<!-- Audio Recording Button -->
<Button Color="@(IsRecording ? ButtonColor.Danger : ButtonColor.Secondary)"
Size=ButtonSize.Small
Outline="true"
@onclick="ToggleAudioRecording"
class=@($"audio-btn {(IsRecording ? "recording" : "")}")
title="@(IsRecording ? "توقف ضبط" : "ضبط صدا")"
>
@if (IsRecording)
{
<Icon Name="IconName.StopCircle" Class="recording-pulse" />
}
else
{
<Icon Name="IconName.Mic" />
}
</Button>
<Button Color="ButtonColor.Primary" Size=ButtonSize.Small @onclick="OnClickSendMsg" Class="send-btn" title="ارسال">
<Icon Name="IconName.Send" />
</Button>
</div>
<!-- Image Preview -->
@if (SelectedImagePreview != null)
{
<div class="mt-2 d-flex align-items-center gap-2">
@@ -170,6 +207,33 @@
<Button Color="ButtonColor.Secondary" Size=ButtonSize.ExtraSmall Outline="true" @onclick="ClearSelectedImage">حذف تصویر</Button>
</div>
}
<!-- Audio Preview -->
@if (RecordedAudioBytes != null)
{
<div class="mt-2 d-flex align-items-center gap-2 audio-preview">
<div class="audio-preview-controls">
<audio controls style="max-width: 250px;">
<source src="@RecordedAudioUrl" type="audio/wav">
</audio>
<div class="audio-preview-info">
<small class="text-muted">@RecordedAudioDuration</small>
</div>
</div>
<Button Color="ButtonColor.Danger" Size=ButtonSize.ExtraSmall Outline="true" @onclick="ClearRecordedAudio">حذف صدا</Button>
</div>
}
<!-- Recording Status -->
@if (IsRecording)
{
<div class="mt-2 recording-status">
<div class="recording-indicator">
<span class="recording-dot"></span>
<span class="recording-text">در حال ضبط... @RecordingTime</span>
</div>
</div>
}
</div>
}
@@ -214,6 +278,16 @@
IBrowserFile? SelectedImageFile = null;
byte[]? SelectedImageBytes = null;
string? SelectedImagePreview = null;
// Audio recording properties
bool IsRecording = false;
string RecordingTime = "00:00";
byte[]? RecordedAudioBytes = null;
string? RecordedAudioUrl = null;
string RecordedAudioDuration = "00:00";
private System.Threading.Timer? recordingTimer;
private DateTime recordingStartTime;
bool chatloading = false;
public bool IsLogin { get; set; } = false;
public bool IsEndFirstProcess { get; set; } = false;
@@ -243,16 +317,118 @@
return value;
}
}
// Audio recording methods
private async Task ToggleAudioRecording()
{
if (IsRecording)
{
await StopAudioRecording();
}
else
{
await StartAudioRecording();
}
}
private async Task StartAudioRecording()
{
try
{
var result = await JS.InvokeAsync<bool>("startAudioRecording");
if (result)
{
IsRecording = true;
recordingStartTime = DateTime.Now;
recordingTimer = new System.Threading.Timer(UpdateRecordingTime, null, 0, 1000);
StateHasChanged();
}
else
{
toastService.Notify(new ToastMessage(ToastType.Warning, "خطا در شروع ضبط صدا"));
}
}
catch (Exception ex)
{
toastService.Notify(new ToastMessage(ToastType.Danger, $"خطا در ضبط صدا: {ex.Message}"));
}
}
private async Task StopAudioRecording()
{
try
{
var audioData = await JS.InvokeAsync<string>("stopAudioRecording");
if (!string.IsNullOrEmpty(audioData))
{
// Convert base64 to bytes
var base64Data = audioData.Split(',')[1];
RecordedAudioBytes = Convert.FromBase64String(base64Data);
RecordedAudioUrl = audioData;
RecordedAudioDuration = RecordingTime;
IsRecording = false;
recordingTimer?.Dispose();
recordingTimer = null;
StateHasChanged();
}
}
catch (Exception ex)
{
toastService.Notify(new ToastMessage(ToastType.Danger, $"خطا در توقف ضبط صدا: {ex.Message}"));
}
finally
{
IsRecording = false;
recordingTimer?.Dispose();
recordingTimer = null;
StateHasChanged();
}
}
private void UpdateRecordingTime(object? state)
{
var elapsed = DateTime.Now - recordingStartTime;
RecordingTime = $"{elapsed.Minutes:D2}:{elapsed.Seconds:D2}";
InvokeAsync(StateHasChanged);
}
private Task ClearRecordedAudio()
{
RecordedAudioBytes = null;
RecordedAudioUrl = null;
RecordedAudioDuration = "00:00";
return Task.CompletedTask;
}
private string GetAudioDataUrl(string? fileType, byte[]? content)
=> (string.IsNullOrWhiteSpace(fileType) || content == null || content.Length == 0)
? string.Empty
: $"data:{fileType};base64,{Convert.ToBase64String(content)}";
private string GetAudioDuration(byte[]? content)
{
// Simple duration calculation based on file size (approximate)
if (content == null || content.Length == 0) return "00:00";
// Assuming 16-bit PCM at 44.1kHz, mono
var bytesPerSecond = 44100 * 2; // 44.1kHz * 2 bytes per sample
var durationSeconds = content.Length / bytesPerSecond;
var minutes = durationSeconds / 60;
var seconds = durationSeconds % 60;
return $"{minutes:D2}:{seconds:D2}";
}
}
@functions {
async Task OnClickSendMsg()
{
if (!string.IsNullOrEmpty(MsgInput) || SelectedImageFile != null)
if (!string.IsNullOrEmpty(MsgInput) || SelectedImageFile != null || RecordedAudioBytes != null)
{
if (LastOpenChat != null)
{
Common.Enums.ConversationType type = Common.Enums.ConversationType.UE;
ChatItemResponseDto? model;
if (SelectedImageFile != null)
{
var bytes = SelectedImageBytes ?? Array.Empty<byte>();
@@ -264,10 +440,23 @@
SelectedImageFile.ContentType,
bytes);
}
else if (RecordedAudioBytes != null)
{
// Send audio message
var fileName = $"audio_{DateTimeOffset.Now.ToUnixTimeSeconds()}.wav";
model = await chatService.ADDChatResponse(
LastOpenChat.ID,
MsgInput,
type,
fileName,
"audio/wav",
RecordedAudioBytes);
}
else
{
model = await chatService.ADDChatResponse(LastOpenChat.ID, MsgInput, type);
}
LastOpenChat?.Responses.Add(model);
LastOpenChat.LastText = MsgInput;
@@ -297,6 +486,11 @@
SelectedImageFile = null;
SelectedImageBytes = null;
SelectedImagePreview = null;
// Clear recorded audio after sending
RecordedAudioBytes = null;
RecordedAudioUrl = null;
RecordedAudioDuration = "00:00";
}
}
@@ -923,6 +1117,192 @@
box-shadow: 0 2px 8px rgba(108, 117, 125, 0.2);
}
/* Audio recording button styling */
.audio-btn {
border-radius: 50%;
width: 38px;
height: 38px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border: 1px solid #e9ecef;
box-shadow: 0 4px 12px rgba(108, 117, 125, 0.2);
transition: all 0.3s ease;
color: #495057;
position: relative;
overflow: hidden;
}
.audio-btn:hover {
transform: translateY(-2px) scale(1.05);
box-shadow: 0 6px 16px rgba(108, 117, 125, 0.3);
background: linear-gradient(135deg, #e9ecef 0%, #dee2e6 100%);
}
.audio-btn:active {
transform: translateY(0) scale(0.95);
box-shadow: 0 2px 8px rgba(108, 117, 125, 0.2);
}
.audio-btn.recording {
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
border-color: #dc3545;
color: white;
animation: recordingPulse 1.5s ease-in-out infinite;
}
.audio-btn.recording:hover {
background: linear-gradient(135deg, #c82333 0%, #bd2130 100%);
transform: translateY(-2px) scale(1.05);
box-shadow: 0 6px 16px rgba(220, 53, 69, 0.4);
}
.recording-pulse {
animation: iconPulse 1s ease-in-out infinite;
}
@@keyframes recordingPulse {
0%, 100% {
box-shadow: 0 4px 12px rgba(220, 53, 69, 0.3);
}
50% {
box-shadow: 0 4px 20px rgba(220, 53, 69, 0.6), 0 0 30px rgba(220, 53, 69, 0.3);
}
}
@@keyframes iconPulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
/* Audio preview styling */
.audio-preview {
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
border: 1px solid #e9ecef;
border-radius: 12px;
padding: 0.75rem;
margin-top: 0.5rem;
}
.audio-preview-controls {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.audio-preview-controls audio {
border-radius: 8px;
background: #f8f9fa;
}
.audio-preview-info {
text-align: center;
color: #6c757d;
font-size: 0.875rem;
}
/* Recording status styling */
.recording-status {
background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
border: 1px solid #ffc107;
border-radius: 12px;
padding: 0.75rem;
margin-top: 0.5rem;
animation: recordingStatusPulse 2s ease-in-out infinite;
}
@@keyframes recordingStatusPulse {
0%, 100% {
box-shadow: 0 2px 8px rgba(255, 193, 7, 0.2);
}
50% {
box-shadow: 0 4px 16px rgba(255, 193, 7, 0.4);
}
}
.recording-indicator {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.recording-dot {
width: 12px;
height: 12px;
background: #dc3545;
border-radius: 50%;
animation: recordingDotPulse 1s ease-in-out infinite;
}
@@keyframes recordingDotPulse {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.7;
transform: scale(1.2);
}
}
.recording-text {
color: #856404;
font-weight: 600;
font-size: 0.875rem;
}
/* Audio message styling in chat */
.audio-message {
display: flex;
flex-direction: column;
gap: 0.5rem;
align-items: center;
}
.audio-message audio {
border-radius: 8px;
background: #f8f9fa;
border: 1px solid #e9ecef;
}
.audio-info {
text-align: center;
color: #6c757d;
font-size: 0.75rem;
}
/* Responsive design for audio elements */
@@media (max-width: 768px) {
.audio-preview-controls audio {
max-width: 200px;
}
.audio-message audio {
max-width: 200px;
}
}
@@media (max-width: 480px) {
.audio-preview-controls audio {
max-width: 180px;
}
.audio-message audio {
max-width: 180px;
}
.recording-text {
font-size: 0.8rem;
}
}
/* Beautiful chat separator styling */
.chat-separator {
text-align: center;
@@ -1209,6 +1589,75 @@
</style>
<script>
// Audio recording variables
let mediaRecorder = null;
let audioChunks = [];
let audioStream = null;
window.startAudioRecording = async () => {
try {
// Request microphone access
audioStream = await navigator.mediaDevices.getUserMedia({ audio: true });
// Create MediaRecorder
mediaRecorder = new MediaRecorder(audioStream, {
mimeType: 'audio/webm;codecs=opus'
});
audioChunks = [];
// Collect audio data
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
audioChunks.push(event.data);
}
};
// Start recording
mediaRecorder.start();
return true;
} catch (error) {
console.error('Error starting audio recording:', error);
return false;
}
};
window.stopAudioRecording = () => {
return new Promise((resolve) => {
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
mediaRecorder.onstop = async () => {
try {
// Create audio blob
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
// Convert to base64
const reader = new FileReader();
reader.onloadend = () => {
// Convert webm to wav format (simplified)
const base64Data = reader.result;
resolve(base64Data);
};
reader.readAsDataURL(audioBlob);
// Stop all tracks
if (audioStream) {
audioStream.getTracks().forEach(track => track.stop());
audioStream = null;
}
} catch (error) {
console.error('Error processing audio:', error);
resolve('');
}
};
mediaRecorder.stop();
} else {
resolve('');
}
});
};
window.scrollToBottom = (elementId) => {
const el = document.getElementById(elementId);
if (el) {