You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

350 lines
11 KiB
Plaintext

3 months ago
@* @page "/chatroom" *@
@page "/"
@inject NavigationManager navigationManager
@using Microsoft.AspNetCore.SignalR.Client;
@using Microsoft.AspNetCore.SignalR;
@using Newtonsoft.Json;
@using Newtonsoft.Json.Linq
@using System.Net.Http;
@using System.Net.Http.Headers;
@using System.Text;
<h1>Blazor LLM Service Test - by PINBlog</h1>
<hr />
@if (!_isChatting)
{
<p>
Enter your name to start chatting:
</p>
<input type="text" maxlength="32" @bind="@_username" />
<input type="password" maxlength="32" @bind="@_password" />
<button type="button" @onclick="@Chat"><span class="oi oi-chat" aria-hidden="true"></span> Chat!</button>
// Error messages
@if (_message != null)
{
<div class="invalid-feedback">@_message</div>
<small id="emailHelp" class="form-text text-muted">@_message</small>
}
}
else
{
// banner to show current user
<div class="alert alert-secondary mt-4" role="alert">
<span class="oi oi-person mr-2" aria-hidden="true"></span>
<span>You are connected as <b>@_username</b></span>
<button class="btn btn-sm btn-warning ml-md-auto" @onclick="@DisconnectAsync">Disconnect</button>
</div>
// display messages
<div id="scrollbox">
@foreach (var item in _messages)
{
@if (item.IsNotice)
{
<div class="alert alert-info">@item.Body</div>
}
else
{
<div class="@item.CSS">
<div class="user">@item.Username</div>
<div class="msg">@item.Body</div>
</div>
}
}
<hr />
<textarea class="input-lg" placeholder="enter your comment" @bind="@_newMessage" disabled=@isTxtDisabled></textarea>
<button class="btn btn-default" @onclick="@(() => SendAsync(_newMessage))" disabled=@isBtnDisabled>Send</button>
</div>
}
@code {
// flag to indicate chat status
private bool _isChatting = false;
// name of the user who will be chatting
private string _username;
private string _password;
// on-screen message
private string _message;
// new message input
private string _newMessage;
// list of messages in chat
private List<Message> _messages = new List<Message>();
private string _hubUrl;
private HubConnection _hubConnection;
private const string _modelname = "[🤖AI]";
private bool isTxtDisabled;
private bool isBtnDisabled;
private LLMService _llmService = new LLMService();
private List<History> _chatHistory = new List<History>();
public async Task Chat()
{
// check username is valid
if (string.IsNullOrWhiteSpace(_username))
{
_message = "Please enter a name";
return;
};
try
{
if(_password.CompareTo("password") != 0)
{
_message = "Password is different";
return;
}
// Start chatting and force refresh UI, ref: https://github.com/dotnet/aspnetcore/issues/22159
_isChatting = true;
await Task.Delay(1);
// remove old messages if any
_messages.Clear();
_chatHistory.Clear();
_chatHistory.Add(new History {
role = "system",
content = "You are an intelligent assistant. You always provide well-reasoned answers that are both correct and helpful." });
// Create the chat client
string baseUrl = navigationManager.BaseUri;
_hubUrl = baseUrl.TrimEnd('/') + BlazorChatSampleHub.HubUrl;
_hubConnection = new HubConnectionBuilder()
.WithUrl(_hubUrl)
.Build();
// 오류 메시지를 받을 수 있도록 이벤트를 추가합니다.
_hubConnection.On<string>("Error", (errorMessage) =>
{
_message = $"ERROR: {errorMessage}";
_isChatting = false;
});
_hubConnection.On<string, string>("Broadcast", BroadcastMessage);
await _hubConnection.StartAsync();
await SendAsync($"[Notice] {_username} joined chat room.");
}
catch (HubException e)
{
_message = $"ERROR: 채팅 클라이언트 시작 실패: {e.Message}";
_isChatting = false;
}
catch (Exception e)
{
_message = $"ERROR: Failed to start chat client: {e.Message}";
_isChatting = false;
}
}
private void BroadcastMessage(string name, string message)
{
if (name.CompareTo(_username) != 0 &&
name.CompareTo(_modelname) != 0)
{
DisconnectAsync();
return;
}
bool isMine = name.Equals(_username, StringComparison.OrdinalIgnoreCase);
_messages.Add(new Message(name, message, isMine));
// Inform blazor the UI needs updating
// StateHasChanged();
// UI 업데이트를 강제로 실행합니다.
InvokeAsync(StateHasChanged);
// UI 업데이트를 강제로 실행합니다. (SignalR 이벤트핸들러)
// InvokeAsync(() => StateHasChanged());
}
private async Task DisconnectAsync()
{
if (_isChatting)
{
await SendAsync($"[Notice] {_username} left chat room.");
await _hubConnection.StopAsync();
await _hubConnection.DisposeAsync();
_hubConnection = null;
_isChatting = false;
}
}
private async Task SendAsync(string message)
{
if (_isChatting && !string.IsNullOrWhiteSpace(message))
{
await _hubConnection.SendAsync("Broadcast", _username, message);
if (!message.StartsWith("[Notice]"))
{
var userMessage = new History { role = "user", content = message };
_chatHistory.Add(userMessage);
try
{
isTxtDisabled = true;
isBtnDisabled = true;
_message = "Generating a response ...";
var response = await _llmService.SendLLMMessageAsync(_chatHistory);
var objs = _llmService.ParseInputData(response).Result;
string fitSentence = string.Empty;
foreach (var data in objs)
{
if (data["choices"][0]["finish_reason"] != null &&
data["choices"][0]["finish_reason"].ToString().CompareTo("stop") == 0)
{
break;
}
fitSentence += data["choices"][0]["delta"]["content"].ToString();
}
await _hubConnection.SendAsync("Broadcast", _modelname, fitSentence);
var aiMessage = new History { role = "assistant", content = fitSentence };
_chatHistory.Add(aiMessage);
// fit contents
int max_context_length = 8192;
int cur_context_length = _chatHistory[0].content.Length; // system
for(int i=_chatHistory.Count-1; i>=0; i--)
{
var content = _llmService.ParseInputData(_chatHistory[i].content).Result;
string fitContent = string.Empty;
foreach (var data in content)
{
if (data["choices"][0]["finish_reason"] != null &&
data["choices"][0]["finish_reason"].ToString().CompareTo("stop") == 0)
{
break;
}
fitContent += data["choices"][0]["delta"]["content"].ToString();
}
cur_context_length += fitContent.Length;
if(cur_context_length > max_context_length)
{
_chatHistory.RemoveRange(1, i);
break;
}
}
}
catch (Exception ex)
{
_chatHistory.Add(new History { role = "error", content = $"Error: {ex.Message}" });
}
finally
{
isTxtDisabled = false;
isBtnDisabled = false;
_message = string.Empty;
}
}
_newMessage = string.Empty;
}
}
private class Message
{
public Message(string username, string body, bool mine)
{
Username = username;
Body = body;
Mine = mine;
}
public string Username { get; set; }
public string Body { get; set; }
public bool Mine { get; set; }
public bool IsNotice => Body.StartsWith("[Notice]");
public string CSS => Mine ? "sent" : "received";
}
private class History
{
public string role { get; set; }
public string content { get; set; }
}
private class LLMService
{
private readonly HttpClient _httpClient;
private const string ApiUrl = "";
// private const string ApiKey = "lm-studio";
public LLMService()
{
_httpClient = new HttpClient();
_httpClient.Timeout = TimeSpan.FromMinutes(5);
}
public async Task<string> SendLLMMessageAsync(List<History> messages)
{
var payload = new
{
model = "lmstudio-community/Meta-Llama-3-8B-Instruct-GGUF",
temperature = 0.8,
stream = true,
messages = messages
};
var content = new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync(ApiUrl, content);
// response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
return responseString;
}
public async Task<List<JObject>> ParseInputData(string inputData)
{
List<JObject> jsonObjects = new List<JObject>();
// Split the input data by newlines and filter out empty lines
var lines = inputData.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
string trimmedLine = line.Trim();
if (trimmedLine.StartsWith("data:"))
{
string jsonString = trimmedLine.Substring(5).Trim();
try
{
JObject jsonObject = JObject.Parse(jsonString);
jsonObjects.Add(jsonObject);
}
catch (JsonException ex)
{
Console.WriteLine($"Failed to parse JSON: {ex.Message}");
}
}
}
return jsonObjects;
}
}
}