...
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
@using HushianWebApp.Service
|
@using HushianWebApp.Service
|
||||||
@using HushianWebApp.Services
|
@using HushianWebApp.Services
|
||||||
@using Microsoft.AspNetCore.SignalR.Client;
|
@using Microsoft.AspNetCore.SignalR.Client;
|
||||||
|
@using System.Threading;
|
||||||
|
|
||||||
@inject NavigationManager NavigationManager
|
@inject NavigationManager NavigationManager
|
||||||
@inject ChatService ChatService
|
@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="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">
|
<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))
|
||||||
|
{
|
||||||
|
@if (msg.FileType.StartsWith("image/"))
|
||||||
{
|
{
|
||||||
<a href="@GetImageDataUrl(msg.FileType, msg.FileContent)" download="@GetDownloadFileName(msg.FileName, msg.FileType)" title="دانلود تصویر" style="display:inline-block">
|
<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;" />
|
<img src="@GetImageDataUrl(msg.FileType, msg.FileContent)" alt="image" style="max-width: 220px; border-radius: 8px; display: block; cursor: pointer;" />
|
||||||
</a>
|
</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))
|
@if (!string.IsNullOrWhiteSpace(msg.text))
|
||||||
{
|
{
|
||||||
<div style="margin-top:6px">@msg.text</div>
|
<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="افزودن تصویر">
|
<Button Color="ButtonColor.Secondary" Size=ButtonSize.Small Outline="true" @onclick="OpenFileDialog" Class="attach-btn" title="افزودن تصویر">
|
||||||
<Icon Name="IconName.Image" />
|
<Icon Name="IconName.Image" />
|
||||||
</Button>
|
</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="ارسال">
|
<Button Color="ButtonColor.Primary" Size=ButtonSize.Small @onclick="OnClickSendMsg" Class="send-btn" title="ارسال">
|
||||||
<Icon Name="IconName.Send" />
|
<Icon Name="IconName.Send" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Image Preview -->
|
||||||
@if (SelectedImagePreview != null)
|
@if (SelectedImagePreview != null)
|
||||||
{
|
{
|
||||||
<div class="mt-2 d-flex align-items-center gap-2">
|
<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>
|
<Button Color="ButtonColor.Secondary" Size=ButtonSize.ExtraSmall Outline="true" @onclick="ClearSelectedImage">حذف تصویر</Button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,6 +278,16 @@
|
|||||||
IBrowserFile? SelectedImageFile = null;
|
IBrowserFile? SelectedImageFile = null;
|
||||||
byte[]? SelectedImageBytes = null;
|
byte[]? SelectedImageBytes = null;
|
||||||
string? SelectedImagePreview = 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;
|
bool chatloading = false;
|
||||||
public bool IsLogin { get; set; } = false;
|
public bool IsLogin { get; set; } = false;
|
||||||
public bool IsEndFirstProcess { get; set; } = false;
|
public bool IsEndFirstProcess { get; set; } = false;
|
||||||
@@ -243,16 +317,118 @@
|
|||||||
return value;
|
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 {
|
@functions {
|
||||||
async Task OnClickSendMsg()
|
async Task OnClickSendMsg()
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(MsgInput) || SelectedImageFile != null)
|
if (!string.IsNullOrEmpty(MsgInput) || SelectedImageFile != null || RecordedAudioBytes != null)
|
||||||
{
|
{
|
||||||
if (LastOpenChat != null)
|
if (LastOpenChat != null)
|
||||||
{
|
{
|
||||||
Common.Enums.ConversationType type = Common.Enums.ConversationType.UE;
|
Common.Enums.ConversationType type = Common.Enums.ConversationType.UE;
|
||||||
ChatItemResponseDto? model;
|
ChatItemResponseDto? model;
|
||||||
|
|
||||||
if (SelectedImageFile != null)
|
if (SelectedImageFile != null)
|
||||||
{
|
{
|
||||||
var bytes = SelectedImageBytes ?? Array.Empty<byte>();
|
var bytes = SelectedImageBytes ?? Array.Empty<byte>();
|
||||||
@@ -264,10 +440,23 @@
|
|||||||
SelectedImageFile.ContentType,
|
SelectedImageFile.ContentType,
|
||||||
bytes);
|
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
|
else
|
||||||
{
|
{
|
||||||
model = await chatService.ADDChatResponse(LastOpenChat.ID, MsgInput, type);
|
model = await chatService.ADDChatResponse(LastOpenChat.ID, MsgInput, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
LastOpenChat?.Responses.Add(model);
|
LastOpenChat?.Responses.Add(model);
|
||||||
LastOpenChat.LastText = MsgInput;
|
LastOpenChat.LastText = MsgInput;
|
||||||
|
|
||||||
@@ -298,6 +487,11 @@
|
|||||||
SelectedImageBytes = null;
|
SelectedImageBytes = null;
|
||||||
SelectedImagePreview = null;
|
SelectedImagePreview = null;
|
||||||
|
|
||||||
|
// Clear recorded audio after sending
|
||||||
|
RecordedAudioBytes = null;
|
||||||
|
RecordedAudioUrl = null;
|
||||||
|
RecordedAudioDuration = "00:00";
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
@@ -923,6 +1117,192 @@
|
|||||||
box-shadow: 0 2px 8px rgba(108, 117, 125, 0.2);
|
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 */
|
/* Beautiful chat separator styling */
|
||||||
.chat-separator {
|
.chat-separator {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -1209,6 +1589,75 @@
|
|||||||
</style>
|
</style>
|
||||||
<script>
|
<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) => {
|
window.scrollToBottom = (elementId) => {
|
||||||
const el = document.getElementById(elementId);
|
const el = document.getElementById(elementId);
|
||||||
if (el) {
|
if (el) {
|
||||||
|
Reference in New Issue
Block a user