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
350 lines
11 KiB
Plaintext
@* @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;
|
|
}
|
|
}
|
|
|
|
}
|