...
This commit is contained in:
@@ -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) {
|
||||
|
Reference in New Issue
Block a user