saved_conversation.rs

  1use crate::{assistant_settings::OpenAiModel, MessageId, MessageMetadata};
  2use anyhow::{anyhow, Result};
  3use collections::HashMap;
  4use fs::Fs;
  5use futures::StreamExt;
  6use regex::Regex;
  7use serde::{Deserialize, Serialize};
  8use std::{
  9    cmp::Reverse,
 10    ffi::OsStr,
 11    path::{Path, PathBuf},
 12    sync::Arc,
 13};
 14use util::paths::CONVERSATIONS_DIR;
 15
 16#[derive(Serialize, Deserialize)]
 17pub struct SavedMessage {
 18    pub id: MessageId,
 19    pub start: usize,
 20}
 21
 22#[derive(Serialize, Deserialize)]
 23pub struct SavedConversation {
 24    pub id: Option<String>,
 25    pub zed: String,
 26    pub version: String,
 27    pub text: String,
 28    pub messages: Vec<SavedMessage>,
 29    pub message_metadata: HashMap<MessageId, MessageMetadata>,
 30    pub summary: String,
 31}
 32
 33impl SavedConversation {
 34    pub const VERSION: &'static str = "0.2.0";
 35
 36    pub async fn load(path: &Path, fs: &dyn Fs) -> Result<Self> {
 37        let saved_conversation = fs.load(path).await?;
 38        let saved_conversation_json =
 39            serde_json::from_str::<serde_json::Value>(&saved_conversation)?;
 40        match saved_conversation_json
 41            .get("version")
 42            .ok_or_else(|| anyhow!("version not found"))?
 43        {
 44            serde_json::Value::String(version) => match version.as_str() {
 45                Self::VERSION => Ok(serde_json::from_value::<Self>(saved_conversation_json)?),
 46                "0.1.0" => {
 47                    let saved_conversation =
 48                        serde_json::from_value::<SavedConversationV0_1_0>(saved_conversation_json)?;
 49                    Ok(Self {
 50                        id: saved_conversation.id,
 51                        zed: saved_conversation.zed,
 52                        version: saved_conversation.version,
 53                        text: saved_conversation.text,
 54                        messages: saved_conversation.messages,
 55                        message_metadata: saved_conversation.message_metadata,
 56                        summary: saved_conversation.summary,
 57                    })
 58                }
 59                _ => Err(anyhow!(
 60                    "unrecognized saved conversation version: {}",
 61                    version
 62                )),
 63            },
 64            _ => Err(anyhow!("version not found on saved conversation")),
 65        }
 66    }
 67}
 68
 69#[derive(Serialize, Deserialize)]
 70struct SavedConversationV0_1_0 {
 71    id: Option<String>,
 72    zed: String,
 73    version: String,
 74    text: String,
 75    messages: Vec<SavedMessage>,
 76    message_metadata: HashMap<MessageId, MessageMetadata>,
 77    summary: String,
 78    api_url: Option<String>,
 79    model: OpenAiModel,
 80}
 81
 82pub struct SavedConversationMetadata {
 83    pub title: String,
 84    pub path: PathBuf,
 85    pub mtime: chrono::DateTime<chrono::Local>,
 86}
 87
 88impl SavedConversationMetadata {
 89    pub async fn list(fs: Arc<dyn Fs>) -> Result<Vec<Self>> {
 90        fs.create_dir(&CONVERSATIONS_DIR).await?;
 91
 92        let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?;
 93        let mut conversations = Vec::<SavedConversationMetadata>::new();
 94        while let Some(path) = paths.next().await {
 95            let path = path?;
 96            if path.extension() != Some(OsStr::new("json")) {
 97                continue;
 98            }
 99
100            let pattern = r" - \d+.zed.json$";
101            let re = Regex::new(pattern).unwrap();
102
103            let metadata = fs.metadata(&path).await?;
104            if let Some((file_name, metadata)) = path
105                .file_name()
106                .and_then(|name| name.to_str())
107                .zip(metadata)
108            {
109                // This is used to filter out conversations saved by the new assistant.
110                if !re.is_match(file_name) {
111                    continue;
112                }
113
114                let title = re.replace(file_name, "");
115                conversations.push(Self {
116                    title: title.into_owned(),
117                    path,
118                    mtime: metadata.mtime.into(),
119                });
120            }
121        }
122        conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime));
123
124        Ok(conversations)
125    }
126}