Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make history persistent #40

Merged
merged 5 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
- Vim keybinding (most common ops)
- Copy text from/to clipboard (works only on the prompt)
- Multiple backends
- Automatically load the last saved chat into history

<br>

Expand Down Expand Up @@ -84,14 +85,12 @@ tenere -c ~/path/to/custom/config.toml

Here are the available general settings:

- `archive_file_name`: the file name where the chat will be saved. By default it is set to `tenere.archive`
- `llm`: the llm model name. Possible values are:
- `chatgpt`
- `llamacpp`
- `ollama`

```toml
archive_file_name = "tenere.archive"
llm = "chatgpt"
```

Expand All @@ -106,7 +105,6 @@ Here is an example with the default key bindings
show_help = '?'
show_history = 'h'
new_chat = 'n'
save_chat = 's'
```

ℹ️ Note
Expand Down Expand Up @@ -185,9 +183,7 @@ More infos about ollama api [here](https://github.com/ollama/ollama/blob/main/do

These are the default key bindings regardless of the focused block.

`ctrl + n`: Start a new chat and save the previous one in history.

`ctrl + s`: Save the current chat or chat history (history pop-up should be visible first) to `tenere.archive` file in the current directory.
`ctrl + n`: Start a new chat and save the previous one in history and save it to `tenere.archive-i` file in `data directory`.

`Tab`: Switch the focus.

Expand Down
15 changes: 0 additions & 15 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ use std::path::PathBuf;

#[derive(Deserialize, Debug)]
pub struct Config {
#[serde(default = "default_archive_file_name")]
pub archive_file_name: String,

#[serde(default)]
pub key_bindings: KeyBindings,

Expand All @@ -24,10 +21,6 @@ pub struct Config {
pub ollama: Option<OllamaConfig>,
}

pub fn default_archive_file_name() -> String {
String::from("tenere.archive")
}

pub fn default_llm_backend() -> LLMBackend {
LLMBackend::ChatGPT
}
Expand Down Expand Up @@ -91,9 +84,6 @@ pub struct KeyBindings {
#[serde(default = "KeyBindings::default_new_chat")]
pub new_chat: char,

#[serde(default = "KeyBindings::default_save_chat")]
pub save_chat: char,

#[serde(default = "KeyBindings::default_stop_stream")]
pub stop_stream: char,
}
Expand All @@ -104,7 +94,6 @@ impl Default for KeyBindings {
show_help: '?',
show_history: 'h',
new_chat: 'n',
save_chat: 's',
stop_stream: 't',
}
}
Expand All @@ -123,10 +112,6 @@ impl KeyBindings {
'n'
}

fn default_save_chat() -> char {
's'
}

fn default_stop_stream() -> char {
't'
}
Expand Down
37 changes: 2 additions & 35 deletions src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};

use ratatui::text::Line;

use crate::notification::{Notification, NotificationLevel};
use std::sync::Arc;
use tokio::sync::Mutex;

Expand Down Expand Up @@ -111,6 +110,8 @@ pub async fn handle_key_events(
.push(app.chat.formatted_chat.clone());

app.history.text.push(app.chat.plain_chat.clone());
// after adding to history, save the chat in file
app.history.save(app.history.text.len() - 1, sender.clone());

app.chat = Chat::default();

Expand All @@ -123,40 +124,6 @@ pub async fn handle_key_events(
app.chat.scroll = 0;
}

// Save chat
KeyCode::Char(c)
if c == app.config.key_bindings.save_chat
&& key_event.modifiers == KeyModifiers::CONTROL =>
{
match app.focused_block {
FocusedBlock::History | FocusedBlock::Preview => {
app.history
.save(app.config.archive_file_name.as_str(), sender.clone());
}
FocusedBlock::Chat | FocusedBlock::Prompt => {
match std::fs::write(
app.config.archive_file_name.clone(),
app.chat.plain_chat.join(""),
) {
Ok(_) => {
let notif = Notification::new(
format!("Chat saved to `{}` file", app.config.archive_file_name),
NotificationLevel::Info,
);

sender.send(Event::Notification(notif)).unwrap();
}
Err(e) => {
let notif = Notification::new(e.to_string(), NotificationLevel::Error);

sender.send(Event::Notification(notif)).unwrap();
}
}
}
_ => (),
}
}

// Switch the focus
KeyCode::Tab => match app.focused_block {
FocusedBlock::Chat => {
Expand Down
4 changes: 0 additions & 4 deletions src/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,6 @@ impl Default for Help {
Cell::from("ctrl + n").bold().yellow(),
"Start new chat and save the previous one to the history",
),
(
Cell::from("ctrl + s").bold().yellow(),
"Save the chat to file in the current directory",
),
(Cell::from("ctrl + h").bold().yellow(), "Show history"),
(
Cell::from("ctrl + t").bold().yellow(),
Expand Down
64 changes: 55 additions & 9 deletions src/history.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
use core::str;
use std::{fs, path::PathBuf};

use tokio::sync::mpsc::UnboundedSender;

use ratatui::{
Expand Down Expand Up @@ -81,17 +84,60 @@ impl History<'_> {
self.state.select(Some(i));
}

pub fn save(&mut self, archive_file_name: &str, sender: UnboundedSender<Event>) {
// check if data directory for the application exists, else it will create it
pub fn check_data_directory_exists(&self, sender: UnboundedSender<Event>) {
if let Some(data_directory) = dirs::data_dir() {
let target_directory = data_directory.join("tenere");

if !target_directory.exists() {
if let Err(e) = fs::create_dir_all(target_directory) {
let notif = Notification::new(e.to_string(), NotificationLevel::Error);
sender.send(Event::Notification(notif)).unwrap();
}
}
}
}

// load chat in the history from data directory
pub fn load_history(&mut self, sender: UnboundedSender<Event>) {
let directory_path: PathBuf = dirs::data_dir().unwrap().join("tenere");

if let Ok(paths) = fs::read_dir(directory_path.clone()) {
// foreach archive file we add it to history
for path in paths {
if path.as_ref().unwrap().file_type().unwrap().is_file() {
self.load_chat_from_file(path.unwrap().path().to_str().unwrap());
}
}

let notif = Notification::new("History loaded".to_string(), NotificationLevel::Info);

sender.send(Event::Notification(notif)).unwrap();
}
}

/// Add to history the archive file if exists
pub fn load_chat_from_file(&mut self, archive_file_name: &str) {
if let Ok(text) = std::fs::read_to_string(archive_file_name) {
// push full conversation in preview
self.preview.text.push(Text::from(text.clone()));
// get first line of the conversation
let first_line: String = text.lines().next().unwrap_or("").to_string();
self.text.push(vec![first_line]);
}
}

// call after adding new chat in history (Starting a new chat)
// with the index of the chat in history to save
pub fn save(&mut self, chat_index_in_history: usize, sender: UnboundedSender<Event>) {
let file_name = format!("tenere.archive-{}", chat_index_in_history);
let file_path: PathBuf = dirs::data_dir().unwrap().join("tenere").join(file_name);

if !self.text.is_empty() {
match std::fs::write(
archive_file_name,
self.text[self.state.selected().unwrap_or(0)].join(""),
) {
match std::fs::write(file_path.clone(), self.text[chat_index_in_history].join("")) {
Ok(_) => {
let notif = Notification::new(
format!("Chat saved to `{}` file", archive_file_name),
NotificationLevel::Info,
);
let notif =
Notification::new("Chat saved".to_string(), NotificationLevel::Info);

sender.send(Event::Notification(notif)).unwrap();
}
Expand Down
7 changes: 7 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ async fn main() -> AppResult<()> {
let mut tui = Tui::new(terminal, events);
tui.init()?;

// create data directory if not exists
app.history
.check_data_directory_exists(tui.events.sender.clone());

// load potential history data from archive files
app.history.load_history(tui.events.sender.clone());

while app.running {
tui.draw(&mut app)?;
match tui.events.next().await? {
Expand Down