Inital Commit for VIVO-Temrinal
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/target
|
||||
Generated
+3268
File diff suppressed because it is too large
Load Diff
+15
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "VIVO-Terminal"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
color-eyre = "0.6.5"
|
||||
ratatui = "0.30.2"
|
||||
crossterm = "0.29.0"
|
||||
tokio = { version = "1.53.1", features = ["full"] }
|
||||
reqwest = { version = "0.13.4", features = ["json"] }
|
||||
axum = "0.8.9"
|
||||
serde = { version = "1.0.229", features = ["derive"] }
|
||||
serde_json = "1.0.151"
|
||||
textwrap = "0.16.2"
|
||||
+290
@@ -0,0 +1,290 @@
|
||||
mod rest;
|
||||
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use crossterm::event;
|
||||
use crossterm::event::{Event, KeyCode, KeyModifiers};
|
||||
use ratatui::{DefaultTerminal, Frame};
|
||||
use ratatui::layout::{Alignment, Constraint, Layout, Position};
|
||||
use ratatui::prelude::Direction;
|
||||
use ratatui::style::{Color, Style};
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Wrap};
|
||||
use crate::rest::REST;
|
||||
use textwrap::wrap;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> color_eyre::Result<()> {
|
||||
color_eyre::install()?;
|
||||
let mut terminal = ratatui::init();
|
||||
let result = App::new().run(&mut terminal);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
input: String,
|
||||
should_quit: bool,
|
||||
pos: usize,
|
||||
messages: Arc<Mutex<Vec<String>>>,
|
||||
rest: REST,
|
||||
list_state: ListState,
|
||||
reply_rx: mpsc::UnboundedReceiver<String>,
|
||||
reply_tx: mpsc::UnboundedSender<String>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new() -> Self {
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
App{
|
||||
input: String::new(),
|
||||
should_quit: false,
|
||||
pos: 0,
|
||||
messages: Arc::new(Mutex::new(Vec::new())),
|
||||
rest: REST::new(),
|
||||
list_state: ListState::default(),
|
||||
reply_rx: rx,
|
||||
reply_tx: tx,
|
||||
}
|
||||
}
|
||||
|
||||
fn run(&mut self, terminal: &mut DefaultTerminal) -> std::io::Result<()> {
|
||||
while !self.should_quit {
|
||||
terminal.draw(|frame| self.render(frame))?;
|
||||
if crossterm::event::poll(Duration::from_millis(500))? {
|
||||
self.handle_event()?;
|
||||
}
|
||||
self.drain_replies();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_event(&mut self) -> std::io::Result<()> {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char(c) => {
|
||||
if(key.modifiers == KeyModifiers::CONTROL)
|
||||
{
|
||||
self.should_quit = true;
|
||||
}
|
||||
else {
|
||||
self.enter_char(c)
|
||||
}
|
||||
},
|
||||
KeyCode::Left => self.move_cursor_left(),
|
||||
KeyCode::Right => self.move_cursor_right(),
|
||||
KeyCode::Home => self.reset_cursor(),
|
||||
KeyCode::End => self.pos = self.input.len() - 1,
|
||||
KeyCode::Backspace => self.delete_char(),
|
||||
KeyCode::Delete => self.delete_forward_char(),
|
||||
KeyCode::Esc => self.should_quit = true,
|
||||
KeyCode::Tab => self.should_quit = true,
|
||||
KeyCode::Up => self.move_view_up(),
|
||||
KeyCode::Down => self.move_view_down(),
|
||||
KeyCode::Enter => {
|
||||
let strMessage = self.input.clone();
|
||||
|
||||
self.push_message(strMessage.clone());
|
||||
|
||||
self.reset_cursor();
|
||||
self.input = String::new();
|
||||
let message = rest::Message::new(
|
||||
strMessage.clone(),
|
||||
"USER".parse().unwrap(),
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
|
||||
let tx = self.reply_tx.clone();
|
||||
let mut rest = self.rest.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
match rest.send(message).await {
|
||||
Ok(reply) => {
|
||||
if(reply.role == "ASSISTANT")
|
||||
{
|
||||
let _ = tx.send(reply.message);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("{}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn drain_replies(&mut self) {
|
||||
while let Ok(msg) = self.reply_rx.try_recv() {
|
||||
self.push_message(msg);
|
||||
}
|
||||
}
|
||||
|
||||
fn move_view_down(&mut self) {
|
||||
let i = match self.list_state.selected() {
|
||||
Some(i) => (i+1).min(self.messages.lock().unwrap().len().saturating_sub(1)),
|
||||
None => 0,
|
||||
};
|
||||
self.list_state.select(Some(i));
|
||||
}
|
||||
|
||||
fn move_view_up(&mut self) {
|
||||
let i = match self.list_state.selected() {
|
||||
Some(i) => i.saturating_sub(1),
|
||||
None => self.messages.lock().unwrap().len().saturating_sub(1),
|
||||
};
|
||||
self.list_state.select(Some(i));
|
||||
}
|
||||
|
||||
fn push_message(&mut self, message: String) {
|
||||
let mut messages = self.messages.lock().unwrap();
|
||||
messages.push(message);
|
||||
let last = messages.len().saturating_sub(1);
|
||||
self.list_state.select(Some(last));
|
||||
}
|
||||
|
||||
fn delete_char(&mut self) {
|
||||
let is_not_cursor_leftmost = self.pos != 0;
|
||||
if is_not_cursor_leftmost {
|
||||
let current_index = self.pos;
|
||||
let from_left_to_current_index = current_index - 1;
|
||||
|
||||
let before_chat_to_delete = self.input.chars().take(from_left_to_current_index);
|
||||
let after_chat_to_delete = self.input.chars().skip(current_index);
|
||||
self.input = before_chat_to_delete.chain(after_chat_to_delete).collect();
|
||||
self.move_cursor_left();
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_forward_char(&mut self) {
|
||||
let is_not_the_rightmost = self.pos != self.input.len();
|
||||
if is_not_the_rightmost {
|
||||
let current_index = self.pos;
|
||||
let from_left_to_current_index = current_index;
|
||||
|
||||
let before_chat_to_delete = self.input.chars().take(from_left_to_current_index);
|
||||
let after_chat_to_delete = self.input.chars().skip(from_left_to_current_index+1);
|
||||
|
||||
self.input = before_chat_to_delete.chain(after_chat_to_delete).collect();
|
||||
//self.move_cursor_left();
|
||||
}
|
||||
}
|
||||
|
||||
fn move_cursor_left(&mut self) {
|
||||
let pos = self.pos.saturating_sub(1);
|
||||
self.pos = self.clamp_cursor(pos);
|
||||
}
|
||||
|
||||
fn move_cursor_right(&mut self) {
|
||||
let pos = self.pos.saturating_add(1);
|
||||
self.pos = self.clamp_cursor(pos);
|
||||
}
|
||||
|
||||
fn clamp_cursor(&self, pos: usize) -> usize {
|
||||
pos.clamp(0, self.input.len())
|
||||
}
|
||||
|
||||
fn reset_cursor(&mut self) {
|
||||
self.pos = 0;
|
||||
}
|
||||
|
||||
fn enter_char(&mut self, new_char: char) {
|
||||
let index = self.byte_index();
|
||||
self.input.insert(index, new_char);
|
||||
self.move_cursor_right();
|
||||
}
|
||||
|
||||
fn byte_index(&self) -> usize {
|
||||
self.input
|
||||
.char_indices()
|
||||
.map(|(index, _)| index)
|
||||
.nth(self.pos)
|
||||
.unwrap_or(self.input.len())
|
||||
}
|
||||
|
||||
fn render(&mut self, frame: &mut Frame) {
|
||||
let layout = Layout::vertical([
|
||||
Constraint::Percentage(70),
|
||||
Constraint::Min(3),
|
||||
]);
|
||||
|
||||
let [chat_area, input_area] = frame.area().layout(&layout);
|
||||
|
||||
if chat_area.height < 10 {
|
||||
// too small — render a warning instead of your normal UI
|
||||
let warning = Paragraph::new("Terminal window too small!")
|
||||
.alignment(Alignment::Center);
|
||||
frame.render_widget(warning, frame.area());
|
||||
return;
|
||||
}
|
||||
|
||||
let messages = self.messages.lock().unwrap();
|
||||
|
||||
let items: Vec<ListItem> = messages
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, msg)| {
|
||||
let mut lines: Vec<Line>= wrap(msg.as_str(), chat_area.width.saturating_sub(2) as usize)
|
||||
.iter()
|
||||
.map(|s| Line::from(s.to_string()))
|
||||
.collect();
|
||||
|
||||
if i != messages.len() - 1 {
|
||||
// separator row — width matches the area you render into
|
||||
let sep = "─".repeat(chat_area.width as usize);
|
||||
lines.push(Line::styled(sep, Style::default().fg(Color::DarkGray)));
|
||||
}
|
||||
|
||||
ListItem::new(lines)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let list = List::new(items)
|
||||
.block(Block::default().borders(Borders::ALL).title("Messages"));
|
||||
|
||||
frame.render_stateful_widget(list, chat_area, &mut self.list_state);
|
||||
|
||||
|
||||
let inner_width = input_area.width.saturating_sub(2) as usize;
|
||||
|
||||
let wrapped_lines = wrap(&self.input, inner_width);
|
||||
|
||||
frame.render_widget(
|
||||
Paragraph::new(self.input.as_str())
|
||||
.block(Block::new().borders(Borders::ALL))
|
||||
.wrap(Wrap { trim: false }),
|
||||
input_area,
|
||||
);
|
||||
|
||||
let (cursor_line, cursor_col) = {
|
||||
let mut remaining = self.pos;
|
||||
let mut line_idx = 0;
|
||||
let mut col = 0;
|
||||
|
||||
for (i, line) in wrapped_lines.iter().enumerate() {
|
||||
let len = line.chars().count();
|
||||
if remaining <= len {
|
||||
line_idx = i;
|
||||
col = remaining;
|
||||
break;
|
||||
}
|
||||
remaining -= len;
|
||||
line_idx = i + 1;
|
||||
col = remaining; // handles case where pos is exactly at end of input
|
||||
}
|
||||
|
||||
(line_idx, col)
|
||||
};
|
||||
|
||||
frame.set_cursor_position(Position::new(
|
||||
input_area.x + cursor_col as u16 + 1,
|
||||
input_area.y + cursor_line as u16 + 1,
|
||||
))
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Message {
|
||||
pub message: String,
|
||||
pub role: String,
|
||||
pub finished: bool,
|
||||
pub no_voice: bool,
|
||||
pub stream: bool,
|
||||
}
|
||||
|
||||
impl Message {
|
||||
pub fn new(message: String, role: String, finished: bool, no_voice: bool, stream: bool) -> Self {
|
||||
Message {
|
||||
message,
|
||||
role,
|
||||
finished,
|
||||
no_voice,
|
||||
stream,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct REST {
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl REST {
|
||||
pub fn new() -> Self {
|
||||
REST{
|
||||
client: Client::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send(&mut self, mut message: Message) -> Result<Message, reqwest::Error> {
|
||||
// enforcing this as the current REST API stack dose not support this.
|
||||
message.stream = false;
|
||||
let res = self.client
|
||||
.post("http://localhost:8080/v1/message/send")
|
||||
.json(&message)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json::<Message>()
|
||||
.await?;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user