From 194e52e47c2a73f126fe26978493f05e38d24573 Mon Sep 17 00:00:00 2001 From: Gabriel Gordon-Hall Date: Mon, 30 Oct 2023 12:18:16 +0000 Subject: [PATCH 01/88] wip: multi-repo wip: multirepo wip: multirepo Use RepoPath in more places Minor things Clippy & some small things Resurrect explain endpoint Add Project to link references Clean up call sites to semantic search Always search on display name We can propagate multiple repos, so this shouldn't be needed Scope fuzzy queries to a project Use RepoRef in RepoPath Use RepoPath for relative_path refs Add back sqlx data Add back branch filters to fuzzy matching This is just awful Eh, need correct json here --------- Co-authored-by: rsdy --- server/bleep/src/agent.rs | 102 ++++++++++-------- server/bleep/src/agent/exchange.rs | 30 ++++-- server/bleep/src/agent/prompts.rs | 16 +-- server/bleep/src/agent/symbol.rs | 35 +++--- server/bleep/src/agent/tools/answer.rs | 29 +++-- server/bleep/src/agent/tools/code.rs | 54 ++++++---- server/bleep/src/agent/tools/path.rs | 27 +++-- server/bleep/src/agent/tools/proc.rs | 40 +++---- server/bleep/src/analytics.rs | 7 +- server/bleep/src/indexes/file.rs | 50 +++++---- server/bleep/src/indexes/reader.rs | 5 +- .../bleep/src/intelligence/code_navigation.rs | 15 ++- server/bleep/src/query/execute.rs | 4 +- server/bleep/src/query/parser.rs | 8 -- server/bleep/src/semantic.rs | 37 +++++-- server/bleep/src/semantic/schema.rs | 6 +- server/bleep/src/webserver/answer.rs | 32 +++--- .../src/webserver/answer/conversations.rs | 18 ++-- server/bleep/src/webserver/intelligence.rs | 2 + server/bleep/src/webserver/studio.rs | 4 +- 20 files changed, 301 insertions(+), 220 deletions(-) diff --git a/server/bleep/src/agent.rs b/server/bleep/src/agent.rs index 09a43e61ca..8366833e91 100644 --- a/server/bleep/src/agent.rs +++ b/server/bleep/src/agent.rs @@ -6,12 +6,13 @@ use tokio::sync::mpsc::Sender; use tracing::{debug, error, info, instrument}; use crate::{ + agent::exchange::RepoPath, analytics::{EventData, QueryEvent}, indexes::reader::{ContentDocument, FileDocument}, llm_gateway::{self, api::FunctionCall}, query::{parser, stopwords::remove_stopwords}, repo::RepoRef, - semantic, + semantic::{self, SemanticSearchParams}, webserver::{ answer::conversations::{self, ConversationId}, middleware::User, @@ -47,9 +48,28 @@ pub enum Error { Processing(anyhow::Error), } +/// A unified way to track a collection of repositories +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub struct Project(pub Vec); + +impl Project { + /// This is a temporary thing to keep backwards compatibility. + /// We should have a UUID here to track this stuff consistently. + pub fn id(&self) -> String { + self.0 + .get(0) + .map(ToString::to_string) + .expect("invalid project configuration") + } + + pub fn repos(&self) -> impl Iterator + '_ { + self.0.iter().map(|r| r.display_name()) + } +} + pub struct Agent { pub app: Application, - pub repo_ref: RepoRef, + pub project: Project, pub exchanges: Vec, pub exchange_tx: Sender, @@ -73,6 +93,13 @@ pub enum ExchangeState { Failed, } +pub struct AgentSemanticSearchParams<'a> { + pub query: parser::Literal<'a>, + pub paths: Vec, + pub project: Project, + pub semantic_params: SemanticSearchParams, +} + /// We use a `Drop` implementation to track agent query cancellation. /// /// Query control flow can be complex, as there are several points where an error may be returned @@ -139,7 +166,6 @@ impl Agent { let event = QueryEvent { query_id: self.query_id, thread_id: self.thread_id, - repo_ref: Some(self.repo_ref.clone()), data, }; self.app.track_query(&self.user, &event); @@ -153,27 +179,23 @@ impl Agent { self.exchanges.last_mut().expect("exchange list was empty") } - fn paths(&self) -> impl Iterator { - self.exchanges - .iter() - .flat_map(|e| e.paths.iter()) - .map(String::as_str) + fn paths(&self) -> impl Iterator { + self.exchanges.iter().flat_map(|e| e.paths.iter()) } - fn get_path_alias(&mut self, path: &str) -> usize { + fn get_path_alias(&mut self, repo_path: &RepoPath) -> usize { // This has to be stored a variable due to a Rust NLL bug: // https://github.com/rust-lang/rust/issues/51826 - let pos = self.paths().position(|p| p == path); + let pos = self.paths().position(|p| p == repo_path); if let Some(i) = pos { i } else { let i = self.paths().count(); - self.last_exchange_mut().paths.push(path.to_owned()); + self.last_exchange_mut().paths.push(repo_path.clone()); i } } - #[instrument(skip(self))] pub async fn step(&mut self, action: Action) -> Result> { info!(?action, %self.thread_id, "executing next action"); @@ -341,15 +363,19 @@ impl Agent { Ok(history) } + #[allow(clippy::too_many_arguments)] async fn semantic_search( &self, - query: parser::Literal<'_>, - paths: Vec, - params: semantic::SemanticSearchParams, + AgentSemanticSearchParams { + query, + paths, + project, + semantic_params, + }: AgentSemanticSearchParams<'_>, ) -> Result> { let paths_set = paths .into_iter() - .map(|p| parser::Literal::Plain(p.into())) + .map(|p| parser::Literal::Plain(p.path.into())) .collect::>(); let paths = if paths_set.is_empty() { @@ -384,47 +410,29 @@ impl Agent { let query = parser::SemanticQuery { target: Some(query), - repos: [parser::Literal::Plain(self.repo_ref.display_name().into())].into(), + repos: project + .repos() + .map(|r| parser::Literal::Plain(r.into())) + .collect(), paths, ..self.last_exchange().query.clone() }; debug!(?query, %self.thread_id, "executing semantic query"); - self.app.semantic.search(&query, params).await + self.app.semantic.search(&query, semantic_params).await } - #[allow(dead_code)] - async fn batch_semantic_search( + async fn get_file_content( &self, - queries: Vec>, - params: semantic::SemanticSearchParams, - ) -> Result> { - let queries = queries - .iter() - .map(|q| parser::SemanticQuery { - target: Some(q.clone()), - repos: [parser::Literal::Plain(self.repo_ref.display_name().into())].into(), - ..self.last_exchange().query.clone() - }) - .collect::>(); - - let queries = queries.iter().collect::>(); - - debug!(?queries, %self.thread_id, "executing semantic query"); - self.app - .semantic - .batch_search(queries.as_slice(), params) - .await - } - - async fn get_file_content(&self, path: &str) -> Result> { + RepoPath { repo, path }: &RepoPath, + ) -> Result> { let branch = self.last_exchange().query.first_branch(); - debug!(%self.repo_ref, path, ?branch, %self.thread_id, "executing file search"); + debug!(%repo, path, ?branch, %self.thread_id, "executing file search"); self.app .indexes .file - .by_path(&self.repo_ref, path, branch.as_deref()) + .by_path(repo, path, branch.as_deref()) .await .with_context(|| format!("failed to read path: {}", path)) } @@ -436,11 +444,11 @@ impl Agent { let branch = self.last_exchange().query.first_branch(); let langs = self.last_exchange().query.langs.iter().map(Deref::deref); - debug!(%self.repo_ref, query, ?branch, %self.thread_id, "executing fuzzy search"); + debug!(?self.project, query, ?branch, %self.thread_id, "executing fuzzy search"); self.app .indexes .file - .fuzzy_path_match(&self.repo_ref, query, branch.as_deref(), langs, 50) + .fuzzy_path_match(self.project.clone(), branch.as_deref(), query, langs, 50) .await } @@ -450,7 +458,7 @@ impl Agent { // NB: This isn't an `async fn` so as to not capture a lifetime. fn store(&mut self) -> impl Future { let sql = Arc::clone(&self.app.sql); - let conversation = (self.repo_ref.clone(), self.exchanges.clone()); + let conversation = (self.project.clone(), self.exchanges.clone()); let conversation_id = self .user .username() diff --git a/server/bleep/src/agent/exchange.rs b/server/bleep/src/agent/exchange.rs index 50b079b8ba..9ed20c7f36 100644 --- a/server/bleep/src/agent/exchange.rs +++ b/server/bleep/src/agent/exchange.rs @@ -1,8 +1,20 @@ -use crate::query::parser::SemanticQuery; +use crate::{query::parser::SemanticQuery, repo::RepoRef}; use std::fmt; use chrono::prelude::{DateTime, Utc}; +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Hash, PartialEq, Eq)] +pub struct RepoPath { + pub repo: RepoRef, + pub path: String, +} + +impl fmt::Display for RepoPath { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} {}", self.repo, self.path) + } +} + /// A continually updated conversation exchange. /// /// This contains the query from the user, the intermediate steps the model takes, and the final @@ -13,7 +25,7 @@ pub struct Exchange { pub query: SemanticQuery<'static>, pub answer: Option, pub search_steps: Vec, - pub paths: Vec, + pub paths: Vec, pub code_chunks: Vec, /// A specifically chosen "focused" code chunk. @@ -110,7 +122,7 @@ pub enum SearchStep { }, Proc { query: String, - paths: Vec, + paths: Vec, response: String, }, } @@ -148,7 +160,7 @@ impl SearchStep { #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub struct CodeChunk { - pub path: String, + pub repo_path: RepoPath, pub alias: usize, pub snippet: String, #[serde(rename = "start")] @@ -168,13 +180,17 @@ impl CodeChunk { impl fmt::Display for CodeChunk { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}: {}\n{}", self.alias, self.path, self.snippet) + write!( + f, + "{}: {}\t{}\n{}", + self.alias, self.repo_path.repo, self.repo_path.path, self.snippet + ) } } -#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Default)] +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] pub struct FocusedChunk { - pub file_path: String, + pub repo_path: RepoPath, pub start_line: usize, pub end_line: usize, } diff --git a/server/bleep/src/agent/prompts.rs b/server/bleep/src/agent/prompts.rs index 0659931284..2591cf83e2 100644 --- a/server/bleep/src/agent/prompts.rs +++ b/server/bleep/src/agent/prompts.rs @@ -1,9 +1,11 @@ +use crate::agent::exchange::RepoPath; + pub fn functions(add_proc: bool) -> serde_json::Value { let mut funcs = serde_json::json!( [ { "name": "code", - "description": "Search the contents of files in a codebase semantically. Results will not necessarily match search terms exactly, but should be related.", + "description": "Search within the files in a codebase semantically. Results will not necessarily match search terms exactly, but should be related.", "parameters": { "type": "object", "properties": { @@ -66,7 +68,7 @@ pub fn functions(add_proc: bool) -> serde_json::Value { "type": "array", "items": { "type": "integer", - "description": "The indices of the paths to search." + "description": "The indices of the paths to search. Search paths that you think are most likely to be relevant." } } }, @@ -79,15 +81,17 @@ pub fn functions(add_proc: bool) -> serde_json::Value { funcs } -pub fn system<'a>(paths: impl IntoIterator) -> String { +pub fn system<'a>(paths: impl IntoIterator) -> String { let mut s = "".to_string(); let mut paths = paths.into_iter().peekable(); if paths.peek().is_some() { - s.push_str("## PATHS ##\nindex, path\n"); + s.push_str("## PATHS ##\nindex, repo, path\n"); for (i, path) in paths.enumerate() { - s.push_str(&format!("{}, {}\n", i, path)); + let repo = path.repo.display_name(); + let path = &path.path; + s.push_str(&format!("{}, {}, {}\n", i, repo, path)); } s.push('\n'); } @@ -117,7 +121,7 @@ pub fn answer_article_prompt(context: &str) -> String { format!( r#"{context}#### -You are an expert programmer called 'bloop' and you are helping a junior colleague answer questions about a codebase using the information above. If their query refers to 'this' or 'it' and there is no other context, assume that it refers to the information above. +You are an expert programmer called 'bloop' and you are helping a junior colleague answer questions about a codebase (consisting of several repos) using the information above. If their query refers to 'this' or 'it' and there is no other context, assume that it refers to the information above. Provide only as much information and code as is necessary to answer the query, but be concise. Keep number of quoted lines to a minimum when possible. If you do not have enough information needed to answer the query, do not make up an answer. Infer as much as possible from the information above. When referring to code, you must provide an example in a code block. diff --git a/server/bleep/src/agent/symbol.rs b/server/bleep/src/agent/symbol.rs index f279724e82..f59c820204 100644 --- a/server/bleep/src/agent/symbol.rs +++ b/server/bleep/src/agent/symbol.rs @@ -5,6 +5,7 @@ use crate::webserver::intelligence::{get_token_info, TokenInfoRequest}; use anyhow::{Context, Result}; use tracing::log::{debug, info, warn}; +use super::exchange::RepoPath; use super::prompts::symbol_classification_prompt; pub struct ChunkWithHoverableSymbols { @@ -30,14 +31,14 @@ impl Agent { .app .indexes .file - .by_path(&self.repo_ref, &chunk.path, None) + .by_path(&chunk.repo_path.repo, &chunk.repo_path.path, None) .await? - .with_context(|| format!("failed to read path: {}", &chunk.path))?; + .with_context(|| format!("failed to read path: {}", &chunk.repo_path))?; let graph = document .symbol_locations .scope_graph() - .with_context(|| format!("no scope graph for file: {}", &chunk.path))?; + .with_context(|| format!("no scope graph for file: {}", &chunk.repo_path))?; let hoverable_ranges = document .hoverable_ranges() @@ -63,13 +64,13 @@ impl Agent { ..(range.end.byte - chunk.start_byte.unwrap_or_default())] .to_string(), token_info_request: TokenInfoRequest { - relative_path: chunk.path.clone(), - repo_ref: self.repo_ref.display_name(), + relative_path: chunk.repo_path.path.clone(), + repo_ref: chunk.repo_path.repo.display_name(), branch: None, start: range.start.byte, end: range.end.byte, }, - path: chunk.path.clone(), + repo_path: chunk.repo_path.clone(), }) .collect::>(); @@ -100,7 +101,10 @@ impl Agent { .data .iter() .map(|occurrence| CodeChunk { - path: filename.clone(), + repo_path: RepoPath { + repo: file_symbols.repo.clone(), + path: filename.clone(), + }, alias: 0, snippet: occurrence.snippet.data.clone(), start_line: occurrence.snippet.line_range.start, @@ -159,7 +163,7 @@ impl Agent { format!( "```{}\n{}```\n\n{}", - c.path.clone(), + c.repo_path.path.clone(), c.snippet.clone(), symbols_string ) @@ -212,7 +216,7 @@ impl Agent { .app .indexes .file - .by_path(&self.repo_ref, &symbol_metadata.path, None) + .by_path(&symbol_metadata.repo_path.repo, &symbol_metadata.repo_path.path, None) .await .unwrap() .unwrap(); @@ -226,13 +230,13 @@ impl Agent { self.app .indexes .file - .by_repo(&self.repo_ref, associated_langs.iter(), None) + .by_repo(&symbol_metadata.repo_path.repo, associated_langs.iter(), None) .await }; get_token_info( symbol_metadata.token_info_request, - &self.repo_ref, + &symbol_metadata.repo_path.repo, self.app.indexes.clone(), &document, &all_docs, @@ -242,7 +246,10 @@ impl Agent { .await .unwrap() .into_iter() - .filter(|file_symbol| file_symbol.file != symbol_metadata.path) + .filter(|file_symbol| { + file_symbol.file != symbol_metadata.repo_path.path + || file_symbol.repo != symbol_metadata.repo_path.repo + }) .collect::>() }, }), @@ -288,7 +295,7 @@ impl Agent { .take(MAX_CHUNKS) .map(|c| { let chunk = CodeChunk { - alias: self.get_path_alias(c.path.as_str()), + alias: self.get_path_alias(&c.repo_path), ..c.clone() }; self.exchanges @@ -307,7 +314,7 @@ impl Agent { pub struct HoverableSymbol { pub name: String, pub token_info_request: TokenInfoRequest, - pub path: String, + pub repo_path: RepoPath, } pub struct Symbol { pub name: String, diff --git a/server/bleep/src/agent/tools/answer.rs b/server/bleep/src/agent/tools/answer.rs index 0f0eb4b46b..35455bcb35 100644 --- a/server/bleep/src/agent/tools/answer.rs +++ b/server/bleep/src/agent/tools/answer.rs @@ -21,18 +21,18 @@ impl Agent { debug!("creating article response"); if aliases.len() == 1 { - let path = self + let repo_path = self .paths() .nth(aliases[0]) .context("invalid path alias passed")?; let doc = self - .get_file_content(path) + .get_file_content(repo_path) .await? .context("path did not exist")?; self.update(Update::Focus(FocusedChunk { - file_path: path.to_owned(), + repo_path: repo_path.clone(), start_line: 0, end_line: doc.content.lines().count(), })) @@ -146,7 +146,7 @@ impl Agent { acc }); - let formatted_snippet = format!("### {} ###\n{snippet}\n\n", chunk.path); + let formatted_snippet = format!("### {} ###\n{snippet}\n\n", chunk.repo_path); let snippet_tokens = bpe.encode_ordinary(&formatted_snippet).len(); @@ -241,16 +241,16 @@ impl Agent { let mut spans_by_path = HashMap::<_, Vec<_>>::new(); for c in self.code_chunks().filter(|c| aliases.contains(&c.alias)) { spans_by_path - .entry(c.path.clone()) + .entry(c.repo_path.clone()) .or_default() .push(c.start_line..c.end_line); } // If there are no relevant code chunks, but there is a focused chunk, we use that. if spans_by_path.is_empty() { - if let Some(chunk) = &self.last_exchange().focused_chunk { + if let Some(ref chunk) = self.last_exchange().focused_chunk { spans_by_path - .entry(chunk.file_path.clone()) + .entry(chunk.repo_path.clone()) .or_default() .push(chunk.start_line..chunk.end_line); } @@ -263,7 +263,6 @@ impl Agent { let lines_by_file = futures::stream::iter(&mut spans_by_path) .then(|(path, spans)| async move { spans.sort_by_key(|c| c.start); - let lines = self_ .get_file_content(path) .await @@ -314,7 +313,7 @@ impl Agent { .iter_mut() .flat_map(|(path, spans)| spans.iter_mut().map(move |s| (path, s))) { - let file_lines = lines_by_file.get(path.as_str()).unwrap().len(); + let file_lines = lines_by_file.get(path).unwrap().len(); let old_span = span.clone(); @@ -358,15 +357,15 @@ impl Agent { let code_chunks = spans_by_path .into_iter() .flat_map(|(path, spans)| spans.into_iter().map(move |s| (path.clone(), s))) - .map(|(path, span)| { - let snippet = lines_by_file.get(&path).unwrap()[span.clone()].join("\n"); + .map(|(repo_path, span)| { + let snippet = lines_by_file.get(&repo_path).unwrap()[span.clone()].join("\n"); CodeChunk { - alias: self.get_path_alias(&path), - path, - snippet, + alias: self.get_path_alias(&repo_path), start_line: span.start, end_line: span.end, + snippet, + repo_path, start_byte: None, end_byte: None, } @@ -381,7 +380,7 @@ impl Agent { let num_trimmed_lines = trimmed_snippet.lines().count(); vec![CodeChunk { alias: chunk.alias, - path: chunk.path.clone(), + repo_path: chunk.repo_path.clone(), snippet: trimmed_snippet.to_string(), start_line: chunk.start_line, end_line: (chunk.start_line + num_trimmed_lines).saturating_sub(1), diff --git a/server/bleep/src/agent/tools/code.rs b/server/bleep/src/agent/tools/code.rs index 16caef7c5a..0aeea4cd9a 100644 --- a/server/bleep/src/agent/tools/code.rs +++ b/server/bleep/src/agent/tools/code.rs @@ -1,39 +1,44 @@ use anyhow::Result; -use tracing::{debug, info, instrument, trace}; +use tracing::{debug, info, trace}; use crate::{ agent::{ - exchange::{CodeChunk, SearchStep, Update}, - prompts, Agent, + exchange::{CodeChunk, RepoPath, SearchStep, Update}, + prompts, Agent, AgentSemanticSearchParams, }, analytics::EventData, llm_gateway, - semantic::SemanticSearchParams, + query::parser::Literal, semantic::SemanticSearchParams, }; impl Agent { - #[instrument(skip(self))] - pub async fn code_search(&mut self, query: &String) -> Result { + pub async fn code_search(&mut self, query: &str) -> Result { const CODE_SEARCH_LIMIT: u64 = 10; const MINIMUM_RESULTS: usize = CODE_SEARCH_LIMIT as usize / 2; self.update(Update::StartStep(SearchStep::Code { - query: query.clone(), + query: query.to_owned(), response: String::new(), })) .await?; + // not sure if this is what we want here, or use the project coupled with the agent. + // + // let repos = self.paths().map(|r| r.repo.to_string()).collect::>(); + let repos = self.project.clone(); + let mut results = self - .semantic_search( - query.into(), - vec![], - SemanticSearchParams { + .semantic_search(AgentSemanticSearchParams { + query: Literal::from(&query.to_string()), + paths: vec![], + project: repos.clone(), + semantic_params: SemanticSearchParams { limit: CODE_SEARCH_LIMIT, offset: 0, threshold: 0.3, exact_match: false, }, - ) + }) .await?; debug!("returned {} results", results.len()); @@ -45,16 +50,17 @@ impl Agent { if !hyde_docs.is_empty() { let hyde_doc = hyde_docs.first().unwrap().into(); let hyde_results = self - .semantic_search( - hyde_doc, - vec![], - SemanticSearchParams { + .semantic_search(AgentSemanticSearchParams { + query: hyde_doc, + paths: vec![], + project: repos, + semantic_params: SemanticSearchParams { limit: CODE_SEARCH_LIMIT, offset: 0, threshold: 0.3, exact_match: false, }, - ) + }) .await?; debug!("returned {} HyDE results", results.len()); @@ -68,16 +74,22 @@ impl Agent { let mut chunks = results .into_iter() .map(|chunk| { - let relative_path = chunk.relative_path; + let repo = chunk.repo_ref; + let path = chunk.relative_path; + + let repo_path = RepoPath { + repo, + path: path.clone(), + }; CodeChunk { - path: relative_path.clone(), - alias: self.get_path_alias(&relative_path), + alias: self.get_path_alias(&repo_path), snippet: chunk.text, start_line: chunk.start_line as usize, end_line: chunk.end_line as usize, start_byte: Some(chunk.start_byte as usize), end_byte: Some(chunk.end_byte as usize), + repo_path, } }) .collect::>(); @@ -105,7 +117,7 @@ impl Agent { .join("\n\n"); self.update(Update::ReplaceStep(SearchStep::Code { - query: query.clone(), + query: query.to_string(), response: response.clone(), })) .await?; diff --git a/server/bleep/src/agent/tools/path.rs b/server/bleep/src/agent/tools/path.rs index 5bbbe6dc02..cb78a4296f 100644 --- a/server/bleep/src/agent/tools/path.rs +++ b/server/bleep/src/agent/tools/path.rs @@ -5,8 +5,8 @@ use tracing::instrument; use crate::{ agent::{ - exchange::{SearchStep, Update}, - Agent, + exchange::{RepoPath, SearchStep, Update}, + Agent, AgentSemanticSearchParams, }, analytics::EventData, semantic::SemanticSearchParams, @@ -25,7 +25,10 @@ impl Agent { let mut paths = self .fuzzy_path_search(query) .await - .map(|c| c.relative_path) + .map(|c| RepoPath { + repo: c.repo_ref, + path: c.relative_path, + }) .collect::>() // TODO: This shouldn't be necessary. Path search should return unique results. .into_iter() .collect::>(); @@ -35,19 +38,23 @@ impl Agent { // If there are no lexical results, perform a semantic search. if paths.is_empty() { let semantic_paths = self - .semantic_search( - query.into(), - vec![], - SemanticSearchParams { + .semantic_search(AgentSemanticSearchParams { + query: query.into(), + paths: vec![], + project: self.project.clone(), + semantic_params: SemanticSearchParams { limit: 30, offset: 0, threshold: 0.0, exact_match: false, }, - ) + }) .await? .into_iter() - .map(|chunk| chunk.relative_path) + .map(|chunk| RepoPath { + repo: chunk.repo_ref, + path: chunk.relative_path, + }) .collect::>() .into_iter() .collect(); @@ -57,7 +64,7 @@ impl Agent { let mut paths = paths .iter() - .map(|p| (self.get_path_alias(p), p.to_string())) + .map(|repo_path| (self.get_path_alias(repo_path), repo_path.path.to_string())) .collect::>(); paths.sort_by(|a: &(usize, String), b| a.0.cmp(&b.0)); // Sort by alias diff --git a/server/bleep/src/agent/tools/proc.rs b/server/bleep/src/agent/tools/proc.rs index 18410de830..672ee8dd4d 100644 --- a/server/bleep/src/agent/tools/proc.rs +++ b/server/bleep/src/agent/tools/proc.rs @@ -3,24 +3,20 @@ use tracing::instrument; use crate::{ agent::{ - exchange::{CodeChunk, SearchStep, Update}, - Agent, + exchange::{CodeChunk, RepoPath, SearchStep, Update}, + Agent, AgentSemanticSearchParams, }, analytics::EventData, - semantic::SemanticSearchParams, + query::parser::Literal, semantic::SemanticSearchParams, }; impl Agent { #[instrument(skip(self))] - pub async fn process_files( - &mut self, - query: &String, - path_aliases: &[usize], - ) -> Result { - let paths = path_aliases + pub async fn process_files(&mut self, query: &str, aliases: &[usize]) -> Result { + let paths = aliases .iter() .copied() - .map(|i| self.paths().nth(i).ok_or(i).map(str::to_owned)) + .map(|i| self.paths().nth(i).ok_or(i).map(Clone::clone)) .collect::, _>>() .map_err(|i| anyhow!("invalid path alias {i}"))?; @@ -31,32 +27,40 @@ impl Agent { })) .await?; + // let relative_paths = paths.iter().map(|p| p.path.clone()).collect::>(); + // not sure we need this? + // let repos = paths.iter().map(|p| p.repo.clone()).collect::>(); + let results = self - .semantic_search( - query.into(), - paths.clone(), - SemanticSearchParams { + .semantic_search(AgentSemanticSearchParams { + query: Literal::from(&query.to_string()), + paths: paths.clone(), + project: self.project.clone(), + semantic_params: SemanticSearchParams { limit: 10, offset: 0, threshold: 0.0, exact_match: true, }, - ) + }) .await?; let mut chunks = results .into_iter() .map(|chunk| { - let relative_path = chunk.relative_path; + let repo_path = RepoPath { + repo: chunk.repo_ref.clone(), + path: chunk.relative_path, + }; CodeChunk { - path: relative_path.clone(), - alias: self.get_path_alias(&relative_path), + alias: self.get_path_alias(&repo_path), snippet: chunk.text, start_line: chunk.start_line as usize, end_line: chunk.end_line as usize, start_byte: Some(chunk.start_byte as usize), end_byte: Some(chunk.end_byte as usize), + repo_path, } }) .collect::>(); diff --git a/server/bleep/src/analytics.rs b/server/bleep/src/analytics.rs index ed7cbf3e13..e7c3565d89 100644 --- a/server/bleep/src/analytics.rs +++ b/server/bleep/src/analytics.rs @@ -1,9 +1,6 @@ use std::{fmt::Debug, sync::Arc}; -use crate::{ - repo::RepoRef, - state::{PersistedState, StateSource}, -}; +use crate::state::{PersistedState, StateSource}; use rudderanalytics::{ client::RudderAnalytics, @@ -18,7 +15,6 @@ use uuid::Uuid; pub struct QueryEvent { pub query_id: Uuid, pub thread_id: Uuid, - pub repo_ref: Option, pub data: EventData, } @@ -241,7 +237,6 @@ impl RudderHub { "device_id": self.device_id(), "query_id": event.query_id, "thread_id": event.thread_id, - "repo_ref": event.repo_ref.as_ref().map(ToString::to_string), "data": event.data, "package_metadata": options.package_metadata, })), diff --git a/server/bleep/src/indexes/file.rs b/server/bleep/src/indexes/file.rs index a219d80262..add42bef60 100644 --- a/server/bleep/src/indexes/file.rs +++ b/server/bleep/src/indexes/file.rs @@ -32,6 +32,7 @@ use super::{ DocumentRead, Indexable, Indexer, }; use crate::{ + agent::Project, background::SyncHandle, cache::{CacheKeys, FileCache, FileCacheSnapshot}, intelligence::TreeSitterFile, @@ -246,9 +247,9 @@ impl Indexer { /// If the regex filter fails to build, an empty list is returned. pub async fn fuzzy_path_match( &self, - repo_ref: &RepoRef, - query_str: &str, + project: Project, branch: Option<&str>, + query_str: &str, langs: impl Iterator, limit: usize, ) -> impl Iterator + '_ { @@ -257,10 +258,7 @@ impl Indexer { let collector = TopDocs::with_limit(5 * limit); // TODO: tune this let file_source = &self.source; - // hits is a mapping between a document address and the number of trigrams in it that - // matched the query - let repo_ref_term = Term::from_field_text(self.source.repo_ref, &repo_ref.to_string()); - let branch_term = branch + let branch_scope = branch .map(|b| { trigrams(b) .map(|token| Term::from_field_text(self.source.branches, token.as_str())) @@ -270,6 +268,21 @@ impl Indexer { .collect::>() }) .map(BooleanQuery::intersection); + + let project_scope = BooleanQuery::union( + project + .repos() + .map(|repo| { + Box::new(TermQuery::new( + Term::from_field_text(self.source.repo_name, &repo), + IndexRecordOption::Basic, + )) as Box + }) + .collect::>(), + ); + + // hits is a mapping between a document address and the number of trigrams in it that + // matched the query let langs_query = BooleanQuery::union( langs .map(|l| Term::from_field_bytes(self.source.lang, l.as_bytes())) @@ -278,28 +291,19 @@ impl Indexer { .map(|q| q as Box) .collect::>(), ); + let mut hits = trigrams(query_str) .flat_map(|s| case_permutations(s.as_str())) .map(|token| Term::from_field_text(self.source.relative_path, token.as_str())) - .map(|term| { - let mut query: Vec> = vec![ - Box::new(TermQuery::new(term, IndexRecordOption::Basic)), - Box::new(TermQuery::new( - repo_ref_term.clone(), - IndexRecordOption::Basic, - )), - Box::new(langs_query.clone()), - ]; - - if let Some(b) = branch_term.as_ref() { - query.push(Box::new(b.clone())); - }; - - BooleanQuery::intersection(query) - }) + .map(|term| TermQuery::new(term, IndexRecordOption::Basic)) .flat_map(|query| { + let mut q: Vec> = + vec![Box::new(project_scope.clone()), Box::new(query)]; + q.extend(branch_scope.clone().map(|q| Box::new(q) as Box)); + q.push(Box::new(langs_query.clone())); + searcher - .search(&query, &collector) + .search(&BooleanQuery::intersection(q), &collector) .expect("failed to search index") .into_iter() .map(move |(_, addr)| addr) diff --git a/server/bleep/src/indexes/reader.rs b/server/bleep/src/indexes/reader.rs index b883425783..be4226fa53 100644 --- a/server/bleep/src/indexes/reader.rs +++ b/server/bleep/src/indexes/reader.rs @@ -12,6 +12,7 @@ use crate::{ compiler::Compiler, parser::{self, Query, Target}, }, + repo::RepoRef, symbol::SymbolLocations, text_range::TextRange, }; @@ -60,7 +61,7 @@ impl ContentDocument { pub struct FileDocument { pub relative_path: String, pub repo_name: String, - pub repo_ref: String, + pub repo_ref: RepoRef, pub lang: Option, pub branches: String, pub indexed: bool, @@ -202,7 +203,7 @@ impl DocumentRead for FileReader { fn read_document(&self, schema: &Self::Schema, doc: tantivy::Document) -> Self::Document { let relative_path = read_text_field(&doc, schema.relative_path); - let repo_ref = read_text_field(&doc, schema.repo_ref); + let repo_ref = read_text_field(&doc, schema.repo_ref).parse().unwrap(); let repo_name = read_text_field(&doc, schema.repo_name); let lang = read_lang_field(&doc, schema.lang); let branches = read_text_field(&doc, schema.branches); diff --git a/server/bleep/src/intelligence/code_navigation.rs b/server/bleep/src/intelligence/code_navigation.rs index 2284d1ea5c..322e607d71 100644 --- a/server/bleep/src/intelligence/code_navigation.rs +++ b/server/bleep/src/intelligence/code_navigation.rs @@ -8,7 +8,7 @@ use super::NodeKind; use crate::{ indexes::reader::ContentDocument, snippet::{Snipper, Snippet}, - text_range::TextRange, + text_range::TextRange, repo::RepoRef, }; use rayon::prelude::*; @@ -19,6 +19,8 @@ pub struct FileSymbols { /// The file to which the following occurrences belong pub file: String, + pub repo: RepoRef, + /// A collection of symbol locations with context in this file pub data: Vec, } @@ -75,6 +77,7 @@ impl<'a, 'b> CodeNavigationContext<'a, 'b> { .flat_map_iter(|idx| { let range = source_sg.graph[idx].range(); let token = Token { + repo: source_doc.repo_ref.parse().unwrap(), relative_path: &source_doc.relative_path, start_byte: range.start.byte, end_byte: range.end.byte, @@ -131,6 +134,7 @@ impl<'a, 'b> CodeNavigationContext<'a, 'b> { all_docs, source_document_idx, token: Token { + repo: source_doc.repo_ref.parse().unwrap(), relative_path: &source_doc.relative_path, start_byte: source_sg.graph[idx].range().start.byte, end_byte: source_sg.graph[idx].range().end.byte, @@ -143,6 +147,7 @@ impl<'a, 'b> CodeNavigationContext<'a, 'b> { .flat_map_iter(|idx| { let range = source_sg.graph[idx].range(); let token = Token { + repo: source_doc.repo_ref.parse().unwrap(), relative_path: &source_doc.relative_path, start_byte: range.start.byte, end_byte: range.end.byte, @@ -318,6 +323,7 @@ impl<'a, 'b> CodeNavigationContext<'a, 'b> { data.is_empty().not().then(|| FileSymbols { file: self.token.relative_path.to_owned(), + repo: self.token.repo.clone(), data, }) } @@ -350,6 +356,7 @@ impl<'a, 'b> CodeNavigationContext<'a, 'b> { data.is_empty().not().then(|| FileSymbols { file: doc.relative_path.to_owned(), + repo: doc.repo_ref.parse().unwrap(), data, }) }) @@ -382,6 +389,7 @@ impl<'a, 'b> CodeNavigationContext<'a, 'b> { data.is_empty().not().then(|| FileSymbols { file: self.token.relative_path.to_owned(), + repo: self.token.repo.clone(), data, }) } @@ -415,6 +423,7 @@ impl<'a, 'b> CodeNavigationContext<'a, 'b> { data.is_empty().not().then(|| FileSymbols { file: doc.relative_path.to_owned(), + repo: doc.repo_ref.parse().unwrap(), data, }) }) @@ -441,12 +450,14 @@ impl<'a, 'b> CodeNavigationContext<'a, 'b> { data.is_empty().not().then(|| FileSymbols { file: self.token.relative_path.to_owned(), + repo: self.token.repo.clone(), data, }) } } pub struct Token<'a> { + pub repo: RepoRef, pub relative_path: &'a str, pub start_byte: usize, pub end_byte: usize, @@ -485,6 +496,7 @@ pub fn imported_ranges( .filter(|idx| source_sg.is_reference(*idx) || source_sg.is_import(*idx)) .filter(|&idx| { let token = Token { + repo: source_document.repo_ref.parse().unwrap(), relative_path: &source_document.relative_path, start_byte: source_sg.graph[idx].range().start.byte, end_byte: source_sg.graph[idx].range().end.byte, @@ -496,6 +508,7 @@ pub fn imported_ranges( .flat_map(|idx| { let range = source_sg.graph[idx].range(); let token = Token { + repo: source_document.repo_ref.parse().unwrap(), relative_path: &source_document.relative_path, start_byte: range.start.byte, end_byte: range.end.byte, diff --git a/server/bleep/src/query/execute.rs b/server/bleep/src/query/execute.rs index 3c47aef59f..7676d36974 100644 --- a/server/bleep/src/query/execute.rs +++ b/server/bleep/src/query/execute.rs @@ -150,7 +150,7 @@ pub struct RepositoryResultData { pub struct FileResultData { repo_name: String, relative_path: HighlightedString, - repo_ref: String, + repo_ref: RepoRef, lang: Option, branches: String, indexed: bool, @@ -161,7 +161,7 @@ impl FileResultData { pub fn new( repo_name: String, relative_path: String, - repo_ref: String, + repo_ref: RepoRef, lang: Option, branches: String, indexed: bool, diff --git a/server/bleep/src/query/parser.rs b/server/bleep/src/query/parser.rs index 3d5890e156..0504fa2952 100644 --- a/server/bleep/src/query/parser.rs +++ b/server/bleep/src/query/parser.rs @@ -314,14 +314,6 @@ impl<'a> Literal<'a> { Literal::Regex(format!("{lhs}\\s+{rhs}").into()) } - /// This drops position information, as it's not intelligible after the merge - #[allow(dead_code)] - fn join_as_plain(self, rhs: Self) -> Option> { - let lhs = self.as_plain()?; - let rhs = rhs.as_plain()?; - Some(Literal::Plain(format!("{lhs} {rhs}").into())) - } - /// Convert this literal into a regex string. /// /// If this literal is a regex, it is returned as-is. If it is a plain text literal, it is diff --git a/server/bleep/src/semantic.rs b/server/bleep/src/semantic.rs index 95be32188b..6ec83414b2 100644 --- a/server/bleep/src/semantic.rs +++ b/server/bleep/src/semantic.rs @@ -101,7 +101,7 @@ impl Payload { HashMap::from([ ("lang".into(), self.lang.to_ascii_lowercase().into()), ("repo_name".into(), self.repo_name.into()), - ("repo_ref".into(), self.repo_ref.into()), + ("repo_ref".into(), self.repo_ref.to_string().into()), ("relative_path".into(), self.relative_path.into()), ("content_hash".into(), self.content_hash.into()), ("snippet".into(), self.text.into()), @@ -527,18 +527,18 @@ impl Semantic { pub async fn search<'a>( &self, - parsed_query: &SemanticQuery<'a>, + query: &SemanticQuery<'a>, params: SemanticSearchParams, ) -> anyhow::Result> { - let Some(query) = parsed_query.target() else { + let Some(query_target) = query.target() else { anyhow::bail!("no search target for query"); }; - let vector = self.embedder.embed(&query).await?; + let vector = self.embedder.embed(&query_target).await?; let SemanticSearchParams { limit, offset, threshold, - exact_match: exact, + exact_match: exact } = params; // TODO: Remove the need for `retrieve_more`. It's here because: @@ -546,9 +546,9 @@ impl Semantic { // In /answer we want to retrieve `limit` results exactly let results = self .search_with( - parsed_query, + query, vector.clone(), - limit * 2, // Retrieve double `limit` and deduplicate + limit * 4, // Retrieve double `limit` and deduplicate offset, threshold, exact, @@ -563,7 +563,7 @@ impl Semantic { let results_lexical = self .search_lexical( - parsed_query, + query, vector.clone(), limit * 2, // Retrieve double `limit` and deduplicate offset, @@ -577,7 +577,7 @@ impl Semantic { .collect::>() })?; let results_lexical = deduplicate_snippets(results_lexical, vector.clone(), limit); - let results_lexical = Self::rank_lexical(results_lexical, &query); + let results_lexical = Self::rank_lexical(results_lexical, &query_target); let merged_results = Self::merge_rrf(results_lexical, results); @@ -667,7 +667,7 @@ impl Semantic { let data = format!("{repo_name}\t{relative_path}\n{}", chunk.data); let payload = Payload { repo_name: repo_name.to_owned(), - repo_ref: repo_ref.to_owned(), + repo_ref: repo_ref.parse().unwrap(), relative_path: relative_path.to_owned(), content_hash: file_cache_key.to_string(), text: chunk.data.to_owned(), @@ -677,7 +677,9 @@ impl Semantic { end_line: chunk.range.end.line as u64, start_byte: chunk.range.start.byte as u64, end_byte: chunk.range.end.byte as u64, - ..Default::default() + id: Default::default(), + embedding: Default::default(), + score: Default::default(), }; (data, payload) @@ -903,18 +905,21 @@ fn mean_pool(embeddings: Vec>) -> Vec { // The value of lambda skews the weightage in favor of either relevance or novelty. // - we add a language diversity factor to the score to encourage a range of languages in the results // - we also add a path diversity factor to the score to encourage a range of paths in the results +// - we also add a repo diversity factor to the score to encourage a range of repos in the results // k: the number of embeddings to select pub fn deduplicate_with_mmr( query_embedding: &[f32], embeddings: &[&[f32]], languages: &[&str], paths: &[&str], + repos: &[&str], lambda: f32, k: usize, ) -> Vec { let mut idxs = vec![]; let mut lang_counts = HashMap::new(); let mut path_counts = HashMap::new(); + let mut repo_counts = HashMap::new(); if embeddings.len() < k { return (0..embeddings.len()).collect(); @@ -946,6 +951,10 @@ pub fn deduplicate_with_mmr( let path_count = path_counts.get(paths[i]).unwrap_or(&1); equation_score += 0.75_f32.powi(*path_count); + // MMR + (3/4)^n where n is the number of times a repo has been selected + let repo_count = repo_counts.get(repos[i]).unwrap_or(&0); + equation_score += 0.75_f32.powi(*repo_count); + if equation_score > best_score { best_score = equation_score; idx_to_add = Some(i); @@ -955,6 +964,7 @@ pub fn deduplicate_with_mmr( idxs.push(i); *lang_counts.entry(languages[i]).or_insert(0) += 1; *path_counts.entry(paths[i]).or_insert(0) += 1; + *repo_counts.entry(repos[i]).or_insert(0) += 1; } } idxs @@ -1011,11 +1021,16 @@ pub fn deduplicate_snippets( .iter() .map(|s| s.relative_path.as_ref()) .collect::>(); + let repos = all_snippets + .iter() + .map(|s| s.repo_name.as_ref()) + .collect::>(); deduplicate_with_mmr( &query_embedding, &embeddings, &languages, &paths, + &repos, lambda, k as usize, ) diff --git a/server/bleep/src/semantic/schema.rs b/server/bleep/src/semantic/schema.rs index 41993acfbf..6bb3469809 100644 --- a/server/bleep/src/semantic/schema.rs +++ b/server/bleep/src/semantic/schema.rs @@ -11,14 +11,16 @@ use qdrant_client::{ }, }; +use crate::repo::RepoRef; + pub(super) const EMBEDDING_DIM: usize = 384; pub type Embedding = Vec; -#[derive(Default, Clone, Debug, serde::Deserialize, serde::Serialize)] +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] pub struct Payload { pub lang: String, pub repo_name: String, - pub repo_ref: String, + pub repo_ref: RepoRef, pub relative_path: String, pub content_hash: String, pub text: String, diff --git a/server/bleep/src/webserver/answer.rs b/server/bleep/src/webserver/answer.rs index b632b0c74f..34bf7534d5 100644 --- a/server/bleep/src/webserver/answer.rs +++ b/server/bleep/src/webserver/answer.rs @@ -20,8 +20,8 @@ use super::middleware::User; use crate::{ agent::{ self, - exchange::{CodeChunk, Exchange, FocusedChunk}, - Action, Agent, ExchangeState, + exchange::{CodeChunk, Exchange, FocusedChunk, RepoPath}, + Action, Agent, ExchangeState, Project, }, analytics::{EventData, QueryEvent}, db::QueryLog, @@ -59,7 +59,6 @@ pub(super) async fn vote( &QueryEvent { query_id: params.query_id, thread_id: params.thread_id, - repo_ref: params.repo_ref, data: EventData::output_stage("vote").with_payload("feedback", params.feedback), }, ); @@ -68,7 +67,7 @@ pub(super) async fn vote( #[derive(Clone, Debug, serde::Deserialize)] pub struct Answer { pub q: String, - pub repo_ref: RepoRef, + pub project: String, #[serde(default = "default_answer_model")] pub answer_model: agent::model::LLMModel, #[serde(default = "default_agent_model")] @@ -108,9 +107,9 @@ pub(super) async fn answer( thread_id: params.thread_id, }; - let (_, mut exchanges) = conversations::load(&app.sql, &conversation_id) + let mut exchanges = conversations::load(&app.sql, &conversation_id) .await? - .unwrap_or_else(|| (params.repo_ref.clone(), Vec::new())); + .unwrap_or_default(); let Answer { parent_exchange_id, @@ -190,7 +189,6 @@ async fn execute_agent( &QueryEvent { query_id, thread_id: params.thread_id, - repo_ref: Some(params.repo_ref), data: EventData::output_stage("error") .with_payload("status", err.status.as_u16()) .with_payload("message", err.message()), @@ -213,9 +211,9 @@ async fn try_execute_agent( Sse> + Send>>>, > { QueryLog::new(&app.sql).insert(¶ms.q).await?; + let project: Project = serde_json::from_str(¶ms.project).unwrap(); let Answer { thread_id, - repo_ref, answer_model, agent_model, .. @@ -262,7 +260,7 @@ async fn try_execute_agent( let mut agent = Agent { app, - repo_ref, + project, exchanges, exchange_tx, llm_gateway, @@ -388,6 +386,10 @@ pub async fn explain( Extension(user): Extension, ) -> super::Result { let query_id = uuid::Uuid::new_v4(); + let repo_path = RepoPath { + repo: params.repo_ref.clone(), + path: params.relative_path.clone(), + }; // We synthesize a virtual `/answer` request. let virtual_req = Answer { @@ -397,7 +399,7 @@ pub async fn explain( params.line_end + 1, params.relative_path ), - repo_ref: params.repo_ref, + project: format!("[\"{}\"]", params.repo_ref), thread_id: params.thread_id, parent_exchange_id: None, answer_model: agent::model::GPT_4_TURBO_24K, @@ -423,7 +425,8 @@ pub async fn explain( let file_content = app .indexes .file - .by_path(&virtual_req.repo_ref, ¶ms.relative_path, None) + // this unwrap is ok, because we instantiate the `virtual_req` above + .by_path(¶ms.repo_ref, ¶ms.relative_path, None) .await .context("file retrieval failed")? .context("did not find requested file")? @@ -437,16 +440,15 @@ pub async fn explain( .join("\n"); let mut exchange = Exchange::new(query_id, query); - exchange.focused_chunk = Some(FocusedChunk { - file_path: params.relative_path.clone(), + repo_path: repo_path.clone(), start_line: params.line_start, end_line: params.line_end, }); - exchange.paths.push(params.relative_path.clone()); + exchange.paths.push(repo_path.clone()); exchange.code_chunks.push(CodeChunk { - path: params.relative_path.clone(), + repo_path: repo_path.clone(), alias: 0, start_line: params.line_start, end_line: params.line_end, diff --git a/server/bleep/src/webserver/answer/conversations.rs b/server/bleep/src/webserver/answer/conversations.rs index 5c37879e41..40e4fabd8e 100644 --- a/server/bleep/src/webserver/answer/conversations.rs +++ b/server/bleep/src/webserver/answer/conversations.rs @@ -5,18 +5,18 @@ use axum::{ Extension, Json, }; use reqwest::StatusCode; -use std::{fmt, str::FromStr}; +use std::fmt; use tracing::info; use crate::{ - agent::exchange::Exchange, + agent::{exchange::Exchange, Project}, db::SqlDb, repo::RepoRef, webserver::{self, middleware::User, Error, ErrorKind}, Application, }; -type Conversation = (RepoRef, Vec); +pub type Conversation = (Project, Vec); #[derive(Hash, PartialEq, Eq, Clone)] pub struct ConversationId { @@ -123,7 +123,7 @@ pub(in crate::webserver) async fn thread( .ok_or_else(|| Error::user("missing user ID"))? .to_owned(); - let (.., exchanges) = load(&app.sql, &ConversationId { thread_id, user_id }) + let exchanges = load(&app.sql, &ConversationId { thread_id, user_id }) .await? .ok_or_else(|| Error::new(ErrorKind::NotFound, "thread was not found"))?; @@ -150,8 +150,8 @@ pub async fn store(db: &SqlDb, id: ConversationId, conversation: Conversation) - .execute(&mut transaction) .await?; - let (repo_ref, exchanges) = conversation; - let repo_ref = repo_ref.to_string(); + let (project, exchanges) = conversation; + let repo_ref = project.id(); let title = exchanges .first() .and_then(|list| list.query()) @@ -178,7 +178,7 @@ pub async fn store(db: &SqlDb, id: ConversationId, conversation: Conversation) - Ok(()) } -pub async fn load(db: &SqlDb, id: &ConversationId) -> Result> { +pub async fn load(db: &SqlDb, id: &ConversationId) -> Result>> { let (user_id, thread_id) = (id.user_id.clone(), id.thread_id.to_string()); let row = sqlx::query! { @@ -195,8 +195,6 @@ pub async fn load(db: &SqlDb, id: &ConversationId) -> Result return Ok(None), }; - let repo_ref = RepoRef::from_str(&row.repo_ref).context("failed to parse repo ref")?; let exchanges = serde_json::from_str(&row.exchanges)?; - - Ok(Some((repo_ref, exchanges))) + Ok(Some(exchanges)) } diff --git a/server/bleep/src/webserver/intelligence.rs b/server/bleep/src/webserver/intelligence.rs index c46747a5bf..9f8103f5e8 100644 --- a/server/bleep/src/webserver/intelligence.rs +++ b/server/bleep/src/webserver/intelligence.rs @@ -334,6 +334,7 @@ pub async fn get_token_info( let ctx: CodeNavigationContext<'_, '_> = CodeNavigationContext { token: Token { + repo: repo_ref.clone(), relative_path: params.relative_path.as_str(), start_byte: params.start, end_byte: params.end, @@ -512,6 +513,7 @@ async fn search_nav( data.is_empty().not().then(|| FileSymbols { file: file.clone(), + repo: repo_ref.clone(), data, }) }) diff --git a/server/bleep/src/webserver/studio.rs b/server/bleep/src/webserver/studio.rs index be9c838a98..ca4aecf844 100644 --- a/server/bleep/src/webserver/studio.rs +++ b/server/bleep/src/webserver/studio.rs @@ -1467,9 +1467,9 @@ pub async fn import( let imported_context = canonicalize_context(exchanges.iter().flat_map(|e| { e.code_chunks.iter().map(|c| ContextFile { - path: c.path.clone(), - hidden: false, repo: repo_ref.parse().unwrap(), + path: c.repo_path.path.clone(), + hidden: false, branch: e.query.branch().next().map(Cow::into_owned), ranges: vec![c.start_line..c.end_line + 1], }) From 93c62eb15ae6f865cf28a41981490a621b4ceba1 Mon Sep 17 00:00:00 2001 From: Gabriel Gordon-Hall Date: Thu, 23 Nov 2023 17:47:27 +0000 Subject: [PATCH 02/88] update answer prompt for multi-repo --- server/bleep/src/agent/exchange.rs | 2 +- server/bleep/src/agent/prompts.rs | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/server/bleep/src/agent/exchange.rs b/server/bleep/src/agent/exchange.rs index 9ed20c7f36..d5b0a3c12b 100644 --- a/server/bleep/src/agent/exchange.rs +++ b/server/bleep/src/agent/exchange.rs @@ -11,7 +11,7 @@ pub struct RepoPath { impl fmt::Display for RepoPath { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} {}", self.repo, self.path) + write!(f, "{}:{}", self.repo, self.path) } } diff --git a/server/bleep/src/agent/prompts.rs b/server/bleep/src/agent/prompts.rs index 2591cf83e2..4c60f52005 100644 --- a/server/bleep/src/agent/prompts.rs +++ b/server/bleep/src/agent/prompts.rs @@ -5,7 +5,7 @@ pub fn functions(add_proc: bool) -> serde_json::Value { [ { "name": "code", - "description": "Search within the files in a codebase semantically. Results will not necessarily match search terms exactly, but should be related.", + "description": "Search the contents of files in a codebase semantically. Results will not necessarily match search terms exactly, but should be related.", "parameters": { "type": "object", "properties": { @@ -68,7 +68,7 @@ pub fn functions(add_proc: bool) -> serde_json::Value { "type": "array", "items": { "type": "integer", - "description": "The indices of the paths to search. Search paths that you think are most likely to be relevant." + "description": "The indices of the paths to search." } } }, @@ -121,27 +121,27 @@ pub fn answer_article_prompt(context: &str) -> String { format!( r#"{context}#### -You are an expert programmer called 'bloop' and you are helping a junior colleague answer questions about a codebase (consisting of several repos) using the information above. If their query refers to 'this' or 'it' and there is no other context, assume that it refers to the information above. +You are an expert programmer called 'bloop' and you are helping a junior colleague answer questions about a codebase using the information above. If their query refers to 'this' or 'it' and there is no other context, assume that it refers to the information above. Provide only as much information and code as is necessary to answer the query, but be concise. Keep number of quoted lines to a minimum when possible. If you do not have enough information needed to answer the query, do not make up an answer. Infer as much as possible from the information above. When referring to code, you must provide an example in a code block. Respect these rules at all times: - Link ALL paths AND code symbols (functions, methods, fields, classes, structs, types, variables, values, definitions, directories, etc) by embedding them in a markdown link, with the URL corresponding to the full path, and the anchor following the form `LX` or `LX-LY`, where X represents the starting line number, and Y represents the ending line number, if the reference is more than one line. - - For example, to refer to lines 50 to 78 in a sentence, respond with something like: The compiler is initialized in [`src/foo.rs`](src/foo.rs#L50-L78) - - For example, to refer to the `new` function on a struct, respond with something like: The [`new`](src/bar.rs#L26-53) function initializes the struct - - For example, to refer to the `foo` field on a struct and link a single line, respond with something like: The [`foo`](src/foo.rs#L138) field contains foos. Do not respond with something like [`foo`](src/foo.rs#L138-L138) - - For example, to refer to a folder `foo`, respond with something like: The files can be found in [`foo`](path/to/foo/) folder + - For example, to refer to lines 50 to 78 in a sentence, respond with something like: The compiler is initialized in [`src/foo.rs`](//local/path/to/repo:src/foo.rs#L50-L78) + - For example, to refer to the `new` function on a struct, respond with something like: The [`new`](github.com/org/repo:src/bar.rs#L26-53) function initializes the struct + - For example, to refer to the `foo` field on a struct and link a single line, respond with something like: The [`foo`](github.com/org/repo:src/foo.rs#L138) field contains foos. Do not respond with something like [`foo`](src/foo.rs#L138-L138) + - For example, to refer to a folder `foo`, respond with something like: The files can be found in [`foo`](//local/path/to/repo:path/to/foo/) folder - Do not print out line numbers directly, only in a link - Do not refer to more lines than necessary when creating a line range, be precise - Do NOT output bare symbols. ALL symbols must include a link - - E.g. Do not simply write `Bar`, write [`Bar`](src/bar.rs#L100-L105). - - E.g. Do not simply write "Foos are functions that create `Foo` values out of thin air." Instead, write: "Foos are functions that create [`Foo`](src/foo.rs#L80-L120) values out of thin air." + - E.g. Do not simply write `Bar`, write [`Bar`](github.com/org/repo:src/bar.rs#L100-L105). + - E.g. Do not simply write "Foos are functions that create `Foo` values out of thin air." Instead, write: "Foos are functions that create [`Foo`](github.com/org/repo:src/foo.rs#L80-L120) values out of thin air." - Link all fields - - E.g. Do not simply write: "It has one main field: `foo`." Instead, write: "It has one main field: [`foo`](src/foo.rs#L193)." + - E.g. Do not simply write: "It has one main field: `foo`." Instead, write: "It has one main field: [`foo`](//local/path/to/repo:src/foo.rs#L193)." - Do NOT link external urls not present in the context, do NOT link urls from the internet - Link all symbols, even when there are multiple in one sentence - - E.g. Do not simply write: "Bars are [`Foo`]( that return a list filled with `Bar` variants." Instead, write: "Bars are functions that return a list filled with [`Bar`](src/bar.rs#L38-L57) variants." + - E.g. Do not simply write: "Bars are [`Foo`]( that return a list filled with `Bar` variants." Instead, write: "Bars are functions that return a list filled with [`Bar`](//local/path/to/repo:src/bar.rs#L38-L57) variants." - If you do not have enough information needed to answer the query, do not make up an answer. Instead respond only with a footnote that asks the user for more information, e.g. `assistant: I'm sorry, I couldn't find what you were looking for, could you provide more information?` - Code blocks MUST be displayed to the user using XML in the following formats: - Do NOT output plain markdown blocks, the user CANNOT see them @@ -164,7 +164,7 @@ println!("hello world!"); println!("hello world!"); Rust -src/main.rs +//local/path/to/repo:src/main.rs 4 5 From 2965d2120bc01550f310345acfa86782de362733 Mon Sep 17 00:00:00 2001 From: calyptobai Date: Thu, 23 Nov 2023 21:20:06 -0500 Subject: [PATCH 03/88] WIP: projects --- .../migrations/20231122012638_projects.sql | 7 + server/bleep/sqlx-data.json | 374 +++++++++--------- server/bleep/src/webserver.rs | 48 +-- server/bleep/src/webserver/studio.rs | 53 ++- 4 files changed, 252 insertions(+), 230 deletions(-) create mode 100644 server/bleep/migrations/20231122012638_projects.sql diff --git a/server/bleep/migrations/20231122012638_projects.sql b/server/bleep/migrations/20231122012638_projects.sql new file mode 100644 index 0000000000..0aa63e4b0c --- /dev/null +++ b/server/bleep/migrations/20231122012638_projects.sql @@ -0,0 +1,7 @@ +CREATE TABLE projects ( + id INTEGER PRIMARY KEY, + name TEXT +); + +ALTER TABLE studios ADD COLUMN project_id INTEGER NOT NULL; +ALTER TABLE conversations ADD COLUMN project_id INTEGER NOT NULL; diff --git a/server/bleep/sqlx-data.json b/server/bleep/sqlx-data.json index ab2885957b..1493675c10 100644 --- a/server/bleep/sqlx-data.json +++ b/server/bleep/sqlx-data.json @@ -1,41 +1,5 @@ { "db": "SQLite", - "02ca4d99b13160cb4c78a793f32bec20760e112f4045d8c31541932b4459d7fe": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int64" - }, - { - "name": "name", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "modified_at!", - "ordinal": 2, - "type_info": "Datetime" - }, - { - "name": "context", - "ordinal": 3, - "type_info": "Text" - } - ], - "nullable": [ - false, - true, - false, - false - ], - "parameters": { - "Right": 1 - } - }, - "query": "SELECT\n s.id,\n s.name,\n ss.modified_at as \"modified_at!\",\n ss.context\n FROM studios s\n INNER JOIN studio_snapshots ss ON s.id = ss.studio_id\n WHERE s.user_id = ? AND (ss.studio_id, ss.modified_at) IN (\n SELECT studio_id, MAX(modified_at)\n FROM studio_snapshots\n GROUP BY studio_id\n )" - }, "069c6404909c217e0b27e974480cce3f592a0d43ece6dec17fbcee37ce7a6ffa": { "describe": { "columns": [ @@ -108,47 +72,23 @@ }, "query": "SELECT context, messages FROM studio_snapshots WHERE id = ?" }, - "0c06bc7f11f6782618297e540890725a1977b1ec6a80849cd28b7f07c1fd5bd4": { + "0c116e27d03cdd17da9a1403cd010dadb28f53756276aa38cdc2caeb2cec6bf6": { "describe": { "columns": [ { - "name": "id!", + "name": "id", "ordinal": 0, "type_info": "Int64" - }, - { - "name": "modified_at", - "ordinal": 1, - "type_info": "Datetime" - }, - { - "name": "context", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "doc_context", - "ordinal": 3, - "type_info": "Text" - }, - { - "name": "messages", - "ordinal": 4, - "type_info": "Text" } ], "nullable": [ - true, - false, - false, - false, - false + true ], "parameters": { - "Right": 2 + "Right": 3 } }, - "query": "SELECT ss.id as 'id!', ss.modified_at, ss.context, ss.doc_context, ss.messages\n FROM studio_snapshots ss\n JOIN studios s ON s.id = ss.studio_id AND s.user_id = ?\n WHERE ss.studio_id = ?\n ORDER BY modified_at DESC" + "query": "DELETE FROM studios WHERE id = ? AND project_id = ? AND user_id = ? RETURNING id" }, "11f5e7122d047f87c398cf56470c284e2037203bc4d1506efc85e7431e2e2f5f": { "describe": { @@ -360,24 +300,6 @@ }, "query": "UPDATE studio_snapshots SET doc_context = ? WHERE id = ?" }, - "476c0b82963b9a2333edec797133770f32c8269a21a17d3165e7785f69e886ab": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int64" - } - ], - "nullable": [ - false - ], - "parameters": { - "Right": 2 - } - }, - "query": "SELECT id FROM studios WHERE id = ? AND user_id = ?" - }, "49f204678451d2c045fc1569707957e41bc170ea2ede754e2a5e660c14347bba": { "describe": { "columns": [ @@ -554,24 +476,6 @@ }, "query": "UPDATE templates SET modified_at = datetime('now') WHERE id = ?" }, - "6668fdad9bc0e6d5c97d6664c3c55062d58e0cd0d29fb91d4c1beb26c5af23a0": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int64" - } - ], - "nullable": [ - true - ], - "parameters": { - "Right": 3 - } - }, - "query": "DELETE FROM studio_snapshots\n WHERE id IN (\n SELECT ss.id\n FROM studio_snapshots ss\n JOIN studios s ON s.id = ss.studio_id AND s.user_id = ?\n WHERE ss.id = ? AND ss.studio_id = ?\n )\n RETURNING id" - }, "671df14b7c9077b95e586690f8c6d3f2eeb0a3942d0b800f272b010fcd2ca97b": { "describe": { "columns": [ @@ -626,24 +530,6 @@ }, "query": "SELECT * FROM tutorial_questions WHERE repo_ref = ?" }, - "69d67f761ba7fb01ffc310f12cd181acb744b76de90e33c039ce7030c27642bc": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int64" - } - ], - "nullable": [ - false - ], - "parameters": { - "Right": 2 - } - }, - "query": "INSERT INTO studios(name, user_id) VALUES (?, ?) RETURNING id" - }, "6e3bfe277ca4506bc389597db418b311c4faabf5af132f81699997e4d766e40d": { "describe": { "columns": [ @@ -772,54 +658,6 @@ }, "query": "INSERT INTO tutorial_questions (question, tag, repo_ref) VALUES (?, ?, ?)" }, - "8c70038e00fa4619a2d77cbf2de3084bafa99e19567cd3bb5cde55f56b5c0070": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int64" - }, - { - "name": "name", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "context", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "doc_context", - "ordinal": 3, - "type_info": "Text" - }, - { - "name": "messages", - "ordinal": 4, - "type_info": "Text" - }, - { - "name": "modified_at", - "ordinal": 5, - "type_info": "Datetime" - } - ], - "nullable": [ - false, - true, - false, - false, - false, - false - ], - "parameters": { - "Right": 3 - } - }, - "query": "SELECT s.id, s.name, ss.context, ss.doc_context, ss.messages, ss.modified_at\n FROM studios s\n INNER JOIN studio_snapshots ss ON ss.id = ?\n WHERE s.id = ? AND s.user_id = ?" - }, "8f99eede8e6c1fb27acc2524c00cebbc2d4e73db8af05599521e3b00c621347f": { "describe": { "columns": [], @@ -880,24 +718,6 @@ }, "query": "UPDATE studio_snapshots SET context = ? WHERE id = ?" }, - "a749617e52fb0bf29cf18eb031b8fad807e1db9bde02736d979e6c870ff13e4a": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int64" - } - ], - "nullable": [ - true - ], - "parameters": { - "Right": 2 - } - }, - "query": "DELETE FROM studios WHERE id = ? AND user_id = ? RETURNING id" - }, "abf57821a0ac6f855a9dc677de87beac319610add247dbff2f4ce9a2eec3ce2a": { "describe": { "columns": [ @@ -960,6 +780,24 @@ }, "query": "INSERT INTO chunk_cache (chunk_hash, file_hash, branches, repo_ref) VALUES (?, ?, ?, ?)" }, + "b7a073c7007ae021f53941a9a2d20fd2d72459f21870e6782d31698c993b54f8": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + } + ], + "nullable": [ + false + ], + "parameters": { + "Right": 3 + } + }, + "query": "INSERT INTO studios (project_id, user_id, name) VALUES (?, ?, ?) RETURNING id" + }, "bc60b0f34fd20feba2da3f16458770424534eacaba75e6f45b8218f32767671b": { "describe": { "columns": [ @@ -990,6 +828,24 @@ }, "query": "SELECT thread_id, created_at, title FROM conversations WHERE user_id = ? AND repo_ref = ? ORDER BY created_at DESC" }, + "bf4f0aa6fe3d9b43141aa95bb47c1774423e7482ef1f9036a85fc8fd4d44e0c9": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + } + ], + "nullable": [ + false + ], + "parameters": { + "Right": 3 + } + }, + "query": "SELECT id FROM studios WHERE id = ? AND project_id = ? AND user_id = ?" + }, "d2b52987aaa4bdc39c04254834c941cad2165eefd02eef46fda413822be91fd0": { "describe": { "columns": [ @@ -1008,23 +864,83 @@ }, "query": "SELECT messages FROM studio_snapshots WHERE id = ?" }, - "d477bfff9f1880a91e700aad60fdd92141c3c5a6494e37ba965289aff8e9a956": { + "d2bb7c2ab03cd19e0721bdc7ca04ed881eb08f4f5de6d5f2361c34211e392d67": { "describe": { "columns": [ { "name": "id", "ordinal": 0, "type_info": "Int64" + }, + { + "name": "name", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "modified_at!", + "ordinal": 2, + "type_info": "Datetime" + }, + { + "name": "context", + "ordinal": 3, + "type_info": "Text" } ], "nullable": [ + false, + true, + false, false ], "parameters": { "Right": 2 } }, - "query": "INSERT INTO studios (user_id, name) VALUES (?, ?) RETURNING id" + "query": "SELECT\n s.id,\n s.name,\n ss.modified_at as \"modified_at!\",\n ss.context\n FROM studios s\n INNER JOIN studio_snapshots ss ON s.id = ss.studio_id\n WHERE s.project_id = ? AND s.user_id = ? AND (ss.studio_id, ss.modified_at) IN (\n SELECT studio_id, MAX(modified_at)\n FROM studio_snapshots\n GROUP BY studio_id\n )" + }, + "d57fe07fbdc98edfc03f93c5d078d7a84fd02e325ea9966d5973160b53fc2994": { + "describe": { + "columns": [ + { + "name": "id!", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "modified_at", + "ordinal": 1, + "type_info": "Datetime" + }, + { + "name": "context", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "doc_context", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "messages", + "ordinal": 4, + "type_info": "Text" + } + ], + "nullable": [ + true, + false, + false, + false, + false + ], + "parameters": { + "Right": 3 + } + }, + "query": "SELECT ss.id as 'id!', ss.modified_at, ss.context, ss.doc_context, ss.messages\n FROM studio_snapshots ss\n JOIN studios s ON s.id = ss.studio_id AND s.project_id = ? AND s.user_id = ?\n WHERE ss.studio_id = ?\n ORDER BY modified_at DESC" }, "d5ee5becde7005920d7094fca5b7974bbf19713b3625fbf6d1a3e198e7cf4de4": { "describe": { @@ -1066,6 +982,24 @@ }, "query": "INSERT INTO file_cache (repo_ref, cache_hash) VALUES (?, ?)" }, + "d98a2b387cc922e144405c0a987f8e63084e1e442dcbb5ac804bef439610577e": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + } + ], + "nullable": [ + false + ], + "parameters": { + "Right": 3 + } + }, + "query": "INSERT INTO studios(name, project_id, user_id) VALUES (?, ?, ?) RETURNING id" + }, "db4077fd7603079ffc8c237ec49a640a6061a06d12499bdb7b39ed3c23c1b38e": { "describe": { "columns": [], @@ -1172,6 +1106,24 @@ }, "query": "SELECT repo_ref, exchanges FROM conversations WHERE user_id = ? AND thread_id = ?" }, + "e985723bc2607c8b88ed8fba38911c885ee661d35eb1c17dd62a35184404a08d": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + } + ], + "nullable": [ + true + ], + "parameters": { + "Right": 4 + } + }, + "query": "DELETE FROM studio_snapshots\n WHERE id IN (\n SELECT ss.id\n FROM studio_snapshots ss\n JOIN studios s ON s.id = ss.studio_id AND s.project_id = ? AND s.user_id = ?\n WHERE ss.id = ? AND ss.studio_id = ?\n )\n RETURNING id" + }, "ec193a038eb7fc3aaca3c3adebcc4dbde01b47ae34ac2df2227c5e5459617182": { "describe": { "columns": [], @@ -1182,6 +1134,54 @@ }, "query": "UPDATE studio_snapshots SET modified_at = datetime('now') WHERE id = ?" }, + "ed32393003d272af7a1da7da3ae1096294849539df5416dbe49e9d2525464bd7": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "name", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "context", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "doc_context", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "messages", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "modified_at", + "ordinal": 5, + "type_info": "Datetime" + } + ], + "nullable": [ + false, + true, + false, + false, + false, + false + ], + "parameters": { + "Right": 4 + } + }, + "query": "SELECT s.id, s.name, ss.context, ss.doc_context, ss.messages, ss.modified_at\n FROM studios s\n INNER JOIN studio_snapshots ss ON ss.id = ?\n WHERE s.id = ? AND s.project_id = ? AND s.user_id = ?" + }, "ed6379e37c16064198f48dbfb91899d74eb346533e3c9ab3814ba67b68d71f51": { "describe": { "columns": [], diff --git a/server/bleep/src/webserver.rs b/server/bleep/src/webserver.rs index f0d6c470c5..900931d5f3 100644 --- a/server/bleep/src/webserver.rs +++ b/server/bleep/src/webserver.rs @@ -96,28 +96,32 @@ pub async fn start(app: Application) -> anyhow::Result<()> { get(answer::conversations::thread), ) .route("/answer/vote", post(answer::vote)) - .route("/studio", post(studio::create)) - .route("/studio", get(studio::list)) - .route( - "/studio/:studio_id", - get(studio::get).patch(studio::patch).delete(studio::delete), - ) - .route("/studio/import", post(studio::import)) - .route("/studio/:studio_id/generate", get(studio::generate)) - .route("/studio/:studio_id/diff", get(studio::diff)) - .route("/studio/:studio_id/diff/apply", post(studio::diff_apply)) - .route("/studio/:studio_id/snapshots", get(studio::list_snapshots)) - .route( - "/studio/:studio_id/snapshots/:snapshot_id", - delete(studio::delete_snapshot), - ) - .route( - "/studio/file-token-count", - post(studio::get_file_token_count), - ) - .route( - "/studio/doc-file-token-count", - post(studio::get_doc_file_token_count), + .nest( + "/projects/:project_id", + Router::new() + .route("/studio", post(studio::create)) + .route("/studio", get(studio::list)) + .route( + "/studio/:studio_id", + get(studio::get).patch(studio::patch).delete(studio::delete), + ) + .route("/studio/import", post(studio::import)) + .route("/studio/:studio_id/generate", get(studio::generate)) + .route("/studio/:studio_id/diff", get(studio::diff)) + .route("/studio/:studio_id/diff/apply", post(studio::diff_apply)) + .route("/studio/:studio_id/snapshots", get(studio::list_snapshots)) + .route( + "/studio/:studio_id/snapshots/:snapshot_id", + delete(studio::delete_snapshot), + ) + .route( + "/studio/file-token-count", + post(studio::get_file_token_count), + ) + .route( + "/studio/doc-file-token-count", + post(studio::get_doc_file_token_count), + ), ) .route("/template", post(template::create)) .route("/template", get(template::list)) diff --git a/server/bleep/src/webserver/studio.rs b/server/bleep/src/webserver/studio.rs index ca4aecf844..08f2cca11d 100644 --- a/server/bleep/src/webserver/studio.rs +++ b/server/bleep/src/webserver/studio.rs @@ -74,6 +74,7 @@ pub struct Create { pub async fn create( app: Extension, user: Extension, + Path(project_id): Path, Json(params): Json, ) -> webserver::Result { let mut transaction = app.sql.begin().await?; @@ -81,7 +82,8 @@ pub async fn create( let user_id = user.username().ok_or_else(no_user_id)?.to_string(); let studio_id: i64 = sqlx::query! { - "INSERT INTO studios (user_id, name) VALUES (?, ?) RETURNING id", + "INSERT INTO studios (project_id, user_id, name) VALUES (?, ?, ?) RETURNING id", + project_id, user_id, params.name, } @@ -170,23 +172,24 @@ pub struct Get { pub async fn get( app: Extension, user: Extension, - Path(id): Path, + Path((project_id, studio_id)): Path<(i64, i64)>, Query(params): Query, ) -> webserver::Result> { let user_id = user.username().ok_or_else(no_user_id)?.to_string(); let snapshot_id = match params.snapshot_id { Some(id) => id, - None => latest_snapshot_id(id, &*app.sql, &user_id).await?, + None => latest_snapshot_id(studio_id, &*app.sql, &user_id).await?, }; let row = sqlx::query! { "SELECT s.id, s.name, ss.context, ss.doc_context, ss.messages, ss.modified_at FROM studios s INNER JOIN studio_snapshots ss ON ss.id = ? - WHERE s.id = ? AND s.user_id = ?", + WHERE s.id = ? AND s.project_id = ? AND s.user_id = ?", snapshot_id, - id, + studio_id, + project_id, user_id, } .fetch_optional(&*app.sql) @@ -223,7 +226,7 @@ pub struct Patch { pub async fn patch( app: Extension, user: Extension, - Path(studio_id): Path, + Path((project_id, studio_id)): Path<(i64, i64)>, Json(patch): Json, ) -> webserver::Result> { let user_id = user.username().ok_or_else(no_user_id)?.to_string(); @@ -237,9 +240,10 @@ pub async fn patch( // Ensure the ID is valid first. sqlx::query!( - "SELECT id FROM studios WHERE id = ? AND user_id = ?", + "SELECT id FROM studios WHERE id = ? AND project_id = ? AND user_id = ?", studio_id, - user_id + project_id, + user_id, ) .fetch_optional(&mut transaction) .await? @@ -331,14 +335,15 @@ pub async fn patch( pub async fn delete( app: Extension, user: Extension, - Path(id): Path, + Path((project_id, studio_id)): Path<(i64, i64)>, ) -> webserver::Result<()> { let user_id = user.username().ok_or_else(no_user_id)?.to_string(); sqlx::query!( - "DELETE FROM studios WHERE id = ? AND user_id = ? RETURNING id", - id, - user_id + "DELETE FROM studios WHERE id = ? AND project_id = ? AND user_id = ? RETURNING id", + studio_id, + project_id, + user_id, ) .fetch_optional(&*app.sql) .await? @@ -358,6 +363,7 @@ pub struct ListItem { pub async fn list( app: Extension, user: Extension, + Path(project_id): Path, ) -> webserver::Result>> { let user_id = user.username().ok_or_else(no_user_id)?.to_string(); @@ -369,11 +375,12 @@ pub async fn list( ss.context FROM studios s INNER JOIN studio_snapshots ss ON s.id = ss.studio_id - WHERE s.user_id = ? AND (ss.studio_id, ss.modified_at) IN ( + WHERE s.project_id = ? AND s.user_id = ? AND (ss.studio_id, ss.modified_at) IN ( SELECT studio_id, MAX(modified_at) FROM studio_snapshots GROUP BY studio_id )", + project_id, user_id, ) .fetch_all(&*app.sql) @@ -686,7 +693,7 @@ fn count_tokens_for_file(path: &str, body: &str, ranges: &[Range]) -> usi pub async fn generate( app: Extension, user: Extension, - Path(studio_id): Path, + Path((_project_id, studio_id)): Path<(i64, i64)>, ) -> webserver::Result> + Send>>>> { let user_id = user.username().ok_or_else(no_user_id)?.to_string(); @@ -929,7 +936,7 @@ mod structured_diff { pub async fn diff( app: Extension, user: Extension, - Path(studio_id): Path, + Path((_project_id, studio_id)): Path<(i64, i64)>, ) -> webserver::Result> { let user_id = user.username().ok_or_else(no_user_id)?.to_string(); @@ -1264,7 +1271,7 @@ async fn validate_add_file( pub async fn diff_apply( app: State, user: Extension, - Path(studio_id): Path, + Path((_project_id, studio_id)): Path<(i64, i64)>, diff: String, ) -> webserver::Result<()> { let user_id = user.username().ok_or_else(no_user_id)?.to_string(); @@ -1424,6 +1431,7 @@ pub struct Import { pub async fn import( app: Extension, user: Extension, + Path(project_id): Path, Query(params): Query, ) -> webserver::Result { let mut transaction = app.sql.begin().await?; @@ -1487,8 +1495,9 @@ pub async fn import( Some(id) => id, None => { sqlx::query!( - "INSERT INTO studios(name, user_id) VALUES (?, ?) RETURNING id", + "INSERT INTO studios(name, project_id, user_id) VALUES (?, ?, ?) RETURNING id", conversation.title, + project_id, user_id, ) .fetch_one(&mut transaction) @@ -1652,16 +1661,17 @@ pub struct Snapshot { pub async fn list_snapshots( app: Extension, user: Extension, - Path(studio_id): Path, + Path((project_id, studio_id)): Path<(i64, i64)>, ) -> webserver::Result>> { let user_id = user.username().ok_or_else(no_user_id)?.to_string(); sqlx::query! { "SELECT ss.id as 'id!', ss.modified_at, ss.context, ss.doc_context, ss.messages FROM studio_snapshots ss - JOIN studios s ON s.id = ss.studio_id AND s.user_id = ? + JOIN studios s ON s.id = ss.studio_id AND s.project_id = ? AND s.user_id = ? WHERE ss.studio_id = ? ORDER BY modified_at DESC", + project_id, user_id, studio_id, } @@ -1686,7 +1696,7 @@ pub async fn list_snapshots( pub async fn delete_snapshot( app: Extension, user: Extension, - Path((studio_id, snapshot_id)): Path<(i64, i64)>, + Path((project_id, studio_id, snapshot_id)): Path<(i64, i64, i64)>, ) -> webserver::Result<()> { let user_id = user.username().ok_or_else(no_user_id)?.to_string(); @@ -1695,10 +1705,11 @@ pub async fn delete_snapshot( WHERE id IN ( SELECT ss.id FROM studio_snapshots ss - JOIN studios s ON s.id = ss.studio_id AND s.user_id = ? + JOIN studios s ON s.id = ss.studio_id AND s.project_id = ? AND s.user_id = ? WHERE ss.id = ? AND ss.studio_id = ? ) RETURNING id", + project_id, user_id, snapshot_id, studio_id, From be504edcae2642159a6effaf8c0f9ccd35d4baff Mon Sep 17 00:00:00 2001 From: calyptobai Date: Fri, 24 Nov 2023 20:47:38 -0500 Subject: [PATCH 04/88] WIP: projects Back-end API changes include: - Addition of `projects` table - Studios now live *inside* a project - Ownership was moved from studios to projects - New routes: - `GET /api/projects`: returns a list of: `[ { id: number, name: string, modified_at: date string } ]` - `POST /api/projects/`: takes in a body like: `{ name: string | null }` Note: there is a default name generated here if not provided, "New Project" This route returns a string body which is the ID - `GET /api/projects/:id`: if the project exists, returns: `{ name: string }` - `POST /api/projects/:id`: updates a project, takes a body of: `{ name: string }` Returns nothing - Additionally, all `/api/studio/...` routes have been moved to `/projects/:id/studios/..` - Note: `studio` was changed to `studios` - Note: all routes remain otherwise unchanged --- .../migrations/20231122012638_projects.sql | 2 + server/bleep/sqlx-data.json | 490 +++++++++++------- server/bleep/src/agent/prompts.rs | 17 +- server/bleep/src/webserver.rs | 68 ++- server/bleep/src/webserver/project.rs | 135 +++++ server/bleep/src/webserver/studio.rs | 169 +++--- 6 files changed, 571 insertions(+), 310 deletions(-) create mode 100644 server/bleep/src/webserver/project.rs diff --git a/server/bleep/migrations/20231122012638_projects.sql b/server/bleep/migrations/20231122012638_projects.sql index 0aa63e4b0c..ca337804bd 100644 --- a/server/bleep/migrations/20231122012638_projects.sql +++ b/server/bleep/migrations/20231122012638_projects.sql @@ -1,7 +1,9 @@ CREATE TABLE projects ( id INTEGER PRIMARY KEY, + user_id INTEGER NOT NULL, name TEXT ); ALTER TABLE studios ADD COLUMN project_id INTEGER NOT NULL; +ALTER TABLE studios DROP COLUMN user_id; ALTER TABLE conversations ADD COLUMN project_id INTEGER NOT NULL; diff --git a/server/bleep/sqlx-data.json b/server/bleep/sqlx-data.json index 1493675c10..ab95db747c 100644 --- a/server/bleep/sqlx-data.json +++ b/server/bleep/sqlx-data.json @@ -72,7 +72,7 @@ }, "query": "SELECT context, messages FROM studio_snapshots WHERE id = ?" }, - "0c116e27d03cdd17da9a1403cd010dadb28f53756276aa38cdc2caeb2cec6bf6": { + "0fda94d4963a3991ff65079f63ba876030ecaeb1bb8fee3d6e729939a73ad4ea": { "describe": { "columns": [ { @@ -82,13 +82,13 @@ } ], "nullable": [ - true + false ], "parameters": { - "Right": 3 + "Right": 2 } }, - "query": "DELETE FROM studios WHERE id = ? AND project_id = ? AND user_id = ? RETURNING id" + "query": "INSERT INTO studios(name, project_id) VALUES (?, ?) RETURNING id" }, "11f5e7122d047f87c398cf56470c284e2037203bc4d1506efc85e7431e2e2f5f": { "describe": { @@ -262,24 +262,6 @@ }, "query": "DELETE FROM conversations WHERE user_id = ? AND thread_id = ?" }, - "3973050a775486df80905b03ef3c8a73d285fb34b03b7820853d5372a329c388": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int64" - } - ], - "nullable": [ - true - ], - "parameters": { - "Right": 2 - } - }, - "query": "SELECT ss.id\n FROM studio_snapshots ss\n JOIN studios s ON s.id = ss.studio_id AND s.user_id = ?\n WHERE ss.studio_id = ?\n ORDER BY ss.modified_at DESC\n LIMIT 1" - }, "454d7dfb50480aae5ad9c8372262d55a302e214e1c7ceb8d62b53832f75bd85b": { "describe": { "columns": [], @@ -456,6 +438,54 @@ }, "query": "SELECT id, index_status, name, url, favicon, description, modified_at FROM docs" }, + "596f303a72529bbc9009d39dd5965fd3b5818595b57a3288cc33063bea5e0eed": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "name", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "context", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "doc_context", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "messages", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "modified_at", + "ordinal": 5, + "type_info": "Datetime" + } + ], + "nullable": [ + false, + true, + false, + false, + false, + false + ], + "parameters": { + "Right": 3 + } + }, + "query": "SELECT s.id, s.name, ss.context, ss.doc_context, ss.messages, ss.modified_at\n FROM studios s\n INNER JOIN studio_snapshots ss ON ss.id = ?\n WHERE s.id = ? AND s.project_id = ?" + }, "5a12c77f8ee2a83b87cb5d9a79015fcc81d39d1466f853f613cb0fd789ace552": { "describe": { "columns": [], @@ -530,6 +560,24 @@ }, "query": "SELECT * FROM tutorial_questions WHERE repo_ref = ?" }, + "6a4def7f50a90ba02442566f45a8a0752eac147f00372c0b7cb3971aa8824333": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + } + ], + "nullable": [ + true + ], + "parameters": { + "Right": 4 + } + }, + "query": "DELETE FROM studio_snapshots\n WHERE id IN (\n SELECT ss.id\n FROM studio_snapshots ss\n JOIN studios s ON s.id = ss.studio_id\n JOIN projects p ON p.id = s.project_id\n WHERE ss.id = ? AND ss.studio_id = ? AND s.project_id = ? AND p.user_id = ?\n )\n RETURNING id" + }, "6e3bfe277ca4506bc389597db418b311c4faabf5af132f81699997e4d766e40d": { "describe": { "columns": [ @@ -602,6 +650,48 @@ }, "query": "SELECT id, name, modified_at, content, user_id IS NULL as \"is_default: bool\"\n FROM templates\n WHERE id = ? AND (user_id = ? OR user_id IS NULL)" }, + "719da56687c5febedfc051a6848e64e28c22120471edc06f0111b140ff0fd95c": { + "describe": { + "columns": [ + { + "name": "id!", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "modified_at", + "ordinal": 1, + "type_info": "Datetime" + }, + { + "name": "context", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "doc_context", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "messages", + "ordinal": 4, + "type_info": "Text" + } + ], + "nullable": [ + true, + false, + false, + false, + false + ], + "parameters": { + "Right": 3 + } + }, + "query": "SELECT ss.id as 'id!', ss.modified_at, ss.context, ss.doc_context, ss.messages\n FROM studio_snapshots ss\n JOIN studios s ON s.id = ss.studio_id AND s.project_id = ?\n JOIN projects p ON p.id = s.project_id\n WHERE ss.studio_id = ? AND p.user_id = ?\n ORDER BY modified_at DESC" + }, "76464f75732fee5c742a23d7a0b95de1def360bae7943a2389944b0177106033": { "describe": { "columns": [ @@ -648,6 +738,24 @@ }, "query": "UPDATE docs SET index_status = ? WHERE id = ?" }, + "7ce3d2c4733a59855501218145216b2367ccf46c8d76c11e9fccf6eb123623ef": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + } + ], + "nullable": [ + false + ], + "parameters": { + "Right": 2 + } + }, + "query": "INSERT INTO studios (project_id, name) VALUES (?, ?) RETURNING id" + }, "881aa78dfa3cd1bc3aa7a6edb8281aec5a972c1f53607d25c4e1f6d03cd3faef": { "describe": { "columns": [], @@ -658,6 +766,24 @@ }, "query": "INSERT INTO tutorial_questions (question, tag, repo_ref) VALUES (?, ?, ?)" }, + "8efc3961c0182990afa0ffb553ecb77b3ad14a777970facb34f74d7e8e7bc1ab": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + } + ], + "nullable": [ + true + ], + "parameters": { + "Right": 3 + } + }, + "query": "UPDATE projects SET name = ? WHERE id = ? AND user_id = ? RETURNING id" + }, "8f99eede8e6c1fb27acc2524c00cebbc2d4e73db8af05599521e3b00c621347f": { "describe": { "columns": [], @@ -668,6 +794,30 @@ }, "query": "UPDATE studio_snapshots SET modified_at = ? WHERE id = ?" }, + "91015fa610163b6e9cf6c6e2b9f1e4519539bd5b651c96155dda6d9f0363a166": { + "describe": { + "columns": [ + { + "name": "name", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "modified_at", + "ordinal": 1, + "type_info": "Datetime" + } + ], + "nullable": [ + true, + true + ], + "parameters": { + "Right": 2 + } + }, + "query": "SELECT name, (\n SELECT ss.modified_at\n FROM studio_snapshots ss\n JOIN studios s ON s.project_id = $1 AND ss.studio_id = s.id\n ORDER BY ss.modified_at DESC\n LIMIT 1\n ) AS modified_at\n FROM projects\n WHERE id = $1 AND user_id = $2" + }, "9146d9c8a7f17cc65c017cb364d1a853a9163b5ece336c0a6ef4e28e8df56a6b": { "describe": { "columns": [], @@ -688,6 +838,24 @@ }, "query": "INSERT INTO studio_snapshots (studio_id, context, doc_context, messages)\n VALUES (?, ?, ?, ?)" }, + "9db35f3045790fbd63f1efc4a96e5a7234f09cc513323320fd145146b03cce2b": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + } + ], + "nullable": [ + false + ], + "parameters": { + "Right": 2 + } + }, + "query": "INSERT INTO projects (user_id, name) VALUES (?, ?) RETURNING id" + }, "9f862a56e79cc9ae6e9b896064a0057335b40225be0a8c8d29d9227de12ae364": { "describe": { "columns": [], @@ -708,6 +876,24 @@ }, "query": "INSERT INTO docs (url, index_status) VALUES (?, ?)" }, + "a333e6cb457d03c6e54002e37908ecd56efe7862eed7a186db8eb2dc559c7f13": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + } + ], + "nullable": [ + true + ], + "parameters": { + "Right": 2 + } + }, + "query": "SELECT ss.id\n FROM studio_snapshots ss\n JOIN studios s ON s.id = ss.studio_id\n JOIN projects p ON s.project_id = p.id\n WHERE ss.studio_id = ? AND p.user_id = ?\n ORDER BY ss.modified_at DESC\n LIMIT 1" + }, "a4278b11c21e533d662043810e7cc8a3fca86cf03989766fffb84302d84394e5": { "describe": { "columns": [], @@ -718,6 +904,36 @@ }, "query": "UPDATE studio_snapshots SET context = ? WHERE id = ?" }, + "a8baec57552045778eb4609e83452c668583bf5a6ff8fec1898523058a061bcb": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "name", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "modified_at", + "ordinal": 2, + "type_info": "Datetime" + } + ], + "nullable": [ + false, + true, + true + ], + "parameters": { + "Right": 1 + } + }, + "query": "SELECT p.id, p.name, (\n SELECT ss.modified_at\n FROM studio_snapshots ss\n JOIN studios s ON s.project_id = p.id AND ss.studio_id = s.id\n ORDER BY ss.modified_at DESC\n LIMIT 1\n ) AS modified_at\n FROM projects p\n WHERE user_id = ?" + }, "abf57821a0ac6f855a9dc677de87beac319610add247dbff2f4ce9a2eec3ce2a": { "describe": { "columns": [ @@ -780,24 +996,6 @@ }, "query": "INSERT INTO chunk_cache (chunk_hash, file_hash, branches, repo_ref) VALUES (?, ?, ?, ?)" }, - "b7a073c7007ae021f53941a9a2d20fd2d72459f21870e6782d31698c993b54f8": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int64" - } - ], - "nullable": [ - false - ], - "parameters": { - "Right": 3 - } - }, - "query": "INSERT INTO studios (project_id, user_id, name) VALUES (?, ?, ?) RETURNING id" - }, "bc60b0f34fd20feba2da3f16458770424534eacaba75e6f45b8218f32767671b": { "describe": { "columns": [ @@ -828,24 +1026,6 @@ }, "query": "SELECT thread_id, created_at, title FROM conversations WHERE user_id = ? AND repo_ref = ? ORDER BY created_at DESC" }, - "bf4f0aa6fe3d9b43141aa95bb47c1774423e7482ef1f9036a85fc8fd4d44e0c9": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int64" - } - ], - "nullable": [ - false - ], - "parameters": { - "Right": 3 - } - }, - "query": "SELECT id FROM studios WHERE id = ? AND project_id = ? AND user_id = ?" - }, "d2b52987aaa4bdc39c04254834c941cad2165eefd02eef46fda413822be91fd0": { "describe": { "columns": [ @@ -864,84 +1044,6 @@ }, "query": "SELECT messages FROM studio_snapshots WHERE id = ?" }, - "d2bb7c2ab03cd19e0721bdc7ca04ed881eb08f4f5de6d5f2361c34211e392d67": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int64" - }, - { - "name": "name", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "modified_at!", - "ordinal": 2, - "type_info": "Datetime" - }, - { - "name": "context", - "ordinal": 3, - "type_info": "Text" - } - ], - "nullable": [ - false, - true, - false, - false - ], - "parameters": { - "Right": 2 - } - }, - "query": "SELECT\n s.id,\n s.name,\n ss.modified_at as \"modified_at!\",\n ss.context\n FROM studios s\n INNER JOIN studio_snapshots ss ON s.id = ss.studio_id\n WHERE s.project_id = ? AND s.user_id = ? AND (ss.studio_id, ss.modified_at) IN (\n SELECT studio_id, MAX(modified_at)\n FROM studio_snapshots\n GROUP BY studio_id\n )" - }, - "d57fe07fbdc98edfc03f93c5d078d7a84fd02e325ea9966d5973160b53fc2994": { - "describe": { - "columns": [ - { - "name": "id!", - "ordinal": 0, - "type_info": "Int64" - }, - { - "name": "modified_at", - "ordinal": 1, - "type_info": "Datetime" - }, - { - "name": "context", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "doc_context", - "ordinal": 3, - "type_info": "Text" - }, - { - "name": "messages", - "ordinal": 4, - "type_info": "Text" - } - ], - "nullable": [ - true, - false, - false, - false, - false - ], - "parameters": { - "Right": 3 - } - }, - "query": "SELECT ss.id as 'id!', ss.modified_at, ss.context, ss.doc_context, ss.messages\n FROM studio_snapshots ss\n JOIN studios s ON s.id = ss.studio_id AND s.project_id = ? AND s.user_id = ?\n WHERE ss.studio_id = ?\n ORDER BY modified_at DESC" - }, "d5ee5becde7005920d7094fca5b7974bbf19713b3625fbf6d1a3e198e7cf4de4": { "describe": { "columns": [ @@ -982,24 +1084,6 @@ }, "query": "INSERT INTO file_cache (repo_ref, cache_hash) VALUES (?, ?)" }, - "d98a2b387cc922e144405c0a987f8e63084e1e442dcbb5ac804bef439610577e": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int64" - } - ], - "nullable": [ - false - ], - "parameters": { - "Right": 3 - } - }, - "query": "INSERT INTO studios(name, project_id, user_id) VALUES (?, ?, ?) RETURNING id" - }, "db4077fd7603079ffc8c237ec49a640a6061a06d12499bdb7b39ed3c23c1b38e": { "describe": { "columns": [], @@ -1106,35 +1190,7 @@ }, "query": "SELECT repo_ref, exchanges FROM conversations WHERE user_id = ? AND thread_id = ?" }, - "e985723bc2607c8b88ed8fba38911c885ee661d35eb1c17dd62a35184404a08d": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int64" - } - ], - "nullable": [ - true - ], - "parameters": { - "Right": 4 - } - }, - "query": "DELETE FROM studio_snapshots\n WHERE id IN (\n SELECT ss.id\n FROM studio_snapshots ss\n JOIN studios s ON s.id = ss.studio_id AND s.project_id = ? AND s.user_id = ?\n WHERE ss.id = ? AND ss.studio_id = ?\n )\n RETURNING id" - }, - "ec193a038eb7fc3aaca3c3adebcc4dbde01b47ae34ac2df2227c5e5459617182": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Right": 1 - } - }, - "query": "UPDATE studio_snapshots SET modified_at = datetime('now') WHERE id = ?" - }, - "ed32393003d272af7a1da7da3ae1096294849539df5416dbe49e9d2525464bd7": { + "ebdb202965998c4e29727dca19a60c82b2a4574530858e29bf6584c8b6e1f32b": { "describe": { "columns": [ { @@ -1148,39 +1204,37 @@ "type_info": "Text" }, { - "name": "context", + "name": "modified_at!", "ordinal": 2, - "type_info": "Text" + "type_info": "Datetime" }, { - "name": "doc_context", + "name": "context", "ordinal": 3, "type_info": "Text" - }, - { - "name": "messages", - "ordinal": 4, - "type_info": "Text" - }, - { - "name": "modified_at", - "ordinal": 5, - "type_info": "Datetime" } ], "nullable": [ false, true, false, - false, - false, false ], "parameters": { - "Right": 4 + "Right": 2 + } + }, + "query": "SELECT\n s.id,\n s.name,\n ss.modified_at as \"modified_at!\",\n ss.context\n FROM studios s\n INNER JOIN studio_snapshots ss ON s.id = ss.studio_id\n INNER JOIN projects p ON p.id = s.project_id\n WHERE s.project_id = ? AND p.user_id = ? AND (ss.studio_id, ss.modified_at) IN (\n SELECT studio_id, MAX(modified_at)\n FROM studio_snapshots\n GROUP BY studio_id\n )" + }, + "ec193a038eb7fc3aaca3c3adebcc4dbde01b47ae34ac2df2227c5e5459617182": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 1 } }, - "query": "SELECT s.id, s.name, ss.context, ss.doc_context, ss.messages, ss.modified_at\n FROM studios s\n INNER JOIN studio_snapshots ss ON ss.id = ?\n WHERE s.id = ? AND s.project_id = ? AND s.user_id = ?" + "query": "UPDATE studio_snapshots SET modified_at = datetime('now') WHERE id = ?" }, "ed6379e37c16064198f48dbfb91899d74eb346533e3c9ab3814ba67b68d71f51": { "describe": { @@ -1202,6 +1256,24 @@ }, "query": "INSERT INTO studio_snapshots(studio_id, context, doc_context, messages)\n SELECT studio_id, context, doc_context, ?\n FROM studio_snapshots\n WHERE id = ?" }, + "ee41c9f473344709b6e7a4bdf29948d236870d54d33938d200d015b174249291": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + } + ], + "nullable": [ + true + ], + "parameters": { + "Right": 3 + } + }, + "query": "DELETE FROM studios\n WHERE id = $1 AND project_id = $2 AND EXISTS (\n SELECT p.id FROM projects p WHERE p.id = $2 AND p.user_id = $3\n )\n RETURNING id" + }, "f91f80f8d1a82a5d79ce50131618877a50c0753a1ccb1f4cee714e274f022907": { "describe": { "columns": [], @@ -1212,6 +1284,24 @@ }, "query": "UPDATE docs SET name = ? WHERE id = ?" }, + "fc9f0b31bbf0316abd2e24cadabfd2e5ea42cb729b69a2f8caa1a3c52f167f63": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + } + ], + "nullable": [ + false + ], + "parameters": { + "Right": 3 + } + }, + "query": "SELECT s.id FROM studios s\n JOIN projects p ON p.id = s.project_id\n WHERE s.id = ? AND p.id = ? AND p.user_id = ?" + }, "fd74b491f6b06bb58c7d62b461094e5463e397bb649ae338c2b1a0e67e6155c3": { "describe": { "columns": [ diff --git a/server/bleep/src/agent/prompts.rs b/server/bleep/src/agent/prompts.rs index 4c60f52005..0fb385d9f2 100644 --- a/server/bleep/src/agent/prompts.rs +++ b/server/bleep/src/agent/prompts.rs @@ -307,6 +307,7 @@ pub fn studio_diff_prompt(context_formatted: &str) -> String { r#"Below are files from a codebase. Your job is to write a Unified Format patch to complete a provided task. To write a unified format patch, surround it in a code block: ```diff Follow these rules strictly: +- Diff paths follow the format `github.com/org/repo:path/to/file.js` for remote repositories, or `local//path/to/repo:path/to/file.js` for local repositories. Make sure to include these in your diff. - You MUST only return a single diff block, no additional commentary. - Keep hunks concise only include a short context for each hunk. - ALWAYS respect input whitespace, to ensure diffs can be applied cleanly! @@ -317,8 +318,8 @@ Follow these rules strictly: # Example outputs ```diff ---- src/index.js -+++ src/index.js +--- github.com/BloopAI/tutorial:src/index.js ++++ github.com/BloopAI/tutorial:src/index.js @@ -10,5 +10,5 @@ const maybeHello = () => {{ if (Math.random() > 0.5) {{ @@ -329,8 +330,8 @@ Follow these rules strictly: ``` ```diff ---- README.md -+++ README.md +--- local//Users/blooper/dev/bloop:README.md ++++ local//Users/blooper/dev/bloop:README.md @@ -1,3 +1,3 @@ # Bloop AI @@ -339,8 +340,8 @@ Follow these rules strictly: ``` ```diff ---- client/src/locales/en.json -+++ client/src/locales/en.json +--- github.com/BloopAI/bloop:client/src/locales/en.json ++++ github.com/BloopAI/bloop:client/src/locales/en.json @@ -21,5 +21,5 @@ "Report a bug": "Report a bug", "Sign In": "Sign In", @@ -354,7 +355,7 @@ Adding a new file: ```diff --- /dev/null -+++ src/sum.rs ++++ local//tmp/test-project:src/sum.rs @@ -0,0 +1,3 @@ +fn sum(a: f32, b: f32) -> f32 {{ + a + b @@ -364,7 +365,7 @@ Adding a new file: Removing an existing file: ```diff ---- src/div.rs +--- local//tmp/another-project:src/div.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn div(a: f32, b: f32) -> f32 {{ diff --git a/server/bleep/src/webserver.rs b/server/bleep/src/webserver.rs index 900931d5f3..8525bd1073 100644 --- a/server/bleep/src/webserver.rs +++ b/server/bleep/src/webserver.rs @@ -25,6 +25,7 @@ pub mod hoverable; mod index; pub mod intelligence; pub mod middleware; +mod project; mod query; mod quota; pub mod repos; @@ -96,32 +97,45 @@ pub async fn start(app: Application) -> anyhow::Result<()> { get(answer::conversations::thread), ) .route("/answer/vote", post(answer::vote)) - .nest( + .route("/projects", get(project::list).post(project::create)) + .route( "/projects/:project_id", - Router::new() - .route("/studio", post(studio::create)) - .route("/studio", get(studio::list)) - .route( - "/studio/:studio_id", - get(studio::get).patch(studio::patch).delete(studio::delete), - ) - .route("/studio/import", post(studio::import)) - .route("/studio/:studio_id/generate", get(studio::generate)) - .route("/studio/:studio_id/diff", get(studio::diff)) - .route("/studio/:studio_id/diff/apply", post(studio::diff_apply)) - .route("/studio/:studio_id/snapshots", get(studio::list_snapshots)) - .route( - "/studio/:studio_id/snapshots/:snapshot_id", - delete(studio::delete_snapshot), - ) - .route( - "/studio/file-token-count", - post(studio::get_file_token_count), - ) - .route( - "/studio/doc-file-token-count", - post(studio::get_doc_file_token_count), - ), + get(project::get).post(project::update), + ) + .route("/projects/:project_id/studios", post(studio::create)) + .route("/projects/:project_id/studios", get(studio::list)) + .route( + "/projects/:project_id/studios/:studio_id", + get(studio::get).patch(studio::patch).delete(studio::delete), + ) + .route("/projects/:project_id/studios/import", post(studio::import)) + .route( + "/projects/:project_id/studios/:studio_id/generate", + get(studio::generate), + ) + .route( + "/projects/:project_id/studios/:studio_id/diff", + get(studio::diff), + ) + .route( + "/projects/:project_id/studios/:studio_id/diff/apply", + post(studio::diff_apply), + ) + .route( + "/projects/:project_id/studios/:studio_id/snapshots", + get(studio::list_snapshots), + ) + .route( + "/projects/:project_id/studios/:studio_id/snapshots/:snapshot_id", + delete(studio::delete_snapshot), + ) + .route( + "/projects/:project_id/studios/file-token-count", + post(studio::get_file_token_count), + ) + .route( + "/projects/:project_id/studios/doc-file-token-count", + post(studio::get_doc_file_token_count), ) .route("/template", post(template::create)) .route("/template", get(template::list)) @@ -359,3 +373,7 @@ async fn health(State(app): State) { // subsystem checks at this stage app.semantic.health_check().await.unwrap() } + +fn no_user_id() -> Error { + Error::user("didn't have user ID") +} diff --git a/server/bleep/src/webserver/project.rs b/server/bleep/src/webserver/project.rs new file mode 100644 index 0000000000..a96d5d5c70 --- /dev/null +++ b/server/bleep/src/webserver/project.rs @@ -0,0 +1,135 @@ +use crate::{webserver, Application}; +use axum::{ + extract::{Path, Query}, + Extension, Json, +}; +use chrono::NaiveDateTime; + +use super::{middleware::User, Error}; + +fn default_name() -> String { + "New Project".into() +} + +#[derive(serde::Serialize)] +pub struct ListItem { + id: i64, + name: String, + modified_at: Option, +} + +pub async fn list( + app: Extension, + user: Extension, +) -> webserver::Result>> { + let user_id = user.username().ok_or_else(super::no_user_id)?.to_string(); + + let projects = sqlx::query! { + "SELECT p.id, p.name, ( + SELECT ss.modified_at + FROM studio_snapshots ss + JOIN studios s ON s.project_id = p.id AND ss.studio_id = s.id + ORDER BY ss.modified_at DESC + LIMIT 1 + ) AS modified_at + FROM projects p + WHERE user_id = ?", + user_id, + } + .fetch_all(&*app.sql) + .await? + .into_iter() + .map(|row| ListItem { + id: row.id, + name: row.name.unwrap_or_else(default_name), + modified_at: row.modified_at, + }) + .collect(); + + Ok(Json(projects)) +} + +#[derive(serde::Deserialize)] +pub struct Create { + name: Option, +} + +pub async fn create( + app: Extension, + user: Extension, + Query(params): Query, +) -> webserver::Result { + let user_id = user.username().ok_or_else(super::no_user_id)?.to_string(); + + let project_id = sqlx::query! { + "INSERT INTO projects (user_id, name) VALUES (?, ?) RETURNING id", + user_id, + params.name, + } + .fetch_one(&*app.sql) + .await? + .id; + + Ok(project_id.to_string()) +} + +#[derive(serde::Serialize)] +pub struct Get { + name: String, +} + +pub async fn get( + app: Extension, + user: Extension, + Path(id): Path, +) -> webserver::Result>> { + let user_id = user.username().ok_or_else(super::no_user_id)?.to_string(); + + let projects = sqlx::query! { + "SELECT name, ( + SELECT ss.modified_at + FROM studio_snapshots ss + JOIN studios s ON s.project_id = $1 AND ss.studio_id = s.id + ORDER BY ss.modified_at DESC + LIMIT 1 + ) AS modified_at + FROM projects + WHERE id = $1 AND user_id = $2", + id, + user_id, + } + .fetch_all(&*app.sql) + .await? + .into_iter() + .map(|row| Get { + name: row.name.unwrap_or_else(default_name), + }) + .collect(); + + Ok(Json(projects)) +} + +#[derive(serde::Deserialize)] +pub struct Update { + name: String, +} + +pub async fn update( + app: Extension, + user: Extension, + Path(id): Path, + Json(update): Json, +) -> webserver::Result<()> { + let user_id = user.username().ok_or_else(super::no_user_id)?.to_string(); + + sqlx::query! { + "UPDATE projects SET name = ? WHERE id = ? AND user_id = ? RETURNING id", + update.name, + id, + user_id, + } + .fetch_one(&*app.sql) + .await + .map(|_id| ()) + .map_err(Error::internal) +} diff --git a/server/bleep/src/webserver/studio.rs b/server/bleep/src/webserver/studio.rs index 08f2cca11d..69c18d1890 100644 --- a/server/bleep/src/webserver/studio.rs +++ b/server/bleep/src/webserver/studio.rs @@ -34,10 +34,6 @@ mod diff; const LLM_GATEWAY_MODEL: &str = "gpt-4-1106-preview"; -fn no_user_id() -> Error { - Error::user("didn't have user ID") -} - fn studio_not_found() -> Error { Error::not_found("unknown code studio ID") } @@ -53,12 +49,13 @@ where sqlx::query! { "SELECT ss.id FROM studio_snapshots ss - JOIN studios s ON s.id = ss.studio_id AND s.user_id = ? - WHERE ss.studio_id = ? + JOIN studios s ON s.id = ss.studio_id + JOIN projects p ON s.project_id = p.id + WHERE ss.studio_id = ? AND p.user_id = ? ORDER BY ss.modified_at DESC LIMIT 1", - user_id, studio_id, + user_id, } .fetch_optional(exec) .await? @@ -79,12 +76,9 @@ pub async fn create( ) -> webserver::Result { let mut transaction = app.sql.begin().await?; - let user_id = user.username().ok_or_else(no_user_id)?.to_string(); - let studio_id: i64 = sqlx::query! { - "INSERT INTO studios (project_id, user_id, name) VALUES (?, ?, ?) RETURNING id", + "INSERT INTO studios (project_id, name) VALUES (?, ?) RETURNING id", project_id, - user_id, params.name, } .fetch_one(&mut transaction) @@ -175,7 +169,7 @@ pub async fn get( Path((project_id, studio_id)): Path<(i64, i64)>, Query(params): Query, ) -> webserver::Result> { - let user_id = user.username().ok_or_else(no_user_id)?.to_string(); + let user_id = user.username().ok_or_else(super::no_user_id)?.to_string(); let snapshot_id = match params.snapshot_id { Some(id) => id, @@ -186,11 +180,10 @@ pub async fn get( "SELECT s.id, s.name, ss.context, ss.doc_context, ss.messages, ss.modified_at FROM studios s INNER JOIN studio_snapshots ss ON ss.id = ? - WHERE s.id = ? AND s.project_id = ? AND s.user_id = ?", + WHERE s.id = ? AND s.project_id = ?", snapshot_id, studio_id, project_id, - user_id, } .fetch_optional(&*app.sql) .await? @@ -229,7 +222,7 @@ pub async fn patch( Path((project_id, studio_id)): Path<(i64, i64)>, Json(patch): Json, ) -> webserver::Result> { - let user_id = user.username().ok_or_else(no_user_id)?.to_string(); + let user_id = user.username().ok_or_else(super::no_user_id)?.to_string(); let mut transaction = app.sql.begin().await?; @@ -240,7 +233,9 @@ pub async fn patch( // Ensure the ID is valid first. sqlx::query!( - "SELECT id FROM studios WHERE id = ? AND project_id = ? AND user_id = ?", + "SELECT s.id FROM studios s + JOIN projects p ON p.id = s.project_id + WHERE s.id = ? AND p.id = ? AND p.user_id = ?", studio_id, project_id, user_id, @@ -337,10 +332,14 @@ pub async fn delete( user: Extension, Path((project_id, studio_id)): Path<(i64, i64)>, ) -> webserver::Result<()> { - let user_id = user.username().ok_or_else(no_user_id)?.to_string(); + let user_id = user.username().ok_or_else(super::no_user_id)?.to_string(); sqlx::query!( - "DELETE FROM studios WHERE id = ? AND project_id = ? AND user_id = ? RETURNING id", + "DELETE FROM studios + WHERE id = $1 AND project_id = $2 AND EXISTS ( + SELECT p.id FROM projects p WHERE p.id = $2 AND p.user_id = $3 + ) + RETURNING id", studio_id, project_id, user_id, @@ -365,7 +364,7 @@ pub async fn list( user: Extension, Path(project_id): Path, ) -> webserver::Result>> { - let user_id = user.username().ok_or_else(no_user_id)?.to_string(); + let user_id = user.username().ok_or_else(super::no_user_id)?.to_string(); let studios = sqlx::query!( "SELECT @@ -375,7 +374,8 @@ pub async fn list( ss.context FROM studios s INNER JOIN studio_snapshots ss ON s.id = ss.studio_id - WHERE s.project_id = ? AND s.user_id = ? AND (ss.studio_id, ss.modified_at) IN ( + INNER JOIN projects p ON p.id = s.project_id + WHERE s.project_id = ? AND p.user_id = ? AND (ss.studio_id, ss.modified_at) IN ( SELECT studio_id, MAX(modified_at) FROM studio_snapshots GROUP BY studio_id @@ -695,7 +695,7 @@ pub async fn generate( user: Extension, Path((_project_id, studio_id)): Path<(i64, i64)>, ) -> webserver::Result> + Send>>>> { - let user_id = user.username().ok_or_else(no_user_id)?.to_string(); + let user_id = user.username().ok_or_else(super::no_user_id)?.to_string(); let snapshot_id = latest_snapshot_id(studio_id, &*app.sql, &user_id).await?; @@ -808,7 +808,7 @@ async fn generate_llm_context( s += "##### PATHS #####\n"; for file in context.iter().filter(|f| !f.hidden) { - s += &format!("{}\n", file.path); + s += &format!("{}:{}\n", file.repo, file.path); } s += "\n##### CODE CHUNKS #####\n\n"; @@ -847,7 +847,7 @@ async fn generate_llm_context( .map(String::as_str) .collect::(); - s += &format!("### {} ###\n{snippet}\n", file.path); + s += &format!("### {}:{} ###\n{snippet}\n", file.repo, file.path); } } @@ -938,7 +938,7 @@ pub async fn diff( user: Extension, Path((_project_id, studio_id)): Path<(i64, i64)>, ) -> webserver::Result> { - let user_id = user.username().ok_or_else(no_user_id)?.to_string(); + let user_id = user.username().ok_or_else(super::no_user_id)?.to_string(); let snapshot_id = latest_snapshot_id(studio_id, &*app.sql, &user_id).await?; @@ -1003,20 +1003,8 @@ pub async fn diff( let response = llm_gateway.chat(&messages, None).await?; let diff_chunks = diff::extract(&response)?.collect::>(); - let (repo, branch) = context - .first() - .map(|cf| (cf.repo.clone(), cf.branch.clone())) - // We make a hard assumption in the design of diffs that a studio can only contain files - // from one repository. This allows us to determine which repository to create new files - // or delete files in, without having to prefix file paths with repository names. - // - // If we can't find *any* files in the context to detect the current repository, - // creating/deleting a file like `index.js` is ambiguous, so we just return an error. - .context("could not determine studio repository, studio didn't contain any files")?; - let valid_chunks = futures::stream::iter(diff_chunks) .map(|mut chunk| { - let (repo, branch) = (repo.clone(), branch.clone()); let app = (*app).clone(); let llm_context = llm_context.clone(); let llm_gateway = llm_gateway.clone(); @@ -1033,14 +1021,16 @@ pub async fn diff( return Ok(None); } + let (repo, path) = parse_diff_path(src)?; + chunk.hunks = rectify_hunks( &app, &llm_context, &llm_gateway, chunk.hunks.iter(), - src, + path, &repo, - branch.as_deref(), + None, ) .await?; @@ -1048,7 +1038,8 @@ pub async fn diff( } (Some(src), None) => { - if validate_delete_file(&app, src, &repo, branch.as_deref()).await? { + let (repo, path) = parse_diff_path(src)?; + if validate_delete_file(&app, path, &repo, None).await? { Ok(Some(chunk)) } else { Ok(None) @@ -1056,7 +1047,8 @@ pub async fn diff( } (None, Some(dst)) => { - if validate_add_file(&app, &chunk, dst, &repo, branch.as_deref()).await? { + let (repo, path) = parse_diff_path(dst)?; + if validate_add_file(&app, &chunk, path, &repo, None).await? { Ok(Some(chunk)) } else { Ok(None) @@ -1081,20 +1073,21 @@ pub async fn diff( for chunk in valid_chunks { let path = chunk.src.as_deref().or(chunk.dst.as_deref()).unwrap(); + let (repo, path) = parse_diff_path(&path)?; let lang = if let Some(l) = file_langs.get(path) { Some(l.clone()) } else { - let detected_lang = if let Some(src) = &chunk.src { + let detected_lang = if chunk.src.is_some() { let doc = app .indexes .file - .by_path(&repo, src, branch.as_deref()) + .by_path(&repo, path, None) .await? .context("path did not exist in the index")?; doc.lang } else { - hyperpolyglot::detect(std::path::Path::new(&chunk.dst.as_deref().unwrap())) + hyperpolyglot::detect(std::path::Path::new(&path)) .ok() .flatten() .map(|detection| detection.language().to_owned()) @@ -1112,7 +1105,7 @@ pub async fn diff( lang: lang.clone(), repo: repo.clone(), - branch: branch.clone(), + branch: None, file: path.to_owned(), hunks: chunk .hunks @@ -1132,6 +1125,16 @@ pub async fn diff( Ok(Json(out)) } +fn parse_diff_path(p: &str) -> Result<(RepoRef, &str)> { + let (repo, path) = p + .split_once(':') + .context("diff path did not conform to repo:path syntax")?; + + let repo = repo.parse().context("repo ref was invalid")?; + + Ok((repo, path)) +} + fn context_repo_branch(context: &[ContextFile]) -> Result<(RepoRef, Option)> { let (repo, branch) = context .first() @@ -1274,7 +1277,7 @@ pub async fn diff_apply( Path((_project_id, studio_id)): Path<(i64, i64)>, diff: String, ) -> webserver::Result<()> { - let user_id = user.username().ok_or_else(no_user_id)?.to_string(); + let user_id = user.username().ok_or_else(super::no_user_id)?.to_string(); let snapshot_id = latest_snapshot_id(studio_id, &*app.sql, &user_id).await?; @@ -1292,13 +1295,27 @@ pub async fn diff_apply( let diff_chunks = diff::relaxed_parse(&diff); - let (repo, branch) = context_repo_branch(&context)?; + let mut dirty_repos = HashSet::new(); for (i, chunk) in diff_chunks.enumerate() { - let mut file_content = if let Some(src) = &chunk.src { + let (repo, path) = chunk + .src + .as_ref() + .or(chunk.dst.as_ref()) + .context("diff was missing src and dst") + .and_then(|p| parse_diff_path(p))?; + + let Some(repo_path) = repo.local_path() else { + error!("cannot apply patch to remote repo"); + continue; + }; + + dirty_repos.insert(repo.clone()); + + let mut file_content = if chunk.src.is_some() { app.indexes .file - .by_path(&repo, src, branch.as_deref()) + .by_path(&repo, &path, None) .await? .context("path did not exist in the index")? .content @@ -1324,13 +1341,9 @@ pub async fn diff_apply( } } - let Some(repo_path) = repo.local_path() else { - error!("cannot apply patch to remote repo"); - continue; - }; + let file_path = repo_path.join(path); - if let Some(dst) = &chunk.dst { - let file_path = repo_path.join(dst); + if chunk.dst.is_some() { std::fs::write(file_path, file_content).context("failed to patch file on disk")?; } else { if !file_content.trim().is_empty() { @@ -1339,21 +1352,22 @@ pub async fn diff_apply( )); } - let file_path = repo_path.join(chunk.src.clone().unwrap()); std::fs::remove_file(file_path).context("failed to delete file on disk")?; } } // Force a re-sync. - let _ = crate::webserver::repos::sync( - Query(webserver::repos::RepoParams { - repo, - shallow: false, - }), - app, - user, - ) - .await?; + for repo in dirty_repos { + let _ = crate::webserver::repos::sync( + Query(webserver::repos::RepoParams { + repo, + shallow: false, + }), + app.clone(), + user.clone(), + ) + .await?; + } Ok(()) } @@ -1366,7 +1380,7 @@ async fn populate_studio_name( user: Extension, studio_id: i64, ) -> webserver::Result<()> { - let user_id = user.username().ok_or_else(no_user_id)?.to_string(); + let user_id = user.username().ok_or_else(super::no_user_id)?.to_string(); let snapshot_id = latest_snapshot_id(studio_id, &*app.sql, &user_id).await?; let needs_name = sqlx::query! { @@ -1436,7 +1450,7 @@ pub async fn import( ) -> webserver::Result { let mut transaction = app.sql.begin().await?; - let user_id = user.username().ok_or_else(no_user_id)?.to_string(); + let user_id = user.username().ok_or_else(super::no_user_id)?.to_string(); let thread_id = params.thread_id.to_string(); @@ -1495,10 +1509,9 @@ pub async fn import( Some(id) => id, None => { sqlx::query!( - "INSERT INTO studios(name, project_id, user_id) VALUES (?, ?, ?) RETURNING id", + "INSERT INTO studios(name, project_id) VALUES (?, ?) RETURNING id", conversation.title, project_id, - user_id, ) .fetch_one(&mut transaction) .await? @@ -1663,17 +1676,18 @@ pub async fn list_snapshots( user: Extension, Path((project_id, studio_id)): Path<(i64, i64)>, ) -> webserver::Result>> { - let user_id = user.username().ok_or_else(no_user_id)?.to_string(); + let user_id = user.username().ok_or_else(super::no_user_id)?.to_string(); sqlx::query! { "SELECT ss.id as 'id!', ss.modified_at, ss.context, ss.doc_context, ss.messages FROM studio_snapshots ss - JOIN studios s ON s.id = ss.studio_id AND s.project_id = ? AND s.user_id = ? - WHERE ss.studio_id = ? + JOIN studios s ON s.id = ss.studio_id AND s.project_id = ? + JOIN projects p ON p.id = s.project_id + WHERE ss.studio_id = ? AND p.user_id = ? ORDER BY modified_at DESC", project_id, - user_id, studio_id, + user_id, } .fetch(&*app.sql) .map_err(Error::internal) @@ -1698,21 +1712,22 @@ pub async fn delete_snapshot( user: Extension, Path((project_id, studio_id, snapshot_id)): Path<(i64, i64, i64)>, ) -> webserver::Result<()> { - let user_id = user.username().ok_or_else(no_user_id)?.to_string(); + let user_id = user.username().ok_or_else(super::no_user_id)?.to_string(); sqlx::query! { "DELETE FROM studio_snapshots WHERE id IN ( SELECT ss.id FROM studio_snapshots ss - JOIN studios s ON s.id = ss.studio_id AND s.project_id = ? AND s.user_id = ? - WHERE ss.id = ? AND ss.studio_id = ? + JOIN studios s ON s.id = ss.studio_id + JOIN projects p ON p.id = s.project_id + WHERE ss.id = ? AND ss.studio_id = ? AND s.project_id = ? AND p.user_id = ? ) RETURNING id", - project_id, - user_id, snapshot_id, studio_id, + project_id, + user_id, } .fetch_optional(&*app.sql) .await? From c3d747f0b0f6b2dd2becb46996ced69efde83e09 Mon Sep 17 00:00:00 2001 From: calyptobai Date: Tue, 28 Nov 2023 12:59:57 -0500 Subject: [PATCH 05/88] WIP: Projects --- server/bleep/sqlx-data.json | 48 +++++++++++++-------------- server/bleep/src/webserver.rs | 2 +- server/bleep/src/webserver/project.rs | 21 +++++++----- 3 files changed, 37 insertions(+), 34 deletions(-) diff --git a/server/bleep/sqlx-data.json b/server/bleep/sqlx-data.json index ab95db747c..c6b167f1ec 100644 --- a/server/bleep/sqlx-data.json +++ b/server/bleep/sqlx-data.json @@ -1,5 +1,29 @@ { "db": "SQLite", + "00ed95c6ae97970d4412af1d3f724dc9bd84c5f1fdb0ce8f4b8abefa9d86eded": { + "describe": { + "columns": [ + { + "name": "name", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "modified_at", + "ordinal": 1, + "type_info": "Datetime" + } + ], + "nullable": [ + true, + true + ], + "parameters": { + "Right": 2 + } + }, + "query": "SELECT name, (\n SELECT ss.modified_at\n FROM studio_snapshots ss\n JOIN studios s ON s.project_id = $1 AND ss.studio_id = s.id\n ORDER BY ss.modified_at DESC\n LIMIT 1\n ) AS modified_at\n FROM projects\n WHERE id = $1 AND user_id = $2\n LIMIT 1" + }, "069c6404909c217e0b27e974480cce3f592a0d43ece6dec17fbcee37ce7a6ffa": { "describe": { "columns": [ @@ -794,30 +818,6 @@ }, "query": "UPDATE studio_snapshots SET modified_at = ? WHERE id = ?" }, - "91015fa610163b6e9cf6c6e2b9f1e4519539bd5b651c96155dda6d9f0363a166": { - "describe": { - "columns": [ - { - "name": "name", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "modified_at", - "ordinal": 1, - "type_info": "Datetime" - } - ], - "nullable": [ - true, - true - ], - "parameters": { - "Right": 2 - } - }, - "query": "SELECT name, (\n SELECT ss.modified_at\n FROM studio_snapshots ss\n JOIN studios s ON s.project_id = $1 AND ss.studio_id = s.id\n ORDER BY ss.modified_at DESC\n LIMIT 1\n ) AS modified_at\n FROM projects\n WHERE id = $1 AND user_id = $2" - }, "9146d9c8a7f17cc65c017cb364d1a853a9163b5ece336c0a6ef4e28e8df56a6b": { "describe": { "columns": [], diff --git a/server/bleep/src/webserver.rs b/server/bleep/src/webserver.rs index 8525bd1073..daa8a6e11d 100644 --- a/server/bleep/src/webserver.rs +++ b/server/bleep/src/webserver.rs @@ -100,7 +100,7 @@ pub async fn start(app: Application) -> anyhow::Result<()> { .route("/projects", get(project::list).post(project::create)) .route( "/projects/:project_id", - get(project::get).post(project::update), + get(project::get).put(project::update), ) .route("/projects/:project_id/studios", post(studio::create)) .route("/projects/:project_id/studios", get(studio::list)) diff --git a/server/bleep/src/webserver/project.rs b/server/bleep/src/webserver/project.rs index a96d5d5c70..aec03829d6 100644 --- a/server/bleep/src/webserver/project.rs +++ b/server/bleep/src/webserver/project.rs @@ -75,17 +75,19 @@ pub async fn create( #[derive(serde::Serialize)] pub struct Get { + id: i64, name: String, + modified_at: Option, } pub async fn get( app: Extension, user: Extension, Path(id): Path, -) -> webserver::Result>> { +) -> webserver::Result> { let user_id = user.username().ok_or_else(super::no_user_id)?.to_string(); - let projects = sqlx::query! { + sqlx::query! { "SELECT name, ( SELECT ss.modified_at FROM studio_snapshots ss @@ -94,19 +96,20 @@ pub async fn get( LIMIT 1 ) AS modified_at FROM projects - WHERE id = $1 AND user_id = $2", + WHERE id = $1 AND user_id = $2 + LIMIT 1", id, user_id, } - .fetch_all(&*app.sql) - .await? - .into_iter() + .fetch_one(&*app.sql) + .await + .map_err(Error::not_found) .map(|row| Get { + id, name: row.name.unwrap_or_else(default_name), + modified_at: row.modified_at, }) - .collect(); - - Ok(Json(projects)) + .map(Json) } #[derive(serde::Deserialize)] From efeb4045f6749ca2c83d7062fc0e5c02b1b259b3 Mon Sep 17 00:00:00 2001 From: calyptobai Date: Tue, 28 Nov 2023 13:02:36 -0500 Subject: [PATCH 06/88] WIP: Projects --- server/bleep/src/webserver/project.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/bleep/src/webserver/project.rs b/server/bleep/src/webserver/project.rs index aec03829d6..48434bcae4 100644 --- a/server/bleep/src/webserver/project.rs +++ b/server/bleep/src/webserver/project.rs @@ -57,7 +57,7 @@ pub struct Create { pub async fn create( app: Extension, user: Extension, - Query(params): Query, + Json(params): Json, ) -> webserver::Result { let user_id = user.username().ok_or_else(super::no_user_id)?.to_string(); From ccbe7de6a2e230ddae2f70761545a40a46967daf Mon Sep 17 00:00:00 2001 From: calyptobai Date: Tue, 28 Nov 2023 21:09:57 -0500 Subject: [PATCH 07/88] WIP: Projects --- server/bleep/sqlx-data.json | 116 ++++++++++-------- server/bleep/src/agent.rs | 4 +- server/bleep/src/analytics.rs | 4 +- server/bleep/src/webserver.rs | 21 ++-- server/bleep/src/webserver/answer.rs | 23 ++-- .../conversations.rs => conversation.rs} | 58 +++++---- 6 files changed, 118 insertions(+), 108 deletions(-) rename server/bleep/src/webserver/{answer/conversations.rs => conversation.rs} (76%) diff --git a/server/bleep/sqlx-data.json b/server/bleep/sqlx-data.json index c6b167f1ec..71c007e10d 100644 --- a/server/bleep/sqlx-data.json +++ b/server/bleep/sqlx-data.json @@ -398,6 +398,16 @@ }, "query": "INSERT INTO templates (name, content, user_id) VALUES (?, ?, ?)" }, + "548957b8a631f8a6388907ca13690348ae429ffe8528dd413f03ed236b4f7fb9": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 3 + } + }, + "query": "DELETE FROM conversations\n WHERE id = $1 AND project_id = $2 AND EXISTS (\n SELECT p.id\n FROM projects p\n WHERE p.id = $2 AND p.user_id = $3\n )" + }, "5776008bf71ba2a90bad43c66a6e622ad71a81e1751c00b62aafa70840997999": { "describe": { "columns": [], @@ -780,6 +790,36 @@ }, "query": "INSERT INTO studios (project_id, name) VALUES (?, ?) RETURNING id" }, + "859c04508d79d73671f404304f28a45b11c104fe356fc75a7720b1d56577548f": { + "describe": { + "columns": [ + { + "name": "id!", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "created_at", + "ordinal": 1, + "type_info": "Int64" + }, + { + "name": "title", + "ordinal": 2, + "type_info": "Text" + } + ], + "nullable": [ + true, + false, + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "SELECT c.id as 'id!', c.created_at, c.title FROM conversations c JOIN projects p ON p.id = c.project_id AND p.user_id = ?\n ORDER BY c.created_at DESC" + }, "881aa78dfa3cd1bc3aa7a6edb8281aec5a972c1f53607d25c4e1f6d03cd3faef": { "describe": { "columns": [], @@ -996,61 +1036,51 @@ }, "query": "INSERT INTO chunk_cache (chunk_hash, file_hash, branches, repo_ref) VALUES (?, ?, ?, ?)" }, - "bc60b0f34fd20feba2da3f16458770424534eacaba75e6f45b8218f32767671b": { + "d2b52987aaa4bdc39c04254834c941cad2165eefd02eef46fda413822be91fd0": { "describe": { "columns": [ { - "name": "thread_id", + "name": "messages", "ordinal": 0, "type_info": "Text" - }, - { - "name": "created_at", - "ordinal": 1, - "type_info": "Int64" - }, - { - "name": "title", - "ordinal": 2, - "type_info": "Text" } ], "nullable": [ - false, - false, false ], + "parameters": { + "Right": 1 + } + }, + "query": "SELECT messages FROM studio_snapshots WHERE id = ?" + }, + "d616a930841d3828f8cc151852bd2cfda4750e713857caedfbe43b3502a0bb45": { + "describe": { + "columns": [], + "nullable": [], "parameters": { "Right": 2 } }, - "query": "SELECT thread_id, created_at, title FROM conversations WHERE user_id = ? AND repo_ref = ? ORDER BY created_at DESC" + "query": "INSERT INTO file_cache (repo_ref, cache_hash) VALUES (?, ?)" }, - "d2b52987aaa4bdc39c04254834c941cad2165eefd02eef46fda413822be91fd0": { + "db4077fd7603079ffc8c237ec49a640a6061a06d12499bdb7b39ed3c23c1b38e": { "describe": { - "columns": [ - { - "name": "messages", - "ordinal": 0, - "type_info": "Text" - } - ], - "nullable": [ - false - ], + "columns": [], + "nullable": [], "parameters": { - "Right": 1 + "Right": 2 } }, - "query": "SELECT messages FROM studio_snapshots WHERE id = ?" + "query": "UPDATE studio_snapshots SET messages = ? WHERE id = ?" }, - "d5ee5becde7005920d7094fca5b7974bbf19713b3625fbf6d1a3e198e7cf4de4": { + "dc1277e6cfacd86b149e82c65b6565805c9ebbfd2f9b01ae384a1261fd1d5723": { "describe": { "columns": [ { - "name": "thread_id", + "name": "id!", "ordinal": 0, - "type_info": "Text" + "type_info": "Int64" }, { "name": "created_at", @@ -1064,35 +1094,15 @@ } ], "nullable": [ - false, + true, false, false ], - "parameters": { - "Right": 1 - } - }, - "query": "SELECT thread_id, created_at, title FROM conversations WHERE user_id = ? ORDER BY created_at DESC" - }, - "d616a930841d3828f8cc151852bd2cfda4750e713857caedfbe43b3502a0bb45": { - "describe": { - "columns": [], - "nullable": [], "parameters": { "Right": 2 } }, - "query": "INSERT INTO file_cache (repo_ref, cache_hash) VALUES (?, ?)" - }, - "db4077fd7603079ffc8c237ec49a640a6061a06d12499bdb7b39ed3c23c1b38e": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Right": 2 - } - }, - "query": "UPDATE studio_snapshots SET messages = ? WHERE id = ?" + "query": "SELECT c.id as 'id!', c.created_at, c.title FROM conversations c JOIN projects p ON p.id = c.project_id AND p.user_id = ? WHERE c.repo_ref = ? ORDER BY c.created_at DESC" }, "dcb7f9427283203bce10fe7d618057ef3eab5f6af2277e7a1ac8ba050609894d": { "describe": { diff --git a/server/bleep/src/agent.rs b/server/bleep/src/agent.rs index 8366833e91..734c77b50f 100644 --- a/server/bleep/src/agent.rs +++ b/server/bleep/src/agent.rs @@ -14,7 +14,7 @@ use crate::{ repo::RepoRef, semantic::{self, SemanticSearchParams}, webserver::{ - answer::conversations::{self, ConversationId}, + conversation::{self, ConversationId}, middleware::User, }, Application, @@ -471,7 +471,7 @@ impl Agent { async move { let result = match conversation_id { Ok(conversation_id) => { - conversations::store(&sql, conversation_id, conversation).await + conversation::store(&sql, conversation_id, conversation).await } Err(e) => Err(e), }; diff --git a/server/bleep/src/analytics.rs b/server/bleep/src/analytics.rs index e7c3565d89..c8ec123405 100644 --- a/server/bleep/src/analytics.rs +++ b/server/bleep/src/analytics.rs @@ -14,7 +14,7 @@ use uuid::Uuid; #[derive(Debug, Clone)] pub struct QueryEvent { pub query_id: Uuid, - pub thread_id: Uuid, + pub conversation_id: i64, pub data: EventData, } @@ -236,7 +236,7 @@ impl RudderHub { properties: Some(json!({ "device_id": self.device_id(), "query_id": event.query_id, - "thread_id": event.thread_id, + "conversation_id": event.conversation_id, "data": event.data, "package_metadata": options.package_metadata, })), diff --git a/server/bleep/src/webserver.rs b/server/bleep/src/webserver.rs index daa8a6e11d..f1478b8f7e 100644 --- a/server/bleep/src/webserver.rs +++ b/server/bleep/src/webserver.rs @@ -18,6 +18,7 @@ pub mod answer; mod autocomplete; mod commits; mod config; +pub mod conversation; mod docs; mod file; mod github; @@ -86,22 +87,22 @@ pub async fn start(app: Application) -> anyhow::Result<()> { .route("/search/code", get(search::semantic_code)) .route("/search/path", get(search::fuzzy_path)) .route("/file", get(file::handle)) - .route("/answer", get(answer::answer)) - .route("/answer/explain", get(answer::explain)) + .route("/projects", get(project::list).post(project::create)) .route( - "/answer/conversations", - get(answer::conversations::list).delete(answer::conversations::delete), + "/projects/:project_id", + get(project::get).put(project::update), ) .route( - "/answer/conversations/:thread_id", - get(answer::conversations::thread), + "/projects/:project_id/conversations", + get(conversation::list).delete(conversation::delete), ) - .route("/answer/vote", post(answer::vote)) - .route("/projects", get(project::list).post(project::create)) .route( - "/projects/:project_id", - get(project::get).put(project::update), + "/projects/:project_id/conversations/:conversation_id", + get(conversation::get), ) + .route("/projects/:project_id/conversations/:conversation_id/answer/vote", post(answer::vote)) + .route("/projects/:project_id/conversations/:conversation_id/answer", get(answer::answer)) + .route("/projects/:project_id/conversations/:conversation_id/answer/explain", get(answer::explain)) .route("/projects/:project_id/studios", post(studio::create)) .route("/projects/:project_id/studios", get(studio::list)) .route( diff --git a/server/bleep/src/webserver/answer.rs b/server/bleep/src/webserver/answer.rs index 34bf7534d5..7977bfd1ad 100644 --- a/server/bleep/src/webserver/answer.rs +++ b/server/bleep/src/webserver/answer.rs @@ -2,7 +2,7 @@ use std::{panic::AssertUnwindSafe, time::Duration}; use anyhow::{anyhow, Context, Result}; use axum::{ - extract::Query, + extract::{Query, Path}, response::{ sse::{self, Sse}, IntoResponse, @@ -14,7 +14,7 @@ use reqwest::StatusCode; use serde_json::json; use tracing::{debug, error, info, warn}; -use self::conversations::ConversationId; +use super::conversation::ConversationId; use super::middleware::User; use crate::{ @@ -27,11 +27,9 @@ use crate::{ db::QueryLog, query::parser::{self, Literal}, repo::RepoRef, - Application, + Application, webserver::conversation, }; -pub mod conversations; - const TIMEOUT_SECS: u64 = 60; #[derive(Clone, Debug, serde::Deserialize)] @@ -72,8 +70,6 @@ pub struct Answer { pub answer_model: agent::model::LLMModel, #[serde(default = "default_agent_model")] pub agent_model: agent::model::LLMModel, - #[serde(default = "default_thread_id")] - pub thread_id: uuid::Uuid, /// Optional id of the parent of the exchange to overwrite /// If this UUID is nil, then overwrite the first exchange in the thread pub parent_exchange_id: Option, @@ -95,6 +91,7 @@ pub(super) async fn answer( Query(params): Query, Extension(app): Extension, Extension(user): Extension, + Path((project_id, conversation_id)): Path<(i64, i64)>, ) -> super::Result { info!(?params.q, "handling /answer query"); let query_id = uuid::Uuid::new_v4(); @@ -104,10 +101,11 @@ pub(super) async fn answer( .username() .ok_or_else(|| super::Error::user("didn't have user ID"))? .to_string(), - thread_id: params.thread_id, + project_id, + conversation_id, }; - let mut exchanges = conversations::load(&app.sql, &conversation_id) + let mut exchanges = conversation::load(&app.sql, &conversation_id) .await? .unwrap_or_default(); @@ -213,17 +211,20 @@ async fn try_execute_agent( QueryLog::new(&app.sql).insert(¶ms.q).await?; let project: Project = serde_json::from_str(¶ms.project).unwrap(); let Answer { - thread_id, answer_model, agent_model, .. } = params.clone(); + let username = user + .username() + .ok_or_else(|| super::Error::user("didn't have user ID"))?; + let llm_gateway = user .llm_gateway(&app) .await? .temperature(0.0) - .session_reference_id(conversation_id.to_string()) + .session_reference_id(format!("{username}::{}", conversation_id.thread_id)) .model(agent_model.model_name); // confirm client compatibility with answer-api diff --git a/server/bleep/src/webserver/answer/conversations.rs b/server/bleep/src/webserver/conversation.rs similarity index 76% rename from server/bleep/src/webserver/answer/conversations.rs rename to server/bleep/src/webserver/conversation.rs index 40e4fabd8e..7ce4fc0d49 100644 --- a/server/bleep/src/webserver/answer/conversations.rs +++ b/server/bleep/src/webserver/conversation.rs @@ -20,19 +20,14 @@ pub type Conversation = (Project, Vec); #[derive(Hash, PartialEq, Eq, Clone)] pub struct ConversationId { - pub thread_id: uuid::Uuid, + pub conversation_id: i64, + pub project_id: i64, pub user_id: String, } -impl fmt::Display for ConversationId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}::{}", self.user_id, self.thread_id) - } -} - #[derive(serde::Serialize)] pub struct ConversationPreview { - pub thread_id: String, + pub id: i64, pub created_at: i64, pub title: String, } @@ -46,6 +41,7 @@ pub(in crate::webserver) async fn list( Extension(user): Extension, Query(query): Query, State(app): State, + Path(project_id): Path, ) -> webserver::Result { let db = app.sql.as_ref(); let user_id = user @@ -56,10 +52,11 @@ pub(in crate::webserver) async fn list( let repo_ref = repo_ref.to_string(); sqlx::query_as! { ConversationPreview, - "SELECT thread_id, created_at, title \ - FROM conversations \ - WHERE user_id = ? AND repo_ref = ? \ - ORDER BY created_at DESC", + "SELECT c.id as 'id!', c.created_at, c.title \ + FROM conversations c \ + JOIN projects p ON p.id = c.project_id AND p.user_id = ? \ + WHERE c.repo_ref = ? \ + ORDER BY c.created_at DESC", user_id, repo_ref, } @@ -68,10 +65,10 @@ pub(in crate::webserver) async fn list( } else { sqlx::query_as! { ConversationPreview, - "SELECT thread_id, created_at, title \ - FROM conversations \ - WHERE user_id = ? \ - ORDER BY created_at DESC", + "SELECT c.id as 'id!', c.created_at, c.title \ + FROM conversations c \ + JOIN projects p ON p.id = c.project_id AND p.user_id = ? + ORDER BY c.created_at DESC", user_id, } .fetch_all(db) @@ -82,15 +79,10 @@ pub(in crate::webserver) async fn list( Ok(Json(conversations)) } -#[derive(serde::Deserialize)] -pub(in crate::webserver) struct Delete { - thread_id: String, -} - pub(in crate::webserver) async fn delete( - Query(params): Query, Extension(user): Extension, State(app): State, + Path((project_id, conversation_id)): Path<(i64, i64)>, ) -> webserver::Result<()> { let db = app.sql.as_ref(); let user_id = user @@ -98,9 +90,15 @@ pub(in crate::webserver) async fn delete( .ok_or_else(|| Error::user("missing user ID"))?; let result = sqlx::query! { - "DELETE FROM conversations WHERE user_id = ? AND thread_id = ?", + "DELETE FROM conversations + WHERE id = $1 AND project_id = $2 AND EXISTS ( + SELECT p.id + FROM projects p + WHERE p.id = $2 AND p.user_id = $3 + )", + conversation_id, + project_id, user_id, - params.thread_id, } .execute(db) .await @@ -113,8 +111,8 @@ pub(in crate::webserver) async fn delete( Ok(()) } -pub(in crate::webserver) async fn thread( - Path(thread_id): Path, +pub(in crate::webserver) async fn get( + Path((project_id, conversation_id)): Path<(i64, i64)>, Extension(user): Extension, State(app): State, ) -> webserver::Result { @@ -123,7 +121,7 @@ pub(in crate::webserver) async fn thread( .ok_or_else(|| Error::user("missing user ID"))? .to_owned(); - let exchanges = load(&app.sql, &ConversationId { thread_id, user_id }) + let exchanges = load(&app.sql, &ConversationId { conversation_id, project_id, user_id }) .await? .ok_or_else(|| Error::new(ErrorKind::NotFound, "thread was not found"))?; @@ -136,11 +134,11 @@ pub(in crate::webserver) async fn thread( } pub async fn store(db: &SqlDb, id: ConversationId, conversation: Conversation) -> Result<()> { - info!("writing conversation {}-{}", id.user_id, id.thread_id); + info!("writing conversation {}-{}", id.user_id, id.conversation_id); let mut transaction = db.begin().await?; // Delete the old conversation for simplicity. This also deletes all its messages. - let (user_id, thread_id) = (id.user_id.clone(), id.thread_id.to_string()); + let (user_id, thread_id) = (id.user_id.clone(), id.conversation_id.to_string()); sqlx::query! { "DELETE FROM conversations \ WHERE user_id = ? AND thread_id = ?", @@ -179,7 +177,7 @@ pub async fn store(db: &SqlDb, id: ConversationId, conversation: Conversation) - } pub async fn load(db: &SqlDb, id: &ConversationId) -> Result>> { - let (user_id, thread_id) = (id.user_id.clone(), id.thread_id.to_string()); + let (user_id, thread_id) = (id.user_id.clone(), id.conversation_id.to_string()); let row = sqlx::query! { "SELECT repo_ref, exchanges FROM conversations \ From 6687f434207d46884cf8d92fdb272315c1b69b39 Mon Sep 17 00:00:00 2001 From: calyptobai Date: Wed, 29 Nov 2023 21:52:09 -0500 Subject: [PATCH 08/88] WIP: Projects - refactoring conversation threads for multi-project context --- .../migrations/20231122012638_projects.sql | 2 + server/bleep/sqlx-data.json | 220 ++++----- server/bleep/src/agent.rs | 46 +- server/bleep/src/agent/tools/answer.rs | 6 +- server/bleep/src/agent/tools/code.rs | 7 +- server/bleep/src/agent/tools/path.rs | 2 +- server/bleep/src/agent/tools/proc.rs | 5 +- server/bleep/src/webserver.rs | 6 +- server/bleep/src/webserver/answer.rs | 428 +++++++++--------- server/bleep/src/webserver/conversation.rs | 211 ++++----- server/bleep/src/webserver/studio.rs | 12 +- 11 files changed, 452 insertions(+), 493 deletions(-) diff --git a/server/bleep/migrations/20231122012638_projects.sql b/server/bleep/migrations/20231122012638_projects.sql index ca337804bd..dbce3c54f1 100644 --- a/server/bleep/migrations/20231122012638_projects.sql +++ b/server/bleep/migrations/20231122012638_projects.sql @@ -7,3 +7,5 @@ CREATE TABLE projects ( ALTER TABLE studios ADD COLUMN project_id INTEGER NOT NULL; ALTER TABLE studios DROP COLUMN user_id; ALTER TABLE conversations ADD COLUMN project_id INTEGER NOT NULL; +ALTER TABLE conversations DROP COLUMN repo_ref; +ALTER TABLE conversations DROP COLUMN user_id; diff --git a/server/bleep/sqlx-data.json b/server/bleep/sqlx-data.json index 71c007e10d..15acf3c90f 100644 --- a/server/bleep/sqlx-data.json +++ b/server/bleep/sqlx-data.json @@ -96,6 +96,60 @@ }, "query": "SELECT context, messages FROM studio_snapshots WHERE id = ?" }, + "08e9cd261503f6d2d286d60b97036cae69d58b12c96897f341f4e5eb3dc68dc9": { + "describe": { + "columns": [ + { + "name": "id!", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "created_at", + "ordinal": 1, + "type_info": "Int64" + }, + { + "name": "title", + "ordinal": 2, + "type_info": "Text" + } + ], + "nullable": [ + true, + false, + false + ], + "parameters": { + "Right": 2 + } + }, + "query": "SELECT c.id as 'id!', c.created_at, c.title FROM conversations c JOIN projects p ON p.id = c.project_id AND p.user_id = ? WHERE p.id = ?\n ORDER BY c.created_at DESC" + }, + "0b7aa6a8c243ac8ca79f52a5ef370ccce954b3635ce5e70a6e80732472a916f2": { + "describe": { + "columns": [ + { + "name": "exchanges", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "thread_id", + "ordinal": 1, + "type_info": "Text" + } + ], + "nullable": [ + false, + false + ], + "parameters": { + "Right": 3 + } + }, + "query": "SELECT c.exchanges, c.thread_id\n FROM conversations c\n JOIN projects p ON p.id = c.project_id AND p.user_id = ?\n WHERE c.project_id = ? AND c.id = ?" + }, "0fda94d4963a3991ff65079f63ba876030ecaeb1bb8fee3d6e729939a73ad4ea": { "describe": { "columns": [ @@ -132,15 +186,23 @@ }, "query": "SELECT context FROM studio_snapshots WHERE id = ?" }, - "13d9aec6f721a649ab89c29c770ae5aa9f1bf34a0e30f6e608b697772774568e": { + "1e8cdc6e2b23d18be0a15b2b79cb7f834d78ad942b29b448be57c0fdfab81d40": { "describe": { - "columns": [], - "nullable": [], + "columns": [ + { + "name": "project_id", + "ordinal": 0, + "type_info": "Int64" + } + ], + "nullable": [ + true + ], "parameters": { - "Right": 5 + "Right": 2 } }, - "query": "INSERT INTO conversations (user_id, thread_id, repo_ref, title, exchanges, created_at) VALUES (?, ?, ?, ?, ?, strftime('%s', 'now'))" + "query": "DELETE FROM conversations\n WHERE thread_id = ? AND EXISTS (\n SELECT p.id\n FROM projects p\n WHERE p.id = project_id AND p.user_id = ?\n )\n RETURNING project_id" }, "26065ed9dd0dfa42b8b943726d85425d0b45b2cafcceb9887ea626040bef9264": { "describe": { @@ -276,16 +338,6 @@ }, "query": "DELETE FROM docs WHERE id = ? RETURNING id" }, - "392b563bb3af6711817fe99335d053691750426762dcde7b0381dc9f69cd804e": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Right": 2 - } - }, - "query": "DELETE FROM conversations WHERE user_id = ? AND thread_id = ?" - }, "454d7dfb50480aae5ad9c8372262d55a302e214e1c7ceb8d62b53832f75bd85b": { "describe": { "columns": [], @@ -398,6 +450,30 @@ }, "query": "INSERT INTO templates (name, content, user_id) VALUES (?, ?, ?)" }, + "5261d3dfd64c97c87d86c9bae61bf51775174086518a4a0493b4b77da3b8cd6a": { + "describe": { + "columns": [ + { + "name": "title", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "exchanges", + "ordinal": 1, + "type_info": "Text" + } + ], + "nullable": [ + false, + false + ], + "parameters": { + "Right": 2 + } + }, + "query": "SELECT c.title, c.exchanges\n FROM conversations c\n JOIN projects p ON p.id = c.project_id AND p.user_id = ?\n WHERE thread_id = ?" + }, "548957b8a631f8a6388907ca13690348ae429ffe8528dd413f03ed236b4f7fb9": { "describe": { "columns": [], @@ -612,36 +688,6 @@ }, "query": "DELETE FROM studio_snapshots\n WHERE id IN (\n SELECT ss.id\n FROM studio_snapshots ss\n JOIN studios s ON s.id = ss.studio_id\n JOIN projects p ON p.id = s.project_id\n WHERE ss.id = ? AND ss.studio_id = ? AND s.project_id = ? AND p.user_id = ?\n )\n RETURNING id" }, - "6e3bfe277ca4506bc389597db418b311c4faabf5af132f81699997e4d766e40d": { - "describe": { - "columns": [ - { - "name": "title", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "repo_ref", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "exchanges", - "ordinal": 2, - "type_info": "Text" - } - ], - "nullable": [ - false, - false, - false - ], - "parameters": { - "Right": 2 - } - }, - "query": "SELECT title, repo_ref, exchanges\n FROM conversations\n WHERE user_id = ? AND thread_id = ?" - }, "6e842ac5eb4b5be53dff501a24b6b91c0557d6a7119480441a60c8e99de7daf1": { "describe": { "columns": [ @@ -790,35 +836,15 @@ }, "query": "INSERT INTO studios (project_id, name) VALUES (?, ?) RETURNING id" }, - "859c04508d79d73671f404304f28a45b11c104fe356fc75a7720b1d56577548f": { + "834600ffd689d32c250771a3118d24fa93e71a6690b14703fe0b7feda05d53b6": { "describe": { - "columns": [ - { - "name": "id!", - "ordinal": 0, - "type_info": "Int64" - }, - { - "name": "created_at", - "ordinal": 1, - "type_info": "Int64" - }, - { - "name": "title", - "ordinal": 2, - "type_info": "Text" - } - ], - "nullable": [ - true, - false, - false - ], + "columns": [], + "nullable": [], "parameters": { - "Right": 1 + "Right": 4 } }, - "query": "SELECT c.id as 'id!', c.created_at, c.title FROM conversations c JOIN projects p ON p.id = c.project_id AND p.user_id = ?\n ORDER BY c.created_at DESC" + "query": "INSERT INTO conversations (\n thread_id, title, exchanges, project_id, created_at\n )\n VALUES (?, ?, ?, ?, strftime('%s', 'now'))" }, "881aa78dfa3cd1bc3aa7a6edb8281aec5a972c1f53607d25c4e1f6d03cd3faef": { "describe": { @@ -1074,36 +1100,6 @@ }, "query": "UPDATE studio_snapshots SET messages = ? WHERE id = ?" }, - "dc1277e6cfacd86b149e82c65b6565805c9ebbfd2f9b01ae384a1261fd1d5723": { - "describe": { - "columns": [ - { - "name": "id!", - "ordinal": 0, - "type_info": "Int64" - }, - { - "name": "created_at", - "ordinal": 1, - "type_info": "Int64" - }, - { - "name": "title", - "ordinal": 2, - "type_info": "Text" - } - ], - "nullable": [ - true, - false, - false - ], - "parameters": { - "Right": 2 - } - }, - "query": "SELECT c.id as 'id!', c.created_at, c.title FROM conversations c JOIN projects p ON p.id = c.project_id AND p.user_id = ? WHERE c.repo_ref = ? ORDER BY c.created_at DESC" - }, "dcb7f9427283203bce10fe7d618057ef3eab5f6af2277e7a1ac8ba050609894d": { "describe": { "columns": [ @@ -1176,30 +1172,6 @@ }, "query": "\n SELECT id, name, url, description, favicon, modified_at, index_status\n FROM docs \n WHERE name LIKE $1 OR description LIKE $1 OR url LIKE $1\n LIMIT ?\n " }, - "e444f39d4fc9219873c7a8565a13e65e4646658631b785431cb64ca0cc5d6ab9": { - "describe": { - "columns": [ - { - "name": "repo_ref", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "exchanges", - "ordinal": 1, - "type_info": "Text" - } - ], - "nullable": [ - false, - false - ], - "parameters": { - "Right": 2 - } - }, - "query": "SELECT repo_ref, exchanges FROM conversations WHERE user_id = ? AND thread_id = ?" - }, "ebdb202965998c4e29727dca19a60c82b2a4574530858e29bf6584c8b6e1f32b": { "describe": { "columns": [ diff --git a/server/bleep/src/agent.rs b/server/bleep/src/agent.rs index 734c77b50f..ba53028da4 100644 --- a/server/bleep/src/agent.rs +++ b/server/bleep/src/agent.rs @@ -14,7 +14,7 @@ use crate::{ repo::RepoRef, semantic::{self, SemanticSearchParams}, webserver::{ - conversation::{self, ConversationId}, + conversation::{self, ConversationId, Conversation}, middleware::User, }, Application, @@ -69,13 +69,11 @@ impl Project { pub struct Agent { pub app: Application, - pub project: Project, - pub exchanges: Vec, + pub conversation: Conversation, pub exchange_tx: Sender, pub llm_gateway: llm_gateway::Client, pub user: User, - pub thread_id: uuid::Uuid, pub query_id: uuid::Uuid, pub answer_model: model::LLMModel, @@ -165,22 +163,22 @@ impl Agent { pub fn track_query(&self, data: EventData) { let event = QueryEvent { query_id: self.query_id, - thread_id: self.thread_id, + thread_id: self.conversation.thread_id, data, }; self.app.track_query(&self.user, &event); } fn last_exchange(&self) -> &Exchange { - self.exchanges.last().expect("exchange list was empty") + self.conversation.exchanges.last().expect("exchange list was empty") } fn last_exchange_mut(&mut self) -> &mut Exchange { - self.exchanges.last_mut().expect("exchange list was empty") + self.conversation.exchanges.last_mut().expect("exchange list was empty") } fn paths(&self) -> impl Iterator { - self.exchanges.iter().flat_map(|e| e.paths.iter()) + self.conversation.exchanges.iter().flat_map(|e| e.paths.iter()) } fn get_path_alias(&mut self, repo_path: &RepoPath) -> usize { @@ -197,14 +195,14 @@ impl Agent { } pub async fn step(&mut self, action: Action) -> Result> { - info!(?action, %self.thread_id, "executing next action"); + info!(?action, %self.conversation.thread_id, "executing next action"); match &action { Action::Query(s) => { self.track_query(EventData::input_stage("query").with_payload("q", s)); // Always make a code search for the user query on the first exchange - if self.exchanges.len() == 1 { + if self.conversation.exchanges.len() == 1 { let keywords = { let keys = remove_stopwords(s); if keys.is_empty() { @@ -292,6 +290,7 @@ impl Agent { const FUNCTION_CALL_INSTRUCTION: &str = "Call a function. Do not answer"; let history = self + .conversation .exchanges .iter() .rev() @@ -418,7 +417,7 @@ impl Agent { ..self.last_exchange().query.clone() }; - debug!(?query, %self.thread_id, "executing semantic query"); + debug!(?query, %self.conversation.thread_id, "executing semantic query"); self.app.semantic.search(&query, semantic_params).await } @@ -428,7 +427,7 @@ impl Agent { ) -> Result> { let branch = self.last_exchange().query.first_branch(); - debug!(%repo, path, ?branch, %self.thread_id, "executing file search"); + debug!(%repo, path, ?branch, %self.conversation.thread_id, "executing file search"); self.app .indexes .file @@ -444,11 +443,11 @@ impl Agent { let branch = self.last_exchange().query.first_branch(); let langs = self.last_exchange().query.langs.iter().map(Deref::deref); - debug!(?self.project, query, ?branch, %self.thread_id, "executing fuzzy search"); + debug!(?query, ?branch, %self.conversation.thread_id, "executing fuzzy search"); self.app .indexes .file - .fuzzy_path_match(self.project.clone(), branch.as_deref(), query, langs, 50) + .fuzzy_path_match(todo!(), branch.as_deref(), query, langs, 50) .await } @@ -458,21 +457,18 @@ impl Agent { // NB: This isn't an `async fn` so as to not capture a lifetime. fn store(&mut self) -> impl Future { let sql = Arc::clone(&self.app.sql); - let conversation = (self.project.clone(), self.exchanges.clone()); - let conversation_id = self + + let user_id = self .user .username() .context("didn't have user ID") - .map(|user_id| ConversationId { - thread_id: self.thread_id, - user_id: user_id.to_owned(), - }); + .map(str::to_owned); + + let conversation = self.conversation.clone(); async move { - let result = match conversation_id { - Ok(conversation_id) => { - conversation::store(&sql, conversation_id, conversation).await - } + let result = match user_id { + Ok(user_id) => conversation.store(&sql, &user_id).await, Err(e) => Err(e), }; @@ -527,7 +523,7 @@ fn trim_history( Ok(history) } -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "lowercase")] pub enum Action { /// A user-provided query. diff --git a/server/bleep/src/agent/tools/answer.rs b/server/bleep/src/agent/tools/answer.rs index 35455bcb35..20eb3bbe7d 100644 --- a/server/bleep/src/agent/tools/answer.rs +++ b/server/bleep/src/agent/tools/answer.rs @@ -194,7 +194,8 @@ impl Agent { fn utter_history(&self) -> impl Iterator + '_ { const ANSWER_MAX_HISTORY_SIZE: usize = 5; - self.exchanges + self.conversation + .exchanges .iter() .rev() .take(ANSWER_MAX_HISTORY_SIZE) @@ -219,7 +220,8 @@ impl Agent { } fn code_chunks(&self) -> impl Iterator + '_ { - self.exchanges + self.conversation + .exchanges .iter() .flat_map(|e| e.code_chunks.iter().cloned()) } diff --git a/server/bleep/src/agent/tools/code.rs b/server/bleep/src/agent/tools/code.rs index 0aeea4cd9a..3625aa52e8 100644 --- a/server/bleep/src/agent/tools/code.rs +++ b/server/bleep/src/agent/tools/code.rs @@ -25,13 +25,13 @@ impl Agent { // not sure if this is what we want here, or use the project coupled with the agent. // // let repos = self.paths().map(|r| r.repo.to_string()).collect::>(); - let repos = self.project.clone(); + let repos = todo!();// self.project.clone(); let mut results = self .semantic_search(AgentSemanticSearchParams { query: Literal::from(&query.to_string()), paths: vec![], - project: repos.clone(), + project: repos, semantic_params: SemanticSearchParams { limit: CODE_SEARCH_LIMIT, offset: 0, @@ -97,7 +97,8 @@ impl Agent { chunks.sort_by(|a, b| a.alias.cmp(&b.alias).then(a.start_line.cmp(&b.start_line))); for chunk in chunks.iter().filter(|c| !c.is_empty()) { - self.exchanges + self.conversation + .exchanges .last_mut() .unwrap() .code_chunks diff --git a/server/bleep/src/agent/tools/path.rs b/server/bleep/src/agent/tools/path.rs index cb78a4296f..0c26344f2b 100644 --- a/server/bleep/src/agent/tools/path.rs +++ b/server/bleep/src/agent/tools/path.rs @@ -41,7 +41,7 @@ impl Agent { .semantic_search(AgentSemanticSearchParams { query: query.into(), paths: vec![], - project: self.project.clone(), + project: todo!(),// self.project.clone(), semantic_params: SemanticSearchParams { limit: 30, offset: 0, diff --git a/server/bleep/src/agent/tools/proc.rs b/server/bleep/src/agent/tools/proc.rs index 672ee8dd4d..39fe5cfba8 100644 --- a/server/bleep/src/agent/tools/proc.rs +++ b/server/bleep/src/agent/tools/proc.rs @@ -35,7 +35,7 @@ impl Agent { .semantic_search(AgentSemanticSearchParams { query: Literal::from(&query.to_string()), paths: paths.clone(), - project: self.project.clone(), + project: todo!(), //self.project.clone(), semantic_params: SemanticSearchParams { limit: 10, offset: 0, @@ -68,7 +68,8 @@ impl Agent { chunks.sort_by(|a, b| a.alias.cmp(&b.alias).then(a.start_line.cmp(&b.start_line))); for chunk in chunks.iter().filter(|c| !c.is_empty()) { - self.exchanges + self.conversation + .exchanges .last_mut() .unwrap() .code_chunks diff --git a/server/bleep/src/webserver.rs b/server/bleep/src/webserver.rs index f1478b8f7e..04d3ea68d2 100644 --- a/server/bleep/src/webserver.rs +++ b/server/bleep/src/webserver.rs @@ -100,9 +100,9 @@ pub async fn start(app: Application) -> anyhow::Result<()> { "/projects/:project_id/conversations/:conversation_id", get(conversation::get), ) - .route("/projects/:project_id/conversations/:conversation_id/answer/vote", post(answer::vote)) - .route("/projects/:project_id/conversations/:conversation_id/answer", get(answer::answer)) - .route("/projects/:project_id/conversations/:conversation_id/answer/explain", get(answer::explain)) + .route("/projects/:project_id/answer/vote", post(answer::vote)) + .route("/projects/:project_id/answer", get(answer::answer)) + .route("/projects/:project_id/answer/explain", get(answer::explain)) .route("/projects/:project_id/studios", post(studio::create)) .route("/projects/:project_id/studios", get(studio::list)) .route( diff --git a/server/bleep/src/webserver/answer.rs b/server/bleep/src/webserver/answer.rs index 7977bfd1ad..11ea9f61fd 100644 --- a/server/bleep/src/webserver/answer.rs +++ b/server/bleep/src/webserver/answer.rs @@ -2,7 +2,7 @@ use std::{panic::AssertUnwindSafe, time::Duration}; use anyhow::{anyhow, Context, Result}; use axum::{ - extract::{Query, Path}, + extract::{Path, Query}, response::{ sse::{self, Sse}, IntoResponse, @@ -27,7 +27,8 @@ use crate::{ db::QueryLog, query::parser::{self, Literal}, repo::RepoRef, - Application, webserver::conversation, + webserver::conversation::{self, Conversation}, + Application, }; const TIMEOUT_SECS: u64 = 60; @@ -65,7 +66,6 @@ pub(super) async fn vote( #[derive(Clone, Debug, serde::Deserialize)] pub struct Answer { pub q: String, - pub project: String, #[serde(default = "default_answer_model")] pub answer_model: agent::model::LLMModel, #[serde(default = "default_agent_model")] @@ -73,6 +73,7 @@ pub struct Answer { /// Optional id of the parent of the exchange to overwrite /// If this UUID is nil, then overwrite the first exchange in the thread pub parent_exchange_id: Option, + pub conversation_id: Option, } fn default_thread_id() -> uuid::Uuid { @@ -91,23 +92,19 @@ pub(super) async fn answer( Query(params): Query, Extension(app): Extension, Extension(user): Extension, - Path((project_id, conversation_id)): Path<(i64, i64)>, + Path(project_id): Path, ) -> super::Result { info!(?params.q, "handling /answer query"); let query_id = uuid::Uuid::new_v4(); - let conversation_id = ConversationId { - user_id: user - .username() - .ok_or_else(|| super::Error::user("didn't have user ID"))? - .to_string(), - project_id, - conversation_id, - }; + let user_id = user.username().ok_or_else(super::no_user_id)?; - let mut exchanges = conversation::load(&app.sql, &conversation_id) - .await? - .unwrap_or_default(); + let mut conversation = match params.conversation_id { + Some(conversation_id) => { + Conversation::load(&app.sql, user_id, project_id, conversation_id).await? + } + None => Conversation::new(), + }; let Answer { parent_exchange_id, @@ -119,14 +116,15 @@ pub(super) async fn answer( let truncate_from_index = if parent_exchange_id.is_nil() { 0 } else { - exchanges + conversation + .exchanges .iter() .position(|e| e.id == *parent_exchange_id) .ok_or_else(|| super::Error::user("parent query id not found in exchanges"))? + 1 }; - exchanges.truncate(truncate_from_index); + conversation.exchanges.truncate(truncate_from_index); } let query = parser::parse_nl(q).context("parse error")?.into_owned(); @@ -142,234 +140,221 @@ pub(super) async fn answer( debug!(?query_target, "parsed query target"); let action = Action::Query(query_target); - exchanges.push(Exchange::new(query_id, query)); + conversation.exchanges.push(Exchange::new(query_id, query)); - execute_agent( - params.clone(), - app.clone(), - user.clone(), + AgentExecutor { + params: params.clone(), + app: app.clone(), + user: user.clone(), query_id, - conversation_id, - exchanges, + project_id, + conversation, action, - ) + } + .execute() .await } -/// Like `try_execute_agent`, but additionally logs errors in our analytics. -async fn execute_agent( +#[derive(Clone)] +struct AgentExecutor { params: Answer, app: Application, user: User, query_id: uuid::Uuid, - conversation_id: ConversationId, - exchanges: Vec, + project_id: i64, + conversation: Conversation, action: Action, -) -> super::Result< - Sse> + Send>>>, -> { - let response = try_execute_agent( - params.clone(), - app.clone(), - user.clone(), - query_id, - conversation_id, - exchanges, - action, - ) - .await; - - if let Err(err) = response.as_ref() { - error!(?err, "failed to handle /answer query"); - - app.track_query( - &user, - &QueryEvent { - query_id, - thread_id: params.thread_id, - data: EventData::output_stage("error") - .with_payload("status", err.status.as_u16()) - .with_payload("message", err.message()), - }, - ); - } - - response } -async fn try_execute_agent( - params: Answer, - app: Application, - user: User, - query_id: uuid::Uuid, - conversation_id: ConversationId, - exchanges: Vec, - mut action: Action, -) -> super::Result< - Sse> + Send>>>, -> { - QueryLog::new(&app.sql).insert(¶ms.q).await?; - let project: Project = serde_json::from_str(¶ms.project).unwrap(); - let Answer { - answer_model, - agent_model, - .. - } = params.clone(); - - let username = user - .username() - .ok_or_else(|| super::Error::user("didn't have user ID"))?; - - let llm_gateway = user - .llm_gateway(&app) - .await? - .temperature(0.0) - .session_reference_id(format!("{username}::{}", conversation_id.thread_id)) - .model(agent_model.model_name); - - // confirm client compatibility with answer-api - match llm_gateway - .is_compatible(env!("CARGO_PKG_VERSION").parse().unwrap()) - .await - { - Ok(res) if res.status() == StatusCode::OK => (), - Ok(res) if res.status() == StatusCode::NOT_ACCEPTABLE => { - let out_of_date = futures::stream::once(async { - Ok(sse::Event::default() - .json_data(serde_json::json!({"Err": "incompatible client"})) - .unwrap()) - }); - return Ok(Sse::new(Box::pin(out_of_date))); - } - Ok(_) => unreachable!(), - Err(err) => { - warn!( - ?err, - "failed to check compatibility ... defaulting to `incompatible`" +type SseDynStream = Sse + Send>>>; + +impl AgentExecutor { + /// Like `try_execute`, but additionally logs errors in our analytics. + async fn execute(&mut self) -> super::Result>> { + let response = self.try_execute().await; + + if let Err(err) = response.as_ref() { + error!(?err, "failed to handle /answer query"); + + self.app.track_query( + &self.user, + &QueryEvent { + query_id: self.query_id, + thread_id: self.conversation.thread_id, + data: EventData::output_stage("error") + .with_payload("status", err.status.as_u16()) + .with_payload("message", err.message()), + }, ); - let failed_to_check = futures::stream::once(async { - Ok(sse::Event::default() - .json_data(serde_json::json!({"Err": "failed to check compatibility"})) - .unwrap()) - }); - return Ok(Sse::new(Box::pin(failed_to_check))); } - }; - let stream = async_stream::try_stream! { + response + } + + async fn try_execute(&mut self) -> super::Result>> { + QueryLog::new(&self.app.sql).insert(&self.params.q).await?; + + let username = self + .user + .username() + .ok_or_else(super::no_user_id)?; + + let llm_gateway = self.user + .llm_gateway(&self.app) + .await? + .temperature(0.0) + .session_reference_id(format!("{username}::{}", self.conversation.thread_id)) + .model(self.params.agent_model.model_name); + + // confirm client compatibility with answer-api + match llm_gateway + .is_compatible(env!("CARGO_PKG_VERSION").parse().unwrap()) + .await + { + Ok(res) if res.status() == StatusCode::OK => (), + Ok(res) if res.status() == StatusCode::NOT_ACCEPTABLE => { + let out_of_date = futures::stream::once(async { + Ok(sse::Event::default() + .json_data(serde_json::json!({"Err": "incompatible client"})) + .unwrap()) + }); + return Ok(Sse::new(Box::pin(out_of_date))); + } + Ok(_) => unreachable!(), + Err(err) => { + warn!( + ?err, + "failed to check compatibility ... defaulting to `incompatible`" + ); + let failed_to_check = futures::stream::once(async { + Ok(sse::Event::default() + .json_data(serde_json::json!({"Err": "failed to check compatibility"})) + .unwrap()) + }); + return Ok(Sse::new(Box::pin(failed_to_check))); + } + }; + + let initial_message = json!({ + "thread_id": self.conversation.thread_id.to_string(), + "query_id": self.query_id + }); + + // let project: Project = serde_json::from_str(&self.params.project).unwrap(); + let Answer { agent_model, answer_model, .. } = self.params.clone(); + let (exchange_tx, exchange_rx) = tokio::sync::mpsc::channel(10); + let mut action = self.action.clone(); let mut agent = Agent { - app, - project, - exchanges, + app: self.app.clone(), + conversation: self.conversation.clone(), exchange_tx, llm_gateway, - user, - thread_id, - query_id, + user: self.user.clone(), + query_id: self.query_id, exchange_state: ExchangeState::Pending, answer_model, agent_model }; - let mut exchange_rx = tokio_stream::wrappers::ReceiverStream::new(exchange_rx); - - let result = 'outer: loop { - // The main loop. Here, we create two streams that operate simultaneously; the update - // stream, which sends updates back to the HTTP event stream response, and the action - // stream, which returns a single item when there is a new action available to execute. - // Both of these operate together, and we repeat the process for every new action. - - use futures::future::FutureExt; - - let left_stream = (&mut exchange_rx).map(Either::Left); - let right_stream = agent - .step(action) - .into_stream() - .map(Either::Right); - - let timeout = Duration::from_secs(TIMEOUT_SECS); - - let mut next = None; - for await item in tokio_stream::StreamExt::timeout( - stream::select(left_stream, right_stream), - timeout, - ) { - match item { - Ok(Either::Left(exchange)) => yield exchange.compressed(), - Ok(Either::Right(next_action)) => match next_action { - Ok(n) => break next = n, - Err(e) => break 'outer Err(agent::Error::Processing(e)), - }, - Err(_) => break 'outer Err(agent::Error::Timeout(timeout)), + let stream = async_stream::try_stream! { + let mut exchange_rx = tokio_stream::wrappers::ReceiverStream::new(exchange_rx); + + let result = 'outer: loop { + // The main loop. Here, we create two streams that operate simultaneously; the update + // stream, which sends updates back to the HTTP event stream response, and the action + // stream, which returns a single item when there is a new action available to execute. + // Both of these operate together, and we repeat the process for every new action. + + use futures::future::FutureExt; + + let left_stream = (&mut exchange_rx).map(Either::Left); + let right_stream = agent + .step(action) + .into_stream() + .map(Either::Right); + + let timeout = Duration::from_secs(TIMEOUT_SECS); + + let mut next = None; + for await item in tokio_stream::StreamExt::timeout( + stream::select(left_stream, right_stream), + timeout, + ) { + match item { + Ok(Either::Left(exchange)) => yield exchange.compressed(), + Ok(Either::Right(next_action)) => match next_action { + Ok(n) => break next = n, + Err(e) => break 'outer Err(agent::Error::Processing(e)), + }, + Err(_) => break 'outer Err(agent::Error::Timeout(timeout)), + } } - } - // NB: Sending updates after all other `await` points in the final `step` call will - // likely not return a pending future due to the internal receiver queue. So, the call - // stack usually continues onwards, ultimately resulting in a `Poll::Ready`, backing out - // of the above loop without ever processing the final message. Here, we empty the - // queue. - while let Some(Some(exchange)) = exchange_rx.next().now_or_never() { - yield exchange.compressed(); - } + // NB: Sending updates after all other `await` points in the final `step` call will + // likely not return a pending future due to the internal receiver queue. So, the call + // stack usually continues onwards, ultimately resulting in a `Poll::Ready`, backing out + // of the above loop without ever processing the final message. Here, we empty the + // queue. + while let Some(Some(exchange)) = exchange_rx.next().now_or_never() { + yield exchange.compressed(); + } - match next { - Some(a) => action = a, - None => break Ok(()), + match next { + Some(a) => action = a, + None => break Ok(()), + } + }; + + agent.complete(result.is_ok()); + + match result { + Ok(_) => {} + Err(agent::Error::Timeout(duration)) => { + warn!("Timeout reached."); + agent.track_query( + EventData::output_stage("error") + .with_payload("timeout", duration.as_secs()), + ); + Err(anyhow!("reached timeout of {duration:?}"))?; + } + Err(agent::Error::Processing(e)) => { + agent.track_query( + EventData::output_stage("error") + .with_payload("message", e.to_string()), + ); + Err(e)?; + } } }; - agent.complete(result.is_ok()); - - match result { - Ok(_) => {} - Err(agent::Error::Timeout(duration)) => { - warn!("Timeout reached."); - agent.track_query( - EventData::output_stage("error") - .with_payload("timeout", duration.as_secs()), - ); - Err(anyhow!("reached timeout of {duration:?}"))?; - } - Err(agent::Error::Processing(e)) => { - agent.track_query( - EventData::output_stage("error") - .with_payload("message", e.to_string()), - ); - Err(e)?; - } - } - }; - - let init_stream = futures::stream::once(async move { - Ok(sse::Event::default() - .json_data(json!({ - "thread_id": params.thread_id.to_string(), - "query_id": query_id, - })) - // This should never happen, so we force an unwrap. - .expect("failed to serialize initialization object")) - }); - - // We know the stream is unwind safe as it doesn't use synchronization primitives like locks. - let answer_stream = AssertUnwindSafe(stream) - .catch_unwind() - .map(|res| res.unwrap_or_else(|_| Err(anyhow!("stream panicked")))) - .map(|ex: Result| { - sse::Event::default() - .json_data(ex.map_err(|e| e.to_string())) - .map_err(anyhow::Error::new) + let init_stream = futures::stream::once(async move { + Ok(sse::Event::default() + .json_data(initial_message) + // This should never happen, so we force an unwrap. + .expect("failed to serialize initialization object")) }); - let done_stream = futures::stream::once(async { Ok(sse::Event::default().data("[DONE]")) }); + // We know the stream is unwind safe as it doesn't use synchronization primitives like locks. + let answer_stream = AssertUnwindSafe(stream) + .catch_unwind() + .map(|res| res.unwrap_or_else(|_| Err(anyhow!("stream panicked")))) + .map(|ex: Result| { + sse::Event::default() + .json_data(ex.map_err(|e| e.to_string())) + .map_err(anyhow::Error::new) + }); + + let done_stream = futures::stream::once(async { Ok(sse::Event::default().data("[DONE]")) }); - let stream = init_stream.chain(answer_stream).chain(done_stream); + let stream = init_stream.chain(answer_stream).chain(done_stream); - Ok(Sse::new(Box::pin(stream))) + Ok(Sse::new(Box::pin(stream))) + } } + #[derive(serde::Deserialize)] pub struct Explain { pub relative_path: String, @@ -377,14 +362,14 @@ pub struct Explain { pub line_end: usize, pub branch: Option, pub repo_ref: RepoRef, - #[serde(default = "default_thread_id")] - pub thread_id: uuid::Uuid, + pub conversation_id: Option, } pub async fn explain( Query(params): Query, Extension(app): Extension, Extension(user): Extension, + Path(project_id): Path, ) -> super::Result { let query_id = uuid::Uuid::new_v4(); let repo_path = RepoPath { @@ -400,21 +385,12 @@ pub async fn explain( params.line_end + 1, params.relative_path ), - project: format!("[\"{}\"]", params.repo_ref), - thread_id: params.thread_id, + conversation_id: params.conversation_id, parent_exchange_id: None, answer_model: agent::model::GPT_4_TURBO_24K, agent_model: agent::model::GPT_4, }; - let conversation_id = ConversationId { - thread_id: params.thread_id, - user_id: user - .username() - .ok_or_else(|| super::Error::user("didn't have user ID"))? - .to_string(), - }; - let mut query = parser::parse_nl(&virtual_req.q) .context("failed to parse virtual answer query")? .into_owned(); @@ -458,16 +434,20 @@ pub async fn explain( end_byte: None, }); + let mut conversation = Conversation::new(); + conversation.exchanges.push(exchange); + let action = Action::Answer { paths: vec![0] }; - execute_agent( - virtual_req, + AgentExecutor { + params: virtual_req, app, user, query_id, - conversation_id, - vec![exchange], + project_id, + conversation, action, - ) + } + .execute() .await } diff --git a/server/bleep/src/webserver/conversation.rs b/server/bleep/src/webserver/conversation.rs index 7ce4fc0d49..d316ae922a 100644 --- a/server/bleep/src/webserver/conversation.rs +++ b/server/bleep/src/webserver/conversation.rs @@ -4,9 +4,11 @@ use axum::{ response::IntoResponse, Extension, Json, }; +use chrono::NaiveDateTime; use reqwest::StatusCode; use std::fmt; use tracing::info; +use uuid::Uuid; use crate::{ agent::{exchange::Exchange, Project}, @@ -16,7 +18,96 @@ use crate::{ Application, }; -pub type Conversation = (Project, Vec); +#[derive(Clone)] +pub struct Conversation { + pub exchanges: Vec, + pub thread_id: Uuid, +} + +impl Conversation { + pub fn new() -> Self { + Self { + exchanges: Vec::new(), + thread_id: Uuid::new_v4(), + } + } + + pub async fn store( + &self, + db: &SqlDb, + user_id: &str, + ) -> Result<()> { + let mut transaction = db.begin().await?; + + // Delete the old conversation for simplicity. This also deletes all its messages. + let project_id = sqlx::query! { + "DELETE FROM conversations + WHERE thread_id = ? AND EXISTS ( + SELECT p.id + FROM projects p + WHERE p.id = project_id AND p.user_id = ? + ) + RETURNING project_id", + self.thread_id, + user_id, + } + .fetch_one(&mut transaction) + .await + .map(|r| r.project_id)?; + + let title = self + .exchanges + .first() + .and_then(|list| list.query()) + .and_then(|q| q.split('\n').next().map(|s| s.to_string())) + .context("couldn't find conversation title")?; + + let exchanges = serde_json::to_string(&self.exchanges)?; + sqlx::query! { + "INSERT INTO conversations ( + thread_id, title, exchanges, project_id, created_at + ) + VALUES (?, ?, ?, ?, strftime('%s', 'now'))", + self.thread_id, + title, + exchanges, + project_id, + } + .execute(&mut transaction) + .await?; + + transaction.commit().await?; + + Ok(()) + } + + pub async fn load( + db: &SqlDb, + user_id: &str, + project_id: i64, + conversation_id: i64, + ) -> webserver::Result { + let row = sqlx::query! { + "SELECT c.exchanges, c.thread_id + FROM conversations c + JOIN projects p ON p.id = c.project_id AND p.user_id = ? + WHERE c.project_id = ? AND c.id = ?", + user_id, + project_id, + conversation_id, + } + .fetch_optional(db.as_ref()) + .await? + .ok_or_else(|| Error::not_found("conversation not found"))?; + + let exchanges = serde_json::from_str(&row.exchanges).map_err(Error::internal)?; + + Ok(Self { + exchanges, + thread_id: row.thread_id.parse().map_err(Error::internal)?, + }) + } +} #[derive(Hash, PartialEq, Eq, Clone)] pub struct ConversationId { @@ -32,14 +123,8 @@ pub struct ConversationPreview { pub title: String, } -#[derive(serde::Deserialize)] -pub(in crate::webserver) struct List { - repo_ref: Option, -} - pub(in crate::webserver) async fn list( Extension(user): Extension, - Query(query): Query, State(app): State, Path(project_id): Path, ) -> webserver::Result { @@ -48,32 +133,18 @@ pub(in crate::webserver) async fn list( .username() .ok_or_else(|| Error::user("missing user ID"))?; - let conversations = if let Some(repo_ref) = query.repo_ref { - let repo_ref = repo_ref.to_string(); - sqlx::query_as! { - ConversationPreview, - "SELECT c.id as 'id!', c.created_at, c.title \ - FROM conversations c \ - JOIN projects p ON p.id = c.project_id AND p.user_id = ? \ - WHERE c.repo_ref = ? \ - ORDER BY c.created_at DESC", - user_id, - repo_ref, - } - .fetch_all(db) - .await - } else { - sqlx::query_as! { - ConversationPreview, - "SELECT c.id as 'id!', c.created_at, c.title \ - FROM conversations c \ - JOIN projects p ON p.id = c.project_id AND p.user_id = ? - ORDER BY c.created_at DESC", - user_id, - } - .fetch_all(db) - .await + let conversations = sqlx::query_as! { + ConversationPreview, + "SELECT c.id as 'id!', c.created_at, c.title \ + FROM conversations c \ + JOIN projects p ON p.id = c.project_id AND p.user_id = ? \ + WHERE p.id = ? + ORDER BY c.created_at DESC", + user_id, + project_id, } + .fetch_all(db) + .await .map_err(Error::internal)?; Ok(Json(conversations)) @@ -111,19 +182,17 @@ pub(in crate::webserver) async fn delete( Ok(()) } +#[axum::debug_handler] pub(in crate::webserver) async fn get( - Path((project_id, conversation_id)): Path<(i64, i64)>, Extension(user): Extension, + Path((project_id, conversation_id)): Path<(i64, i64)>, State(app): State, ) -> webserver::Result { - let user_id = user - .username() - .ok_or_else(|| Error::user("missing user ID"))? - .to_owned(); + let user_id = user.username().ok_or_else(super::no_user_id)?; - let exchanges = load(&app.sql, &ConversationId { conversation_id, project_id, user_id }) + let exchanges = Conversation::load(&app.sql, user_id, project_id, conversation_id) .await? - .ok_or_else(|| Error::new(ErrorKind::NotFound, "thread was not found"))?; + .exchanges; let exchanges = exchanges .into_iter() @@ -132,67 +201,3 @@ pub(in crate::webserver) async fn get( Ok(Json(exchanges)) } - -pub async fn store(db: &SqlDb, id: ConversationId, conversation: Conversation) -> Result<()> { - info!("writing conversation {}-{}", id.user_id, id.conversation_id); - let mut transaction = db.begin().await?; - - // Delete the old conversation for simplicity. This also deletes all its messages. - let (user_id, thread_id) = (id.user_id.clone(), id.conversation_id.to_string()); - sqlx::query! { - "DELETE FROM conversations \ - WHERE user_id = ? AND thread_id = ?", - user_id, - thread_id, - } - .execute(&mut transaction) - .await?; - - let (project, exchanges) = conversation; - let repo_ref = project.id(); - let title = exchanges - .first() - .and_then(|list| list.query()) - .and_then(|q| q.split('\n').next().map(|s| s.to_string())) - .context("couldn't find conversation title")?; - - let exchanges = serde_json::to_string(&exchanges)?; - sqlx::query! { - "INSERT INTO conversations (\ - user_id, thread_id, repo_ref, title, exchanges, created_at\ - ) \ - VALUES (?, ?, ?, ?, ?, strftime('%s', 'now'))", - user_id, - thread_id, - repo_ref, - title, - exchanges, - } - .execute(&mut transaction) - .await?; - - transaction.commit().await?; - - Ok(()) -} - -pub async fn load(db: &SqlDb, id: &ConversationId) -> Result>> { - let (user_id, thread_id) = (id.user_id.clone(), id.conversation_id.to_string()); - - let row = sqlx::query! { - "SELECT repo_ref, exchanges FROM conversations \ - WHERE user_id = ? AND thread_id = ?", - user_id, - thread_id, - } - .fetch_optional(db.as_ref()) - .await?; - - let row = match row { - Some(r) => r, - None => return Ok(None), - }; - - let exchanges = serde_json::from_str(&row.exchanges)?; - Ok(Some(exchanges)) -} diff --git a/server/bleep/src/webserver/studio.rs b/server/bleep/src/webserver/studio.rs index 69c18d1890..d8ade28e99 100644 --- a/server/bleep/src/webserver/studio.rs +++ b/server/bleep/src/webserver/studio.rs @@ -1440,7 +1440,7 @@ pub struct Import { pub studio_id: Option, } -/// Returns a new studio UUID, or the `?studio_id=...` query param if present. +/// Returns a new studio ID, or the `?studio_id=...` query param if present. #[allow(clippy::single_range_in_vec_init)] pub async fn import( app: Extension, @@ -1455,9 +1455,10 @@ pub async fn import( let thread_id = params.thread_id.to_string(); let conversation = sqlx::query! { - "SELECT title, repo_ref, exchanges - FROM conversations - WHERE user_id = ? AND thread_id = ?", + "SELECT c.title, c.exchanges + FROM conversations c + JOIN projects p ON p.id = c.project_id AND p.user_id = ? + WHERE thread_id = ?", user_id, thread_id, } @@ -1465,7 +1466,6 @@ pub async fn import( .await? .ok_or_else(|| Error::not_found("conversation not found"))?; - let repo_ref = conversation.repo_ref; let exchanges = serde_json::from_str::>(&conversation.exchanges) .context("couldn't deserialize exchange list")?; @@ -1489,7 +1489,7 @@ pub async fn import( let imported_context = canonicalize_context(exchanges.iter().flat_map(|e| { e.code_chunks.iter().map(|c| ContextFile { - repo: repo_ref.parse().unwrap(), + repo: c.repo_path.repo.clone(), path: c.repo_path.path.clone(), hidden: false, branch: e.query.branch().next().map(Cow::into_owned), From d098fb983b91d58275f940b3224046223594105e Mon Sep 17 00:00:00 2001 From: calyptobai Date: Thu, 30 Nov 2023 21:47:00 -0500 Subject: [PATCH 09/88] WIP: Projects As part of this change, we add some new routes for project repo associations. - `GET /projects/:id/repos` returns a list of: `[ { ref: string } ]` - `POST /projects/:id/repos`: takes in a body like: `{ ref: string }` This adds the repo by repo ref to the list of repos in a project - `DELETE /projects/:id/repos/:repo_ref` deletes the repo from the project repo list --- .../migrations/20231122012638_projects.sql | 6 ++ server/bleep/sqlx-data.json | 100 ++++++++++++++++++ server/bleep/src/agent.rs | 18 +++- server/bleep/src/analytics.rs | 4 +- server/bleep/src/indexes/file.rs | 11 +- server/bleep/src/webserver.rs | 10 +- server/bleep/src/webserver/project.rs | 20 ++++ server/bleep/src/webserver/project/repo.rs | 95 +++++++++++++++++ 8 files changed, 253 insertions(+), 11 deletions(-) create mode 100644 server/bleep/src/webserver/project/repo.rs diff --git a/server/bleep/migrations/20231122012638_projects.sql b/server/bleep/migrations/20231122012638_projects.sql index dbce3c54f1..80a379ba94 100644 --- a/server/bleep/migrations/20231122012638_projects.sql +++ b/server/bleep/migrations/20231122012638_projects.sql @@ -9,3 +9,9 @@ ALTER TABLE studios DROP COLUMN user_id; ALTER TABLE conversations ADD COLUMN project_id INTEGER NOT NULL; ALTER TABLE conversations DROP COLUMN repo_ref; ALTER TABLE conversations DROP COLUMN user_id; + +CREATE TABLE project_repos ( + id INTEGER PRIMARY KEY, + project_id INTEGER NOT NULL, + repo_ref TEXT NOT NULL +); diff --git a/server/bleep/sqlx-data.json b/server/bleep/sqlx-data.json index 15acf3c90f..3b5e14e115 100644 --- a/server/bleep/sqlx-data.json +++ b/server/bleep/sqlx-data.json @@ -440,6 +440,24 @@ }, "query": "DELETE FROM chunk_cache WHERE chunk_hash = ? AND file_hash = ?" }, + "51613ed54060deefe909ecdf9f88a27a88f2a5b246589cebf67599953b6b13a0": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + } + ], + "nullable": [ + true + ], + "parameters": { + "Right": 3 + } + }, + "query": "DELETE FROM project_repos\n WHERE project_id = $1 AND repo_ref = $2 AND EXISTS (\n SELECT id\n FROM projects\n WHERE id = $1 AND user_id = $3\n )\n RETURNING id" + }, "523e2fd0c4f2c3318e894af3537b1c2e503e0865fcaabc4bdda43960ca0ef45c": { "describe": { "columns": [], @@ -640,6 +658,24 @@ }, "query": "SELECT messages, context FROM studio_snapshots WHERE id = ?" }, + "6767e546a8e1db775647021a74ebcb7868ce6b1e3c5b354cb5ee58464a1c3514": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + } + ], + "nullable": [ + true + ], + "parameters": { + "Right": 2 + } + }, + "query": "DELETE FROM projects WHERE id = ? AND user_id = ? RETURNING id" + }, "69c8b59ce4be3fc6edb58563bf69f55ea5dca4646b0ba05820e5d1b2b07c3c82": { "describe": { "columns": [ @@ -970,6 +1006,16 @@ }, "query": "UPDATE studio_snapshots SET context = ? WHERE id = ?" }, + "a72a44c5f6db62bc0ffc92ce0e06cdcdcadfb02ce33399a167bbf216e7a039ce": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 2 + } + }, + "query": "INSERT INTO project_repos (project_id, repo_ref) VALUES ($1, $2)" + }, "a8baec57552045778eb4609e83452c668583bf5a6ff8fec1898523058a061bcb": { "describe": { "columns": [ @@ -1052,6 +1098,24 @@ }, "query": "UPDATE templates SET name = ? WHERE id = ?" }, + "b194d61bd132827ab30dd215cb91c3c3d70db79f576b93f22373ec1d7b4f6489": { + "describe": { + "columns": [ + { + "name": "repo_ref", + "ordinal": 0, + "type_info": "Text" + } + ], + "nullable": [ + false + ], + "parameters": { + "Right": 2 + } + }, + "query": "SELECT repo_ref\n FROM project_repos\n WHERE project_id = $1 AND EXISTS (\n SELECT p.id\n FROM projects p\n WHERE p.id = $1 AND p.user_id = $2\n )" + }, "b3ebaeec21c90aa9ebc59a808e03c661839d0a0eaa86ad2bf4251e895f8e0a03": { "describe": { "columns": [], @@ -1062,6 +1126,24 @@ }, "query": "INSERT INTO chunk_cache (chunk_hash, file_hash, branches, repo_ref) VALUES (?, ?, ?, ?)" }, + "c1b0276926023df7e91a219e7720b0f5ade8d1b13de577dce4d78d31075a497b": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + } + ], + "nullable": [ + false + ], + "parameters": { + "Right": 2 + } + }, + "query": "SELECT id FROM projects WHERE id = ? AND user_id = ?" + }, "d2b52987aaa4bdc39c04254834c941cad2165eefd02eef46fda413822be91fd0": { "describe": { "columns": [ @@ -1100,6 +1182,24 @@ }, "query": "UPDATE studio_snapshots SET messages = ? WHERE id = ?" }, + "dc6c684b6a31f9a64024b0fcec346517fd9696a4b398151cc3809164c17778db": { + "describe": { + "columns": [ + { + "name": "repo_ref", + "ordinal": 0, + "type_info": "Text" + } + ], + "nullable": [ + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "SELECT pr.repo_ref\n FROM project_repos pr\n INNER JOIN projects p ON p.id = pr.project_id AND p.user_id = ?" + }, "dcb7f9427283203bce10fe7d618057ef3eab5f6af2277e7a1ac8ba050609894d": { "describe": { "columns": [ diff --git a/server/bleep/src/agent.rs b/server/bleep/src/agent.rs index ba53028da4..0d8a4c241b 100644 --- a/server/bleep/src/agent.rs +++ b/server/bleep/src/agent.rs @@ -1,4 +1,4 @@ -use std::{ops::Deref, sync::Arc, time::Duration}; +use std::{ops::Deref, sync::Arc, time::Duration, str::FromStr}; use anyhow::{anyhow, Context, Result}; use futures::{Future, TryStreamExt}; @@ -443,11 +443,25 @@ impl Agent { let branch = self.last_exchange().query.first_branch(); let langs = self.last_exchange().query.langs.iter().map(Deref::deref); + let user_id = self.user.username().expect("didn't have user ID"); + + let repos = sqlx::query! { + "SELECT pr.repo_ref + FROM project_repos pr + INNER JOIN projects p ON p.id = pr.project_id AND p.user_id = ?", + user_id, + } + .fetch_all(&*self.app.sql) + .await + .expect("failed to fetch repo associations") + .into_iter() + .filter_map(|r| RepoRef::from_str(&r.repo_ref).ok()); + debug!(?query, ?branch, %self.conversation.thread_id, "executing fuzzy search"); self.app .indexes .file - .fuzzy_path_match(todo!(), branch.as_deref(), query, langs, 50) + .fuzzy_path_match(repos, branch.as_deref(), query, langs, 50) .await } diff --git a/server/bleep/src/analytics.rs b/server/bleep/src/analytics.rs index c8ec123405..e7c3565d89 100644 --- a/server/bleep/src/analytics.rs +++ b/server/bleep/src/analytics.rs @@ -14,7 +14,7 @@ use uuid::Uuid; #[derive(Debug, Clone)] pub struct QueryEvent { pub query_id: Uuid, - pub conversation_id: i64, + pub thread_id: Uuid, pub data: EventData, } @@ -236,7 +236,7 @@ impl RudderHub { properties: Some(json!({ "device_id": self.device_id(), "query_id": event.query_id, - "conversation_id": event.conversation_id, + "thread_id": event.thread_id, "data": event.data, "package_metadata": options.package_metadata, })), diff --git a/server/bleep/src/indexes/file.rs b/server/bleep/src/indexes/file.rs index add42bef60..2bba1c4341 100644 --- a/server/bleep/src/indexes/file.rs +++ b/server/bleep/src/indexes/file.rs @@ -247,7 +247,7 @@ impl Indexer { /// If the regex filter fails to build, an empty list is returned. pub async fn fuzzy_path_match( &self, - project: Project, + repos: impl Iterator, branch: Option<&str>, query_str: &str, langs: impl Iterator, @@ -269,12 +269,11 @@ impl Indexer { }) .map(BooleanQuery::intersection); - let project_scope = BooleanQuery::union( - project - .repos() + let repo_scope = BooleanQuery::union( + repos .map(|repo| { Box::new(TermQuery::new( - Term::from_field_text(self.source.repo_name, &repo), + Term::from_field_text(self.source.repo_name, &repo.to_string()), IndexRecordOption::Basic, )) as Box }) @@ -298,7 +297,7 @@ impl Indexer { .map(|term| TermQuery::new(term, IndexRecordOption::Basic)) .flat_map(|query| { let mut q: Vec> = - vec![Box::new(project_scope.clone()), Box::new(query)]; + vec![Box::new(repo_scope.clone()), Box::new(query)]; q.extend(branch_scope.clone().map(|q| Box::new(q) as Box)); q.push(Box::new(langs_query.clone())); diff --git a/server/bleep/src/webserver.rs b/server/bleep/src/webserver.rs index 04d3ea68d2..546cf7a3e7 100644 --- a/server/bleep/src/webserver.rs +++ b/server/bleep/src/webserver.rs @@ -90,7 +90,15 @@ pub async fn start(app: Application) -> anyhow::Result<()> { .route("/projects", get(project::list).post(project::create)) .route( "/projects/:project_id", - get(project::get).put(project::update), + get(project::get).put(project::update).delete(project::delete), + ) + .route( + "/projects/:project_id/repos", + get(project::repo::list).post(project::repo::add) + ) + .route( + "/projects/:project_id/repos/:repo_ref", + delete(project::repo::delete) ) .route( "/projects/:project_id/conversations", diff --git a/server/bleep/src/webserver/project.rs b/server/bleep/src/webserver/project.rs index 48434bcae4..7d03832543 100644 --- a/server/bleep/src/webserver/project.rs +++ b/server/bleep/src/webserver/project.rs @@ -7,6 +7,8 @@ use chrono::NaiveDateTime; use super::{middleware::User, Error}; +pub mod repo; + fn default_name() -> String { "New Project".into() } @@ -136,3 +138,21 @@ pub async fn update( .map(|_id| ()) .map_err(Error::internal) } + +pub async fn delete( + app: Extension, + user: Extension, + Path(id): Path, +) -> webserver::Result<()> { + let user_id = user.username().ok_or_else(super::no_user_id)?; + + sqlx::query! { + "DELETE FROM projects WHERE id = ? AND user_id = ? RETURNING id", + id, + user_id, + } + .fetch_optional(&*app.sql) + .await? + .map(|_id| ()) + .ok_or_else(|| Error::not_found("could not find project")) +} diff --git a/server/bleep/src/webserver/project/repo.rs b/server/bleep/src/webserver/project/repo.rs new file mode 100644 index 0000000000..7eb0ab1eda --- /dev/null +++ b/server/bleep/src/webserver/project/repo.rs @@ -0,0 +1,95 @@ +use axum::{Extension, Json, extract::Path}; + +use crate::{Application, webserver::{middleware::User, self, Error}, repo::RepoRef}; + +#[derive(serde::Serialize)] +pub struct ListItem { + #[serde(rename = "ref")] + repo_ref: String, +} + +pub async fn list( + app: Extension, + user: Extension, + Path(project_id): Path, +) -> webserver::Result>> { + let user_id = user.username().ok_or_else(webserver::no_user_id)?.to_string(); + + sqlx::query_as! { + ListItem, + "SELECT repo_ref + FROM project_repos + WHERE project_id = $1 AND EXISTS ( + SELECT p.id + FROM projects p + WHERE p.id = $1 AND p.user_id = $2 + )", + project_id, + user_id, + } + .fetch_all(&*app.sql) + .await + .map(Json) + .map_err(Error::internal) +} + +#[derive(serde::Deserialize)] +pub struct Add { + #[serde(rename = "ref")] + repo_ref: RepoRef, +} + +pub async fn add( + app: Extension, + user: Extension, + Path(project_id): Path, + Json(params): Json, +) -> webserver::Result<()> { + let user_id = user.username().ok_or_else(webserver::no_user_id)?.to_string(); + + sqlx::query! { + "SELECT id FROM projects WHERE id = ? AND user_id = ?", + project_id, + user_id, + } + .fetch_optional(&*app.sql) + .await? + .ok_or_else(|| Error::not_found("project not found"))?; + + let repo_ref = params.repo_ref.to_string(); + + sqlx::query! { + "INSERT INTO project_repos (project_id, repo_ref) VALUES ($1, $2)", + project_id, + repo_ref, + } + .execute(&*app.sql) + .await + .map(|_| ()) + .map_err(Error::internal) +} + +pub async fn delete( + app: Extension, + user: Extension, + Path((project_id, repo_ref)): Path<(i64, String)>, +) -> webserver::Result<()> { + let user_id = user.username().ok_or_else(webserver::no_user_id)?.to_string(); + + sqlx::query! { + "DELETE FROM project_repos + WHERE project_id = $1 AND repo_ref = $2 AND EXISTS ( + SELECT id + FROM projects + WHERE id = $1 AND user_id = $3 + ) + RETURNING id", + project_id, + repo_ref, + user_id, + } + .fetch_optional(&*app.sql) + .await? + .map(|_| ()) + .ok_or_else(|| Error::not_found("project repo not found")) +} From fa7b62aa4a3c93c75099da0a52e8dcbd665532ed Mon Sep 17 00:00:00 2001 From: calyptobai Date: Wed, 20 Dec 2023 00:43:50 -0500 Subject: [PATCH 10/88] WIP: Projects As part of this change, we now return a complete `Repo` object when retrieving `GET /projects/:id/repos`. Tooling for the agent was also adjusted. --- server/bleep/sqlx-data.json | 18 +++++++++ server/bleep/src/agent.rs | 29 +++++++++----- server/bleep/src/agent/tools/code.rs | 9 ++--- server/bleep/src/agent/tools/path.rs | 2 +- server/bleep/src/agent/tools/proc.rs | 6 +-- server/bleep/src/webserver.rs | 8 ++-- server/bleep/src/webserver/answer.rs | 27 ++++++++++--- server/bleep/src/webserver/conversation.rs | 6 +-- server/bleep/src/webserver/project/repo.rs | 45 ++++++++++++++-------- server/bleep/src/webserver/repos.rs | 2 +- 10 files changed, 100 insertions(+), 52 deletions(-) diff --git a/server/bleep/sqlx-data.json b/server/bleep/sqlx-data.json index 3b5e14e115..51468210a0 100644 --- a/server/bleep/sqlx-data.json +++ b/server/bleep/sqlx-data.json @@ -1016,6 +1016,24 @@ }, "query": "INSERT INTO project_repos (project_id, repo_ref) VALUES ($1, $2)" }, + "a7d8d84f5c97f5223d048bc3cdef92580195491edb3fc4c1757886a47be880bf": { + "describe": { + "columns": [ + { + "name": "repo_ref", + "ordinal": 0, + "type_info": "Text" + } + ], + "nullable": [ + false + ], + "parameters": { + "Right": 2 + } + }, + "query": "SELECT repo_ref\n FROM project_repos\n WHERE project_id = $1 AND EXISTS (\n SELECT id\n FROM projects\n WHERE id = $1 AND user_id = $2\n )" + }, "a8baec57552045778eb4609e83452c668583bf5a6ff8fec1898523058a061bcb": { "describe": { "columns": [ diff --git a/server/bleep/src/agent.rs b/server/bleep/src/agent.rs index 0d8a4c241b..b15fbfe27e 100644 --- a/server/bleep/src/agent.rs +++ b/server/bleep/src/agent.rs @@ -1,4 +1,4 @@ -use std::{ops::Deref, sync::Arc, time::Duration, str::FromStr}; +use std::{ops::Deref, str::FromStr, sync::Arc, time::Duration}; use anyhow::{anyhow, Context, Result}; use futures::{Future, TryStreamExt}; @@ -14,7 +14,7 @@ use crate::{ repo::RepoRef, semantic::{self, SemanticSearchParams}, webserver::{ - conversation::{self, ConversationId, Conversation}, + conversation::{self, Conversation, ConversationId}, middleware::User, }, Application, @@ -75,6 +75,7 @@ pub struct Agent { pub llm_gateway: llm_gateway::Client, pub user: User, pub query_id: uuid::Uuid, + pub repo_refs: Vec, pub answer_model: model::LLMModel, pub agent_model: model::LLMModel, @@ -94,7 +95,7 @@ pub enum ExchangeState { pub struct AgentSemanticSearchParams<'a> { pub query: parser::Literal<'a>, pub paths: Vec, - pub project: Project, + pub repos: Vec, pub semantic_params: SemanticSearchParams, } @@ -170,15 +171,24 @@ impl Agent { } fn last_exchange(&self) -> &Exchange { - self.conversation.exchanges.last().expect("exchange list was empty") + self.conversation + .exchanges + .last() + .expect("exchange list was empty") } fn last_exchange_mut(&mut self) -> &mut Exchange { - self.conversation.exchanges.last_mut().expect("exchange list was empty") + self.conversation + .exchanges + .last_mut() + .expect("exchange list was empty") } fn paths(&self) -> impl Iterator { - self.conversation.exchanges.iter().flat_map(|e| e.paths.iter()) + self.conversation + .exchanges + .iter() + .flat_map(|e| e.paths.iter()) } fn get_path_alias(&mut self, repo_path: &RepoPath) -> usize { @@ -368,7 +378,7 @@ impl Agent { AgentSemanticSearchParams { query, paths, - project, + repos, semantic_params, }: AgentSemanticSearchParams<'_>, ) -> Result> { @@ -409,8 +419,9 @@ impl Agent { let query = parser::SemanticQuery { target: Some(query), - repos: project - .repos() + repos: repos + .iter() + .map(RepoRef::to_string) .map(|r| parser::Literal::Plain(r.into())) .collect(), paths, diff --git a/server/bleep/src/agent/tools/code.rs b/server/bleep/src/agent/tools/code.rs index 3625aa52e8..d45296c23a 100644 --- a/server/bleep/src/agent/tools/code.rs +++ b/server/bleep/src/agent/tools/code.rs @@ -22,16 +22,13 @@ impl Agent { })) .await?; - // not sure if this is what we want here, or use the project coupled with the agent. - // - // let repos = self.paths().map(|r| r.repo.to_string()).collect::>(); - let repos = todo!();// self.project.clone(); + let repos = self.repo_refs.clone(); let mut results = self .semantic_search(AgentSemanticSearchParams { query: Literal::from(&query.to_string()), paths: vec![], - project: repos, + repos: repos.clone(), semantic_params: SemanticSearchParams { limit: CODE_SEARCH_LIMIT, offset: 0, @@ -53,7 +50,7 @@ impl Agent { .semantic_search(AgentSemanticSearchParams { query: hyde_doc, paths: vec![], - project: repos, + repos, semantic_params: SemanticSearchParams { limit: CODE_SEARCH_LIMIT, offset: 0, diff --git a/server/bleep/src/agent/tools/path.rs b/server/bleep/src/agent/tools/path.rs index 0c26344f2b..9650d87986 100644 --- a/server/bleep/src/agent/tools/path.rs +++ b/server/bleep/src/agent/tools/path.rs @@ -41,7 +41,7 @@ impl Agent { .semantic_search(AgentSemanticSearchParams { query: query.into(), paths: vec![], - project: todo!(),// self.project.clone(), + repos: self.repo_refs.clone(), semantic_params: SemanticSearchParams { limit: 30, offset: 0, diff --git a/server/bleep/src/agent/tools/proc.rs b/server/bleep/src/agent/tools/proc.rs index 39fe5cfba8..81b53cd3a8 100644 --- a/server/bleep/src/agent/tools/proc.rs +++ b/server/bleep/src/agent/tools/proc.rs @@ -27,15 +27,13 @@ impl Agent { })) .await?; - // let relative_paths = paths.iter().map(|p| p.path.clone()).collect::>(); - // not sure we need this? - // let repos = paths.iter().map(|p| p.repo.clone()).collect::>(); + let repos = paths.iter().map(|p| p.repo.clone()).collect::>(); let results = self .semantic_search(AgentSemanticSearchParams { query: Literal::from(&query.to_string()), paths: paths.clone(), - project: todo!(), //self.project.clone(), + repos, semantic_params: SemanticSearchParams { limit: 10, offset: 0, diff --git a/server/bleep/src/webserver.rs b/server/bleep/src/webserver.rs index 546cf7a3e7..b5de990102 100644 --- a/server/bleep/src/webserver.rs +++ b/server/bleep/src/webserver.rs @@ -90,15 +90,17 @@ pub async fn start(app: Application) -> anyhow::Result<()> { .route("/projects", get(project::list).post(project::create)) .route( "/projects/:project_id", - get(project::get).put(project::update).delete(project::delete), + get(project::get) + .put(project::update) + .delete(project::delete), ) .route( "/projects/:project_id/repos", - get(project::repo::list).post(project::repo::add) + get(project::repo::list).post(project::repo::add), ) .route( "/projects/:project_id/repos/:repo_ref", - delete(project::repo::delete) + delete(project::repo::delete), ) .route( "/projects/:project_id/conversations", diff --git a/server/bleep/src/webserver/answer.rs b/server/bleep/src/webserver/answer.rs index 11ea9f61fd..1154ef2751 100644 --- a/server/bleep/src/webserver/answer.rs +++ b/server/bleep/src/webserver/answer.rs @@ -194,12 +194,27 @@ impl AgentExecutor { async fn try_execute(&mut self) -> super::Result>> { QueryLog::new(&self.app.sql).insert(&self.params.q).await?; - let username = self - .user - .username() - .ok_or_else(super::no_user_id)?; + let username = self.user.username().ok_or_else(super::no_user_id)?; + + let repo_refs = sqlx::query! { + "SELECT repo_ref + FROM project_repos + WHERE project_id = $1 AND EXISTS ( + SELECT id + FROM projects + WHERE id = $1 AND user_id = $2 + )", + self.project_id, + username, + } + .fetch_all(&*self.app.sql) + .await? + .into_iter() + .filter_map(|row| row.repo_ref.parse().ok()) + .collect(); - let llm_gateway = self.user + let llm_gateway = self + .user .llm_gateway(&self.app) .await? .temperature(0.0) @@ -253,6 +268,7 @@ impl AgentExecutor { llm_gateway, user: self.user.clone(), query_id: self.query_id, + repo_refs, exchange_state: ExchangeState::Pending, answer_model, agent_model @@ -354,7 +370,6 @@ impl AgentExecutor { } } - #[derive(serde::Deserialize)] pub struct Explain { pub relative_path: String, diff --git a/server/bleep/src/webserver/conversation.rs b/server/bleep/src/webserver/conversation.rs index d316ae922a..e79e7e994a 100644 --- a/server/bleep/src/webserver/conversation.rs +++ b/server/bleep/src/webserver/conversation.rs @@ -32,11 +32,7 @@ impl Conversation { } } - pub async fn store( - &self, - db: &SqlDb, - user_id: &str, - ) -> Result<()> { + pub async fn store(&self, db: &SqlDb, user_id: &str) -> Result<()> { let mut transaction = db.begin().await?; // Delete the old conversation for simplicity. This also deletes all its messages. diff --git a/server/bleep/src/webserver/project/repo.rs b/server/bleep/src/webserver/project/repo.rs index 7eb0ab1eda..fe9c9cbcfd 100644 --- a/server/bleep/src/webserver/project/repo.rs +++ b/server/bleep/src/webserver/project/repo.rs @@ -1,22 +1,22 @@ -use axum::{Extension, Json, extract::Path}; +use axum::{extract::Path, Extension, Json}; -use crate::{Application, webserver::{middleware::User, self, Error}, repo::RepoRef}; - -#[derive(serde::Serialize)] -pub struct ListItem { - #[serde(rename = "ref")] - repo_ref: String, -} +use crate::{ + repo::RepoRef, + webserver::{self, middleware::User, repos::Repo, Error}, + Application, +}; pub async fn list( app: Extension, user: Extension, Path(project_id): Path, -) -> webserver::Result>> { - let user_id = user.username().ok_or_else(webserver::no_user_id)?.to_string(); +) -> webserver::Result>> { + let user_id = user + .username() + .ok_or_else(webserver::no_user_id)? + .to_string(); - sqlx::query_as! { - ListItem, + let repos = sqlx::query! { "SELECT repo_ref FROM project_repos WHERE project_id = $1 AND EXISTS ( @@ -28,9 +28,14 @@ pub async fn list( user_id, } .fetch_all(&*app.sql) - .await - .map(Json) - .map_err(Error::internal) + .await? + .into_iter() + .filter_map(|row| row.repo_ref.parse().ok()) + .filter_map(|repo_ref| app.repo_pool.get(&repo_ref)) + .map(|entry| Repo::from((entry.key(), entry.get()))) + .collect(); + + Ok(Json(repos)) } #[derive(serde::Deserialize)] @@ -45,7 +50,10 @@ pub async fn add( Path(project_id): Path, Json(params): Json, ) -> webserver::Result<()> { - let user_id = user.username().ok_or_else(webserver::no_user_id)?.to_string(); + let user_id = user + .username() + .ok_or_else(webserver::no_user_id)? + .to_string(); sqlx::query! { "SELECT id FROM projects WHERE id = ? AND user_id = ?", @@ -74,7 +82,10 @@ pub async fn delete( user: Extension, Path((project_id, repo_ref)): Path<(i64, String)>, ) -> webserver::Result<()> { - let user_id = user.username().ok_or_else(webserver::no_user_id)?.to_string(); + let user_id = user + .username() + .ok_or_else(webserver::no_user_id)? + .to_string(); sqlx::query! { "DELETE FROM project_repos diff --git a/server/bleep/src/webserver/repos.rs b/server/bleep/src/webserver/repos.rs index 7e49638db4..67bbccb243 100644 --- a/server/bleep/src/webserver/repos.rs +++ b/server/bleep/src/webserver/repos.rs @@ -24,7 +24,7 @@ pub(crate) struct Branch { } #[derive(Serialize, Debug, Eq)] -pub(crate) struct Repo { +pub struct Repo { pub(super) provider: Backend, pub(super) name: String, #[serde(rename = "ref")] From 5374ada3e37290469b7d2fc54a4129a71fac9241 Mon Sep 17 00:00:00 2001 From: calyptobai Date: Mon, 4 Dec 2023 18:10:09 -0500 Subject: [PATCH 11/88] WIP: Projects Here, we add `most_common_langs` to `GET /projects` and`GET /projects/:id` We also add routes for project to doc associations. - `GET /projects/:id/docs` returns a list of: ``` [ { id: number, url: string, index_status: string, name: null | string, favicon: null | string, description: null | string, modified_at: date string, } ] ``` - `POST /projects/:id/docs`: takes in a body like: `{ doc_id: number }` This adds the doc by ID to the list of docs in a project - `DELETE /projects/:id/docs/:doc_id` deletes the doc from the project doc list --- .../20231201200442_project_docs.sql | 5 + server/bleep/sqlx-data.json | 124 ++++++++++++++++++ server/bleep/src/webserver.rs | 8 ++ server/bleep/src/webserver/project.rs | 77 +++++++++-- server/bleep/src/webserver/project/doc.rs | 111 ++++++++++++++++ 5 files changed, 312 insertions(+), 13 deletions(-) create mode 100644 server/bleep/migrations/20231201200442_project_docs.sql create mode 100644 server/bleep/src/webserver/project/doc.rs diff --git a/server/bleep/migrations/20231201200442_project_docs.sql b/server/bleep/migrations/20231201200442_project_docs.sql new file mode 100644 index 0000000000..54d1e6070a --- /dev/null +++ b/server/bleep/migrations/20231201200442_project_docs.sql @@ -0,0 +1,5 @@ +CREATE TABLE project_docs ( + id INTEGER PRIMARY KEY, + project_id INTEGER NOT NULL, + doc_id INTEGER NOT NULL +); diff --git a/server/bleep/sqlx-data.json b/server/bleep/sqlx-data.json index 51468210a0..fe19ad5cb3 100644 --- a/server/bleep/sqlx-data.json +++ b/server/bleep/sqlx-data.json @@ -338,6 +338,48 @@ }, "query": "DELETE FROM docs WHERE id = ? RETURNING id" }, + "400b01ce2735d2606363727d3ad2b2e829775ea081d4cc2dc83b19e226061c1e": { + "describe": { + "columns": [ + { + "name": "repo_ref", + "ordinal": 0, + "type_info": "Text" + } + ], + "nullable": [ + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "SELECT repo_ref\n FROM project_repos\n WHERE project_id = ?" + }, + "4118c5a7a20ea12c56aefd31ce0cb4c81f4bd969380e8bdf6db1cc894fd8be4f": { + "describe": { + "columns": [ + { + "name": "project_id", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "repo_ref", + "ordinal": 1, + "type_info": "Text" + } + ], + "nullable": [ + false, + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "SELECT pr.project_id, pr.repo_ref\n FROM project_repos pr\n JOIN projects p ON p.id = pr.project_id AND p.user_id = ?" + }, "454d7dfb50480aae5ad9c8372262d55a302e214e1c7ceb8d62b53832f75bd85b": { "describe": { "columns": [], @@ -724,6 +766,16 @@ }, "query": "DELETE FROM studio_snapshots\n WHERE id IN (\n SELECT ss.id\n FROM studio_snapshots ss\n JOIN studios s ON s.id = ss.studio_id\n JOIN projects p ON p.id = s.project_id\n WHERE ss.id = ? AND ss.studio_id = ? AND s.project_id = ? AND p.user_id = ?\n )\n RETURNING id" }, + "6bba4ade0d1e5cc62dc28a64ad5d2aa9cfdef463725d1cbac9af2cd5086b7a48": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 2 + } + }, + "query": "INSERT INTO project_docs (project_id, doc_id) VALUES ($1, $2)" + }, "6e842ac5eb4b5be53dff501a24b6b91c0557d6a7119480441a60c8e99de7daf1": { "describe": { "columns": [ @@ -872,6 +924,24 @@ }, "query": "INSERT INTO studios (project_id, name) VALUES (?, ?) RETURNING id" }, + "804524ce5653a0df70fb32bb7cba5dd4593ac0f3236db279ba0829b76526ea4a": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + } + ], + "nullable": [ + true + ], + "parameters": { + "Right": 3 + } + }, + "query": "DELETE FROM project_docs\n WHERE project_id = $1 AND doc_id = $2 AND EXISTS (\n SELECT id\n FROM projects\n WHERE id = $1 AND user_id = $3\n )\n RETURNING id" + }, "834600ffd689d32c250771a3118d24fa93e71a6690b14703fe0b7feda05d53b6": { "describe": { "columns": [], @@ -1162,6 +1232,60 @@ }, "query": "SELECT id FROM projects WHERE id = ? AND user_id = ?" }, + "d2aa09b48fdb1d608c6034758e3d43509abb7ebe114d81724409d664a46fc5df": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "url", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "index_status", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "name", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "favicon", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "description", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "modified_at", + "ordinal": 6, + "type_info": "Datetime" + } + ], + "nullable": [ + false, + false, + false, + true, + true, + true, + false + ], + "parameters": { + "Right": 2 + } + }, + "query": "SELECT d.id, d.url, d.index_status, d.name, d.favicon, d.description, d.modified_at\n FROM project_docs pd\n INNER JOIN docs d ON d.id = pd.id\n WHERE project_id = $1 AND EXISTS (\n SELECT p.id\n FROM projects p\n WHERE p.id = $1 AND p.user_id = $2\n )" + }, "d2b52987aaa4bdc39c04254834c941cad2165eefd02eef46fda413822be91fd0": { "describe": { "columns": [ diff --git a/server/bleep/src/webserver.rs b/server/bleep/src/webserver.rs index b5de990102..8812c1255d 100644 --- a/server/bleep/src/webserver.rs +++ b/server/bleep/src/webserver.rs @@ -102,6 +102,14 @@ pub async fn start(app: Application) -> anyhow::Result<()> { "/projects/:project_id/repos/:repo_ref", delete(project::repo::delete), ) + .route( + "/projects/:project_id/docs", + get(project::doc::list).post(project::doc::add), + ) + .route( + "/projects/:project_id/docs/:doc_id", + delete(project::doc::delete), + ) .route( "/projects/:project_id/conversations", get(conversation::list).delete(conversation::delete), diff --git a/server/bleep/src/webserver/project.rs b/server/bleep/src/webserver/project.rs index 7d03832543..8d17f1d6cf 100644 --- a/server/bleep/src/webserver/project.rs +++ b/server/bleep/src/webserver/project.rs @@ -1,13 +1,17 @@ +use std::collections::HashMap; + use crate::{webserver, Application}; use axum::{ extract::{Path, Query}, Extension, Json, }; use chrono::NaiveDateTime; +use futures::TryStreamExt; -use super::{middleware::User, Error}; +use super::{middleware::User, repos::Repo, Error}; pub mod repo; +pub mod doc; fn default_name() -> String { "New Project".into() @@ -18,6 +22,7 @@ pub struct ListItem { id: i64, name: String, modified_at: Option, + most_common_langs: Vec, } pub async fn list( @@ -39,16 +44,45 @@ pub async fn list( user_id, } .fetch_all(&*app.sql) + .await?; + + let most_common_langs = sqlx::query! { + "SELECT pr.project_id, pr.repo_ref + FROM project_repos pr + JOIN projects p ON p.id = pr.project_id AND p.user_id = ?", + user_id, + } + .fetch_all(&*app.sql) .await? .into_iter() - .map(|row| ListItem { - id: row.id, - name: row.name.unwrap_or_else(default_name), - modified_at: row.modified_at, + .filter_map(|row| { + let repo_ref = row.repo_ref.parse().ok()?; + let pool_entry = app.repo_pool.get(&repo_ref)?; + let repo = Repo::from((&repo_ref, pool_entry.get())); + Some((row.project_id, repo.most_common_lang?)) }) - .collect(); - - Ok(Json(projects)) + .fold( + HashMap::<_, Vec<_>>::new(), + |mut a, (project_id, most_common_lang)| { + a.entry(project_id).or_default().push(most_common_lang); + a + }, + ); + + let list = projects + .into_iter() + .map(|row| ListItem { + id: row.id, + name: row.name.unwrap_or_else(default_name), + modified_at: row.modified_at, + most_common_langs: most_common_langs + .get(&row.id) + .map(Vec::clone) + .unwrap_or_default(), + }) + .collect(); + + Ok(Json(list)) } #[derive(serde::Deserialize)] @@ -80,6 +114,7 @@ pub struct Get { id: i64, name: String, modified_at: Option, + most_common_langs: Vec, } pub async fn get( @@ -89,7 +124,7 @@ pub async fn get( ) -> webserver::Result> { let user_id = user.username().ok_or_else(super::no_user_id)?.to_string(); - sqlx::query! { + let row = sqlx::query! { "SELECT name, ( SELECT ss.modified_at FROM studio_snapshots ss @@ -105,13 +140,29 @@ pub async fn get( } .fetch_one(&*app.sql) .await - .map_err(Error::not_found) - .map(|row| Get { + .map_err(Error::not_found)?; + + let most_common_langs = sqlx::query! { + "SELECT repo_ref + FROM project_repos + WHERE project_id = ?", + id, + } + .fetch_all(&*app.sql) + .await? + .into_iter() + .filter_map(|row| row.repo_ref.parse().ok()) + .filter_map(|repo_ref| app.repo_pool.get(&repo_ref)) + .map(|entry| Repo::from((entry.key(), entry.get()))) + .filter_map(|repo| repo.most_common_lang) + .collect(); + + Ok(Json(Get { id, name: row.name.unwrap_or_else(default_name), modified_at: row.modified_at, - }) - .map(Json) + most_common_langs, + })) } #[derive(serde::Deserialize)] diff --git a/server/bleep/src/webserver/project/doc.rs b/server/bleep/src/webserver/project/doc.rs new file mode 100644 index 0000000000..348346983d --- /dev/null +++ b/server/bleep/src/webserver/project/doc.rs @@ -0,0 +1,111 @@ +use axum::{extract::Path, Extension, Json}; +use chrono::NaiveDateTime; + +use crate::{ + webserver::{self, middleware::User, Error}, + Application, +}; + +#[derive(serde::Serialize)] +pub struct Doc { + id: i64, + url: String, + index_status: String, + name: Option, + favicon: Option, + description: Option, + modified_at: NaiveDateTime, +} + +pub async fn list( + app: Extension, + user: Extension, + Path(project_id): Path, +) -> webserver::Result>> { + let user_id = user + .username() + .ok_or_else(webserver::no_user_id)? + .to_string(); + + let docs = sqlx::query_as! { + Doc, + "SELECT d.id, d.url, d.index_status, d.name, d.favicon, d.description, d.modified_at + FROM project_docs pd + INNER JOIN docs d ON d.id = pd.id + WHERE project_id = $1 AND EXISTS ( + SELECT p.id + FROM projects p + WHERE p.id = $1 AND p.user_id = $2 + )", + project_id, + user_id, + } + .fetch_all(&*app.sql) + .await?; + + Ok(Json(docs)) +} + +#[derive(serde::Deserialize)] +pub struct Add { + doc_id: i64, +} + +pub async fn add( + app: Extension, + user: Extension, + Path(project_id): Path, + Json(params): Json, +) -> webserver::Result<()> { + let user_id = user + .username() + .ok_or_else(webserver::no_user_id)? + .to_string(); + + sqlx::query! { + "SELECT id FROM projects WHERE id = ? AND user_id = ?", + project_id, + user_id, + } + .fetch_optional(&*app.sql) + .await? + .ok_or_else(|| Error::not_found("project not found"))?; + + sqlx::query! { + "INSERT INTO project_docs (project_id, doc_id) VALUES ($1, $2)", + project_id, + params.doc_id, + } + .execute(&*app.sql) + .await + .map(|_| ()) + .map_err(Error::internal) +} + +pub async fn delete( + app: Extension, + user: Extension, + Path((project_id, doc_id)): Path<(i64, i64)>, +) -> webserver::Result<()> { + let user_id = user + .username() + .ok_or_else(webserver::no_user_id)? + .to_string(); + + sqlx::query! { + "DELETE FROM project_docs + WHERE project_id = $1 AND doc_id = $2 AND EXISTS ( + SELECT id + FROM projects + WHERE id = $1 AND user_id = $3 + ) + RETURNING id", + project_id, + doc_id, + user_id, + } + .fetch_optional(&*app.sql) + .await? + .map(|_| ()) + .ok_or_else(|| Error::not_found("project doc not found")) +} From eb26d9ff138f9a1546ec1740ae2f65a78ef83f36 Mon Sep 17 00:00:00 2001 From: calyptobai Date: Tue, 5 Dec 2023 14:13:52 -0500 Subject: [PATCH 12/88] WIP: Projects Add branches to `project_repo` associations --- .../migrations/20231122012638_projects.sql | 3 +- server/bleep/sqlx-data.json | 104 ++++++++++-------- server/bleep/src/agent.rs | 15 ++- server/bleep/src/webserver/project/repo.rs | 32 ++++-- 4 files changed, 94 insertions(+), 60 deletions(-) diff --git a/server/bleep/migrations/20231122012638_projects.sql b/server/bleep/migrations/20231122012638_projects.sql index 80a379ba94..8c95a34c34 100644 --- a/server/bleep/migrations/20231122012638_projects.sql +++ b/server/bleep/migrations/20231122012638_projects.sql @@ -13,5 +13,6 @@ ALTER TABLE conversations DROP COLUMN user_id; CREATE TABLE project_repos ( id INTEGER PRIMARY KEY, project_id INTEGER NOT NULL, - repo_ref TEXT NOT NULL + repo_ref TEXT NOT NULL, + branch TEXT ); diff --git a/server/bleep/sqlx-data.json b/server/bleep/sqlx-data.json index fe19ad5cb3..318ea44400 100644 --- a/server/bleep/sqlx-data.json +++ b/server/bleep/sqlx-data.json @@ -24,6 +24,16 @@ }, "query": "SELECT name, (\n SELECT ss.modified_at\n FROM studio_snapshots ss\n JOIN studios s ON s.project_id = $1 AND ss.studio_id = s.id\n ORDER BY ss.modified_at DESC\n LIMIT 1\n ) AS modified_at\n FROM projects\n WHERE id = $1 AND user_id = $2\n LIMIT 1" }, + "0411fe6b12497b63d08f6fb4d0dffba5d74895105b80a76c8300c8054ccabb2b": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 3 + } + }, + "query": "INSERT INTO project_repos (project_id, repo_ref, branch) VALUES ($1, $2, $3)" + }, "069c6404909c217e0b27e974480cce3f592a0d43ece6dec17fbcee37ce7a6ffa": { "describe": { "columns": [ @@ -186,6 +196,54 @@ }, "query": "SELECT context FROM studio_snapshots WHERE id = ?" }, + "147e29626a2c6adc98c563344b9ce31656bbaa7b024ee1ce64d8ad8e40494d8f": { + "describe": { + "columns": [ + { + "name": "repo_ref", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "branch", + "ordinal": 1, + "type_info": "Text" + } + ], + "nullable": [ + false, + true + ], + "parameters": { + "Right": 2 + } + }, + "query": "SELECT repo_ref, branch\n FROM project_repos\n WHERE project_id = $1 AND EXISTS (\n SELECT p.id\n FROM projects p\n WHERE p.id = $1 AND p.user_id = $2\n )" + }, + "16c994183e3bb07ba8d6029c16d712222ce107cd1976889059c01780cb351dcd": { + "describe": { + "columns": [ + { + "name": "repo_ref", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "branch", + "ordinal": 1, + "type_info": "Text" + } + ], + "nullable": [ + false, + true + ], + "parameters": { + "Right": 1 + } + }, + "query": "SELECT pr.repo_ref, pr.branch\n FROM project_repos pr\n INNER JOIN projects p ON p.id = pr.project_id AND p.user_id = ?" + }, "1e8cdc6e2b23d18be0a15b2b79cb7f834d78ad942b29b448be57c0fdfab81d40": { "describe": { "columns": [ @@ -1076,16 +1134,6 @@ }, "query": "UPDATE studio_snapshots SET context = ? WHERE id = ?" }, - "a72a44c5f6db62bc0ffc92ce0e06cdcdcadfb02ce33399a167bbf216e7a039ce": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Right": 2 - } - }, - "query": "INSERT INTO project_repos (project_id, repo_ref) VALUES ($1, $2)" - }, "a7d8d84f5c97f5223d048bc3cdef92580195491edb3fc4c1757886a47be880bf": { "describe": { "columns": [ @@ -1186,24 +1234,6 @@ }, "query": "UPDATE templates SET name = ? WHERE id = ?" }, - "b194d61bd132827ab30dd215cb91c3c3d70db79f576b93f22373ec1d7b4f6489": { - "describe": { - "columns": [ - { - "name": "repo_ref", - "ordinal": 0, - "type_info": "Text" - } - ], - "nullable": [ - false - ], - "parameters": { - "Right": 2 - } - }, - "query": "SELECT repo_ref\n FROM project_repos\n WHERE project_id = $1 AND EXISTS (\n SELECT p.id\n FROM projects p\n WHERE p.id = $1 AND p.user_id = $2\n )" - }, "b3ebaeec21c90aa9ebc59a808e03c661839d0a0eaa86ad2bf4251e895f8e0a03": { "describe": { "columns": [], @@ -1324,24 +1354,6 @@ }, "query": "UPDATE studio_snapshots SET messages = ? WHERE id = ?" }, - "dc6c684b6a31f9a64024b0fcec346517fd9696a4b398151cc3809164c17778db": { - "describe": { - "columns": [ - { - "name": "repo_ref", - "ordinal": 0, - "type_info": "Text" - } - ], - "nullable": [ - false - ], - "parameters": { - "Right": 1 - } - }, - "query": "SELECT pr.repo_ref\n FROM project_repos pr\n INNER JOIN projects p ON p.id = pr.project_id AND p.user_id = ?" - }, "dcb7f9427283203bce10fe7d618057ef3eab5f6af2277e7a1ac8ba050609894d": { "describe": { "columns": [ diff --git a/server/bleep/src/agent.rs b/server/bleep/src/agent.rs index b15fbfe27e..3c8c11994a 100644 --- a/server/bleep/src/agent.rs +++ b/server/bleep/src/agent.rs @@ -451,13 +451,12 @@ impl Agent { &'a self, query: &str, ) -> impl Iterator + 'a { - let branch = self.last_exchange().query.first_branch(); let langs = self.last_exchange().query.langs.iter().map(Deref::deref); let user_id = self.user.username().expect("didn't have user ID"); - let repos = sqlx::query! { - "SELECT pr.repo_ref + let (repos, branches): (Vec<_>, Vec<_>) = sqlx::query! { + "SELECT pr.repo_ref, pr.branch FROM project_repos pr INNER JOIN projects p ON p.id = pr.project_id AND p.user_id = ?", user_id, @@ -466,13 +465,19 @@ impl Agent { .await .expect("failed to fetch repo associations") .into_iter() - .filter_map(|r| RepoRef::from_str(&r.repo_ref).ok()); + .filter_map(|row| { + let repo_ref = RepoRef::from_str(&row.repo_ref).ok()?; + Some((repo_ref, row.branch)) + }) + .unzip(); + + let branch = branches.first().cloned().flatten(); debug!(?query, ?branch, %self.conversation.thread_id, "executing fuzzy search"); self.app .indexes .file - .fuzzy_path_match(repos, branch.as_deref(), query, langs, 50) + .fuzzy_path_match(repos.into_iter(), branch.as_deref(), query, langs, 50) .await } diff --git a/server/bleep/src/webserver/project/repo.rs b/server/bleep/src/webserver/project/repo.rs index fe9c9cbcfd..63cbe6dbeb 100644 --- a/server/bleep/src/webserver/project/repo.rs +++ b/server/bleep/src/webserver/project/repo.rs @@ -6,18 +6,25 @@ use crate::{ Application, }; +#[derive(serde::Serialize)] +pub struct ListItem { + // TODO: Can we remove this in favour of just the `repo_ref`? + repo: Repo, + branch: Option, +} + pub async fn list( app: Extension, user: Extension, Path(project_id): Path, -) -> webserver::Result>> { +) -> webserver::Result>> { let user_id = user .username() .ok_or_else(webserver::no_user_id)? .to_string(); - let repos = sqlx::query! { - "SELECT repo_ref + let list = sqlx::query! { + "SELECT repo_ref, branch FROM project_repos WHERE project_id = $1 AND EXISTS ( SELECT p.id @@ -30,18 +37,26 @@ pub async fn list( .fetch_all(&*app.sql) .await? .into_iter() - .filter_map(|row| row.repo_ref.parse().ok()) - .filter_map(|repo_ref| app.repo_pool.get(&repo_ref)) - .map(|entry| Repo::from((entry.key(), entry.get()))) + .filter_map(|row| { + let repo_ref = row.repo_ref.parse().ok()?; + let entry = app.repo_pool.get(&repo_ref)?; + let repo = Repo::from((entry.key(), entry.get())); + + Some(ListItem { + repo, + branch: row.branch, + }) + }) .collect(); - Ok(Json(repos)) + Ok(Json(list)) } #[derive(serde::Deserialize)] pub struct Add { #[serde(rename = "ref")] repo_ref: RepoRef, + branch: Option, } pub async fn add( @@ -67,9 +82,10 @@ pub async fn add( let repo_ref = params.repo_ref.to_string(); sqlx::query! { - "INSERT INTO project_repos (project_id, repo_ref) VALUES ($1, $2)", + "INSERT INTO project_repos (project_id, repo_ref, branch) VALUES ($1, $2, $3)", project_id, repo_ref, + params.branch, } .execute(&*app.sql) .await From 8088dba97d434f8a9d4d9e95de094e9d703895f5 Mon Sep 17 00:00:00 2001 From: calyptobai Date: Wed, 6 Dec 2023 17:02:23 -0500 Subject: [PATCH 13/88] WIP: Projects Add constraints and foreign keys on new project models --- server/bleep/migrations/20231122012638_projects.sql | 9 +++++---- server/bleep/migrations/20231201200442_project_docs.sql | 5 +++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/server/bleep/migrations/20231122012638_projects.sql b/server/bleep/migrations/20231122012638_projects.sql index 8c95a34c34..507590963b 100644 --- a/server/bleep/migrations/20231122012638_projects.sql +++ b/server/bleep/migrations/20231122012638_projects.sql @@ -4,15 +4,16 @@ CREATE TABLE projects ( name TEXT ); -ALTER TABLE studios ADD COLUMN project_id INTEGER NOT NULL; +ALTER TABLE studios ADD COLUMN project_id INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE; ALTER TABLE studios DROP COLUMN user_id; -ALTER TABLE conversations ADD COLUMN project_id INTEGER NOT NULL; +ALTER TABLE conversations ADD COLUMN project_id INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE; ALTER TABLE conversations DROP COLUMN repo_ref; ALTER TABLE conversations DROP COLUMN user_id; CREATE TABLE project_repos ( id INTEGER PRIMARY KEY, - project_id INTEGER NOT NULL, + project_id INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE, repo_ref TEXT NOT NULL, - branch TEXT + branch TEXT, + UNIQUE (project_id, repo_ref) ); diff --git a/server/bleep/migrations/20231201200442_project_docs.sql b/server/bleep/migrations/20231201200442_project_docs.sql index 54d1e6070a..22e440b2ff 100644 --- a/server/bleep/migrations/20231201200442_project_docs.sql +++ b/server/bleep/migrations/20231201200442_project_docs.sql @@ -1,5 +1,6 @@ CREATE TABLE project_docs ( id INTEGER PRIMARY KEY, - project_id INTEGER NOT NULL, - doc_id INTEGER NOT NULL + project_id INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE, + doc_id INTEGER NOT NULL, + UNIQUE (project_id, doc_id) ); From 92415c7147075f476e1f4ffc3218fafd6c7086fa Mon Sep 17 00:00:00 2001 From: calyptobai Date: Mon, 11 Dec 2023 18:20:48 -0500 Subject: [PATCH 14/88] WIP: Projects Amongst other patches, we introduce some API changes here. - We move `/q` to `/projects/:id/q`: - This no longer takes a `repo_ref` argument. Now, this route will infer the related repositories based on repos associated with the requested project. - We move `/search/path` to `/projects/:id/search/path` - We add a new `GET /folder` route, which is like `/file` but retrieves directory data. Internally, this route makes an `open:`-style query. --- server/bleep/src/indexes/file.rs | 23 +++++-- server/bleep/src/query/execute.rs | 15 +++-- server/bleep/src/webserver.rs | 6 +- server/bleep/src/webserver/file.rs | 90 ++++++++++++++++++++++++--- server/bleep/src/webserver/project.rs | 2 +- server/bleep/src/webserver/query.rs | 7 ++- server/bleep/src/webserver/search.rs | 22 +++++-- 7 files changed, 134 insertions(+), 31 deletions(-) diff --git a/server/bleep/src/indexes/file.rs b/server/bleep/src/indexes/file.rs index 2bba1c4341..d5d3592dea 100644 --- a/server/bleep/src/indexes/file.rs +++ b/server/bleep/src/indexes/file.rs @@ -366,7 +366,7 @@ impl Indexer { pub async fn skim_fuzzy_path_match( &self, - repo_ref: &RepoRef, + repo_refs: impl IntoIterator, query_str: &str, branch: Option<&str>, limit: usize, @@ -374,10 +374,21 @@ impl Indexer { let searcher = self.reader.searcher(); let file_source = &self.source; - let repo_ref_term = Box::new(TermQuery::new( - Term::from_field_text(self.source.repo_ref, &repo_ref.to_string()), - IndexRecordOption::Basic, - )); + let repo_ref_terms = { + let term_queries = repo_refs + .into_iter() + .map(|repo_ref| { + TermQuery::new( + Term::from_field_text(self.source.repo_ref, &repo_ref.to_string()), + IndexRecordOption::Basic, + ) + }) + .map(|q| Box::new(q) as Box) + .collect::>(); + + Box::new(BooleanQuery::union(term_queries)) + }; + let branch_term = branch .map(|b| { trigrams(b) @@ -397,7 +408,7 @@ impl Indexer { [ Some(Box::new(TermQuery::new(term, IndexRecordOption::Basic)) as Box), - Some(Box::clone(&repo_ref_term) as Box), + Some(Box::clone(&repo_ref_terms) as Box), branch_term .as_ref() .map(Box::clone) diff --git a/server/bleep/src/query/execute.rs b/server/bleep/src/query/execute.rs index 7676d36974..7693762b33 100644 --- a/server/bleep/src/query/execute.rs +++ b/server/bleep/src/query/execute.rs @@ -45,9 +45,14 @@ pub struct ApiQuery { /// A query written in the bloop query language pub q: String, - /// Optional RepoRef to constrain the search. If not provided, search all repos - #[serde(default)] - pub repo_ref: Option, + /// Project ID. + // NB: We implement methods directly on this struct, which need access to the project ID + // associated with this request. This doesn't fit our API; we obtain the project ID via the + // router and not via URL query parameters. The abstraction here likely needs to be reworked a + // bit, as this can be improved. For now, we just add a skipped field, and manually set it + // after deserialization. TODO: Fix this. + #[serde(skip)] + pub project_id: i64, #[serde(default)] pub page: usize, @@ -68,11 +73,11 @@ pub struct ApiQuery { /// The number of lines of context in the snippet before the search result #[serde(alias = "cb", default = "default_context")] - context_before: usize, + pub context_before: usize, /// The number of lines of context in the snippet after the search result #[serde(alias = "ca", default = "default_context")] - context_after: usize, + pub context_after: usize, } #[derive(Serialize)] diff --git a/server/bleep/src/webserver.rs b/server/bleep/src/webserver.rs index 8812c1255d..a0d11a6e0a 100644 --- a/server/bleep/src/webserver.rs +++ b/server/bleep/src/webserver.rs @@ -50,8 +50,6 @@ pub async fn start(app: Application) -> anyhow::Result<()> { let mut api = Router::new() .route("/config", get(config::get).put(config::put)) - // querying - .route("/q", get(query::handle)) // autocomplete .route("/autocomplete", get(autocomplete::handle)) // indexing @@ -85,8 +83,8 @@ pub async fn start(app: Application) -> anyhow::Result<()> { .route("/token-value", get(intelligence::token_value)) // misc .route("/search/code", get(search::semantic_code)) - .route("/search/path", get(search::fuzzy_path)) .route("/file", get(file::handle)) + .route("/folder", get(file::folder)) .route("/projects", get(project::list).post(project::create)) .route( "/projects/:project_id", @@ -118,6 +116,8 @@ pub async fn start(app: Application) -> anyhow::Result<()> { "/projects/:project_id/conversations/:conversation_id", get(conversation::get), ) + .route("/projects/:project_id/q", get(query::handle)) + .route("/projects/:project_id/search/path", get(search::fuzzy_path)) .route("/projects/:project_id/answer/vote", post(answer::vote)) .route("/projects/:project_id/answer", get(answer::answer)) .route("/projects/:project_id/answer/explain", get(answer::explain)) diff --git a/server/bleep/src/webserver/file.rs b/server/bleep/src/webserver/file.rs index 3b7c8805b8..6f9d554dde 100644 --- a/server/bleep/src/webserver/file.rs +++ b/server/bleep/src/webserver/file.rs @@ -2,13 +2,21 @@ use std::{path::PathBuf, sync::Arc}; use anyhow::Context; use axum::{extract::Query, Extension, Json}; +use tracing::warn; -use crate::repo::RepoRef; +use crate::{ + indexes::reader::OpenReader, + query::{ + execute::{ApiQuery, DirectoryData, ExecuteQuery, QueryResult}, + parser, + }, + repo::RepoRef, +}; use super::prelude::*; #[derive(Debug, serde::Deserialize)] -pub(super) struct Params { +pub(super) struct FileParams { pub repo_ref: RepoRef, pub path: PathBuf, pub branch: Option, @@ -29,7 +37,7 @@ pub(super) struct FileResponse { impl super::ApiResponse for FileResponse {} pub(super) async fn handle<'a>( - Query(params): Query, + Query(params): Query, Extension(indexes): Extension>, ) -> Result>, Error> { let doc = indexes @@ -49,7 +57,11 @@ pub(super) async fn handle<'a>( })) } -fn split_by_lines<'a>(text: &'a str, indices: &[u32], params: &Params) -> Result<&'a str, Error> { +fn split_by_lines<'a>( + text: &'a str, + indices: &[u32], + params: &FileParams, +) -> Result<&'a str, Error> { let char_start = match params.line_start { Some(1) => 0, Some(line_start) if line_start > 1 => { @@ -70,6 +82,66 @@ fn split_by_lines<'a>(text: &'a str, indices: &[u32], params: &Params) -> Result Ok(&text[char_start..=char_end]) } +#[derive(Debug, serde::Deserialize)] +pub(super) struct FolderParams { + pub repo_ref: RepoRef, + pub path: PathBuf, + pub branch: Option, +} + +pub(super) async fn folder( + Query(params): Query, + Extension(indexes): Extension>, +) -> Result, Error> { + let reader = OpenReader; + + let query = parser::Query { + open: Some(true), + repo: Some(parser::Literal::from(¶ms.repo_ref.indexed_name())), + path: Some(parser::Literal::from(params.path.to_string_lossy())), + branch: params.branch.map(|b| parser::Literal::from(&b)), + ..Default::default() + }; + + // NB: This argument is not actually used in `OpenReader::execute`. We have two options to + // simplify this: + // + // 1. Refactor the open reader in order to extract common logic so that we can re-use it here + // 2. Remove the open reader entirely, replacing it with this route and the `/file` route + // + // Until we decide what to do here, we continue by just creating a dummy parameter. + let api_query = ApiQuery { + q: String::new(), + project_id: 0, + page: 0, + page_size: 0, + calculate_totals: false, + context_before: 0, + context_after: 0, + }; + + let mut results = reader + .execute(&indexes.file, &[query], &api_query) + .await + .context("failed to execute open query")? + .data + .into_iter() + .filter_map(|qr| match qr { + QueryResult::Directory(d) => Some(d), + _ => None, + }); + + let output = results + .next() + .context("`open:` query returned no results")?; + + if results.next().is_some() { + warn!("`open:` query returned multiple results, ignoring all but first"); + } + + Ok(Json(output)) +} + #[cfg(test)] mod tests { use super::*; @@ -92,7 +164,7 @@ cccccc split_by_lines( text, &indices, - &Params { + &FileParams { repo_ref: "local//repo".into(), path: "file".into(), line_start: None, @@ -108,7 +180,7 @@ cccccc split_by_lines( text, &indices, - &Params { + &FileParams { repo_ref: "local//repo".into(), path: "file".into(), line_start: Some(1), @@ -124,7 +196,7 @@ cccccc split_by_lines( text, &indices, - &Params { + &FileParams { repo_ref: "local//repo".into(), path: "file".into(), line_start: Some(2), @@ -140,7 +212,7 @@ cccccc split_by_lines( text, &indices, - &Params { + &FileParams { repo_ref: "local//repo".into(), path: "file".into(), line_start: Some(3), @@ -156,7 +228,7 @@ cccccc split_by_lines( text, &indices, - &Params { + &FileParams { repo_ref: "local//repo".into(), path: "file".into(), line_start: Some(2), diff --git a/server/bleep/src/webserver/project.rs b/server/bleep/src/webserver/project.rs index 8d17f1d6cf..382c4291ef 100644 --- a/server/bleep/src/webserver/project.rs +++ b/server/bleep/src/webserver/project.rs @@ -10,8 +10,8 @@ use futures::TryStreamExt; use super::{middleware::User, repos::Repo, Error}; -pub mod repo; pub mod doc; +pub mod repo; fn default_name() -> String { "New Project".into() diff --git a/server/bleep/src/webserver/query.rs b/server/bleep/src/webserver/query.rs index 1fce5846a8..ebd0105626 100644 --- a/server/bleep/src/webserver/query.rs +++ b/server/bleep/src/webserver/query.rs @@ -1,15 +1,18 @@ -use axum::extract::State; +use axum::extract::{Path, State}; use super::prelude::*; use crate::{db::QueryLog, query::execute::ApiQuery, Application}; pub(super) async fn handle( - Query(api_params): Query, + Path(project_id): Path, + Query(mut api_params): Query, Extension(indexes): Extension>, State(app): State, ) -> impl IntoResponse { QueryLog::new(&app.sql).insert(&api_params.q).await?; + api_params.project_id = project_id; + Arc::new(api_params) .query(indexes) .await diff --git a/server/bleep/src/webserver/search.rs b/server/bleep/src/webserver/search.rs index d49a2b37c8..25b943c45a 100644 --- a/server/bleep/src/webserver/search.rs +++ b/server/bleep/src/webserver/search.rs @@ -7,7 +7,9 @@ use crate::{ parser::{self}, }, semantic::{self, Semantic}, + Application, }; +use axum::extract::Path; use tracing::error; pub(super) async fn semantic_code( @@ -26,8 +28,11 @@ pub(super) async fn semantic_code( } } +#[axum::debug_handler] pub(super) async fn fuzzy_path( + Path(project_id): Path, Query(args): Query, + Extension(app): Extension, Extension(indexes): Extension>, ) -> Result { let q = parser::parse_nl(&args.q).map_err(|err| { @@ -41,15 +46,22 @@ pub(super) async fn fuzzy_path( Error::new(ErrorKind::UpstreamService, "Query has no target") })?; - let repo_ref = args.repo_ref.as_ref().ok_or_else(|| { - error!("No repo_ref provided"); - Error::new(ErrorKind::UpstreamService, "No repo_ref provided") - })?; + let repo_refs = sqlx::query! { + "SELECT repo_ref + FROM project_repos + WHERE project_id = ?", + project_id, + } + .fetch_all(&*app.sql) + .await? + .into_iter() + .map(|row| row.repo_ref) + .filter_map(|rr| rr.parse().ok()); let data = indexes .file .skim_fuzzy_path_match( - repo_ref, + repo_refs, target, q.first_branch().as_deref(), args.page_size, From 76dac267723033a7372b817199869a1fb8759dfc Mon Sep 17 00:00:00 2001 From: Gabriel Gordon-Hall Date: Wed, 13 Dec 2023 16:27:30 +0000 Subject: [PATCH 15/88] fix repo tantivy search (#1174) --- server/bleep/src/query/execute.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/bleep/src/query/execute.rs b/server/bleep/src/query/execute.rs index 7693762b33..5e4739db90 100644 --- a/server/bleep/src/query/execute.rs +++ b/server/bleep/src/query/execute.rs @@ -548,7 +548,7 @@ impl ExecuteQuery for RepoReader { .iter() .filter(|q| self.query_matches(q)) .filter_map(|q| { - let regex_str = q.path.as_ref()?.regex_str(); + let regex_str = q.repo.as_ref()?.regex_str(); let case_insensitive = !q.is_case_sensitive(); let regex = RegexBuilder::new(®ex_str) .case_insensitive(case_insensitive) From f151485561a60dcba8aeaa81cd7430787e0e3a42 Mon Sep 17 00:00:00 2001 From: calyptobai Date: Wed, 13 Dec 2023 12:59:41 -0500 Subject: [PATCH 16/88] Fix conversation store/load --- server/bleep/sqlx-data.json | 28 ++++++++-------------- server/bleep/src/webserver/answer.rs | 4 ++-- server/bleep/src/webserver/conversation.rs | 21 +++++++++------- 3 files changed, 24 insertions(+), 29 deletions(-) diff --git a/server/bleep/sqlx-data.json b/server/bleep/sqlx-data.json index 318ea44400..8f51861dcd 100644 --- a/server/bleep/sqlx-data.json +++ b/server/bleep/sqlx-data.json @@ -244,24 +244,6 @@ }, "query": "SELECT pr.repo_ref, pr.branch\n FROM project_repos pr\n INNER JOIN projects p ON p.id = pr.project_id AND p.user_id = ?" }, - "1e8cdc6e2b23d18be0a15b2b79cb7f834d78ad942b29b448be57c0fdfab81d40": { - "describe": { - "columns": [ - { - "name": "project_id", - "ordinal": 0, - "type_info": "Int64" - } - ], - "nullable": [ - true - ], - "parameters": { - "Right": 2 - } - }, - "query": "DELETE FROM conversations\n WHERE thread_id = ? AND EXISTS (\n SELECT p.id\n FROM projects p\n WHERE p.id = project_id AND p.user_id = ?\n )\n RETURNING project_id" - }, "26065ed9dd0dfa42b8b943726d85425d0b45b2cafcceb9887ea626040bef9264": { "describe": { "columns": [ @@ -1426,6 +1408,16 @@ }, "query": "\n SELECT id, name, url, description, favicon, modified_at, index_status\n FROM docs \n WHERE name LIKE $1 OR description LIKE $1 OR url LIKE $1\n LIMIT ?\n " }, + "e9757c76de272638fb9738bf229d0b91285ae4cd9824ca6a16175ccdc4373efb": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 2 + } + }, + "query": "DELETE FROM conversations\n WHERE thread_id = ? AND EXISTS (\n SELECT p.id\n FROM projects p\n WHERE p.id = project_id AND p.user_id = ?\n )" + }, "ebdb202965998c4e29727dca19a60c82b2a4574530858e29bf6584c8b6e1f32b": { "describe": { "columns": [ diff --git a/server/bleep/src/webserver/answer.rs b/server/bleep/src/webserver/answer.rs index 1154ef2751..2002c97cdd 100644 --- a/server/bleep/src/webserver/answer.rs +++ b/server/bleep/src/webserver/answer.rs @@ -103,7 +103,7 @@ pub(super) async fn answer( Some(conversation_id) => { Conversation::load(&app.sql, user_id, project_id, conversation_id).await? } - None => Conversation::new(), + None => Conversation::new(project_id), }; let Answer { @@ -449,7 +449,7 @@ pub async fn explain( end_byte: None, }); - let mut conversation = Conversation::new(); + let mut conversation = Conversation::new(project_id); conversation.exchanges.push(exchange); let action = Action::Answer { paths: vec![0] }; diff --git a/server/bleep/src/webserver/conversation.rs b/server/bleep/src/webserver/conversation.rs index e79e7e994a..1947d98061 100644 --- a/server/bleep/src/webserver/conversation.rs +++ b/server/bleep/src/webserver/conversation.rs @@ -22,13 +22,15 @@ use crate::{ pub struct Conversation { pub exchanges: Vec, pub thread_id: Uuid, + pub project_id: i64, } impl Conversation { - pub fn new() -> Self { + pub fn new(project_id: i64) -> Self { Self { exchanges: Vec::new(), thread_id: Uuid::new_v4(), + project_id, } } @@ -36,20 +38,18 @@ impl Conversation { let mut transaction = db.begin().await?; // Delete the old conversation for simplicity. This also deletes all its messages. - let project_id = sqlx::query! { + sqlx::query! { "DELETE FROM conversations WHERE thread_id = ? AND EXISTS ( SELECT p.id FROM projects p WHERE p.id = project_id AND p.user_id = ? - ) - RETURNING project_id", + )", self.thread_id, user_id, } - .fetch_one(&mut transaction) - .await - .map(|r| r.project_id)?; + .execute(&mut transaction) + .await?; let title = self .exchanges @@ -59,15 +59,17 @@ impl Conversation { .context("couldn't find conversation title")?; let exchanges = serde_json::to_string(&self.exchanges)?; + let thread_id = self.thread_id.to_string(); + sqlx::query! { "INSERT INTO conversations ( thread_id, title, exchanges, project_id, created_at ) VALUES (?, ?, ?, ?, strftime('%s', 'now'))", - self.thread_id, + thread_id, title, exchanges, - project_id, + self.project_id, } .execute(&mut transaction) .await?; @@ -101,6 +103,7 @@ impl Conversation { Ok(Self { exchanges, thread_id: row.thread_id.parse().map_err(Error::internal)?, + project_id, }) } } From 997db175661bf7cdad69acd093e828b054e9f68b Mon Sep 17 00:00:00 2001 From: calyptobai Date: Wed, 13 Dec 2023 12:59:41 -0500 Subject: [PATCH 17/88] Add `PUT /projects/:id/repos`, change `DELETE /projects/:id/repos/:id` We add `PUT /projects/:id/repos`, which accepts an object: ``` { "ref": repo ref, "branch": branch name or NULL } ``` Additionally, `DELETE /projects/:id/repos/:repo_ref` was changed to just `DELETE /projects/:id/repos/:id`, where the `repo_ref` value was moved to a JSON object in the request body: ``` { "ref": repo ref } ``` --- server/bleep/sqlx-data.json | 18 ++++++++ server/bleep/src/webserver.rs | 4 +- server/bleep/src/webserver/project/repo.rs | 51 +++++++++++++++++++++- 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/server/bleep/sqlx-data.json b/server/bleep/sqlx-data.json index 8f51861dcd..47d645219b 100644 --- a/server/bleep/sqlx-data.json +++ b/server/bleep/sqlx-data.json @@ -1408,6 +1408,24 @@ }, "query": "\n SELECT id, name, url, description, favicon, modified_at, index_status\n FROM docs \n WHERE name LIKE $1 OR description LIKE $1 OR url LIKE $1\n LIMIT ?\n " }, + "e352cd10053f43e1e586da8a933868b5329fb1468292769587f130d7bc8f6ca3": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + } + ], + "nullable": [ + true + ], + "parameters": { + "Right": 3 + } + }, + "query": "UPDATE project_repos\n SET branch = ?\n WHERE project_id = ? AND repo_ref = ?\n RETURNING id" + }, "e9757c76de272638fb9738bf229d0b91285ae4cd9824ca6a16175ccdc4373efb": { "describe": { "columns": [], diff --git a/server/bleep/src/webserver.rs b/server/bleep/src/webserver.rs index a0d11a6e0a..22adcd057f 100644 --- a/server/bleep/src/webserver.rs +++ b/server/bleep/src/webserver.rs @@ -97,8 +97,8 @@ pub async fn start(app: Application) -> anyhow::Result<()> { get(project::repo::list).post(project::repo::add), ) .route( - "/projects/:project_id/repos/:repo_ref", - delete(project::repo::delete), + "/projects/:project_id/repos/", + delete(project::repo::delete).put(project::repo::put), ) .route( "/projects/:project_id/docs", diff --git a/server/bleep/src/webserver/project/repo.rs b/server/bleep/src/webserver/project/repo.rs index 63cbe6dbeb..eae3d22037 100644 --- a/server/bleep/src/webserver/project/repo.rs +++ b/server/bleep/src/webserver/project/repo.rs @@ -93,10 +93,17 @@ pub async fn add( .map_err(Error::internal) } +#[derive(serde::Deserialize)] +pub struct Delete { + #[serde(rename = "ref")] + repo_ref: String, +} + pub async fn delete( app: Extension, user: Extension, - Path((project_id, repo_ref)): Path<(i64, String)>, + Path(project_id): Path, + Json(Delete { repo_ref }): Json, ) -> webserver::Result<()> { let user_id = user .username() @@ -120,3 +127,45 @@ pub async fn delete( .map(|_| ()) .ok_or_else(|| Error::not_found("project repo not found")) } + +#[derive(serde::Deserialize)] +pub struct Put { + #[serde(rename = "ref")] + repo_ref: String, + branch: Option, +} + +pub async fn put( + app: Extension, + user: Extension, + Path(project_id): Path, + Json(params): Json, +) -> webserver::Result<()> { + let user_id = user + .username() + .ok_or_else(webserver::no_user_id)? + .to_string(); + + sqlx::query! { + "SELECT id FROM projects WHERE id = ? AND user_id = ?", + project_id, + user_id, + } + .fetch_optional(&*app.sql) + .await? + .ok_or_else(|| Error::not_found("project not found"))?; + + sqlx::query! { + "UPDATE project_repos + SET branch = ? + WHERE project_id = ? AND repo_ref = ? + RETURNING id", + params.branch, + project_id, + params.repo_ref, + } + .fetch_optional(&*app.sql) + .await? + .map(|_| ()) + .ok_or_else(|| Error::not_found("association between project ID and repo ref not found")) +} From 924f778d6987a6f4daef4b32a16e83042edbe7a6 Mon Sep 17 00:00:00 2001 From: calyptobai Date: Wed, 13 Dec 2023 16:01:25 -0500 Subject: [PATCH 18/88] Avoid JSON body in `DELETE /projects/:id/repos` Now, we use a query parameter to indicate the repo ref: `?ref=...` --- server/bleep/src/webserver/project/repo.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/server/bleep/src/webserver/project/repo.rs b/server/bleep/src/webserver/project/repo.rs index eae3d22037..9e88b97f7f 100644 --- a/server/bleep/src/webserver/project/repo.rs +++ b/server/bleep/src/webserver/project/repo.rs @@ -1,4 +1,7 @@ -use axum::{extract::Path, Extension, Json}; +use axum::{ + extract::{Path, Query}, + Extension, Json, +}; use crate::{ repo::RepoRef, @@ -103,7 +106,7 @@ pub async fn delete( app: Extension, user: Extension, Path(project_id): Path, - Json(Delete { repo_ref }): Json, + Query(Delete { repo_ref }): Query, ) -> webserver::Result<()> { let user_id = user .username() From 1b3d880478a1d85d49446e458039feced8f2e267 Mon Sep 17 00:00:00 2001 From: calyptobai Date: Wed, 13 Dec 2023 18:23:09 -0500 Subject: [PATCH 19/88] Filter repos in semantic search by query repos, if present --- server/bleep/src/agent.rs | 19 +++++++++++++++---- server/bleep/src/agent/tools/code.rs | 6 +++--- server/bleep/src/agent/tools/path.rs | 2 +- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/server/bleep/src/agent.rs b/server/bleep/src/agent.rs index 3c8c11994a..9d1abbe5c9 100644 --- a/server/bleep/src/agent.rs +++ b/server/bleep/src/agent.rs @@ -13,10 +13,7 @@ use crate::{ query::{parser, stopwords::remove_stopwords}, repo::RepoRef, semantic::{self, SemanticSearchParams}, - webserver::{ - conversation::{self, Conversation, ConversationId}, - middleware::User, - }, + webserver::{conversation::Conversation, middleware::User}, Application, }; @@ -204,6 +201,20 @@ impl Agent { } } + fn relevant_repos(&self) -> Vec { + let query_repos = self.last_exchange().query.repos().collect::>(); + + if query_repos.is_empty() { + self.repo_refs.clone() + } else { + self.repo_refs + .iter() + .filter(|r| query_repos.contains(&r.indexed_name().into())) + .cloned() + .collect() + } + } + pub async fn step(&mut self, action: Action) -> Result> { info!(?action, %self.conversation.thread_id, "executing next action"); diff --git a/server/bleep/src/agent/tools/code.rs b/server/bleep/src/agent/tools/code.rs index d45296c23a..f40fe1d6d4 100644 --- a/server/bleep/src/agent/tools/code.rs +++ b/server/bleep/src/agent/tools/code.rs @@ -22,13 +22,13 @@ impl Agent { })) .await?; - let repos = self.repo_refs.clone(); + let relevant_repos = self.relevant_repos(); let mut results = self .semantic_search(AgentSemanticSearchParams { query: Literal::from(&query.to_string()), paths: vec![], - repos: repos.clone(), + repos: relevant_repos.clone(), semantic_params: SemanticSearchParams { limit: CODE_SEARCH_LIMIT, offset: 0, @@ -50,7 +50,7 @@ impl Agent { .semantic_search(AgentSemanticSearchParams { query: hyde_doc, paths: vec![], - repos, + repos: relevant_repos, semantic_params: SemanticSearchParams { limit: CODE_SEARCH_LIMIT, offset: 0, diff --git a/server/bleep/src/agent/tools/path.rs b/server/bleep/src/agent/tools/path.rs index 9650d87986..c1434cc330 100644 --- a/server/bleep/src/agent/tools/path.rs +++ b/server/bleep/src/agent/tools/path.rs @@ -41,7 +41,7 @@ impl Agent { .semantic_search(AgentSemanticSearchParams { query: query.into(), paths: vec![], - repos: self.repo_refs.clone(), + repos: self.relevant_repos(), semantic_params: SemanticSearchParams { limit: 30, offset: 0, From 8c0b20d12293405ecabb160874f3a00806a5679a Mon Sep 17 00:00:00 2001 From: calyptobai Date: Thu, 14 Dec 2023 17:42:17 -0500 Subject: [PATCH 20/88] Sync docs in background, whether stream still exists --- server/bleep/src/webserver/docs.rs | 33 +++++++++++++++++++----------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/server/bleep/src/webserver/docs.rs b/server/bleep/src/webserver/docs.rs index 1d2ce552fb..e1fa50ed7e 100644 --- a/server/bleep/src/webserver/docs.rs +++ b/server/bleep/src/webserver/docs.rs @@ -6,6 +6,7 @@ use axum::{ }, }; use futures::stream::{Stream, StreamExt}; +use tokio_stream::wrappers::ReceiverStream; use tracing::error; use crate::{ @@ -71,18 +72,26 @@ pub async fn sync( DocEvent::new("sync").with_payload("url", ¶ms.url), ) }); - Sse::new(Box::pin( - app.indexes - .doc - .clone() - .sync(params.url) - .await - .map(|result| { - Ok(Event::default() - .json_data(result.as_ref().map_err(ToString::to_string)) - .unwrap()) - }), - )) + + let (tx, rx) = tokio::sync::mpsc::channel(100); + + tokio::spawn(async move { + let stream = app.indexes.doc.clone().sync(params.url).await; + + futures::pin_mut!(stream); + + while let Some(t) = stream.next().await { + // We intentionally ignore errors so that this stream is consumed in the background, + // regardless of whether the receiver still exists. + let _ = tx.send(t); + } + }); + + Sse::new(Box::pin(ReceiverStream::new(rx).map(|result| { + Ok(Event::default() + .json_data(result.as_ref().map_err(ToString::to_string)) + .unwrap()) + }))) .keep_alive(KeepAlive::default()) } From ae8bbee7fc7a476a8f8286e54f76ab5c319ff4af Mon Sep 17 00:00:00 2001 From: calyptobai Date: Thu, 14 Dec 2023 17:42:17 -0500 Subject: [PATCH 21/88] Return thread_id in conversation routes - `GET /projects/:id/conversations/:id` now returns an object like: ``` { thread_id: string, exchanges: [...] } ``` Note that previously, this just returned the list of exchanges. - `GET /projects/:id/conversations` now returns an additional field in each item: ``` { thread_id: string, ...previous fields } ``` --- server/bleep/sqlx-data.json | 66 ++++++++++++---------- server/bleep/src/webserver/answer.rs | 2 - server/bleep/src/webserver/conversation.rs | 37 ++++++------ 3 files changed, 54 insertions(+), 51 deletions(-) diff --git a/server/bleep/sqlx-data.json b/server/bleep/sqlx-data.json index 47d645219b..f842366189 100644 --- a/server/bleep/sqlx-data.json +++ b/server/bleep/sqlx-data.json @@ -106,36 +106,6 @@ }, "query": "SELECT context, messages FROM studio_snapshots WHERE id = ?" }, - "08e9cd261503f6d2d286d60b97036cae69d58b12c96897f341f4e5eb3dc68dc9": { - "describe": { - "columns": [ - { - "name": "id!", - "ordinal": 0, - "type_info": "Int64" - }, - { - "name": "created_at", - "ordinal": 1, - "type_info": "Int64" - }, - { - "name": "title", - "ordinal": 2, - "type_info": "Text" - } - ], - "nullable": [ - true, - false, - false - ], - "parameters": { - "Right": 2 - } - }, - "query": "SELECT c.id as 'id!', c.created_at, c.title FROM conversations c JOIN projects p ON p.id = c.project_id AND p.user_id = ? WHERE p.id = ?\n ORDER BY c.created_at DESC" - }, "0b7aa6a8c243ac8ca79f52a5ef370ccce954b3635ce5e70a6e80732472a916f2": { "describe": { "columns": [ @@ -482,6 +452,42 @@ }, "query": "SELECT cache_hash FROM file_cache WHERE repo_ref = ?" }, + "4a7ef503bb663992c6bf6567324e9bacbca61e9c03e23ba24a2773d7efa68043": { + "describe": { + "columns": [ + { + "name": "id!", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "thread_id", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "created_at", + "ordinal": 2, + "type_info": "Int64" + }, + { + "name": "title", + "ordinal": 3, + "type_info": "Text" + } + ], + "nullable": [ + true, + false, + false, + false + ], + "parameters": { + "Right": 2 + } + }, + "query": "SELECT c.id as 'id!', c.thread_id, c.created_at, c.title FROM conversations c JOIN projects p ON p.id = c.project_id AND p.user_id = ? WHERE p.id = ?\n ORDER BY c.created_at DESC" + }, "4aacc9795c1a466afdc7c5cedcdf1a6b67652a7ddcdee0399e67024e087d75a9": { "describe": { "columns": [], diff --git a/server/bleep/src/webserver/answer.rs b/server/bleep/src/webserver/answer.rs index 2002c97cdd..6f330b1c2d 100644 --- a/server/bleep/src/webserver/answer.rs +++ b/server/bleep/src/webserver/answer.rs @@ -14,8 +14,6 @@ use reqwest::StatusCode; use serde_json::json; use tracing::{debug, error, info, warn}; -use super::conversation::ConversationId; - use super::middleware::User; use crate::{ agent::{ diff --git a/server/bleep/src/webserver/conversation.rs b/server/bleep/src/webserver/conversation.rs index 1947d98061..02ba8a0fe3 100644 --- a/server/bleep/src/webserver/conversation.rs +++ b/server/bleep/src/webserver/conversation.rs @@ -6,7 +6,7 @@ use axum::{ }; use chrono::NaiveDateTime; use reqwest::StatusCode; -use std::fmt; +use std::{fmt, mem}; use tracing::info; use uuid::Uuid; @@ -18,10 +18,11 @@ use crate::{ Application, }; -#[derive(Clone)] +#[derive(Clone, serde::Serialize)] pub struct Conversation { pub exchanges: Vec, pub thread_id: Uuid, + #[serde(skip)] pub project_id: i64, } @@ -116,8 +117,9 @@ pub struct ConversationId { } #[derive(serde::Serialize)] -pub struct ConversationPreview { +pub struct ListItem { pub id: i64, + pub thread_id: String, pub created_at: i64, pub title: String, } @@ -126,15 +128,15 @@ pub(in crate::webserver) async fn list( Extension(user): Extension, State(app): State, Path(project_id): Path, -) -> webserver::Result { +) -> webserver::Result>> { let db = app.sql.as_ref(); let user_id = user .username() .ok_or_else(|| Error::user("missing user ID"))?; - let conversations = sqlx::query_as! { - ConversationPreview, - "SELECT c.id as 'id!', c.created_at, c.title \ + sqlx::query_as! { + ListItem, + "SELECT c.id as 'id!', c.thread_id, c.created_at, c.title \ FROM conversations c \ JOIN projects p ON p.id = c.project_id AND p.user_id = ? \ WHERE p.id = ? @@ -144,9 +146,8 @@ pub(in crate::webserver) async fn list( } .fetch_all(db) .await - .map_err(Error::internal)?; - - Ok(Json(conversations)) + .map(Json) + .map_err(Error::internal) } pub(in crate::webserver) async fn delete( @@ -186,17 +187,15 @@ pub(in crate::webserver) async fn get( Extension(user): Extension, Path((project_id, conversation_id)): Path<(i64, i64)>, State(app): State, -) -> webserver::Result { +) -> webserver::Result> { let user_id = user.username().ok_or_else(super::no_user_id)?; - let exchanges = Conversation::load(&app.sql, user_id, project_id, conversation_id) - .await? - .exchanges; + let mut conversation = + Conversation::load(&app.sql, user_id, project_id, conversation_id).await?; - let exchanges = exchanges - .into_iter() - .map(|ex| ex.compressed()) - .collect::>(); + for ex in &mut conversation.exchanges { + *ex = mem::take(ex).compressed(); + } - Ok(Json(exchanges)) + Ok(Json(conversation)) } From 10e15072b11cffc024cb25fd99e0aef405272810 Mon Sep 17 00:00:00 2001 From: calyptobai Date: Thu, 14 Dec 2023 17:42:17 -0500 Subject: [PATCH 22/88] Fix handling of `conversation_id` with `/answer` --- server/bleep/sqlx-data.json | 66 +++++++++++++++------- server/bleep/src/webserver/answer.rs | 13 ++++- server/bleep/src/webserver/conversation.rs | 64 ++++++++++++++------- 3 files changed, 101 insertions(+), 42 deletions(-) diff --git a/server/bleep/sqlx-data.json b/server/bleep/sqlx-data.json index f842366189..b8673de415 100644 --- a/server/bleep/sqlx-data.json +++ b/server/bleep/sqlx-data.json @@ -106,6 +106,16 @@ }, "query": "SELECT context, messages FROM studio_snapshots WHERE id = ?" }, + "0905057375a1d628fd57d3cc8ac5f9664711df0fcc886beb7ae14f661d121dfe": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 5 + } + }, + "query": "INSERT INTO conversations (\n id, thread_id, title, exchanges, project_id, created_at\n )\n VALUES (?, ?, ?, ?, ?, strftime('%s', 'now'))" + }, "0b7aa6a8c243ac8ca79f52a5ef370ccce954b3635ce5e70a6e80732472a916f2": { "describe": { "columns": [ @@ -518,6 +528,24 @@ }, "query": "INSERT INTO query_log (raw_query) VALUES (?)" }, + "4f8ac5ae1005cb9f30a211b05dbb803586cb3cc8991687b9e1f8fcbed308ff29": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + } + ], + "nullable": [ + false + ], + "parameters": { + "Right": 4 + } + }, + "query": "INSERT INTO conversations (\n thread_id, title, exchanges, project_id, created_at\n )\n VALUES (?, ?, ?, ?, strftime('%s', 'now'))\n RETURNING id" + }, "5128142bf657cfde043a1b53834d40980caa3e9ae5fd6f4d7f30d89be512f105": { "describe": { "columns": [], @@ -988,16 +1016,6 @@ }, "query": "DELETE FROM project_docs\n WHERE project_id = $1 AND doc_id = $2 AND EXISTS (\n SELECT id\n FROM projects\n WHERE id = $1 AND user_id = $3\n )\n RETURNING id" }, - "834600ffd689d32c250771a3118d24fa93e71a6690b14703fe0b7feda05d53b6": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Right": 4 - } - }, - "query": "INSERT INTO conversations (\n thread_id, title, exchanges, project_id, created_at\n )\n VALUES (?, ?, ?, ?, strftime('%s', 'now'))" - }, "881aa78dfa3cd1bc3aa7a6edb8281aec5a972c1f53607d25c4e1f6d03cd3faef": { "describe": { "columns": [], @@ -1232,6 +1250,24 @@ }, "query": "INSERT INTO chunk_cache (chunk_hash, file_hash, branches, repo_ref) VALUES (?, ?, ?, ?)" }, + "bdc5bead3461cb22ac8882b2e01fca9c3aa15db524a9e8bd897cd2b5d5769c55": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + } + ], + "nullable": [ + true + ], + "parameters": { + "Right": 2 + } + }, + "query": "DELETE FROM conversations\n WHERE thread_id = ? AND EXISTS (\n SELECT p.id\n FROM projects p\n WHERE p.id = project_id AND p.user_id = ?\n )\n RETURNING id" + }, "c1b0276926023df7e91a219e7720b0f5ade8d1b13de577dce4d78d31075a497b": { "describe": { "columns": [ @@ -1432,16 +1468,6 @@ }, "query": "UPDATE project_repos\n SET branch = ?\n WHERE project_id = ? AND repo_ref = ?\n RETURNING id" }, - "e9757c76de272638fb9738bf229d0b91285ae4cd9824ca6a16175ccdc4373efb": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Right": 2 - } - }, - "query": "DELETE FROM conversations\n WHERE thread_id = ? AND EXISTS (\n SELECT p.id\n FROM projects p\n WHERE p.id = project_id AND p.user_id = ?\n )" - }, "ebdb202965998c4e29727dca19a60c82b2a4574530858e29bf6584c8b6e1f32b": { "describe": { "columns": [ diff --git a/server/bleep/src/webserver/answer.rs b/server/bleep/src/webserver/answer.rs index 6f330b1c2d..9f09280a6a 100644 --- a/server/bleep/src/webserver/answer.rs +++ b/server/bleep/src/webserver/answer.rs @@ -14,6 +14,8 @@ use reqwest::StatusCode; use serde_json::json; use tracing::{debug, error, info, warn}; +use super::conversation::ConversationId; + use super::middleware::User; use crate::{ agent::{ @@ -104,6 +106,8 @@ pub(super) async fn answer( None => Conversation::new(project_id), }; + let conversation_id = conversation.store(&app.sql, user_id).await?; + let Answer { parent_exchange_id, q, @@ -147,6 +151,7 @@ pub(super) async fn answer( query_id, project_id, conversation, + conversation_id, action, } .execute() @@ -161,6 +166,7 @@ struct AgentExecutor { query_id: uuid::Uuid, project_id: i64, conversation: Conversation, + conversation_id: i64, action: Action, } @@ -250,7 +256,8 @@ impl AgentExecutor { let initial_message = json!({ "thread_id": self.conversation.thread_id.to_string(), - "query_id": self.query_id + "query_id": self.query_id, + "conversation_id": self.conversation_id, }); // let project: Project = serde_json::from_str(&self.params.project).unwrap(); @@ -450,6 +457,9 @@ pub async fn explain( let mut conversation = Conversation::new(project_id); conversation.exchanges.push(exchange); + let user_id = user.username().ok_or_else(super::no_user_id)?; + let conversation_id = conversation.store(&app.sql, user_id).await?; + let action = Action::Answer { paths: vec![0] }; AgentExecutor { @@ -459,6 +469,7 @@ pub async fn explain( query_id, project_id, conversation, + conversation_id, action, } .execute() diff --git a/server/bleep/src/webserver/conversation.rs b/server/bleep/src/webserver/conversation.rs index 02ba8a0fe3..f13d7e2023 100644 --- a/server/bleep/src/webserver/conversation.rs +++ b/server/bleep/src/webserver/conversation.rs @@ -35,49 +35,71 @@ impl Conversation { } } - pub async fn store(&self, db: &SqlDb, user_id: &str) -> Result<()> { + pub async fn store(&self, db: &SqlDb, user_id: &str) -> Result { let mut transaction = db.begin().await?; + let thread_id = self.thread_id.to_string(); + // Delete the old conversation for simplicity. This also deletes all its messages. - sqlx::query! { + let id = sqlx::query! { "DELETE FROM conversations WHERE thread_id = ? AND EXISTS ( SELECT p.id FROM projects p WHERE p.id = project_id AND p.user_id = ? - )", - self.thread_id, + ) + RETURNING id", + thread_id, user_id, } - .execute(&mut transaction) - .await?; + .fetch_optional(&mut transaction) + .await? + .map(|row| row.id.unwrap()); let title = self .exchanges .first() .and_then(|list| list.query()) .and_then(|q| q.split('\n').next().map(|s| s.to_string())) - .context("couldn't find conversation title")?; + .unwrap_or_else(|| "New Conversation".to_owned()); let exchanges = serde_json::to_string(&self.exchanges)?; - let thread_id = self.thread_id.to_string(); - sqlx::query! { - "INSERT INTO conversations ( - thread_id, title, exchanges, project_id, created_at - ) - VALUES (?, ?, ?, ?, strftime('%s', 'now'))", - thread_id, - title, - exchanges, - self.project_id, - } - .execute(&mut transaction) - .await?; + let id = if let Some(id) = id { + sqlx::query! { + "INSERT INTO conversations ( + id, thread_id, title, exchanges, project_id, created_at + ) + VALUES (?, ?, ?, ?, ?, strftime('%s', 'now'))", + id, + thread_id, + title, + exchanges, + self.project_id, + } + .execute(&mut transaction) + .await?; + id + } else { + sqlx::query! { + "INSERT INTO conversations ( + thread_id, title, exchanges, project_id, created_at + ) + VALUES (?, ?, ?, ?, strftime('%s', 'now')) + RETURNING id", + thread_id, + title, + exchanges, + self.project_id, + } + .fetch_one(&mut transaction) + .await? + .id + }; transaction.commit().await?; - Ok(()) + Ok(id) } pub async fn load( From 242960ad1bae2e918bb0c99bef84973c60ded346 Mon Sep 17 00:00:00 2001 From: calyptobai Date: Fri, 15 Dec 2023 08:32:06 -0500 Subject: [PATCH 23/88] Fix routing for `DELETE /projects/:id/conversations/:id` --- server/bleep/src/webserver.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/bleep/src/webserver.rs b/server/bleep/src/webserver.rs index 22adcd057f..c16e6d9159 100644 --- a/server/bleep/src/webserver.rs +++ b/server/bleep/src/webserver.rs @@ -110,11 +110,11 @@ pub async fn start(app: Application) -> anyhow::Result<()> { ) .route( "/projects/:project_id/conversations", - get(conversation::list).delete(conversation::delete), + get(conversation::list), ) .route( "/projects/:project_id/conversations/:conversation_id", - get(conversation::get), + get(conversation::get).delete(conversation::delete), ) .route("/projects/:project_id/q", get(query::handle)) .route("/projects/:project_id/search/path", get(search::fuzzy_path)) From c65ec7fa46297f0935c08511274a609b259d6ab8 Mon Sep 17 00:00:00 2001 From: calyptobai Date: Tue, 19 Dec 2023 17:07:29 -0500 Subject: [PATCH 24/88] Use `conversation_id` instead of `thread_id` in `GET /answer` Rather than returning an initial JSON object, we introduce a new `ChatEvent` type, and return the conversation ID on stream end upon successful store. --- server/bleep/src/webserver/answer.rs | 72 +++++++++++++++------------- 1 file changed, 39 insertions(+), 33 deletions(-) diff --git a/server/bleep/src/webserver/answer.rs b/server/bleep/src/webserver/answer.rs index 9f09280a6a..0683f98fa0 100644 --- a/server/bleep/src/webserver/answer.rs +++ b/server/bleep/src/webserver/answer.rs @@ -106,8 +106,6 @@ pub(super) async fn answer( None => Conversation::new(project_id), }; - let conversation_id = conversation.store(&app.sql, user_id).await?; - let Answer { parent_exchange_id, q, @@ -151,7 +149,6 @@ pub(super) async fn answer( query_id, project_id, conversation, - conversation_id, action, } .execute() @@ -166,10 +163,22 @@ struct AgentExecutor { query_id: uuid::Uuid, project_id: i64, conversation: Conversation, - conversation_id: i64, action: Action, } +#[derive(serde::Serialize)] +enum AnswerEvent { + ChatEvent(Exchange), + StreamEnd(StreamEnd), +} + +#[derive(serde::Serialize)] +struct StreamEnd { + thread_id: String, + query_id: uuid::Uuid, + conversation_id: i64, +} + type SseDynStream = Sse + Send>>>; impl AgentExecutor { @@ -254,14 +263,12 @@ impl AgentExecutor { } }; - let initial_message = json!({ - "thread_id": self.conversation.thread_id.to_string(), - "query_id": self.query_id, - "conversation_id": self.conversation_id, - }); - // let project: Project = serde_json::from_str(&self.params.project).unwrap(); - let Answer { agent_model, answer_model, .. } = self.params.clone(); + let Answer { + agent_model, + answer_model, + .. + } = self.params.clone(); let (exchange_tx, exchange_rx) = tokio::sync::mpsc::channel(10); @@ -276,7 +283,7 @@ impl AgentExecutor { repo_refs, exchange_state: ExchangeState::Pending, answer_model, - agent_model + agent_model, }; let stream = async_stream::try_stream! { @@ -304,7 +311,7 @@ impl AgentExecutor { timeout, ) { match item { - Ok(Either::Left(exchange)) => yield exchange.compressed(), + Ok(Either::Left(exchange)) => yield AnswerEvent::ChatEvent(exchange.compressed()), Ok(Either::Right(next_action)) => match next_action { Ok(n) => break next = n, Err(e) => break 'outer Err(agent::Error::Processing(e)), @@ -319,7 +326,7 @@ impl AgentExecutor { // of the above loop without ever processing the final message. Here, we empty the // queue. while let Some(Some(exchange)) = exchange_rx.next().now_or_never() { - yield exchange.compressed(); + yield AnswerEvent::ChatEvent(exchange.compressed()); } match next { @@ -331,7 +338,21 @@ impl AgentExecutor { agent.complete(result.is_ok()); match result { - Ok(_) => {} + Ok(_) => { + let conversation_id = agent.conversation.store( + &agent.app.sql, + agent.user.username().context("agent failed to get user ID")?, + ) + .await?; + + let final_message = StreamEnd { + thread_id: agent.conversation.thread_id.to_string(), + query_id: agent.query_id, + conversation_id, + }; + + yield AnswerEvent::StreamEnd(final_message); + } Err(agent::Error::Timeout(duration)) => { warn!("Timeout reached."); agent.track_query( @@ -347,30 +368,19 @@ impl AgentExecutor { ); Err(e)?; } - } + }; }; - let init_stream = futures::stream::once(async move { - Ok(sse::Event::default() - .json_data(initial_message) - // This should never happen, so we force an unwrap. - .expect("failed to serialize initialization object")) - }); - // We know the stream is unwind safe as it doesn't use synchronization primitives like locks. - let answer_stream = AssertUnwindSafe(stream) + let stream = AssertUnwindSafe(stream) .catch_unwind() .map(|res| res.unwrap_or_else(|_| Err(anyhow!("stream panicked")))) - .map(|ex: Result| { + .map(|ex: Result| { sse::Event::default() .json_data(ex.map_err(|e| e.to_string())) .map_err(anyhow::Error::new) }); - let done_stream = futures::stream::once(async { Ok(sse::Event::default().data("[DONE]")) }); - - let stream = init_stream.chain(answer_stream).chain(done_stream); - Ok(Sse::new(Box::pin(stream))) } } @@ -457,9 +467,6 @@ pub async fn explain( let mut conversation = Conversation::new(project_id); conversation.exchanges.push(exchange); - let user_id = user.username().ok_or_else(super::no_user_id)?; - let conversation_id = conversation.store(&app.sql, user_id).await?; - let action = Action::Answer { paths: vec![0] }; AgentExecutor { @@ -469,7 +476,6 @@ pub async fn explain( query_id, project_id, conversation, - conversation_id, action, } .execute() From 7da9a325fa5bee0f02ae3687df211a7ab4c2fa8a Mon Sep 17 00:00:00 2001 From: calyptobai Date: Thu, 21 Dec 2023 09:28:59 -0500 Subject: [PATCH 25/88] Return errors with debug formatting --- server/bleep/src/webserver/answer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/bleep/src/webserver/answer.rs b/server/bleep/src/webserver/answer.rs index 0683f98fa0..7a528a79ab 100644 --- a/server/bleep/src/webserver/answer.rs +++ b/server/bleep/src/webserver/answer.rs @@ -377,7 +377,7 @@ impl AgentExecutor { .map(|res| res.unwrap_or_else(|_| Err(anyhow!("stream panicked")))) .map(|ex: Result| { sse::Event::default() - .json_data(ex.map_err(|e| e.to_string())) + .json_data(ex.map_err(|e| format!("{e:?}"))) .map_err(anyhow::Error::new) }); From 2db0859617fd936e3aa4ca876a3d990e2fb83aa4 Mon Sep 17 00:00:00 2001 From: calyptobai Date: Tue, 9 Jan 2024 13:50:55 -0500 Subject: [PATCH 26/88] Fix more rebase errors --- server/bleep/src/agent/symbol.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/bleep/src/agent/symbol.rs b/server/bleep/src/agent/symbol.rs index f59c820204..3f428bdd9c 100644 --- a/server/bleep/src/agent/symbol.rs +++ b/server/bleep/src/agent/symbol.rs @@ -298,7 +298,8 @@ impl Agent { alias: self.get_path_alias(&c.repo_path), ..c.clone() }; - self.exchanges + self.conversation + .exchanges .last_mut() .unwrap() .code_chunks From 00552aad5ce741868838f36f3e56fa3e5d20dfaf Mon Sep 17 00:00:00 2001 From: Gabriel Gordon-Hall Date: Thu, 4 Jan 2024 10:04:31 +0000 Subject: [PATCH 27/88] Indexing status reporting improvements (#1192) * repo index status reporting fixes * report whether is resync in index progress --- server/bleep/src/background.rs | 2 ++ server/bleep/src/background/control.rs | 15 ++++++++ server/bleep/src/background/sync.rs | 50 ++++++++++++++------------ server/bleep/src/repo.rs | 8 ++--- 4 files changed, 46 insertions(+), 29 deletions(-) diff --git a/server/bleep/src/background.rs b/server/bleep/src/background.rs index bee1cf9554..6734e5d1be 100644 --- a/server/bleep/src/background.rs +++ b/server/bleep/src/background.rs @@ -35,6 +35,8 @@ pub fn rayon_pool() -> &'static ThreadPool { pub struct Progress { #[serde(rename = "ref")] reporef: RepoRef, + #[serde(rename = "rsync")] + resync: bool, #[serde(rename = "b")] branch_filter: Option, #[serde(rename = "ev")] diff --git a/server/bleep/src/background/control.rs b/server/bleep/src/background/control.rs index f3e4d760ea..b05369cbbf 100644 --- a/server/bleep/src/background/control.rs +++ b/server/bleep/src/background/control.rs @@ -26,6 +26,9 @@ pub struct SyncPipes { /// Together with `filter_updates`, it uniquely identifies the sync process reporef: RepoRef, + /// Is this a re-sync? + resync: bool, + /// Together with `reporef`, it uniquely identifies the sync process filter_updates: FilterUpdate, @@ -42,11 +45,13 @@ pub struct SyncPipes { impl SyncPipes { pub(super) fn new( reporef: RepoRef, + resync: bool, filter_updates: FilterUpdate, progress: super::ProgressStream, ) -> Self { Self { reporef, + resync, progress, filter_updates, git_interrupt: Default::default(), @@ -58,6 +63,7 @@ impl SyncPipes { // clear any state stored on the frontend _ = self.progress.send(Progress { reporef: self.reporef.clone(), + resync: self.resync, branch_filter: self.filter_updates.branch_filter.clone(), event: ProgressEvent::IndexPercent(None), }); @@ -70,6 +76,7 @@ impl SyncPipes { name: Default::default(), progress: self.progress.clone(), reporef: self.reporef.clone(), + resync: self.resync, filter_updates: self.filter_updates.clone(), } } @@ -77,6 +84,7 @@ impl SyncPipes { pub(crate) fn index_percent(&self, current: u8) { _ = self.progress.send(Progress { reporef: self.reporef.clone(), + resync: self.resync, branch_filter: self.filter_updates.branch_filter.clone(), event: ProgressEvent::IndexPercent(Some(current)), }); @@ -85,6 +93,7 @@ impl SyncPipes { pub(crate) fn status(&self, new: SyncStatus) { _ = self.progress.send(Progress { reporef: self.reporef.clone(), + resync: self.resync, branch_filter: self.filter_updates.branch_filter.clone(), event: ProgressEvent::StatusChange(new), }); @@ -132,6 +141,9 @@ pub(crate) struct GitSync { /// Copy from `SyncPipes`, because we can't make this a referential type reporef: RepoRef, + /// Copy from `SyncPipes`, because we can't make this a referential type + resync: bool, + /// Copy from `SyncPipes`, because we can't make this a referential type filter_updates: FilterUpdate, @@ -181,6 +193,7 @@ impl gix::progress::Count for GitSync { let current = ((step as f32 / self.max.load(Ordering::SeqCst) as f32) * 100f32) as u8; _ = self.progress.send(Progress { reporef: self.reporef.clone(), + resync: self.resync, branch_filter: self.filter_updates.branch_filter.clone(), event: ProgressEvent::IndexPercent(Some(current.min(100))), }); @@ -218,6 +231,7 @@ impl gix::progress::Count for GitSync { _ = self.progress.send(Progress { reporef: self.reporef.clone(), + resync: self.resync, branch_filter: self.filter_updates.branch_filter.clone(), event: ProgressEvent::IndexPercent(Some(current.min(100))), }); @@ -258,6 +272,7 @@ impl gix::progress::NestedProgress for GitSync { cnt: self.cnt.clone(), filter_updates: self.filter_updates.clone(), reporef: self.reporef.clone(), + resync: self.resync, progress, name, id, diff --git a/server/bleep/src/background/sync.rs b/server/bleep/src/background/sync.rs index fdc4aa5dc8..9c84ff0f3c 100644 --- a/server/bleep/src/background/sync.rs +++ b/server/bleep/src/background/sync.rs @@ -164,7 +164,6 @@ impl SyncHandle { }; let (exited, exit_signal) = flume::bounded(1); - let pipes = SyncPipes::new(reporef.clone(), filter_updates.clone(), status); let current = app .repo_pool .entry_async(reporef.clone()) @@ -196,6 +195,13 @@ impl SyncHandle { } }); + let pipes = SyncPipes::new( + reporef.clone(), + current.get().last_index_unix_secs != 0, + filter_updates.clone(), + status, + ); + // if we're not upgrading from shallow to full checkout // this seems to be a speed optimization for git operations if !shallow && !current.get().shallow { @@ -289,9 +295,7 @@ impl SyncHandle { error!(?err, "failed to generate tutorial questions"); } } - - // technically `sync_done_with` does this, but we want to send notifications - self.set_status(|repo| repo.sync_status.clone()) + self.set_status(|_| SyncStatus::Done) } Err(SyncError::Cancelled) => self.set_status(|_| SyncStatus::Cancelled), Err(err) => self.set_status(|_| SyncStatus::Error { @@ -514,25 +518,26 @@ impl SyncHandle { &self, updater: impl FnOnce(&Repository) -> SyncStatus, ) -> Option { - let new_status = self.app.repo_pool.update(&self.reporef, move |_k, repo| { - let new_status = (updater)(repo); - let old_status = std::mem::replace(&mut repo.sync_status, new_status); - - if !matches!(repo.sync_status, SyncStatus::Queued) - || matches!(old_status, SyncStatus::Syncing) - { - repo.pub_sync_status = repo.sync_status.clone(); - } + let (new_status, old_status) = + self.app.repo_pool.update(&self.reporef, move |_k, repo| { + let new_status = (updater)(repo); + let old_status = std::mem::replace(&mut repo.sync_status, new_status); + + if !matches!(repo.sync_status, SyncStatus::Queued) + || matches!(old_status, SyncStatus::Syncing) + { + repo.pub_sync_status = repo.sync_status.clone(); + } - if matches!( - repo.sync_status, - SyncStatus::Error { .. } | SyncStatus::Done - ) { - repo.locked = false; - } + if matches!( + repo.sync_status, + SyncStatus::Error { .. } | SyncStatus::Done + ) { + repo.locked = false; + } - repo.sync_status.clone() - })?; + (repo.sync_status.clone(), old_status) + })?; if let SyncStatus::Error { ref message } = new_status { error!(?self.reporef, err=?message, "indexing failed"); @@ -540,7 +545,7 @@ impl SyncHandle { debug!(?self.reporef, ?new_status, "new status"); } - if !matches!(new_status, SyncStatus::Queued) { + if !matches!(new_status, SyncStatus::Queued) && new_status != old_status { self.pipes.status(new_status.clone()); } Some(new_status) @@ -563,7 +568,6 @@ impl SyncHandle { Some(Ok(repo)) => { let new_status = repo.sync_status.clone(); debug!(?self.reporef, ?new_status, "new status"); - self.pipes.status(new_status); Ok(repo) } Some(err) => err, diff --git a/server/bleep/src/repo.rs b/server/bleep/src/repo.rs index 2024ca1e22..9600770501 100644 --- a/server/bleep/src/repo.rs +++ b/server/bleep/src/repo.rs @@ -354,12 +354,8 @@ impl Repository { if shallow { self.sync_status = SyncStatus::Shallow - } else { - if let Some(ref ff) = filter_update.file_filter { - self.file_filter = ff.patch_into(&self.file_filter); - } - - self.sync_status = SyncStatus::Done + } else if let Some(ref ff) = filter_update.file_filter { + self.file_filter = ff.patch_into(&self.file_filter); }; } } From a21ccff82526e491193f7972e6b1bdafeec9e921 Mon Sep 17 00:00:00 2001 From: akshay Date: Wed, 10 Jan 2024 19:19:23 +0530 Subject: [PATCH 28/88] rework sync logic for docs (#1186) * rework sync logic for docs - replace `/sync` with `/enqueue`; a non-streaming replacement to add items to the doc-sync queue - introduce `/status` and `/cancel`; to stream updates for a syncing document or to cancel a sync job - convert `/resync` to http from sse - internal updates to `/list` to work with the new queue system * track metadata update in progress stream * handle possible error state --- Cargo.lock | 1 + server/bleep/Cargo.toml | 2 +- server/bleep/src/indexes/doc.rs | 418 +++++++++++++++++++++-------- server/bleep/src/webserver.rs | 4 +- server/bleep/src/webserver/docs.rs | 42 ++- 5 files changed, 324 insertions(+), 143 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cc7a409f30..ffb038e61c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7691,6 +7691,7 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio", + "tokio-util", ] [[package]] diff --git a/server/bleep/Cargo.toml b/server/bleep/Cargo.toml index ae3a38d421..9ac29bb042 100644 --- a/server/bleep/Cargo.toml +++ b/server/bleep/Cargo.toml @@ -42,7 +42,7 @@ harness = false tantivy = { version = "0.21.0", features = ["mmap"] } tantivy-columnar = "0.2.0" tokio = { version = "1.32.0", features = ["macros", "process", "rt", "rt-multi-thread", "io-std", "io-util", "sync", "fs"] } -tokio-stream = "0.1.14" +tokio-stream = { version = "0.1.14", features = ["sync"]} async-trait = "0.1.73" async-stream = "0.3.5" flume = "0.10.14" diff --git a/server/bleep/src/indexes/doc.rs b/server/bleep/src/indexes/doc.rs index 32bb5f8792..7ddbf3223b 100644 --- a/server/bleep/src/indexes/doc.rs +++ b/server/bleep/src/indexes/doc.rs @@ -8,7 +8,7 @@ use tantivy::{ tokenizer::NgramTokenizer, }; use thiserror::Error; -use tokio::sync::Mutex; +use tokio::sync::{Mutex, RwLock}; use tracing::{error, info, trace}; use crate::{ @@ -25,11 +25,27 @@ pub struct Doc { sql: SqlDb, section_index: tantivy::Index, section_schema: schema::Section, - buffer_size: usize, + index_writer: Arc>, + index_reader: Arc, + index_queue: Arc>>, +} + +// Wrapper around a tokio::task::JoinHandle containing metadata about the task. All task +// progress is collected by a reciever in the SyncHandle. +struct SyncHandle { + id: i64, + url: String, + name: Option, + favicon: Option, + description: Option, + status: &'static str, + join_handle: Option>>, + progress_stream: tokio::sync::watch::Receiver, } static STATUS_DONE: &str = "done"; static STATUS_INDEXING: &str = "indexing"; +static STATUS_ERROR: &str = "error"; #[derive(serde::Serialize)] pub struct SqlRecord { @@ -44,6 +60,9 @@ pub struct SqlRecord { #[derive(serde::Serialize, Clone)] pub enum Progress { + Init(i64), + SetMetadata, + Err(String), Update(Update), Done(i64), } @@ -91,16 +110,6 @@ pub enum Error { } impl Doc { - fn index_writer(&self) -> Result { - self.section_index - .writer(self.buffer_size) - .map_err(Error::Tantivy) - } - - fn index_reader(&self) -> Result { - self.section_index.reader().map_err(Error::Tantivy) - } - /// Initialize docs index pub fn create( sql: SqlDb, @@ -117,6 +126,10 @@ impl Doc { section_schema.schema.clone(), ) .map_err(|e| Error::Initialize(e.to_string()))?; + let index_writer = Arc::new(Mutex::new( + section_index.writer(buffer_size).map_err(Error::Tantivy)?, + )); + let index_reader = Arc::new(section_index.reader().map_err(Error::Tantivy)?); section_index .set_multithread_executor(max_threads) @@ -130,7 +143,9 @@ impl Doc { sql, section_index, section_schema, - buffer_size, + index_reader, + index_writer, + index_queue: Arc::new(RwLock::new(Vec::new())), }) } @@ -138,6 +153,12 @@ impl Doc { where &'a mut E: sqlx::Executor<'a, Database = sqlx::Sqlite>, { + let mut index_queue = self.index_queue.write().await; + let sync_handle = index_queue + .iter_mut() + .find(|job| job.id == id) + .ok_or(Error::InvalidDocId(id))?; + sync_handle.name = Some(title.to_string()); sqlx::query! { "UPDATE docs SET name = ? WHERE id = ?", title, @@ -158,6 +179,12 @@ impl Doc { where &'a mut E: sqlx::Executor<'a, Database = sqlx::Sqlite>, { + let mut index_queue = self.index_queue.write().await; + let sync_handle = index_queue + .iter_mut() + .find(|job| job.id == id) + .ok_or(Error::InvalidDocId(id))?; + sync_handle.favicon = Some(favicon.to_string()); sqlx::query! { "UPDATE docs SET favicon = ? WHERE id = ?", favicon, @@ -178,6 +205,12 @@ impl Doc { where &'a mut E: sqlx::Executor<'a, Database = sqlx::Sqlite>, { + let mut index_queue = self.index_queue.write().await; + let sync_handle = index_queue + .iter_mut() + .find(|job| job.id == id) + .ok_or(Error::InvalidDocId(id))?; + sync_handle.description = Some(description.to_string()); sqlx::query! { "UPDATE docs SET description = ? WHERE id = ?", description, @@ -248,94 +281,213 @@ impl Doc { /// /// The sqlite DB stores metadata about the doc-provider, and the tantivy index stores /// searchable page content. - pub async fn sync(self, url: url::Url) -> impl Stream> { - try_stream! { - // check if index writer is available - let index_writer = Arc::new(Mutex::new(self.index_writer()?)); + pub async fn enqueue(self, url: url::Url) -> Result { + // check if index writer is available + + let mut transaction = self.sql.begin().await?; + let url_string = url.to_string(); + let id = sqlx::query! { + "INSERT INTO docs (url, index_status) VALUES (?, ?)", + url_string, + STATUS_INDEXING, + } + .execute(&mut transaction) + .await? + .last_insert_rowid(); - let mut transaction = self.sql.begin().await?; + let (tx, rx) = tokio::sync::watch::channel(Progress::Init(id)); + let self_ = self.clone(); + let url_ = url.clone(); + let mut lock = self.index_queue.write().await; + let sync_handle = SyncHandle { + id, + url: url.to_string(), + name: None, + favicon: None, + description: None, + status: STATUS_INDEXING, + join_handle: None, + progress_stream: rx, + }; + lock.push(sync_handle); + lock.last_mut().unwrap().join_handle = Some(tokio::task::spawn(async move { + self_.begin_sync_job(id, url_, tx, transaction).await + })); - // add entry to sqlite - let url_string = url.to_string(); - let id = sqlx::query! { - "INSERT INTO docs (url, index_status) VALUES (?, ?)", - url_string, - STATUS_INDEXING, - } - .execute(&mut transaction) - .await? - .last_insert_rowid(); + Ok(id) + } - let mut is_meta_set = false; - let stream = - self.clone() - .insert_into_tantivy(id, url.clone(), Arc::clone(&index_writer)); - let mut discovered_count = 0; - for await progress in stream { - // populate metadata in sqlite - // - // the first scraped doc that contains metadata is used to populate - // metadata in sqlite - this is typically the base_url entered by the user, - // if the base_url does not contain any metadata, we move on the the second - // scraped url - if let Progress::Update(update) = progress.clone() { - discovered_count = update.discovered_count; - if update.url == url || (!update.metadata.is_empty() && !is_meta_set) { - // do not set meta for this doc provider in subsequent turns - is_meta_set = true; - self.set_metadata(&update.metadata, id, &url, &mut transaction).await; - }; - } - yield progress; + async fn begin_sync_job( + self, + id: i64, + url: url::Url, + tx: tokio::sync::watch::Sender, + mut transaction: sqlx::Transaction<'_, sqlx::Sqlite>, + ) -> Result<(), Error> { + let mut is_meta_set = false; + let mut stream = Box::pin(self.clone().insert_into_tantivy(id, url.clone())); + let mut discovered_count = 0; + + while let Some(progress) = stream.next().await { + // populate metadata in sqlite + // + // the first scraped doc that contains metadata is used to populate + // metadata in sqlite - this is typically the base_url entered by the user, + // if the base_url does not contain any metadata, we move on the the second + // scraped url + if let Progress::Update(update) = progress.clone() { + discovered_count = update.discovered_count; + if update.url == url || (!update.metadata.is_empty() && !is_meta_set) { + // do not set meta for this doc provider in subsequent turns + is_meta_set = true; + self.set_metadata(&update.metadata, id, &url, &mut transaction) + .await; + let _ = tx.send(Progress::SetMetadata); + }; } + let _ = tx.send(progress); // TODO: log err here + } - // scraped doc, but no pages - if discovered_count == 0 { - // delete sql entry - sqlx::query!("DELETE FROM docs WHERE id = ? RETURNING id", id) - .fetch_optional(&mut transaction) - .await? - .ok_or(Error::InvalidDocId(id))?; - error!(doc_source = url.as_str(), "no docs found at url"); - // return error - Err(Error::EmptyDocs(url))?; - } + // scraped doc, but no pages + if discovered_count == 0 { + // delete sql entry + sqlx::query!("DELETE FROM docs WHERE id = ? RETURNING id", id) + .fetch_optional(&mut transaction) + .await? + .ok_or(Error::InvalidDocId(id))?; + error!(doc_source = url.as_str(), %id, "no docs found at url"); + + let error = Error::EmptyDocs(url); + + // send error down the stream + let _ = tx.send(Progress::Err(error.to_string())); + + // send job status to error + self.index_queue + .write() + .await + .iter_mut() + .find(|job| job.id == id) + .map(|job| job.status = STATUS_ERROR); - self.set_index_status(STATUS_DONE, id, &mut transaction).await?; - transaction.commit().await?; + // return error + Err(error)?; } + + self.set_index_status(STATUS_DONE, id, &mut transaction) + .await?; + transaction.commit().await?; + + // remove handle from queue + let mut lock = self.index_queue.write().await; + lock.retain(|handle| handle.id != id); + Ok(()) } - /// Update documentation in the index - this will rescrape the entire website - pub async fn resync(self, id: i64) -> impl Stream> { + pub async fn status(self, id: i64) -> impl Stream> { try_stream! { - let url = sqlx::query!("SELECT url FROM docs WHERE id = ?", id) - .fetch_optional(&*self.sql) - .await? - .ok_or(Error::InvalidDocId(id))? - .url; - let url = url::Url::parse(&url).map_err(|e| Error::UrlParse(url, e))?; - - // delete old docs from tantivy - self.index_writer()? - .delete_term(Term::from_field_i64(self.section_schema.doc_id, id)); - self.index_writer()?.commit()?; - - sqlx::query! { - "UPDATE docs SET modified_at = datetime('now') WHERE id = ?", - id, + let lock = self.index_queue.read().await; + let handle = lock.iter().find(|handle| handle.id == id); + match handle { + Some(h) => { + let s = tokio_stream::wrappers::WatchStream::from_changes(h.progress_stream.clone()); + for await progress in s { + yield progress; + } + } + None => { + let _ = self.list_one(id).await?; + yield Progress::Done(id); + } } - .execute(&*self.sql) - .await?; + } + } - let index_writer = Arc::new(Mutex::new(self.index_writer()?)); + /// Cancel a running index job + pub async fn cancel(self, id: i64) -> Result { + let lock = self.index_queue.read().await; + let handle = lock + .iter() + .find(|handle| handle.id == id) + .ok_or(Error::InvalidDocId(id))?; + handle + .join_handle + .as_ref() + .ok_or(Error::InvalidDocId(id))? + .abort(); + drop(lock); + + // remove handle from queue + let mut lock = self.index_queue.write().await; + lock.retain(|handle| handle.id != id); + + // bring tantivy back to last saved state + // + // - if this is a sync job: this rolls back to before the sync started + // - if this is a resync job: this rolls back to before the old copy was deleted + match self.index_writer.lock().await.rollback() { + Ok(_) => info!(%id, "successfully cancelled sync job, rolled back to old copy"), + Err(e) => error!(%id, %e, "tantivy rollback failed"), + }; - let stream = self - .insert_into_tantivy(id, url, Arc::clone(&index_writer)); - for await progress in stream { - yield progress; - } + Ok(id) + } + + /// Update documentation in the index - this will rescrape the entire website + /// + /// We may only resync ids that are persisted to the sql db, this ensures that resync cannot + /// be called on a currently syncing job + pub async fn resync(self, id: i64) -> Result { + let record = sqlx::query!( + "SELECT id, index_status, name, url, favicon, description, modified_at FROM docs WHERE id = ?", + id + ) + .fetch_one(&*self.sql) + .await + .map_err(|_| Error::InvalidDocId(id))?; + + let url = + url::Url::parse(&record.url).map_err(|e| Error::UrlParse(record.url.clone(), e))?; + + // delete old docs from tantivy + // + // create a checkpoint before deletion, so we can revert to here if the job is cancelled + self.index_writer.lock().await.commit(); + self.index_writer + .lock() + .await + .delete_term(Term::from_field_i64(self.section_schema.doc_id, id)); + + // update modified time + sqlx::query! { + "UPDATE docs SET modified_at = datetime('now') WHERE id = ?", + id, } + .execute(&*self.sql) + .await?; + + // create a new sync job for this doc-id + let (tx, rx) = tokio::sync::watch::channel(Progress::Init(id)); + let transaction = self.sql.begin().await?; + let self_ = self.clone(); + let url_ = url.clone(); + + let mut lock = self.index_queue.write().await; + lock.push(SyncHandle { + id, + url: url.to_string(), + name: record.name.clone(), + favicon: record.favicon.clone(), + status: STATUS_INDEXING, + description: record.description.clone(), + join_handle: None, + progress_stream: rx, + }); + lock.last_mut().unwrap().join_handle = Some(tokio::task::spawn(async move { + self_.begin_sync_job(id, url_, tx, transaction).await + })); + + Ok(id) } /// Remove this doc source from tantivy and sqlite @@ -348,16 +500,17 @@ impl Doc { .id; // delete entry from tantivy - self.index_writer()? - .delete_term(Term::from_field_i64(self.section_schema.doc_id, id)); - self.index_writer()?.commit()?; + let mut index_writer = self.index_writer.lock().await; + index_writer.delete_term(Term::from_field_i64(self.section_schema.doc_id, id)); + index_writer.commit()?; Ok(id) } /// List all synced doc sources pub async fn list(&self) -> Result, Error> { - Ok(sqlx::query!( + // the sqlite db contains indexed and resyncing doc-ids + let mut indexed = sqlx::query!( "SELECT id, index_status, name, url, favicon, description, modified_at FROM docs" ) .fetch_all(&*self.sql) @@ -372,19 +525,60 @@ impl Doc { url: record.url, modified_at: record.modified_at, }) - .collect::>()) + .collect::>(); + + // the index queue contains both sync and resync doc-ids + let index_queue = self.index_queue.read().await; + let syncing = index_queue + .iter() + .map(|job| SqlRecord { + id: job.id, + url: job.url.clone(), + name: job.name.clone(), + favicon: job.favicon.clone(), + description: job.description.clone(), + index_status: job.status.to_string(), + modified_at: chrono::Utc::now().naive_local(), + }) + .collect::>(); + + // if there is a resync job already, remove from list of indexed docs + indexed.retain(|x| !syncing.iter().any(|y| y.id == x.id)); + indexed.extend(syncing); + indexed.sort_by_key(|record| record.id); + + Ok(indexed) } - /// List a synced doc source by id + /// List a doc source by id. + /// + /// This doc-source could be in the sync-queue, if it is syncing/resyncing, + /// or in the sqlite db if it has been indexed completely pub async fn list_one(&self, id: i64) -> Result { - let record = sqlx::query!( + let queued_item = self + .index_queue + .read() + .await + .iter() + .find(|job| job.id == id) + .map(|job| SqlRecord { + id: job.id, + url: job.url.clone(), + name: job.name.clone(), + favicon: job.favicon.clone(), + description: job.description.clone(), + index_status: job.status.to_string(), + modified_at: chrono::Utc::now().naive_local(), + }); + + let indexed_item = sqlx::query!( "SELECT id, index_status, name, url, favicon, description, modified_at FROM docs WHERE id = ?", id ) .fetch_one(&*self.sql) - .await?; - - Ok(SqlRecord { + .await + .ok() + .map(|record| SqlRecord { id: record.id, name: record.name, index_status: record.index_status, @@ -392,7 +586,11 @@ impl Doc { favicon: record.favicon, url: record.url, modified_at: record.modified_at, - }) + }); + + Ok(queued_item + .or(indexed_item) + .ok_or(Error::InvalidDocId(id))?) } /// Search for doc source by title @@ -434,7 +632,7 @@ impl Doc { id: i64, ) -> Result, Error> { // use the tantivy index for section search - let reader = self.index_reader()?; + let reader = Arc::clone(&self.index_reader); let searcher = reader.searcher(); let doc_id_query = Box::new(TermQuery::new( @@ -603,7 +801,7 @@ impl Doc { } pub async fn list_sections(&self, limit: usize, id: i64) -> Result, Error> { - let reader = self.index_reader()?; + let reader = Arc::clone(&self.index_reader); let searcher = reader.searcher(); let doc_id_query = Box::new(TermQuery::new( Term::from_field_i64(self.section_schema.doc_id, id), @@ -622,7 +820,7 @@ impl Doc { /// Scroll pages in a doc pub async fn list_pages(&self, limit: usize, id: i64) -> Result, Error> { - let reader = self.index_reader()?; + let reader = Arc::clone(&self.index_reader); let searcher = reader.searcher(); let doc_id_query = Box::new(TermQuery::new( Term::from_field_i64(self.section_schema.doc_id, id), @@ -650,7 +848,7 @@ impl Doc { id: i64, relative_url: S, ) -> Result, Error> { - let reader = self.index_reader()?; + let reader = Arc::clone(&self.index_reader); let searcher = reader.searcher(); let doc_id_query = Box::new(TermQuery::new( Term::from_field_i64(self.section_schema.doc_id, id), @@ -685,10 +883,7 @@ impl Doc { } pub fn contains_url(&self, url: &url::Url) -> bool { - let Ok(reader) = self.index_reader() else { - return false; - }; - + let reader = Arc::clone(&self.index_reader); let searcher = reader.searcher(); let query = Box::new(TermQuery::new( Term::from_field_text(self.section_schema.absolute_url, url.as_str()), @@ -704,12 +899,7 @@ impl Doc { } /// Scrape & insert a doc source into tantivy and return doc metadata if available - fn insert_into_tantivy( - self, - id: i64, - doc_source: url::Url, - index_writer: Arc>, - ) -> impl Stream { + fn insert_into_tantivy(self, id: i64, doc_source: url::Url) -> impl Stream { stream! { let mut scraper = Scraper::with_config(Config::new(doc_source.clone())); let mut stream = Box::pin(scraper.complete()); @@ -729,7 +919,7 @@ impl Doc { } let doc_source = doc_source.clone(); let section_schema = self.section_schema.clone(); - let index_writer = Arc::clone(&index_writer); + let index_writer = Arc::clone(&self.index_writer); let cache = Arc::clone(&point_ids); handles.push(tokio::task::spawn(async move { let (section_ids, tantivy_docs_to_insert) = doc.sections(id, &doc_source, §ion_schema); @@ -746,7 +936,7 @@ impl Doc { futures::future::join_all(handles).await; trace!(%id, url = doc_source.as_str(), "commiting doc-provider to index"); - match index_writer.lock().await.commit() { + match self.index_writer.lock().await.commit() { Ok(_) => info!(%id, url = doc_source.as_str(), "index complete"), Err(e) => error!(%id, url = doc_source.as_str(), %e, "tantivy commit failed"), } diff --git a/server/bleep/src/webserver.rs b/server/bleep/src/webserver.rs index c16e6d9159..1126391f6b 100644 --- a/server/bleep/src/webserver.rs +++ b/server/bleep/src/webserver.rs @@ -62,11 +62,13 @@ pub async fn start(app: Application) -> anyhow::Result<()> { Router::new() .route("/", get(docs::list)) // list all doc providers .route("/search", get(docs::search)) // text search over doc providers - .route("/sync", get(docs::sync)) // index a new doc provider + .route("/enqueue", get(docs::enqueue)) // enqueue a new url to begin syncing .route("/verify", get(docs::verify)) // verify if a doc url is valid .route("/:id", get(docs::list_one)) // list a doc provider by id .route("/:id", delete(docs::delete)) // delete a doc provider by id .route("/:id/resync", get(docs::resync)) // resync a doc provider by id + .route("/:id/status", get(docs::status)) // query sync status of an existing doc source + .route("/:id/cancel", get(docs::cancel)) // cancel an index job .route("/:id/search", get(docs::search_with_id)) // search/list sections of a doc provider .route("/:id/list", get(docs::list_with_id)) // list pages of a doc provider .route("/:id/fetch", get(docs::fetch)), // fetch all sections of a page of a doc provider diff --git a/server/bleep/src/webserver/docs.rs b/server/bleep/src/webserver/docs.rs index e1fa50ed7e..8a0d8138b4 100644 --- a/server/bleep/src/webserver/docs.rs +++ b/server/bleep/src/webserver/docs.rs @@ -6,7 +6,6 @@ use axum::{ }, }; use futures::stream::{Stream, StreamExt}; -use tokio_stream::wrappers::ReceiverStream; use tracing::error; use crate::{ @@ -20,7 +19,7 @@ use std::convert::Infallible; // schema #[derive(serde::Deserialize)] -pub struct Sync { +pub struct Enqueue { url: url::Url, } @@ -61,11 +60,11 @@ pub async fn delete(State(app): State, Path(id): Path) -> Resu Ok(Json(app.indexes.doc.delete(id).await?)) } -pub async fn sync( +pub async fn enqueue( State(app): State, + Query(params): Query, Extension(user): Extension, - Query(params): Query, -) -> Sse>> { +) -> Result> { app.with_analytics(|hub| { hub.track_doc( &user, @@ -73,33 +72,14 @@ pub async fn sync( ) }); - let (tx, rx) = tokio::sync::mpsc::channel(100); - - tokio::spawn(async move { - let stream = app.indexes.doc.clone().sync(params.url).await; - - futures::pin_mut!(stream); - - while let Some(t) = stream.next().await { - // We intentionally ignore errors so that this stream is consumed in the background, - // regardless of whether the receiver still exists. - let _ = tx.send(t); - } - }); - - Sse::new(Box::pin(ReceiverStream::new(rx).map(|result| { - Ok(Event::default() - .json_data(result.as_ref().map_err(ToString::to_string)) - .unwrap()) - }))) - .keep_alive(KeepAlive::default()) + Ok(Json(app.indexes.doc.clone().enqueue(params.url).await?)) } -pub async fn resync( +pub async fn status( State(app): State, Path(id): Path, ) -> Sse>> { - Sse::new(Box::pin(app.indexes.doc.clone().resync(id).await.map( + Sse::new(Box::pin(app.indexes.doc.clone().status(id).await.map( |result| { Ok(Event::default() .json_data(result.as_ref().map_err(ToString::to_string)) @@ -109,6 +89,14 @@ pub async fn resync( .keep_alive(KeepAlive::default()) } +pub async fn cancel(State(app): State, Path(id): Path) -> Result> { + Ok(Json(app.indexes.doc.clone().cancel(id).await?)) +} + +pub async fn resync(State(app): State, Path(id): Path) -> Result> { + Ok(Json(app.indexes.doc.clone().resync(id).await?)) +} + pub async fn search( State(app): State, Query(params): Query, From 9cf39f495abff3c588767ea4059a57c1f7100595 Mon Sep 17 00:00:00 2001 From: calyptobai Date: Wed, 10 Jan 2024 09:34:17 -0500 Subject: [PATCH 29/88] Fix tests --- server/bleep/src/webserver/intelligence.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/bleep/src/webserver/intelligence.rs b/server/bleep/src/webserver/intelligence.rs index 9f8103f5e8..23d86b3383 100644 --- a/server/bleep/src/webserver/intelligence.rs +++ b/server/bleep/src/webserver/intelligence.rs @@ -533,6 +533,7 @@ mod tests { "data": [ { "file": "server/bleep/src/symbol.rs", + "repo": "github.com/BloopAI/bloop", "data": [{ "kind": "definition", "range": { @@ -550,6 +551,7 @@ mod tests { }, { "file": "server/bleep/src/intelligence/scope_resolution.rs", + "repo": "github.com/BloopAI/bloop", "data": [{ "kind": "reference", "range": { @@ -571,6 +573,7 @@ mod tests { data: vec![ FileSymbols { file: "server/bleep/src/symbol.rs".into(), + repo: "github.com/BloopAI/bloop".parse().unwrap(), data: vec![Occurrence { kind: OccurrenceKind::Definition, range: TextRange { @@ -596,6 +599,7 @@ mod tests { }, FileSymbols { file: "server/bleep/src/intelligence/scope_resolution.rs".into(), + repo: "github.com/BloopAI/bloop".parse().unwrap(), data: vec![Occurrence { kind: OccurrenceKind::Reference, range: TextRange { From 00820d6b046b66391ef6ccf21d030e7568af10ec Mon Sep 17 00:00:00 2001 From: calyptobai Date: Wed, 10 Jan 2024 09:34:30 -0500 Subject: [PATCH 30/88] Run cargo fmt --- server/bleep/src/agent/symbol.rs | 12 ++++++++++-- server/bleep/src/agent/tools/code.rs | 3 ++- server/bleep/src/agent/tools/path.rs | 2 +- server/bleep/src/agent/tools/proc.rs | 3 ++- server/bleep/src/intelligence/code_navigation.rs | 3 ++- server/bleep/src/semantic.rs | 2 +- 6 files changed, 18 insertions(+), 7 deletions(-) diff --git a/server/bleep/src/agent/symbol.rs b/server/bleep/src/agent/symbol.rs index 3f428bdd9c..b3099f77f0 100644 --- a/server/bleep/src/agent/symbol.rs +++ b/server/bleep/src/agent/symbol.rs @@ -216,7 +216,11 @@ impl Agent { .app .indexes .file - .by_path(&symbol_metadata.repo_path.repo, &symbol_metadata.repo_path.path, None) + .by_path( + &symbol_metadata.repo_path.repo, + &symbol_metadata.repo_path.path, + None, + ) .await .unwrap() .unwrap(); @@ -230,7 +234,11 @@ impl Agent { self.app .indexes .file - .by_repo(&symbol_metadata.repo_path.repo, associated_langs.iter(), None) + .by_repo( + &symbol_metadata.repo_path.repo, + associated_langs.iter(), + None, + ) .await }; diff --git a/server/bleep/src/agent/tools/code.rs b/server/bleep/src/agent/tools/code.rs index f40fe1d6d4..08af78958b 100644 --- a/server/bleep/src/agent/tools/code.rs +++ b/server/bleep/src/agent/tools/code.rs @@ -8,7 +8,8 @@ use crate::{ }, analytics::EventData, llm_gateway, - query::parser::Literal, semantic::SemanticSearchParams, + query::parser::Literal, + semantic::SemanticSearchParams, }; impl Agent { diff --git a/server/bleep/src/agent/tools/path.rs b/server/bleep/src/agent/tools/path.rs index c1434cc330..35ea8b4fb6 100644 --- a/server/bleep/src/agent/tools/path.rs +++ b/server/bleep/src/agent/tools/path.rs @@ -42,7 +42,7 @@ impl Agent { query: query.into(), paths: vec![], repos: self.relevant_repos(), - semantic_params: SemanticSearchParams { + semantic_params: SemanticSearchParams { limit: 30, offset: 0, threshold: 0.0, diff --git a/server/bleep/src/agent/tools/proc.rs b/server/bleep/src/agent/tools/proc.rs index 81b53cd3a8..f5c7b81e68 100644 --- a/server/bleep/src/agent/tools/proc.rs +++ b/server/bleep/src/agent/tools/proc.rs @@ -7,7 +7,8 @@ use crate::{ Agent, AgentSemanticSearchParams, }, analytics::EventData, - query::parser::Literal, semantic::SemanticSearchParams, + query::parser::Literal, + semantic::SemanticSearchParams, }; impl Agent { diff --git a/server/bleep/src/intelligence/code_navigation.rs b/server/bleep/src/intelligence/code_navigation.rs index 322e607d71..0f18f69a16 100644 --- a/server/bleep/src/intelligence/code_navigation.rs +++ b/server/bleep/src/intelligence/code_navigation.rs @@ -7,8 +7,9 @@ use std::{collections::HashSet, ops::Not}; use super::NodeKind; use crate::{ indexes::reader::ContentDocument, + repo::RepoRef, snippet::{Snipper, Snippet}, - text_range::TextRange, repo::RepoRef, + text_range::TextRange, }; use rayon::prelude::*; diff --git a/server/bleep/src/semantic.rs b/server/bleep/src/semantic.rs index 6ec83414b2..b5d5a18e7c 100644 --- a/server/bleep/src/semantic.rs +++ b/server/bleep/src/semantic.rs @@ -538,7 +538,7 @@ impl Semantic { limit, offset, threshold, - exact_match: exact + exact_match: exact, } = params; // TODO: Remove the need for `retrieve_more`. It's here because: From b1219108c5f4c4bcac6a63179f09ace08ff216b2 Mon Sep 17 00:00:00 2001 From: Gabriel Gordon-Hall Date: Fri, 12 Jan 2024 17:40:06 +0000 Subject: [PATCH 31/88] Path search edits (#1200) * add repo name to path tool answer, use skim_fuzzy_path_match instead of fuzzy_path_match and use only repos from the project * filter fuzzy path search by language and remove unused code paths --------- Co-authored-by: rafael <22560219+rmuller-ml@users.noreply.github.com> --- server/bleep/src/agent.rs | 4 +- server/bleep/src/agent/tools/path.rs | 2 +- server/bleep/src/indexes/file.rs | 229 +++------------------------ server/bleep/src/webserver/search.rs | 1 + 4 files changed, 23 insertions(+), 213 deletions(-) diff --git a/server/bleep/src/agent.rs b/server/bleep/src/agent.rs index 9d1abbe5c9..9c8886aea6 100644 --- a/server/bleep/src/agent.rs +++ b/server/bleep/src/agent.rs @@ -463,7 +463,6 @@ impl Agent { query: &str, ) -> impl Iterator + 'a { let langs = self.last_exchange().query.langs.iter().map(Deref::deref); - let user_id = self.user.username().expect("didn't have user ID"); let (repos, branches): (Vec<_>, Vec<_>) = sqlx::query! { @@ -480,6 +479,7 @@ impl Agent { let repo_ref = RepoRef::from_str(&row.repo_ref).ok()?; Some((repo_ref, row.branch)) }) + .filter(|(repo_ref, _)| self.repo_refs.contains(repo_ref)) .unzip(); let branch = branches.first().cloned().flatten(); @@ -488,7 +488,7 @@ impl Agent { self.app .indexes .file - .fuzzy_path_match(repos.into_iter(), branch.as_deref(), query, langs, 50) + .skim_fuzzy_path_match(repos.into_iter(), query, branch.as_deref(), langs, 50) .await } diff --git a/server/bleep/src/agent/tools/path.rs b/server/bleep/src/agent/tools/path.rs index 35ea8b4fb6..74f8c276fc 100644 --- a/server/bleep/src/agent/tools/path.rs +++ b/server/bleep/src/agent/tools/path.rs @@ -64,7 +64,7 @@ impl Agent { let mut paths = paths .iter() - .map(|repo_path| (self.get_path_alias(repo_path), repo_path.path.to_string())) + .map(|repo_path| (self.get_path_alias(repo_path), repo_path.to_string())) .collect::>(); paths.sort_by(|a: &(usize, String), b| a.0.cmp(&b.0)); // Sort by alias diff --git a/server/bleep/src/indexes/file.rs b/server/bleep/src/indexes/file.rs index d5d3592dea..6d866d6206 100644 --- a/server/bleep/src/indexes/file.rs +++ b/server/bleep/src/indexes/file.rs @@ -1,5 +1,5 @@ use std::{ - collections::{HashMap, HashSet}, + collections::HashSet, panic::AssertUnwindSafe, path::{Path, PathBuf}, sync::atomic::{AtomicU64, Ordering}, @@ -32,7 +32,6 @@ use super::{ DocumentRead, Indexable, Indexer, }; use crate::{ - agent::Project, background::SyncHandle, cache::{CacheKeys, FileCache, FileCacheSnapshot}, intelligence::TreeSitterFile, @@ -239,136 +238,12 @@ impl Indexable for File { } impl Indexer { - /// Search this index for paths fuzzily matching a given string. - /// - /// For example, the string `Cargo` can return documents whose path is `foo/Cargo.toml`, - /// or `bar/Cargo.lock`. Constructs regexes that permit an edit-distance of 2. - /// - /// If the regex filter fails to build, an empty list is returned. - pub async fn fuzzy_path_match( - &self, - repos: impl Iterator, - branch: Option<&str>, - query_str: &str, - langs: impl Iterator, - limit: usize, - ) -> impl Iterator + '_ { - // lifted from query::compiler - let searcher = self.reader.searcher(); - let collector = TopDocs::with_limit(5 * limit); // TODO: tune this - let file_source = &self.source; - - let branch_scope = branch - .map(|b| { - trigrams(b) - .map(|token| Term::from_field_text(self.source.branches, token.as_str())) - .map(|term| TermQuery::new(term, IndexRecordOption::Basic)) - .map(Box::new) - .map(|q| q as Box) - .collect::>() - }) - .map(BooleanQuery::intersection); - - let repo_scope = BooleanQuery::union( - repos - .map(|repo| { - Box::new(TermQuery::new( - Term::from_field_text(self.source.repo_name, &repo.to_string()), - IndexRecordOption::Basic, - )) as Box - }) - .collect::>(), - ); - - // hits is a mapping between a document address and the number of trigrams in it that - // matched the query - let langs_query = BooleanQuery::union( - langs - .map(|l| Term::from_field_bytes(self.source.lang, l.as_bytes())) - .map(|t| TermQuery::new(t, IndexRecordOption::Basic)) - .map(Box::new) - .map(|q| q as Box) - .collect::>(), - ); - - let mut hits = trigrams(query_str) - .flat_map(|s| case_permutations(s.as_str())) - .map(|token| Term::from_field_text(self.source.relative_path, token.as_str())) - .map(|term| TermQuery::new(term, IndexRecordOption::Basic)) - .flat_map(|query| { - let mut q: Vec> = - vec![Box::new(repo_scope.clone()), Box::new(query)]; - q.extend(branch_scope.clone().map(|q| Box::new(q) as Box)); - q.push(Box::new(langs_query.clone())); - - searcher - .search(&BooleanQuery::intersection(q), &collector) - .expect("failed to search index") - .into_iter() - .map(move |(_, addr)| addr) - }) - .fold(HashMap::new(), |mut map: HashMap<_, usize>, hit| { - *map.entry(hit).or_insert(0) += 1; - map - }) - .into_iter() - .map(move |(addr, count)| { - let retrieved_doc = searcher - .doc(addr) - .expect("failed to get document by address"); - let doc = FileReader.read_document(file_source, retrieved_doc); - (doc, count) - }) - .collect::>(); - - // order hits in - // - decsending order of number of matched trigrams - // - alphabetical order of relative paths to break ties - // - // - // for a list of hits like so: - // - // apple.rs 2 - // ball.rs 3 - // cat.rs 2 - // - // the ordering produced is: - // - // ball.rs 3 -- highest number of hits - // apple.rs 2 -- same numeber of hits, but alphabetically preceeds cat.rs - // cat.rs 2 - // - hits.sort_by(|(this_doc, this_count), (other_doc, other_count)| { - let order_count_desc = other_count.cmp(this_count); - let order_path_asc = this_doc - .relative_path - .as_str() - .cmp(other_doc.relative_path.as_str()); - - order_count_desc.then(order_path_asc) - }); - - let regex_filter = build_fuzzy_regex_filter(query_str); - - // if the regex filter fails to build for some reason, the filter defaults to returning - // false and zero results are produced - hits.into_iter() - .map(|(doc, _)| doc) - .filter(move |doc| { - regex_filter - .as_ref() - .map(|f| f.is_match(&doc.relative_path)) - .unwrap_or_default() - }) - .filter(|doc| !doc.relative_path.ends_with('/')) // omit directories - .take(limit) - } - pub async fn skim_fuzzy_path_match( &self, repo_refs: impl IntoIterator, query_str: &str, branch: Option<&str>, + langs: impl Iterator, limit: usize, ) -> impl Iterator + '_ { let searcher = self.reader.searcher(); @@ -400,6 +275,19 @@ impl Indexer { }) .map(BooleanQuery::intersection) .map(Box::new); + + let langs_term = langs + .map(|l| Term::from_field_bytes(self.source.lang, l.as_bytes())) + .map(|t| TermQuery::new(t, IndexRecordOption::Basic)) + .map(Box::new) + .map(|q| q as Box) + .collect::>(); + + let langs_term = match langs_term.len() { + 0 => None, + _ => Some(Box::new(BooleanQuery::union(langs_term))), + }; + let search_terms = trigrams(query_str) .flat_map(|s| case_permutations(s.as_str())) .map(|token| Term::from_field_text(self.source.relative_path, token.as_str())) @@ -413,6 +301,10 @@ impl Indexer { .as_ref() .map(Box::clone) .map(|t| t as Box), + langs_term + .as_ref() + .map(Box::clone) + .map(|t| t as Box), ] .into_iter() .flatten() @@ -886,86 +778,3 @@ impl RepoFile { )) } } - -fn build_fuzzy_regex_filter(query_str: &str) -> Option { - fn additions(s: &str, i: usize, j: usize) -> String { - if i > j { - additions(s, j, i) - } else { - let mut s = s.to_owned(); - s.insert_str(j, ".?"); - s.insert_str(i, ".?"); - s - } - } - - fn replacements(s: &str, i: usize, j: usize) -> String { - if i > j { - replacements(s, j, i) - } else { - let mut s = s.to_owned(); - s.remove(j); - s.insert_str(j, ".?"); - - s.remove(i); - s.insert_str(i, ".?"); - - s - } - } - - fn one_of_each(s: &str, i: usize, j: usize) -> String { - if i > j { - one_of_each(s, j, i) - } else { - let mut s = s.to_owned(); - s.remove(j); - s.insert_str(j, ".?"); - - s.insert_str(i, ".?"); - s - } - } - - let all_regexes = (query_str.char_indices().map(|(idx, _)| idx)) - .flat_map(|i| (query_str.char_indices().map(|(idx, _)| idx)).map(move |j| (i, j))) - .filter(|(i, j)| i <= j) - .flat_map(|(i, j)| { - let mut v = vec![]; - if j != query_str.len() { - v.push(one_of_each(query_str, i, j)); - v.push(replacements(query_str, i, j)); - } - v.push(additions(query_str, i, j)); - v - }); - - regex::RegexSetBuilder::new(all_regexes) - // Increased from the default to account for long paths. At the time of writing, - // the default was `10 * (1 << 20)`. - .size_limit(10 * (1 << 25)) - .case_insensitive(true) - .build() - .ok() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn fuzzy_multibyte_should_compile() { - let multibyte_str = "查询解析器在哪"; - let filter = build_fuzzy_regex_filter(multibyte_str); - assert!(filter.is_some()); - - // tests removal of second character - assert!(filter.as_ref().unwrap().is_match("查解析器在哪")); - - // tests replacement of second character with `n` - assert!(filter.as_ref().unwrap().is_match("查n析器在哪")); - - // tests addition of character `n` - assert!(filter.as_ref().unwrap().is_match("查询解析器在哪n")); - } -} diff --git a/server/bleep/src/webserver/search.rs b/server/bleep/src/webserver/search.rs index 25b943c45a..1c88cd8e15 100644 --- a/server/bleep/src/webserver/search.rs +++ b/server/bleep/src/webserver/search.rs @@ -64,6 +64,7 @@ pub(super) async fn fuzzy_path( repo_refs, target, q.first_branch().as_deref(), + std::iter::empty(), args.page_size, ) .await From 829a7e0314d4a791452e005c2c76b9722b62547a Mon Sep 17 00:00:00 2001 From: calyptobai <111788964+calyptobai@users.noreply.github.com> Date: Mon, 15 Jan 2024 22:17:50 -0500 Subject: [PATCH 32/88] Add repos to answer action prompt and step prompt (#1198) * Add repos to answer action prompt and step prompt * limit number of tokens for symbol classification * tweak prompt text --------- Co-authored-by: Gabriel Gordon-Hall --- server/bleep/src/agent/prompts.rs | 25 ++++++++++++++++++------- server/bleep/src/agent/symbol.rs | 3 ++- server/bleep/src/agent/tools/answer.rs | 7 ++++++- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/server/bleep/src/agent/prompts.rs b/server/bleep/src/agent/prompts.rs index 0fb385d9f2..c65dea0ab5 100644 --- a/server/bleep/src/agent/prompts.rs +++ b/server/bleep/src/agent/prompts.rs @@ -1,3 +1,5 @@ +use std::collections::HashSet; + use crate::agent::exchange::RepoPath; pub fn functions(add_proc: bool) -> serde_json::Value { @@ -82,13 +84,22 @@ pub fn functions(add_proc: bool) -> serde_json::Value { } pub fn system<'a>(paths: impl IntoIterator) -> String { + let paths = paths.into_iter().collect::>(); + let mut s = "".to_string(); - let mut paths = paths.into_iter().peekable(); + let repos = paths.iter().map(|rp| &rp.repo).collect::>(); + + s.push_str("## REPOS ##\n"); + for repo in repos { + s.push_str(&format!("{repo}\n")); + } + + let mut iter = paths.into_iter().peekable(); - if paths.peek().is_some() { - s.push_str("## PATHS ##\nindex, repo, path\n"); - for (i, path) in paths.enumerate() { + if iter.peek().is_some() { + s.push_str("\n## PATHS ##\nindex, repo, path\n"); + for (i, path) in iter.enumerate() { let repo = path.repo.display_name(); let path = &path.path; s.push_str(&format!("{}, {}, {}\n", i, repo, path)); @@ -101,7 +112,7 @@ pub fn system<'a>(paths: impl IntoIterator) -> String { - ALWAYS call a function, DO NOT answer the question directly, even if the query is not in English - DO NOT call a function that you've used before with the same arguments -- DO NOT assume the structure of the codebase, or the existence of files or folders +- DO NOT assume the structure of the indexed repos (listed above), or the existence of files or folders - Your queries to functions.code or functions.path should be significantly different to previous queries - Call functions.none with paths that you are confident will help answer the user's query, include paths containing the information needed for a complete answer including definitions and references - If the user query is general (e.g. 'What does this do?', 'What is this repo?') look for READMEs, documentation and entry points in the code (main files, index files, api files etc.) @@ -121,7 +132,7 @@ pub fn answer_article_prompt(context: &str) -> String { format!( r#"{context}#### -You are an expert programmer called 'bloop' and you are helping a junior colleague answer questions about a codebase using the information above. If their query refers to 'this' or 'it' and there is no other context, assume that it refers to the information above. +You are an expert programmer called 'bloop' and you are helping a junior colleague answer questions about some repos using the information above. If their query refers to 'this' or 'it' and there is no other context, assume that it refers to the information above. Provide only as much information and code as is necessary to answer the query, but be concise. Keep number of quoted lines to a minimum when possible. If you do not have enough information needed to answer the query, do not make up an answer. Infer as much as possible from the information above. When referring to code, you must provide an example in a code block. @@ -397,7 +408,7 @@ pub fn symbol_classification_prompt(snippets: &str) -> String { Above are code chunks and non-local symbols that have been extracted from the chunks. Each chunk is followed by an enumerated list of symbols that it contains. Given a user query, select the symbol which is most relevant to it, e.g. the references or definition of this symbol would help somebody answer the query. Symbols which are language builtins or which come from third party libraries are unlikely to be helpful. -Do not answer with the symbol name, use the symbol index. +Do not answer with the symbol name, use the symbol index. If none of the symbols are relevant, answer with 0. ### Examples ### Q: how does ranking work? diff --git a/server/bleep/src/agent/symbol.rs b/server/bleep/src/agent/symbol.rs index b3099f77f0..d40f7e90e6 100644 --- a/server/bleep/src/agent/symbol.rs +++ b/server/bleep/src/agent/symbol.rs @@ -163,7 +163,7 @@ impl Agent { format!( "```{}\n{}```\n\n{}", - c.repo_path.path.clone(), + c.repo_path, c.snippet.clone(), symbols_string ) @@ -182,6 +182,7 @@ impl Agent { .clone() .model("gpt-4-0613") .temperature(0.0) + .max_tokens(5) .chat(&messages, None) .await { diff --git a/server/bleep/src/agent/tools/answer.rs b/server/bleep/src/agent/tools/answer.rs index 20eb3bbe7d..b8e48a59a0 100644 --- a/server/bleep/src/agent/tools/answer.rs +++ b/server/bleep/src/agent/tools/answer.rs @@ -115,8 +115,13 @@ impl Agent { debug!(?paths, ?aliases, "created filtered path alias list"); + s += "##### REPOS #####\n"; + for repo in self.relevant_repos() { + s += &format!("{repo}\n"); + } + if !aliases.is_empty() { - s += "##### PATHS #####\n"; + s += "\n##### PATHS #####\n"; for alias in &aliases { let path = &paths[*alias]; From b672f2ad5ec5ab95a3ef57c8170a180ce322d7c6 Mon Sep 17 00:00:00 2001 From: calyptobai <111788964+calyptobai@users.noreply.github.com> Date: Tue, 16 Jan 2024 12:21:29 -0500 Subject: [PATCH 33/88] Restrict queries on `/q` to only return results valid for a project (#1203) We rewrite the parsed set of queries to restrict them such that they only return valid results. * Restrict queries on `/q` to only return results valid for a project * Add project ID to autocomplete * Fix repo autocomplete --- server/bleep/sqlx-data.json | 60 +++++++---- server/bleep/src/query/execute.rs | 114 ++++++++++++++++++++- server/bleep/src/query/parser.rs | 18 ++++ server/bleep/src/webserver.rs | 6 +- server/bleep/src/webserver/autocomplete.rs | 33 ++++-- server/bleep/src/webserver/query.rs | 7 +- 6 files changed, 199 insertions(+), 39 deletions(-) diff --git a/server/bleep/sqlx-data.json b/server/bleep/sqlx-data.json index b8673de415..c7a6f1485b 100644 --- a/server/bleep/sqlx-data.json +++ b/server/bleep/sqlx-data.json @@ -278,6 +278,30 @@ }, "query": "SELECT id, index_status, name, url, favicon, description, modified_at FROM docs WHERE id = ?" }, + "291848ee7ef54ea247a2f83e89d2dd8e96024ba2fe65d0e443ecc94942aa6fa9": { + "describe": { + "columns": [ + { + "name": "repo_ref", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "branch", + "ordinal": 1, + "type_info": "Text" + } + ], + "nullable": [ + false, + true + ], + "parameters": { + "Right": 1 + } + }, + "query": "SELECT repo_ref, branch\n FROM project_repos\n WHERE project_id = ?" + }, "2d33f9119b3b56c55378080c5c95aa91fcb495ceb39caaa4f2541d8b2aa408ae": { "describe": { "columns": [], @@ -1074,6 +1098,24 @@ }, "query": "INSERT INTO studio_snapshots (studio_id, context, doc_context, messages)\n VALUES (?, ?, ?, ?)" }, + "95eaff0006df7f12604154c46113197e9440f06520672e2d7409cd0b831d83c2": { + "describe": { + "columns": [ + { + "name": "repo_ref", + "ordinal": 0, + "type_info": "Text" + } + ], + "nullable": [ + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "SELECT repo_ref\n FROM project_repos\n WHERE project_id = ?" + }, "9db35f3045790fbd63f1efc4a96e5a7234f09cc513323320fd145146b03cce2b": { "describe": { "columns": [ @@ -1378,24 +1420,6 @@ }, "query": "UPDATE studio_snapshots SET messages = ? WHERE id = ?" }, - "dcb7f9427283203bce10fe7d618057ef3eab5f6af2277e7a1ac8ba050609894d": { - "describe": { - "columns": [ - { - "name": "url", - "ordinal": 0, - "type_info": "Text" - } - ], - "nullable": [ - false - ], - "parameters": { - "Right": 1 - } - }, - "query": "SELECT url FROM docs WHERE id = ?" - }, "deae1c1c2619ec6e76e0b5fcc526bbabbc1d66642efc6158a793068221ebd019": { "describe": { "columns": [ diff --git a/server/bleep/src/query/execute.rs b/server/bleep/src/query/execute.rs index 5e4739db90..e1fcd715fe 100644 --- a/server/bleep/src/query/execute.rs +++ b/server/bleep/src/query/execute.rs @@ -1,4 +1,5 @@ use std::{ + borrow::Cow, collections::{HashMap, HashSet}, sync::Arc, }; @@ -12,6 +13,7 @@ use crate::{ }, repo::RepoRef, snippet::{HighlightedString, SnippedFile, Snipper}, + Application, }; use anyhow::{bail, Result}; @@ -231,11 +233,113 @@ pub trait ExecuteQuery { } impl ApiQuery { - pub async fn query(self: Arc, indexes: Arc) -> Result { - let query = self.q.clone(); - let compiled = parser::parse(&query)?; - tracing::debug!("compiled query as {compiled:?}"); - self.query_with(indexes, compiled).await + pub async fn query(self: Arc, app: &Application) -> Result { + let raw_query = self.q.clone(); + let queries = self + .restrict_queries(parser::parse(&raw_query)?, app) + .await?; + tracing::debug!("compiled query as {queries:?}"); + self.query_with(Arc::clone(&app.indexes), queries).await + } + + /// This restricts a set of input parser queries. + /// + /// We trim down the input by: + /// + /// 1. Discarding all queries that reference repos not in the queried project + /// 2. Regenerating more specific queries for those without repo restrictions, such that there + /// is a new query generated per repo that exists in the project. + /// + /// The idea here is to allow us to restrict the possible input space of queried documents to + /// be more specific as required by the project state. + /// + /// The `subset` flag indicates whether repo name matching is whole-string, or whether the + /// string must only be a substring of an existing repo. This is useful in autocomplete + /// scenarios, where we want to restrict queries such that they are not fully typed out. + pub async fn restrict_queries<'a>( + &self, + queries: impl IntoIterator>, + app: &Application, + ) -> Result>> { + let repo_branches = sqlx::query! { + "SELECT repo_ref, branch + FROM project_repos + WHERE project_id = ?", + self.project_id, + } + .fetch_all(&*app.sql) + .await? + .into_iter() + .map(|row| { + ( + row.repo_ref.parse::().unwrap().indexed_name(), + row.branch, + ) + }) + .collect::>(); + + let mut out = Vec::new(); + + for q in queries { + if let Some(r) = q.repo_str() { + // The branch that this project has loaded this repo with. + let project_branch = repo_branches.get(&r).map(Option::as_ref).flatten(); + + // If the branch doesn't match what we expect, drop the query. + if q.branch_str().as_ref() == project_branch { + out.push(q); + } + } else { + for (r, b) in &repo_branches { + out.push(parser::Query { + repo: Some(parser::Literal::from(r)), + branch: b.as_ref().map(|b| parser::Literal::from(b)), + ..q.clone() + }); + } + } + } + + Ok(out) + } + + /// This restricts a set of input repo-only queries. + /// + /// This is useful for autocomplete queries, which are effectively just `repo:foo`, where the + /// repo name may be partially written. + pub async fn restrict_repo_queries<'a>( + &self, + queries: impl IntoIterator>, + app: &Application, + ) -> Result>> { + let repo_refs = sqlx::query! { + "SELECT repo_ref + FROM project_repos + WHERE project_id = ?", + self.project_id, + } + .fetch_all(&*app.sql) + .await? + .into_iter() + .map(|row| row.repo_ref.parse::().unwrap().indexed_name()) + .collect::>(); + + let mut out = Vec::new(); + + for q in queries { + if let Some(r) = q.repo_str() { + for m in repo_refs.iter().filter(|r2| r2.contains(&r)) { + out.push(parser::Query { + repo: Some(parser::Literal::from(m)), + ..Default::default() + }); + } + } + } + + out.dedup(); + + Ok(out) } pub async fn query_with( diff --git a/server/bleep/src/query/parser.rs b/server/bleep/src/query/parser.rs index 0504fa2952..4f82efc00a 100644 --- a/server/bleep/src/query/parser.rs +++ b/server/bleep/src/query/parser.rs @@ -168,6 +168,24 @@ impl<'a> Query<'a> { } } +impl<'a> Query<'a> { + /// Get the `repo` value for this query as a plain string. + pub fn repo_str(&self) -> Option { + self.repo + .as_ref() + .and_then(Literal::as_plain) + .map(Cow::into_owned) + } + + /// Get the `branch` value for this query as a plain string. + pub fn branch_str(&self) -> Option { + self.branch + .as_ref() + .and_then(Literal::as_plain) + .map(Cow::into_owned) + } +} + impl<'a> Target<'a> { /// Get the inner literal for this target, regardless of the variant. pub fn literal_mut(&'a mut self) -> &mut Literal<'a> { diff --git a/server/bleep/src/webserver.rs b/server/bleep/src/webserver.rs index 1126391f6b..369a70a092 100644 --- a/server/bleep/src/webserver.rs +++ b/server/bleep/src/webserver.rs @@ -50,8 +50,6 @@ pub async fn start(app: Application) -> anyhow::Result<()> { let mut api = Router::new() .route("/config", get(config::get).put(config::put)) - // autocomplete - .route("/autocomplete", get(autocomplete::handle)) // indexing .route("/index", get(index::handle)) // repo management @@ -119,6 +117,10 @@ pub async fn start(app: Application) -> anyhow::Result<()> { get(conversation::get).delete(conversation::delete), ) .route("/projects/:project_id/q", get(query::handle)) + .route( + "/projects/:project_id/autocomplete", + get(autocomplete::handle), + ) .route("/projects/:project_id/search/path", get(search::fuzzy_path)) .route("/projects/:project_id/answer/vote", post(answer::vote)) .route("/projects/:project_id/answer", get(answer::answer)) diff --git a/server/bleep/src/webserver/autocomplete.rs b/server/bleep/src/webserver/autocomplete.rs index b6afd07085..523429d351 100644 --- a/server/bleep/src/webserver/autocomplete.rs +++ b/server/bleep/src/webserver/autocomplete.rs @@ -1,19 +1,21 @@ -use std::{collections::HashMap, sync::Arc}; +use std::collections::HashMap; use super::prelude::*; use crate::{ - indexes::{ - reader::{ContentReader, FileReader, RepoReader}, - Indexes, - }, + indexes::reader::{ContentReader, FileReader, RepoReader}, query::{ execute::{ApiQuery, ExecuteQuery, QueryResult}, languages, parser, parser::{Literal, Target}, }, + Application, }; -use axum::{extract::Query, response::IntoResponse as IntoAxumResponse, Extension}; +use axum::{ + extract::{Path, Query}, + response::IntoResponse as IntoAxumResponse, + Extension, +}; use futures::{stream, StreamExt, TryStreamExt}; use serde::Serialize; @@ -36,12 +38,15 @@ pub struct AutocompleteParams { pub(super) async fn handle( Query(mut api_params): Query, Query(ac_params): Query, - Extension(indexes): Extension>, + Path(project_id): Path, + Extension(app): Extension, ) -> Result { // Override page_size and set to low value api_params.page = 0; api_params.page_size = 8; + api_params.project_id = project_id; + let mut partial_lang = None; let mut has_target = false; @@ -114,17 +119,25 @@ pub(super) async fn handle( ); } + // NB: This restricts queries in a repo-specific way. This might need to be generalized if + // we still use the other autocomplete fields. + let repo_queries = api_params + .restrict_repo_queries(queries.clone(), &app) + .await?; + + dbg!(&queries, &repo_queries); + let mut engines = vec![]; if ac_params.content { - engines.push(ContentReader.execute(&indexes.file, &queries, &api_params)); + engines.push(ContentReader.execute(&app.indexes.file, &queries, &api_params)); } if ac_params.repo { - engines.push(RepoReader.execute(&indexes.repo, &queries, &api_params)); + engines.push(RepoReader.execute(&app.indexes.repo, &repo_queries, &api_params)); } if ac_params.file { - engines.push(FileReader.execute(&indexes.file, &queries, &api_params)); + engines.push(FileReader.execute(&app.indexes.file, &queries, &api_params)); } let (langs, list) = stream::iter(engines) diff --git a/server/bleep/src/webserver/query.rs b/server/bleep/src/webserver/query.rs index ebd0105626..498e6a80d2 100644 --- a/server/bleep/src/webserver/query.rs +++ b/server/bleep/src/webserver/query.rs @@ -1,4 +1,4 @@ -use axum::extract::{Path, State}; +use axum::extract::Path; use super::prelude::*; use crate::{db::QueryLog, query::execute::ApiQuery, Application}; @@ -6,15 +6,14 @@ use crate::{db::QueryLog, query::execute::ApiQuery, Application}; pub(super) async fn handle( Path(project_id): Path, Query(mut api_params): Query, - Extension(indexes): Extension>, - State(app): State, + Extension(app): Extension, ) -> impl IntoResponse { QueryLog::new(&app.sql).insert(&api_params.q).await?; api_params.project_id = project_id; Arc::new(api_params) - .query(indexes) + .query(&app) .await .map(json) .map_err(super::Error::from) From c9b09002ab0ec644774526121f77aacb43681120 Mon Sep 17 00:00:00 2001 From: Gabriel Gordon-Hall Date: Wed, 17 Jan 2024 10:55:48 +0000 Subject: [PATCH 34/88] fix default for repo deduplication in semantic search --- server/bleep/src/semantic.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/bleep/src/semantic.rs b/server/bleep/src/semantic.rs index b5d5a18e7c..c6f60628e2 100644 --- a/server/bleep/src/semantic.rs +++ b/server/bleep/src/semantic.rs @@ -952,7 +952,7 @@ pub fn deduplicate_with_mmr( equation_score += 0.75_f32.powi(*path_count); // MMR + (3/4)^n where n is the number of times a repo has been selected - let repo_count = repo_counts.get(repos[i]).unwrap_or(&0); + let repo_count = repo_counts.get(repos[i]).unwrap_or(&1); equation_score += 0.75_f32.powi(*repo_count); if equation_score > best_score { From 6d139755047686dc8101036d14bf8fb01d7de955 Mon Sep 17 00:00:00 2001 From: calyptobai Date: Wed, 17 Jan 2024 12:59:39 -0500 Subject: [PATCH 35/88] Fix autocomplete for path and lang queries --- server/bleep/src/webserver/autocomplete.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/server/bleep/src/webserver/autocomplete.rs b/server/bleep/src/webserver/autocomplete.rs index 523429d351..1968d8c8a7 100644 --- a/server/bleep/src/webserver/autocomplete.rs +++ b/server/bleep/src/webserver/autocomplete.rs @@ -125,11 +125,13 @@ pub(super) async fn handle( .restrict_repo_queries(queries.clone(), &app) .await?; - dbg!(&queries, &repo_queries); + let restricted_queries = api_params + .restrict_queries(queries.clone(), &app) + .await?; let mut engines = vec![]; if ac_params.content { - engines.push(ContentReader.execute(&app.indexes.file, &queries, &api_params)); + engines.push(ContentReader.execute(&app.indexes.file, &restricted_queries, &api_params)); } if ac_params.repo { @@ -137,7 +139,7 @@ pub(super) async fn handle( } if ac_params.file { - engines.push(FileReader.execute(&app.indexes.file, &queries, &api_params)); + engines.push(FileReader.execute(&app.indexes.file, &restricted_queries, &api_params)); } let (langs, list) = stream::iter(engines) From 9591707dafb82c47117c7aff7ba9139bd812a8d5 Mon Sep 17 00:00:00 2001 From: calyptobai Date: Wed, 17 Jan 2024 15:29:52 -0500 Subject: [PATCH 36/88] Send back context data in studio list route --- server/bleep/src/webserver/studio.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/server/bleep/src/webserver/studio.rs b/server/bleep/src/webserver/studio.rs index d8ade28e99..4c46bb30e9 100644 --- a/server/bleep/src/webserver/studio.rs +++ b/server/bleep/src/webserver/studio.rs @@ -357,6 +357,14 @@ pub struct ListItem { modified_at: NaiveDateTime, repos: Vec, most_common_ext: String, + context: Vec, +} + +#[derive(serde::Serialize)] +struct ListItemContextFile { + path: String, + hidden: bool, + ranges: Vec>, } pub async fn list( @@ -421,6 +429,14 @@ pub async fn list( modified_at: studio.modified_at, repos: repos.into_iter().collect::>(), most_common_ext, + context: context + .iter() + .map(|c| ListItemContextFile { + path: c.path.clone(), + hidden: c.hidden, + ranges: c.ranges.clone(), + }) + .collect(), }; list_items.push(list_item); From f2a6cf84e237ce0c3b4336bf0ae6f025ffd7b0c2 Mon Sep 17 00:00:00 2001 From: calyptobai <111788964+calyptobai@users.noreply.github.com> Date: Fri, 19 Jan 2024 05:08:49 -0500 Subject: [PATCH 37/88] Fix code search on local repos (#1204) * Fix code search on local repos We were making semantic queries with the full stringified repo ref. Instead, we should have been constructing a semantic query using the repository display name. It seems that this was fixed coincidentally in #1190 via a condition (which might now be possible to remove). * Use indexed name in semantic query construction --- server/bleep/src/agent.rs | 2 +- server/bleep/src/semantic.rs | 9 +-------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/server/bleep/src/agent.rs b/server/bleep/src/agent.rs index 9c8886aea6..6c7d3778ca 100644 --- a/server/bleep/src/agent.rs +++ b/server/bleep/src/agent.rs @@ -432,7 +432,7 @@ impl Agent { target: Some(query), repos: repos .iter() - .map(RepoRef::to_string) + .map(RepoRef::indexed_name) .map(|r| parser::Literal::Plain(r.into())) .collect(), paths, diff --git a/server/bleep/src/semantic.rs b/server/bleep/src/semantic.rs index c6f60628e2..46cbca2188 100644 --- a/server/bleep/src/semantic.rs +++ b/server/bleep/src/semantic.rs @@ -808,14 +808,7 @@ fn build_conditions( let repo_filter = { let conditions = query .repos() - .map(|r| { - if r.contains('/') && !r.starts_with("github.com/") { - format!("github.com/{r}") - } else { - r.to_string() - } - }) - .map(|r| make_kv_keyword_filter("repo_name", r.as_ref()).into()) + .map(|r| make_kv_keyword_filter("repo_name", &r).into()) .collect::>(); // one of the above repos should match if conditions.is_empty() { From b78b336bfa5c6d2ace2ee4282830c2256ac7969b Mon Sep 17 00:00:00 2001 From: Anastasiia Solop <35258279+anastasiya1155@users.noreply.github.com> Date: Fri, 19 Jan 2024 06:39:41 -0500 Subject: [PATCH 38/88] Add fields to list studios (projects) (#1205) * return token counter, doc_context and full context in list studios route --- server/bleep/sqlx-data.json | 16 +++++++++-- server/bleep/src/webserver/autocomplete.rs | 4 +-- server/bleep/src/webserver/studio.rs | 32 ++++++++++------------ 3 files changed, 30 insertions(+), 22 deletions(-) diff --git a/server/bleep/sqlx-data.json b/server/bleep/sqlx-data.json index c7a6f1485b..665580f8c7 100644 --- a/server/bleep/sqlx-data.json +++ b/server/bleep/sqlx-data.json @@ -1492,7 +1492,7 @@ }, "query": "UPDATE project_repos\n SET branch = ?\n WHERE project_id = ? AND repo_ref = ?\n RETURNING id" }, - "ebdb202965998c4e29727dca19a60c82b2a4574530858e29bf6584c8b6e1f32b": { + "e72d027c61c96b02889751f6817129ea73d23383be3dae5fc1bffdc6e7b2c46d": { "describe": { "columns": [ { @@ -1514,19 +1514,31 @@ "name": "context", "ordinal": 3, "type_info": "Text" + }, + { + "name": "doc_context", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "messages", + "ordinal": 5, + "type_info": "Text" } ], "nullable": [ false, true, false, + false, + false, false ], "parameters": { "Right": 2 } }, - "query": "SELECT\n s.id,\n s.name,\n ss.modified_at as \"modified_at!\",\n ss.context\n FROM studios s\n INNER JOIN studio_snapshots ss ON s.id = ss.studio_id\n INNER JOIN projects p ON p.id = s.project_id\n WHERE s.project_id = ? AND p.user_id = ? AND (ss.studio_id, ss.modified_at) IN (\n SELECT studio_id, MAX(modified_at)\n FROM studio_snapshots\n GROUP BY studio_id\n )" + "query": "SELECT\n s.id,\n s.name,\n ss.modified_at as \"modified_at!\",\n ss.context,\n ss.doc_context,\n ss.messages\n FROM studios s\n INNER JOIN studio_snapshots ss ON s.id = ss.studio_id\n INNER JOIN projects p ON p.id = s.project_id\n WHERE s.project_id = ? AND p.user_id = ? AND (ss.studio_id, ss.modified_at) IN (\n SELECT studio_id, MAX(modified_at)\n FROM studio_snapshots\n GROUP BY studio_id\n )" }, "ec193a038eb7fc3aaca3c3adebcc4dbde01b47ae34ac2df2227c5e5459617182": { "describe": { diff --git a/server/bleep/src/webserver/autocomplete.rs b/server/bleep/src/webserver/autocomplete.rs index 1968d8c8a7..9ed9a45309 100644 --- a/server/bleep/src/webserver/autocomplete.rs +++ b/server/bleep/src/webserver/autocomplete.rs @@ -125,9 +125,7 @@ pub(super) async fn handle( .restrict_repo_queries(queries.clone(), &app) .await?; - let restricted_queries = api_params - .restrict_queries(queries.clone(), &app) - .await?; + let restricted_queries = api_params.restrict_queries(queries.clone(), &app).await?; let mut engines = vec![]; if ac_params.content { diff --git a/server/bleep/src/webserver/studio.rs b/server/bleep/src/webserver/studio.rs index 4c46bb30e9..c57442907e 100644 --- a/server/bleep/src/webserver/studio.rs +++ b/server/bleep/src/webserver/studio.rs @@ -357,14 +357,9 @@ pub struct ListItem { modified_at: NaiveDateTime, repos: Vec, most_common_ext: String, - context: Vec, -} - -#[derive(serde::Serialize)] -struct ListItemContextFile { - path: String, - hidden: bool, - ranges: Vec>, + context: Vec, + doc_context: Vec, + token_counts: TokenCounts, } pub async fn list( @@ -379,7 +374,9 @@ pub async fn list( s.id, s.name, ss.modified_at as \"modified_at!\", - ss.context + ss.context, + ss.doc_context, + ss.messages FROM studios s INNER JOIN studio_snapshots ss ON s.id = ss.studio_id INNER JOIN projects p ON p.id = s.project_id @@ -399,6 +396,10 @@ pub async fn list( for studio in studios { let context: Vec = serde_json::from_str(&studio.context).map_err(Error::internal)?; + let doc_context: Vec = + serde_json::from_str(&studio.doc_context).map_err(Error::internal)?; + let messages: Vec = + serde_json::from_str(&studio.messages).map_err(Error::internal)?; let repos: HashSet = context.iter().map(|file| file.repo.name.clone()).collect(); @@ -423,20 +424,17 @@ pub async fn list( .unwrap_or_default() .to_owned(); + let token_counts = token_counts((*app).clone(), &messages, &context, &doc_context).await?; + let list_item = ListItem { id: studio.id, name: studio.name.unwrap_or_else(default_studio_name), modified_at: studio.modified_at, repos: repos.into_iter().collect::>(), most_common_ext, - context: context - .iter() - .map(|c| ListItemContextFile { - path: c.path.clone(), - hidden: c.hidden, - ranges: c.ranges.clone(), - }) - .collect(), + context, + doc_context, + token_counts, }; list_items.push(list_item); From 122995a2cf6f48715c95e482d1c561c772f97559 Mon Sep 17 00:00:00 2001 From: Gabriel Gordon-Hall Date: Fri, 19 Jan 2024 12:51:45 +0000 Subject: [PATCH 39/88] make studio prompt multi-repo (#1208) --- server/bleep/src/agent/prompts.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/bleep/src/agent/prompts.rs b/server/bleep/src/agent/prompts.rs index c65dea0ab5..059b18679b 100644 --- a/server/bleep/src/agent/prompts.rs +++ b/server/bleep/src/agent/prompts.rs @@ -270,14 +270,14 @@ You must use the following formatting rules at all times: }} ```` - When quoting code in a code block, use the following info string format: language:LANG,path:PATH - - For example, to quote `src/main.c`: - ````language:c,path:src/main.c + - For example, to quote `github.com/org/repo:src/main.c`: + ````language:c,path:github.com/org/repo:src/main.c int main() {{ printf("hello world!"); }} ```` - - For example, to quote `index.js`: - ````language:javascript,path:index.js + - For example, to quote `local//path/to/repo:index.js`: + ````language:javascript,path:local//path/to/repo:index.js console.log("hello world!") ```` - Basic markdown is otherwise allowed"# From f699f935ac1ff047f0afc36fb34d17928cd7efce Mon Sep 17 00:00:00 2001 From: Anastasiia Solop <35258279+anastasiya1155@users.noreply.github.com> Date: Mon, 22 Jan 2024 12:14:15 -0500 Subject: [PATCH 40/88] save onboarding status on user profile (#1210) --- server/bleep/src/user.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/bleep/src/user.rs b/server/bleep/src/user.rs index 5310f75b15..207fc358ea 100644 --- a/server/bleep/src/user.rs +++ b/server/bleep/src/user.rs @@ -14,6 +14,8 @@ pub struct UserProfile { prompt_guide: PromptGuideState, #[serde(default = "default_allow_session_recordings")] allow_session_recordings: bool, + #[serde(default = "default_is_tutorial_finished")] + is_tutorial_finished: bool, } impl Default for UserProfile { @@ -22,6 +24,7 @@ impl Default for UserProfile { username: None, prompt_guide: PromptGuideState::Active, allow_session_recordings: default_allow_session_recordings(), + is_tutorial_finished: default_is_tutorial_finished(), } } } @@ -29,3 +32,6 @@ impl Default for UserProfile { fn default_allow_session_recordings() -> bool { true } +fn default_is_tutorial_finished() -> bool { + false +} From a567c2e437b71d00a637883b38a99dc16b1ae0a1 Mon Sep 17 00:00:00 2001 From: Anastasiia Solop <35258279+anastasiya1155@users.noreply.github.com> Date: Wed, 24 Jan 2024 13:50:51 -0500 Subject: [PATCH 41/88] Anastasiia/autocomplete page size (#1211) don't override page_size from api_params --- server/bleep/src/webserver/autocomplete.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/server/bleep/src/webserver/autocomplete.rs b/server/bleep/src/webserver/autocomplete.rs index 9ed9a45309..c945590246 100644 --- a/server/bleep/src/webserver/autocomplete.rs +++ b/server/bleep/src/webserver/autocomplete.rs @@ -43,7 +43,6 @@ pub(super) async fn handle( ) -> Result { // Override page_size and set to low value api_params.page = 0; - api_params.page_size = 8; api_params.project_id = project_id; From 00a5b7b3d6b927f3d832928c80fc95aeb9523f86 Mon Sep 17 00:00:00 2001 From: Anastasiia Solop <35258279+anastasiya1155@users.noreply.github.com> Date: Fri, 26 Jan 2024 06:49:26 -0500 Subject: [PATCH 42/88] return token counts for studio snapshots (#1212) --- server/bleep/src/webserver/studio.rs | 29 ++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/server/bleep/src/webserver/studio.rs b/server/bleep/src/webserver/studio.rs index c57442907e..7aace09bb5 100644 --- a/server/bleep/src/webserver/studio.rs +++ b/server/bleep/src/webserver/studio.rs @@ -1683,6 +1683,7 @@ pub struct Snapshot { context: Vec, doc_context: Vec, messages: Vec, + token_counts: TokenCounts, } pub async fn list_snapshots( @@ -1705,16 +1706,24 @@ pub async fn list_snapshots( } .fetch(&*app.sql) .map_err(Error::internal) - .and_then(|r| async move { - Ok(Snapshot { - id: r.id, - modified_at: r.modified_at, - context: serde_json::from_str(&r.context).context("failed to deserialize context")?, - doc_context: serde_json::from_str(&r.doc_context) - .context("failed to deserialize doc context")?, - messages: serde_json::from_str(&r.messages) - .context("failed to deserialize messages")?, - }) + .and_then(|r| { + let app = (*app).clone(); + async move { + let context: Vec = serde_json::from_str(&r.context).context("failed to deserialize context")?; + let doc_context: Vec = serde_json::from_str(&r.doc_context).context("failed to deserialize doc context")?; + let messages: Vec = serde_json::from_str(&r.messages).context("failed to deserialize messages")?; + + let token_counts = token_counts(app, &messages, &context, &doc_context).await?; + + Ok(Snapshot { + id: r.id, + modified_at: r.modified_at, + context, + doc_context, + messages, + token_counts, + }) + } }) .try_collect::>() .await From a3450428a80d5538cdae64df551d142ec64521b7 Mon Sep 17 00:00:00 2001 From: Gabriel Gordon-Hall Date: Fri, 26 Jan 2024 14:09:01 +0000 Subject: [PATCH 43/88] fix clippy (#1213) --- server/bleep/src/agent.rs | 19 ------------- server/bleep/src/indexes/doc.rs | 14 +++++---- server/bleep/src/lib.rs | 2 +- server/bleep/src/query/execute.rs | 5 ++-- server/bleep/src/webserver/answer.rs | 12 ++------ server/bleep/src/webserver/conversation.rs | 14 ++++----- server/bleep/src/webserver/project.rs | 6 +--- server/bleep/src/webserver/studio.rs | 33 ++++++---------------- 8 files changed, 29 insertions(+), 76 deletions(-) diff --git a/server/bleep/src/agent.rs b/server/bleep/src/agent.rs index 6c7d3778ca..1bf397843b 100644 --- a/server/bleep/src/agent.rs +++ b/server/bleep/src/agent.rs @@ -45,25 +45,6 @@ pub enum Error { Processing(anyhow::Error), } -/// A unified way to track a collection of repositories -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] -pub struct Project(pub Vec); - -impl Project { - /// This is a temporary thing to keep backwards compatibility. - /// We should have a UUID here to track this stuff consistently. - pub fn id(&self) -> String { - self.0 - .get(0) - .map(ToString::to_string) - .expect("invalid project configuration") - } - - pub fn repos(&self) -> impl Iterator + '_ { - self.0.iter().map(|r| r.display_name()) - } -} - pub struct Agent { pub app: Application, pub conversation: Conversation, diff --git a/server/bleep/src/indexes/doc.rs b/server/bleep/src/indexes/doc.rs index 7ddbf3223b..ff0c0b1c12 100644 --- a/server/bleep/src/indexes/doc.rs +++ b/server/bleep/src/indexes/doc.rs @@ -23,6 +23,7 @@ use std::{collections::HashSet, sync::Arc}; #[derive(Clone)] pub struct Doc { sql: SqlDb, + #[allow(unused)] section_index: tantivy::Index, section_schema: schema::Section, index_writer: Arc>, @@ -363,12 +364,15 @@ impl Doc { let _ = tx.send(Progress::Err(error.to_string())); // send job status to error - self.index_queue + if let Some(job) = self + .index_queue .write() .await .iter_mut() .find(|job| job.id == id) - .map(|job| job.status = STATUS_ERROR); + { + job.status = STATUS_ERROR; + } // return error Err(error)?; @@ -452,7 +456,7 @@ impl Doc { // delete old docs from tantivy // // create a checkpoint before deletion, so we can revert to here if the job is cancelled - self.index_writer.lock().await.commit(); + let _ = self.index_writer.lock().await.commit(); self.index_writer .lock() .await @@ -588,9 +592,7 @@ impl Doc { modified_at: record.modified_at, }); - Ok(queued_item - .or(indexed_item) - .ok_or(Error::InvalidDocId(id))?) + queued_item.or(indexed_item).ok_or(Error::InvalidDocId(id)) } /// Search for doc source by title diff --git a/server/bleep/src/lib.rs b/server/bleep/src/lib.rs index 53e76d996e..dac3529e7a 100644 --- a/server/bleep/src/lib.rs +++ b/server/bleep/src/lib.rs @@ -8,7 +8,7 @@ unused_qualifications )] #![warn(unused_crate_dependencies)] -#![allow(elided_lifetimes_in_paths)] +#![allow(elided_lifetimes_in_paths, clippy::diverging_sub_expression)] #[cfg(all(feature = "onnx", feature = "metal"))] compile_error!("cannot enable `onnx` and `metal` at the same time"); diff --git a/server/bleep/src/query/execute.rs b/server/bleep/src/query/execute.rs index e1fcd715fe..4647c41da6 100644 --- a/server/bleep/src/query/execute.rs +++ b/server/bleep/src/query/execute.rs @@ -1,5 +1,4 @@ use std::{ - borrow::Cow, collections::{HashMap, HashSet}, sync::Arc, }; @@ -283,7 +282,7 @@ impl ApiQuery { for q in queries { if let Some(r) = q.repo_str() { // The branch that this project has loaded this repo with. - let project_branch = repo_branches.get(&r).map(Option::as_ref).flatten(); + let project_branch = repo_branches.get(&r).and_then(Option::as_ref); // If the branch doesn't match what we expect, drop the query. if q.branch_str().as_ref() == project_branch { @@ -293,7 +292,7 @@ impl ApiQuery { for (r, b) in &repo_branches { out.push(parser::Query { repo: Some(parser::Literal::from(r)), - branch: b.as_ref().map(|b| parser::Literal::from(b)), + branch: b.as_ref().map(parser::Literal::from), ..q.clone() }); } diff --git a/server/bleep/src/webserver/answer.rs b/server/bleep/src/webserver/answer.rs index 7a528a79ab..4fb5b2be06 100644 --- a/server/bleep/src/webserver/answer.rs +++ b/server/bleep/src/webserver/answer.rs @@ -11,23 +11,20 @@ use axum::{ }; use futures::{future::Either, stream, StreamExt}; use reqwest::StatusCode; -use serde_json::json; use tracing::{debug, error, info, warn}; -use super::conversation::ConversationId; - use super::middleware::User; use crate::{ agent::{ self, exchange::{CodeChunk, Exchange, FocusedChunk, RepoPath}, - Action, Agent, ExchangeState, Project, + Action, Agent, ExchangeState, }, analytics::{EventData, QueryEvent}, db::QueryLog, query::parser::{self, Literal}, repo::RepoRef, - webserver::conversation::{self, Conversation}, + webserver::conversation::Conversation, Application, }; @@ -76,10 +73,6 @@ pub struct Answer { pub conversation_id: Option, } -fn default_thread_id() -> uuid::Uuid { - uuid::Uuid::new_v4() -} - fn default_answer_model() -> agent::model::LLMModel { agent::model::GPT_4_TURBO_24K } @@ -166,6 +159,7 @@ struct AgentExecutor { action: Action, } +#[allow(clippy::large_enum_variant)] #[derive(serde::Serialize)] enum AnswerEvent { ChatEvent(Exchange), diff --git a/server/bleep/src/webserver/conversation.rs b/server/bleep/src/webserver/conversation.rs index f13d7e2023..47111d4c61 100644 --- a/server/bleep/src/webserver/conversation.rs +++ b/server/bleep/src/webserver/conversation.rs @@ -1,20 +1,16 @@ -use anyhow::{Context, Result}; +use anyhow::Result; use axum::{ - extract::{Path, Query, State}, - response::IntoResponse, + extract::{Path, State}, Extension, Json, }; -use chrono::NaiveDateTime; use reqwest::StatusCode; -use std::{fmt, mem}; -use tracing::info; +use std::mem; use uuid::Uuid; use crate::{ - agent::{exchange::Exchange, Project}, + agent::exchange::Exchange, db::SqlDb, - repo::RepoRef, - webserver::{self, middleware::User, Error, ErrorKind}, + webserver::{self, middleware::User, Error}, Application, }; diff --git a/server/bleep/src/webserver/project.rs b/server/bleep/src/webserver/project.rs index 382c4291ef..68dc06292b 100644 --- a/server/bleep/src/webserver/project.rs +++ b/server/bleep/src/webserver/project.rs @@ -1,12 +1,8 @@ use std::collections::HashMap; use crate::{webserver, Application}; -use axum::{ - extract::{Path, Query}, - Extension, Json, -}; +use axum::{extract::Path, Extension, Json}; use chrono::NaiveDateTime; -use futures::TryStreamExt; use super::{middleware::User, repos::Repo, Error}; diff --git a/server/bleep/src/webserver/studio.rs b/server/bleep/src/webserver/studio.rs index 7aace09bb5..90b2a617bb 100644 --- a/server/bleep/src/webserver/studio.rs +++ b/server/bleep/src/webserver/studio.rs @@ -70,7 +70,6 @@ pub struct Create { pub async fn create( app: Extension, - user: Extension, Path(project_id): Path, Json(params): Json, ) -> webserver::Result { @@ -811,7 +810,6 @@ pub async fn generate( Ok(Sse::new(Box::pin(stream))) } -#[allow(clippy::single_range_in_vec_init)] async fn generate_llm_context( app: Application, context: &[ContextFile], @@ -1087,7 +1085,7 @@ pub async fn diff( for chunk in valid_chunks { let path = chunk.src.as_deref().or(chunk.dst.as_deref()).unwrap(); - let (repo, path) = parse_diff_path(&path)?; + let (repo, path) = parse_diff_path(path)?; let lang = if let Some(l) = file_langs.get(path) { Some(l.clone()) } else { @@ -1149,21 +1147,6 @@ fn parse_diff_path(p: &str) -> Result<(RepoRef, &str)> { Ok((repo, path)) } -fn context_repo_branch(context: &[ContextFile]) -> Result<(RepoRef, Option)> { - let (repo, branch) = context - .first() - .map(|cf| (cf.repo.clone(), cf.branch.clone())) - // We make a hard assumption in the design of diffs that a studio can only contain files - // from one repository. This allows us to determine which repository to create new files - // or delete files in, without having to prefix file paths with repository names. - // - // If we can't find *any* files in the context to detect the current repository, - // creating/deleting a file like `index.js` is ambiguous, so we just return an error. - .context("could not determine studio repository, studio didn't contain any files")?; - - Ok((repo, branch)) -} - async fn rectify_hunks( app: &Application, llm_context: &str, @@ -1304,7 +1287,7 @@ pub async fn diff_apply( .map(|row| row.context) .ok_or_else(studio_not_found)?; - let context = + let _context = serde_json::from_str::>(&context_json).map_err(Error::internal)?; let diff_chunks = diff::relaxed_parse(&diff); @@ -1329,7 +1312,7 @@ pub async fn diff_apply( let mut file_content = if chunk.src.is_some() { app.indexes .file - .by_path(&repo, &path, None) + .by_path(&repo, path, None) .await? .context("path did not exist in the index")? .content @@ -1455,7 +1438,6 @@ pub struct Import { } /// Returns a new studio ID, or the `?studio_id=...` query param if present. -#[allow(clippy::single_range_in_vec_init)] pub async fn import( app: Extension, user: Extension, @@ -1709,9 +1691,12 @@ pub async fn list_snapshots( .and_then(|r| { let app = (*app).clone(); async move { - let context: Vec = serde_json::from_str(&r.context).context("failed to deserialize context")?; - let doc_context: Vec = serde_json::from_str(&r.doc_context).context("failed to deserialize doc context")?; - let messages: Vec = serde_json::from_str(&r.messages).context("failed to deserialize messages")?; + let context: Vec = + serde_json::from_str(&r.context).context("failed to deserialize context")?; + let doc_context: Vec = serde_json::from_str(&r.doc_context) + .context("failed to deserialize doc context")?; + let messages: Vec = + serde_json::from_str(&r.messages).context("failed to deserialize messages")?; let token_counts = token_counts(app, &messages, &context, &doc_context).await?; From 97233887fb43fcd57156da46445ca56e14c538f1 Mon Sep 17 00:00:00 2001 From: Gabriel Gordon-Hall Date: Fri, 26 Jan 2024 14:10:04 +0000 Subject: [PATCH 44/88] return None if parent commit does not exist (#1214) --- server/bleep/src/commits.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/bleep/src/commits.rs b/server/bleep/src/commits.rs index 976c06ed35..d56f191fdd 100644 --- a/server/bleep/src/commits.rs +++ b/server/bleep/src/commits.rs @@ -66,7 +66,7 @@ impl<'a> Iterator for CommitIterator<'a> { return None; }; - let parent_commit = parent_id.object().unwrap().into_commit(); + let parent_commit = parent_id.object().ok()?.into_commit(); let mut stats = DiffStat { commit_message: self .commit From cf3ca709fc0aab47d9a8fd27da7df12983fe7a45 Mon Sep 17 00:00:00 2001 From: Anastasiia Solop <35258279+anastasiya1155@users.noreply.github.com> Date: Fri, 26 Jan 2024 11:32:11 -0500 Subject: [PATCH 45/88] fix SQL query that retrieves a list of docs for project (#1216) --- server/bleep/sqlx-data.json | 108 +++++++++++----------- server/bleep/src/webserver/project/doc.rs | 2 +- 2 files changed, 55 insertions(+), 55 deletions(-) diff --git a/server/bleep/sqlx-data.json b/server/bleep/sqlx-data.json index 665580f8c7..ff43696a4e 100644 --- a/server/bleep/sqlx-data.json +++ b/server/bleep/sqlx-data.json @@ -1200,6 +1200,60 @@ }, "query": "SELECT repo_ref\n FROM project_repos\n WHERE project_id = $1 AND EXISTS (\n SELECT id\n FROM projects\n WHERE id = $1 AND user_id = $2\n )" }, + "a80c38e828ea4c3657ead89145e263af0ca709039ad9d57f45ddbde88af1acf0": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "url", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "index_status", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "name", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "favicon", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "description", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "modified_at", + "ordinal": 6, + "type_info": "Datetime" + } + ], + "nullable": [ + false, + false, + false, + true, + true, + true, + false + ], + "parameters": { + "Right": 2 + } + }, + "query": "SELECT d.id, d.url, d.index_status, d.name, d.favicon, d.description, d.modified_at\n FROM project_docs pd\n INNER JOIN docs d ON d.id = pd.doc_id\n WHERE project_id = $1 AND EXISTS (\n SELECT p.id\n FROM projects p\n WHERE p.id = $1 AND p.user_id = $2\n )" + }, "a8baec57552045778eb4609e83452c668583bf5a6ff8fec1898523058a061bcb": { "describe": { "columns": [ @@ -1328,60 +1382,6 @@ }, "query": "SELECT id FROM projects WHERE id = ? AND user_id = ?" }, - "d2aa09b48fdb1d608c6034758e3d43509abb7ebe114d81724409d664a46fc5df": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int64" - }, - { - "name": "url", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "index_status", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "name", - "ordinal": 3, - "type_info": "Text" - }, - { - "name": "favicon", - "ordinal": 4, - "type_info": "Text" - }, - { - "name": "description", - "ordinal": 5, - "type_info": "Text" - }, - { - "name": "modified_at", - "ordinal": 6, - "type_info": "Datetime" - } - ], - "nullable": [ - false, - false, - false, - true, - true, - true, - false - ], - "parameters": { - "Right": 2 - } - }, - "query": "SELECT d.id, d.url, d.index_status, d.name, d.favicon, d.description, d.modified_at\n FROM project_docs pd\n INNER JOIN docs d ON d.id = pd.id\n WHERE project_id = $1 AND EXISTS (\n SELECT p.id\n FROM projects p\n WHERE p.id = $1 AND p.user_id = $2\n )" - }, "d2b52987aaa4bdc39c04254834c941cad2165eefd02eef46fda413822be91fd0": { "describe": { "columns": [ diff --git a/server/bleep/src/webserver/project/doc.rs b/server/bleep/src/webserver/project/doc.rs index 348346983d..31c18936d1 100644 --- a/server/bleep/src/webserver/project/doc.rs +++ b/server/bleep/src/webserver/project/doc.rs @@ -31,7 +31,7 @@ pub async fn list( Doc, "SELECT d.id, d.url, d.index_status, d.name, d.favicon, d.description, d.modified_at FROM project_docs pd - INNER JOIN docs d ON d.id = pd.id + INNER JOIN docs d ON d.id = pd.doc_id WHERE project_id = $1 AND EXISTS ( SELECT p.id FROM projects p From 5171c467a2d00ad8ea332dad6c6d308c37a716e1 Mon Sep 17 00:00:00 2001 From: Gabriel Gordon-Hall Date: Tue, 30 Jan 2024 13:14:16 +0000 Subject: [PATCH 46/88] Fix autocomplete repo match (#1218) * make repo_name ; make autocomplete and folder queries case insensitive; remove repo.display_name() * use stringified repo_ref in prompt --- server/bleep/src/agent/prompts.rs | 3 ++- server/bleep/src/agent/symbol.rs | 2 +- server/bleep/src/indexes/schema.rs | 2 +- server/bleep/src/query/compiler.rs | 2 +- server/bleep/src/repo.rs | 11 +---------- server/bleep/src/webserver/autocomplete.rs | 4 ++++ server/bleep/src/webserver/file.rs | 1 + server/bleep/src/webserver/repos.rs | 2 +- 8 files changed, 12 insertions(+), 15 deletions(-) diff --git a/server/bleep/src/agent/prompts.rs b/server/bleep/src/agent/prompts.rs index 059b18679b..0f3bb4d5fe 100644 --- a/server/bleep/src/agent/prompts.rs +++ b/server/bleep/src/agent/prompts.rs @@ -100,7 +100,8 @@ pub fn system<'a>(paths: impl IntoIterator) -> String { if iter.peek().is_some() { s.push_str("\n## PATHS ##\nindex, repo, path\n"); for (i, path) in iter.enumerate() { - let repo = path.repo.display_name(); + let repo = &path.repo; + let repo = &format!("{repo}"); let path = &path.path; s.push_str(&format!("{}, {}, {}\n", i, repo, path)); } diff --git a/server/bleep/src/agent/symbol.rs b/server/bleep/src/agent/symbol.rs index d40f7e90e6..a5fe2a4765 100644 --- a/server/bleep/src/agent/symbol.rs +++ b/server/bleep/src/agent/symbol.rs @@ -65,7 +65,7 @@ impl Agent { .to_string(), token_info_request: TokenInfoRequest { relative_path: chunk.repo_path.path.clone(), - repo_ref: chunk.repo_path.repo.display_name(), + repo_ref: chunk.repo_path.repo.indexed_name(), branch: None, start: range.start.byte, end: range.end.byte, diff --git a/server/bleep/src/indexes/schema.rs b/server/bleep/src/indexes/schema.rs index 54ac40fe96..f7f0cd961a 100644 --- a/server/bleep/src/indexes/schema.rs +++ b/server/bleep/src/indexes/schema.rs @@ -80,7 +80,7 @@ impl File { let repo_disk_path = builder.add_text_field("repo_disk_path", STRING); let repo_ref = builder.add_text_field("repo_ref", STRING | STORED); - let repo_name = builder.add_text_field("repo_name", trigram.clone()); + let repo_name = builder.add_text_field("repo_name", STRING | STORED); let relative_path = builder.add_text_field("relative_path", trigram.clone()); let content = builder.add_text_field("content", trigram.clone()); diff --git a/server/bleep/src/query/compiler.rs b/server/bleep/src/query/compiler.rs index 68414c4a64..97920efb00 100644 --- a/server/bleep/src/query/compiler.rs +++ b/server/bleep/src/query/compiler.rs @@ -255,7 +255,7 @@ pub fn case_permutations(s: &str) -> impl Iterator { // Make sure not to overflow. The end condition is a mask with the highest bit set, and we use // `u32` masks. - debug_assert!(chars.len() <= 31); + debug_assert!(chars.len() <= 5); let num_chars = chars.len(); diff --git a/server/bleep/src/repo.rs b/server/bleep/src/repo.rs index 9600770501..f064577a22 100644 --- a/server/bleep/src/repo.rs +++ b/server/bleep/src/repo.rs @@ -97,23 +97,14 @@ impl RepoRef { pub fn indexed_name(&self) -> String { // Local repos indexed as: dirname - // Github repos indexed as: github.com/org/repo + // Github repos indexed as: org/repo match self.backend { Backend::Local => Path::new(&self.name) .file_name() .expect("last component is `..`") .to_string_lossy() .into(), - Backend::Github => format!("{}", self), - } - } - - pub fn display_name(&self) -> String { - match self.backend { - // org_name/repo_name Backend::Github => self.name.to_owned(), - // repo_name - Backend::Local => self.indexed_name(), } } diff --git a/server/bleep/src/webserver/autocomplete.rs b/server/bleep/src/webserver/autocomplete.rs index c945590246..1684f5e341 100644 --- a/server/bleep/src/webserver/autocomplete.rs +++ b/server/bleep/src/webserver/autocomplete.rs @@ -52,6 +52,10 @@ pub(super) async fn handle( let queries = parser::parse(&api_params.q) .map_err(Error::user)? .into_iter() + .map(|q| parser::Query { + case_sensitive: Some(true), + ..q + }) .map(|mut q| { let keywords = &["lang:", "path:", "repo:"]; diff --git a/server/bleep/src/webserver/file.rs b/server/bleep/src/webserver/file.rs index 6f9d554dde..07f5ba726e 100644 --- a/server/bleep/src/webserver/file.rs +++ b/server/bleep/src/webserver/file.rs @@ -100,6 +100,7 @@ pub(super) async fn folder( repo: Some(parser::Literal::from(¶ms.repo_ref.indexed_name())), path: Some(parser::Literal::from(params.path.to_string_lossy())), branch: params.branch.map(|b| parser::Literal::from(&b)), + case_sensitive: Some(true), ..Default::default() }; diff --git a/server/bleep/src/webserver/repos.rs b/server/bleep/src/webserver/repos.rs index 67bbccb243..636cb9d86b 100644 --- a/server/bleep/src/webserver/repos.rs +++ b/server/bleep/src/webserver/repos.rs @@ -128,7 +128,7 @@ impl From<(&RepoRef, &Repository)> for Repo { Repo { provider: key.backend(), - name: key.display_name(), + name: key.indexed_name(), repo_ref: key.clone(), sync_status: repo.pub_sync_status.clone(), local_duplicates: vec![], From 6590317e7a5d57977b62d57aa56fe3056e7e18fe Mon Sep 17 00:00:00 2001 From: Gabriel Gordon-Hall Date: Tue, 30 Jan 2024 13:17:45 +0000 Subject: [PATCH 47/88] bump version to 0.6.0 --- Cargo.lock | 4 ++-- apps/desktop/src-tauri/Cargo.toml | 2 +- apps/desktop/src-tauri/tauri.conf.json | 2 +- client/package.json | 2 +- server/bleep/Cargo.toml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ffb038e61c..3351c75ea0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -479,7 +479,7 @@ dependencies = [ [[package]] name = "bleep" -version = "0.5.12" +version = "0.6.0" dependencies = [ "anyhow", "async-stream", @@ -598,7 +598,7 @@ dependencies = [ [[package]] name = "bloop" -version = "0.5.12" +version = "0.6.0" dependencies = [ "anyhow", "bleep", diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 57d0ae70b8..84210e2e18 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bloop" -version = "0.5.12" +version = "0.6.0" description = "Search code. Fast." authors = ["Bloop AI Developers"] license = "Apache-2.0" diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index 67b63bbe92..6919c06b6e 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -8,7 +8,7 @@ }, "package": { "productName": "bloop", - "version": "0.5.12" + "version": "0.6.0" }, "tauri": { "allowlist": { diff --git a/client/package.json b/client/package.json index 812f967070..3351e7405b 100644 --- a/client/package.json +++ b/client/package.json @@ -1,7 +1,7 @@ { "name": "@bloop/client", "private": true, - "version": "0.5.12", + "version": "0.6.0", "scripts": { "dev": "vite", "build": "tsc && vite build", diff --git a/server/bleep/Cargo.toml b/server/bleep/Cargo.toml index 9ac29bb042..233bc1eb2e 100644 --- a/server/bleep/Cargo.toml +++ b/server/bleep/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bleep" -version = "0.5.12" +version = "0.6.0" edition = "2021" default-run = "bleep" build = "build.rs" From bfa3656b0437a6e74556bc8711221ef5ce0f3ecd Mon Sep 17 00:00:00 2001 From: akshay Date: Tue, 30 Jan 2024 13:28:43 +0000 Subject: [PATCH 48/88] fix blocking status endpoint (#1217) the status reporting endpoint can drop the lock over the tantivy index once it has a handle to the progress stream. --- server/bleep/src/indexes/doc.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/server/bleep/src/indexes/doc.rs b/server/bleep/src/indexes/doc.rs index ff0c0b1c12..7c1898c89e 100644 --- a/server/bleep/src/indexes/doc.rs +++ b/server/bleep/src/indexes/doc.rs @@ -395,6 +395,7 @@ impl Doc { match handle { Some(h) => { let s = tokio_stream::wrappers::WatchStream::from_changes(h.progress_stream.clone()); + drop(lock); for await progress in s { yield progress; } From 4fe891107a631f10279928f4dc2712168858e56a Mon Sep 17 00:00:00 2001 From: calyptobai Date: Tue, 30 Jan 2024 10:31:49 -0500 Subject: [PATCH 49/88] Migrate existing databases to new project schema --- .../migrations/20231122012638_projects.sql | 102 ++++++++++- server/bleep/sqlx-data.json | 120 +++++++++++++ server/bleep/src/db.rs | 163 +++++++++++++++++- 3 files changed, 379 insertions(+), 6 deletions(-) diff --git a/server/bleep/migrations/20231122012638_projects.sql b/server/bleep/migrations/20231122012638_projects.sql index 507590963b..881acceb50 100644 --- a/server/bleep/migrations/20231122012638_projects.sql +++ b/server/bleep/migrations/20231122012638_projects.sql @@ -1,14 +1,87 @@ +-- This is a partial migration to migrate to the new `projects` structure. Here, we try to migrate +-- as much of the data as possible. However, we cannot migrate everything as some JSON data has to +-- be directly manipulated, in a way that is difficult with plain SQLite. As a compromise, we create +-- a `rust_migrations` table, and keep track of logical changes there, intended to be applied +-- immediately after all other pending database migrations. + CREATE TABLE projects ( id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL, name TEXT ); -ALTER TABLE studios ADD COLUMN project_id INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE; -ALTER TABLE studios DROP COLUMN user_id; -ALTER TABLE conversations ADD COLUMN project_id INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE; -ALTER TABLE conversations DROP COLUMN repo_ref; -ALTER TABLE conversations DROP COLUMN user_id; +-- First, we have to modify studios & snapshots. This involves recreating the `studios` table, and +-- consequently also the `studio_snapshots` table, as SQLite doesn't allow us to dynamically modify +-- foreign key constraints. So, we instead recreate both tables and copy data as required. + +ALTER TABLE studios RENAME TO studios_old; +ALTER TABLE studio_snapshots RENAME TO studio_snapshots_old; + +ALTER TABLE studios_old ADD COLUMN project_id INTEGER; +ALTER TABLE projects ADD COLUMN studio_id_tmp INTEGER; + +INSERT INTO projects (studio_id_tmp, user_id, name) +SELECT id, user_id, name FROM studios_old; + +UPDATE studios_old +SET project_id = (SELECT id FROM projects WHERE projects.studio_id_tmp = studios_old.id); + +CREATE TABLE studios ( + id INTEGER PRIMARY KEY, + name TEXT, + project_id INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE +); + +CREATE TABLE studio_snapshots ( + id INTEGER PRIMARY KEY, + studio_id INTEGER NOT NULL REFERENCES studios (id) ON DELETE CASCADE, + + modified_at DATETIME NOT NULL DEFAULT (datetime('now')), + + -- JSON serialized fields + context TEXT NOT NULL, + messages TEXT NOT NULL, + doc_context TEXT NOT NULL DEFAULT '[]' +); + +INSERT INTO studios (id, name, project_id) +SELECT id, name, project_id FROM studios_old; + +INSERT INTO studio_snapshots (id, studio_id, modified_at, context, messages, doc_context) +SELECT id, studio_id, modified_at, context, messages, doc_context FROM studio_snapshots_old; + +DROP TABLE studios_old; +DROP TABLE studio_snapshots_old; + +ALTER TABLE projects DROP COLUMN studio_id_tmp; + +-- Next, we can update the conversations table, following a similar process. + +ALTER TABLE conversations RENAME TO conversations_old; +ALTER TABLE conversations_old ADD COLUMN project_id INTEGER; +ALTER TABLE projects ADD COLUMN conversation_id_tmp; + +INSERT INTO projects (conversation_id_tmp, user_id, name) +SELECT id, user_id, title FROM conversations_old; + +UPDATE conversations_old +SET project_id = (SELECT id FROM projects WHERE projects.conversation_id_tmp = conversations_old.id); + +CREATE TABLE conversations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + project_id INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE, + created_at INTEGER NOT NULL, + thread_id TEXT NOT NULL, + title TEXT NOT NULL, + + -- JSON serialized fields + exchanges TEXT NOT NULL +); + +INSERT INTO conversations (id, project_id, created_at, thread_id, title, exchanges) +SELECT id, project_id, created_at, thread_id, title, exchanges FROM conversations_old; + +-- NB: We keep the `conversations_old` table around briefly, so that we can also create entries in `project_repos`: CREATE TABLE project_repos ( id INTEGER PRIMARY KEY, @@ -17,3 +90,22 @@ CREATE TABLE project_repos ( branch TEXT, UNIQUE (project_id, repo_ref) ); + +INSERT INTO project_repos (project_id, repo_ref) +SELECT project_id, repo_ref FROM conversations_old; + +DROP TABLE conversations_old; + +-- Finally, we create a new table to keep track of whether our manually-written Rust logic to +-- migrate JSON data has been applied. + +CREATE TABLE rust_migrations ( + id INTEGER PRIMARY KEY, + + -- A textual reference we use in the Rust code to check the status of this logical migration. + ref TEXT NOT NULL, + + applied BOOL NOT NULL +); + +INSERT INTO rust_migrations (ref, applied) VALUES ("project_migration", false); diff --git a/server/bleep/sqlx-data.json b/server/bleep/sqlx-data.json index ff43696a4e..8a73cf1678 100644 --- a/server/bleep/sqlx-data.json +++ b/server/bleep/sqlx-data.json @@ -382,6 +382,16 @@ }, "query": "DELETE FROM docs WHERE id = ? RETURNING id" }, + "3a8e50b31077e4aa516fc2492aa65b57e23a68fb23a53aabb9134a768cbb26f7": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 2 + } + }, + "query": "INSERT INTO project_repos (project_id, repo_ref)\n SELECT $1, $2\n WHERE NOT EXISTS (\n SELECT 1 FROM project_repos WHERE project_id = $1 AND repo_ref = $2\n )" + }, "400b01ce2735d2606363727d3ad2b2e829775ea081d4cc2dc83b19e226061c1e": { "describe": { "columns": [ @@ -570,6 +580,30 @@ }, "query": "INSERT INTO conversations (\n thread_id, title, exchanges, project_id, created_at\n )\n VALUES (?, ?, ?, ?, strftime('%s', 'now'))\n RETURNING id" }, + "4fc7072141dbc26c66bacc951f0d352ece1c8dda0289bd5c06d23fac709064b1": { + "describe": { + "columns": [ + { + "name": "context", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "project_id", + "ordinal": 1, + "type_info": "Int64" + } + ], + "nullable": [ + false, + false + ], + "parameters": { + "Right": 0 + } + }, + "query": "SELECT\n context,\n (SELECT project_id FROM studios WHERE studios.id = studio_snapshots.studio_id) AS project_id\n FROM studio_snapshots" + }, "5128142bf657cfde043a1b53834d40980caa3e9ae5fd6f4d7f30d89be512f105": { "describe": { "columns": [], @@ -958,6 +992,16 @@ }, "query": "SELECT ss.id as 'id!', ss.modified_at, ss.context, ss.doc_context, ss.messages\n FROM studio_snapshots ss\n JOIN studios s ON s.id = ss.studio_id AND s.project_id = ?\n JOIN projects p ON p.id = s.project_id\n WHERE ss.studio_id = ? AND p.user_id = ?\n ORDER BY modified_at DESC" }, + "755ae8f05f5a0ae7c0942d5982abdc523a79cc3675f58bcc170a16e6999683b8": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 2 + } + }, + "query": "UPDATE conversations SET exchanges = ? WHERE id = ?" + }, "76464f75732fee5c742a23d7a0b95de1def360bae7943a2389944b0177106033": { "describe": { "columns": [ @@ -1154,6 +1198,16 @@ }, "query": "INSERT INTO docs (url, index_status) VALUES (?, ?)" }, + "a2783e84301bf5a6639a03097ce52f91ec1240ea7c1df3e51c4d26098206729f": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 0 + } + }, + "query": "UPDATE rust_migrations SET applied = true WHERE ref = 'project_migration'" + }, "a333e6cb457d03c6e54002e37908ecd56efe7862eed7a186db8eb2dc559c7f13": { "describe": { "columns": [ @@ -1382,6 +1436,24 @@ }, "query": "SELECT id FROM projects WHERE id = ? AND user_id = ?" }, + "c60995349620b88d6c1fce3fbd4988a16715bcb8c4a579312b11f26d86f061a3": { + "describe": { + "columns": [ + { + "name": "repo_ref", + "ordinal": 0, + "type_info": "Text" + } + ], + "nullable": [ + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "SELECT repo_ref FROM project_repos WHERE project_id = ?" + }, "d2b52987aaa4bdc39c04254834c941cad2165eefd02eef46fda413822be91fd0": { "describe": { "columns": [ @@ -1474,6 +1546,36 @@ }, "query": "\n SELECT id, name, url, description, favicon, modified_at, index_status\n FROM docs \n WHERE name LIKE $1 OR description LIKE $1 OR url LIKE $1\n LIMIT ?\n " }, + "dfcac17e60fb1250bf08d6f53d7f0c7ca45c0c8a22d17c4640f2e8c04050a767": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "project_id", + "ordinal": 1, + "type_info": "Int64" + }, + { + "name": "exchanges", + "ordinal": 2, + "type_info": "Text" + } + ], + "nullable": [ + false, + false, + false + ], + "parameters": { + "Right": 0 + } + }, + "query": "SELECT id, project_id, exchanges FROM conversations" + }, "e352cd10053f43e1e586da8a933868b5329fb1468292769587f130d7bc8f6ca3": { "describe": { "columns": [ @@ -1540,6 +1642,24 @@ }, "query": "SELECT\n s.id,\n s.name,\n ss.modified_at as \"modified_at!\",\n ss.context,\n ss.doc_context,\n ss.messages\n FROM studios s\n INNER JOIN studio_snapshots ss ON s.id = ss.studio_id\n INNER JOIN projects p ON p.id = s.project_id\n WHERE s.project_id = ? AND p.user_id = ? AND (ss.studio_id, ss.modified_at) IN (\n SELECT studio_id, MAX(modified_at)\n FROM studio_snapshots\n GROUP BY studio_id\n )" }, + "e95b48b7690809a8a16b642414139a00e975b1a08a6dda02521ca9d43a485017": { + "describe": { + "columns": [ + { + "name": "applied", + "ordinal": 0, + "type_info": "Bool" + } + ], + "nullable": [ + false + ], + "parameters": { + "Right": 0 + } + }, + "query": "SELECT applied FROM rust_migrations WHERE ref = 'project_migration'" + }, "ec193a038eb7fc3aaca3c3adebcc4dbde01b47ae34ac2df2227c5e5459617182": { "describe": { "columns": [], diff --git a/server/bleep/src/db.rs b/server/bleep/src/db.rs index 460685cb22..bf3e890a88 100644 --- a/server/bleep/src/db.rs +++ b/server/bleep/src/db.rs @@ -1,6 +1,7 @@ use std::{path::Path, sync::Arc}; use anyhow::{Context, Result}; +use futures::TryFutureExt; use sqlx::SqlitePool; use tracing::{debug, error}; @@ -38,12 +39,18 @@ pub async fn initialize(config: &Configuration) -> Result { async fn connect(url: &str) -> Result { let pool = SqlitePool::connect(url).await?; - if let Err(e) = sqlx::migrate!().run(&pool).await { + if let Err(e) = sqlx::migrate!() + .run(&pool) + .map_err(anyhow::Error::from) + .and_then(|_| logical_migrations(&pool)) + .await + { // We manually close the pool here to ensure file handles are properly cleaned up on // Windows. pool.close().await; Err(e)? } else { + logical_migrations(&pool).await?; Ok(pool) } } @@ -54,3 +61,157 @@ fn reset(data_dir: &str) -> Result<()> { let bk_path = db_path.with_extension("db.bk"); std::fs::rename(db_path, bk_path).context("failed to backup old database") } + +async fn logical_migrations(db: &SqlitePool) -> Result<()> { + // A series of logically applied migrations. + project_migration(db).await +} + +async fn project_migration(db: &SqlitePool) -> Result<()> { + let applied = + sqlx::query! { "SELECT applied FROM rust_migrations WHERE ref = 'project_migration'" } + .fetch_one(db) + .await? + .applied; + + if applied { + return Ok(()); + } + + let conversations = sqlx::query! { "SELECT id, project_id, exchanges FROM conversations" } + .fetch_all(db) + .await?; + + for row in conversations { + // As part of this migration, we assume each conversation project only ever had 1 repo. + // It's not possible for there to be more than one after the accompanying SQL migration. + let project_repo = sqlx::query! { + "SELECT repo_ref FROM project_repos WHERE project_id = ?", + row.project_id, + } + .fetch_one(db) + .await?; + + let mut exchanges = serde_json::from_str::(&row.exchanges) + .context("did not find valid JSON in `exchanges`")?; + + fixup_exchange(&mut exchanges, &project_repo.repo_ref) + .context("`exchanges` was malformed")?; + + let exchanges_json = serde_json::to_string(&exchanges)?; + + sqlx::query! { + "UPDATE conversations SET exchanges = ? WHERE id = ?", + exchanges_json, + row.id, + } + .execute(db) + .await?; + } + + let studio_snapshots = sqlx::query! { + "SELECT + context, + (SELECT project_id FROM studios WHERE studios.id = studio_snapshots.studio_id) AS project_id + FROM studio_snapshots" + } + .fetch_all(db) + .await?; + + for ss in studio_snapshots { + let context = + serde_json::from_str(&ss.context).context("did not find valid JSON in `context`")?; + + for repo_ref in studio_context_repos(&context).context("invalid studio `context` JSON")? { + sqlx::query! { + "INSERT INTO project_repos (project_id, repo_ref) + SELECT $1, $2 + WHERE NOT EXISTS ( + SELECT 1 FROM project_repos WHERE project_id = $1 AND repo_ref = $2 + )", + ss.project_id, + repo_ref, + } + .execute(db) + .await?; + } + } + + sqlx::query! { "UPDATE rust_migrations SET applied = true WHERE ref = 'project_migration'" } + .execute(db) + .await?; + + Ok(()) +} + +fn fixup_exchange(exchanges: &mut serde_json::Value, repo_ref: &str) -> Option<()> { + for exchange in exchanges.as_array_mut()? { + let exchange = exchange.as_object_mut()?; + + // First we fixup the top-level `paths` field. + for p in exchange.get_mut("paths")?.as_array_mut()? { + let path = p.as_str()?.to_owned(); + *p = serde_json::json!({ + "repo": repo_ref, + "path": path, + }); + } + + // Then, we replace `CodeChunk::path` with `CodeChunk::repo_path`. + for cc in exchange.get_mut("code_chunks")?.as_array_mut()? { + let cc = cc.as_object_mut()?; + let path = cc.remove("path")?.as_str()?.to_owned(); + cc.insert( + "repo_path".to_owned(), + serde_json::json!({ + "repo": repo_ref, + "path": path, + }), + ); + } + + // Similarly, update `focused_chunk`, if it exists. + match exchange.get_mut("focused_chunk") { + Some(serde_json::Value::Null) | None => {} + Some(serde_json::Value::Object(fc)) => { + let file_path = fc.remove("file_path"); + + fc.insert( + "repo_path".to_owned(), + serde_json::json!({ + "repo": repo_ref, + "path": file_path, + }), + ); + } + Some(_) => return None, + } + + // Finally, we can update the search steps. + for step in exchange.get_mut("search_steps")?.as_array_mut()? { + let step = step.as_object_mut()?; + + if step.get("type").and_then(|v| v.as_str()) == Some("proc") { + let content = step.get_mut("content")?.as_object_mut()?; + + for p in content.get_mut("paths")?.as_array_mut()? { + let path = p.as_str()?.to_owned(); + *p = serde_json::json!({ + "repo": repo_ref, + "path": path, + }); + } + } + } + } + + Some(()) +} + +fn studio_context_repos(context: &serde_json::Value) -> Option> { + let mut repos = Vec::new(); + for context_file in context.as_array()? { + repos.push(context_file.as_object()?.get("repo")?.as_str()?); + } + Some(repos) +} From 5b2d2c94a5d06506e4ae423d6685fede60d9578b Mon Sep 17 00:00:00 2001 From: calyptobai Date: Tue, 30 Jan 2024 11:05:33 -0500 Subject: [PATCH 50/88] Fix lints --- server/bleep/src/webserver/studio.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/bleep/src/webserver/studio.rs b/server/bleep/src/webserver/studio.rs index 90b2a617bb..42bdf3d98a 100644 --- a/server/bleep/src/webserver/studio.rs +++ b/server/bleep/src/webserver/studio.rs @@ -845,6 +845,7 @@ async fn generate_llm_context( .map(|(i, s)| format!("{} {s}\n", i + 1)) .collect::>(); + #[allow(clippy::single_range_in_vec_init)] let ranges = if file.ranges.is_empty() { vec![0..lines.len()] } else { @@ -1484,6 +1485,7 @@ pub async fn import( }; let imported_context = canonicalize_context(exchanges.iter().flat_map(|e| { + #[allow(clippy::single_range_in_vec_init)] e.code_chunks.iter().map(|c| ContextFile { repo: c.repo_path.repo.clone(), path: c.repo_path.path.clone(), From 144179434044014108d2020ec7f2f4a402ab416c Mon Sep 17 00:00:00 2001 From: anastasiia Date: Fri, 24 Nov 2023 13:45:16 -0500 Subject: [PATCH 51/88] app redesign --- apps/desktop/src-tauri/tauri.conf.json | 5 +- apps/desktop/src/App.tsx | 57 +- apps/desktop/src/SplashScreen.tsx | 2 +- apps/desktop/src/TextSearch.tsx | 2 +- client/public/bloopHeadMascot.png | Bin 11248 -> 308958 bytes client/public/bloopHeadMascotLight.png | Bin 0 -> 451288 bytes client/public/stripe_logo.png | Bin 0 -> 2650 bytes client/src/App.tsx | 459 +---- client/src/CloudApp.tsx | 31 +- client/src/CommandBar/Body/Item.tsx | 153 ++ client/src/CommandBar/Body/Section.tsx | 57 + client/src/CommandBar/Body/SectionDivider.tsx | 15 + client/src/CommandBar/Body/index.tsx | 60 + client/src/CommandBar/Footer/HintButton.tsx | 33 + client/src/CommandBar/Footer/index.tsx | 58 + client/src/CommandBar/Header/ChipItem.tsx | 13 + client/src/CommandBar/Header/index.tsx | 126 ++ client/src/CommandBar/index.tsx | 86 + client/src/CommandBar/steps/AddNewRepo.tsx | 132 ++ client/src/CommandBar/steps/CreateProject.tsx | 82 + client/src/CommandBar/steps/Documentation.tsx | 200 +++ client/src/CommandBar/steps/Initial.tsx | 412 +++++ client/src/CommandBar/steps/LocalRepos.tsx | 171 ++ .../steps/ManageRepos/ActionsDropdown.tsx | 203 +++ .../CommandBar/steps/ManageRepos/index.tsx | 217 +++ .../steps/PrivateRepos/ActionsDropdown.tsx | 89 + .../CommandBar/steps/PrivateRepos/index.tsx | 124 ++ client/src/CommandBar/steps/PublicRepos.tsx | 102 ++ client/src/CommandBar/steps/SeachFiles.tsx | 103 ++ client/src/CommandBar/steps/ToggleTheme.tsx | 75 + client/src/CommandBar/steps/items/DocItem.tsx | 209 +++ .../src/CommandBar/steps/items/RepoItem.tsx | 314 ++++ .../Desktop/FeaturesStep/Feature.tsx | 2 +- .../Onboarding/Desktop/FeaturesStep/index.tsx | 26 +- .../Onboarding/Desktop/UserForm/Step1.tsx | 60 +- .../Onboarding/Desktop/UserForm/Step2.tsx | 21 +- .../Onboarding/Desktop/UserForm/index.tsx | 32 +- .../{pages => }/Onboarding/Desktop/index.tsx | 28 +- .../Onboarding/SelfServe/index.tsx | 32 +- client/src/{pages => }/Onboarding/index.tsx | 18 +- .../ChatTab/ActionsDropdown.tsx | 74 + .../ChatTab/ChatPersistentState.tsx | 636 +++++++ .../ChatTab/Conversation.tsx | 88 + .../ChatTab/DeprecatedClientModal.tsx | 68 + .../ChatTab}/Input/InputCore.tsx | 83 +- .../CurrentTabContent/ChatTab/Input/index.tsx | 321 ++++ .../ChatTab}/Input/mentionPlugin.ts | 26 +- .../CurrentTabContent/ChatTab}/Input/nodes.ts | 33 +- .../ChatTab}/Input/placeholderPlugin.ts | 0 .../CurrentTabContent/ChatTab}/Input/utils.ts | 12 +- .../ChatTab/Message/LoadingStep.tsx | 43 + .../Message}/UserParsedQuery/LangChip.tsx | 4 +- .../Message}/UserParsedQuery/PathChip.tsx | 8 +- .../Message/UserParsedQuery/RepoChip.tsx | 22 + .../Message}/UserParsedQuery/index.tsx | 7 +- .../ChatTab/Message/index.tsx | 259 +++ .../ChatTab/ScrollableContent.tsx | 90 + .../ChatTab/StarterMessage.tsx | 100 ++ .../CurrentTabContent/ChatTab/index.tsx | 141 ++ .../Project/CurrentTabContent/DropTarget.tsx | 53 + .../Project/CurrentTabContent/EmptyTab.tsx | 37 + .../FileTab/ActionsDropdown.tsx | 39 + .../CurrentTabContent/FileTab/index.tsx | 306 ++++ .../CurrentTabContent/Header/AddTabButton.tsx | 64 + .../Header/AddTabDropdown.tsx | 78 + .../CurrentTabContent/Header/TabButton.tsx | 229 +++ .../CurrentTabContent/Header/index.tsx | 117 ++ .../src/Project/CurrentTabContent/index.tsx | 112 ++ client/src/Project/EmptyProject.tsx | 60 + .../LeftSidebar/NavPanel/Conversations.tsx | 127 ++ .../NavPanel/ConversationsDropdown.tsx | 39 + .../LeftSidebar/NavPanel/CoversationEntry.tsx | 36 + .../src/Project/LeftSidebar/NavPanel/Repo.tsx | 226 +++ .../LeftSidebar/NavPanel/RepoDropdown.tsx | 295 +++ .../LeftSidebar/NavPanel/RepoEntry.tsx | 231 +++ .../Project/LeftSidebar/NavPanel/index.tsx | 91 + .../RegexSearchPanel/AutocompleteMenu.tsx | 85 + .../RegexSearchPanel/AutocompleteMenuItem.tsx | 100 ++ .../RegexSearchPanel/Results/CodeLine.tsx} | 117 +- .../RegexSearchPanel/Results/CodeResult.tsx | 124 ++ .../RegexSearchPanel/Results/FileResult.tsx | 156 ++ .../RegexSearchPanel/Results/RepoResult.tsx | 125 ++ .../LeftSidebar/RegexSearchPanel/index.tsx | 284 +++ client/src/Project/LeftSidebar/index.tsx | 127 ++ client/src/Project/RightTab.tsx | 38 + client/src/Project/index.tsx | 134 ++ client/src/ProjectSettings/General.tsx | 96 + client/src/ProjectSettings/index.tsx | 60 + .../Settings/General/AnswerSpeedDropdown.tsx | 39 + client/src/Settings/General/index.tsx | 132 ++ .../Settings/Preferences/LanguageDropdown.tsx | 32 + .../Settings/Preferences/ThemeDropdown.tsx | 52 + client/src/Settings/Preferences/index.tsx | 89 + .../src/Settings/Subscription/BenefitItem.tsx | 17 + client/src/Settings/Subscription/CardFree.tsx | 50 + client/src/Settings/Subscription/CardPaid.tsx | 76 + client/src/Settings/Subscription/Confetti.tsx | 154 ++ client/src/Settings/Subscription/index.tsx | 201 +++ client/src/Settings/index.tsx | 80 + client/src/circleProgress.css | 234 --- .../Accordion/Accordion.stories.tsx | 22 - client/src/components/Accordion/index.tsx | 104 -- .../src/components/AddStudioContext/index.tsx | 188 -- client/src/components/Badge/index.tsx | 56 +- .../Breadcrumbs/BreadcrumbSection.tsx | 8 +- .../Breadcrumbs/Breadcrumbs.stories.tsx | 120 -- .../Breadcrumbs/BreadcrumbsCollapsed.tsx | 86 +- .../PathContainer.tsx} | 30 +- client/src/components/Breadcrumbs/index.tsx | 5 +- .../src/components/Button/Button.stories.tsx | 170 -- client/src/components/Button/index.tsx | 79 +- .../AllCoversations/ConversationListItem.tsx | 54 - .../Chat/ChatBody/AllCoversations/index.tsx | 130 -- .../components/Chat/ChatBody/Conversation.tsx | 91 - .../ConversationMessage/MessageFeedback.tsx | 165 -- .../ChatBody/ConversationMessage/index.tsx | 238 --- .../components/Chat/ChatBody/FirstMessage.tsx | 70 - client/src/components/Chat/ChatBody/index.tsx | 75 - .../Chat/ChatFooter/DeprecatedClientModal.tsx | 66 - .../Chat/ChatFooter/InputLoader.tsx | 74 - .../components/Chat/ChatFooter/NLInput.tsx | 217 --- .../Chat/ChatFooter/SuggestionItem.tsx | 64 - .../Chat/ChatFooter/Suggestions.tsx | 80 - .../src/components/Chat/ChatFooter/index.tsx | 128 -- .../AnswerSpeedSelector/SelectionItem.tsx | 36 - .../ChatHeader/AnswerSpeedSelector/index.tsx | 99 -- .../src/components/Chat/ChatHeader/index.tsx | 74 - client/src/components/Chat/ChipButton.tsx | 31 - .../components/Chat/FeedbackBtns/Downvote.tsx | 27 - .../components/Chat/FeedbackBtns/Upvote.tsx | 27 - client/src/components/Chat/index.tsx | 483 ----- .../FileChip.tsx | 78 +- .../ClearButton/ClearButton.stories.tsx | 14 - .../Search => Code/CodeBlockSearch}/index.tsx | 153 +- .../components/Code/CodeFragment/index.tsx | 206 +++ .../Code/CodeFull/SelectionPopup.tsx | 136 ++ .../{CodeBlock => Code}/CodeFull/Token.tsx | 26 +- .../Code/CodeFull/VirtualizedCode.tsx | 143 ++ client/src/components/Code/CodeFull/index.tsx | 338 ++++ client/src/components/Code/CodeLine.tsx | 118 ++ client/src/components/Code/CodeToken.tsx | 42 + .../CodeBlock/Code/CodeContainer.tsx | 150 -- .../components/CodeBlock/Code/CodeLine.tsx | 283 --- .../components/CodeBlock/Code/CodeToken.tsx | 39 - .../CodeBlock/CodeBlock.stories.tsx | 205 --- .../components/CodeBlock/CodeDiff/index.tsx | 191 -- .../CodeBlock/CodeFull/CodeContainer.tsx | 206 --- .../CodeBlock/CodeFull/CodeContainerFull.tsx | 244 --- .../CodeFull/CodeContainerVirtualized.tsx | 224 --- .../CodeBlock/CodeFull/ExplainButton.tsx | 150 -- .../CodeBlock/CodeFull/FoldButton.tsx | 27 - .../components/CodeBlock/CodeFull/index.tsx | 418 ----- .../CodeFullSelectable/CodeContainer.tsx | 263 --- .../CodeBlock/CodeFullSelectable/CodeLine.tsx | 128 -- .../CodeFullSelectable/FoldButton.tsx | 27 - .../CodeFullSelectable/LazyLinesContainer.tsx | 106 -- .../CodeFullSelectable/SelectionHandler.tsx | 178 -- .../CodeFullSelectable/SelectionRect.tsx | 62 - .../CodeBlock/CodeFullSelectable/index.tsx | 172 -- .../components/CodeBlock/MiniMap/index.tsx | 72 - .../components/CodeBlock/SearchFile/index.tsx | 92 - .../components/CodeBlock/SearchRepo/index.tsx | 66 - .../CodeSymbolIcons.stories.tsx | 96 - .../src/components/CodeSymbolIcon/index.tsx | 65 - .../components/Comment/Comment.stories.tsx | 37 - .../Comment/CommentItemExpanded.tsx | 94 - client/src/components/Comment/index.tsx | 55 - .../components/ConfirmationPopup/index.tsx | 36 - .../ContextMenu/ContextMenuItem/Item.tsx | 128 -- .../ContextMenuItem/ItemSelectable.tsx | 38 - .../ContextMenuItem/ItemShared.tsx | 60 - client/src/components/ContextMenu/index.tsx | 210 --- .../components/Dropdown/Dropdown.stories.tsx | 71 - .../src/components/Dropdown/Normal/index.tsx | 86 - .../Dropdown/Section/SectionItem.tsx | 112 ++ .../Dropdown/Section/SectionLabel.tsx | 13 + .../src/components/Dropdown/Section/index.tsx | 22 + .../components/Dropdown/WithIcon/index.tsx | 98 - client/src/components/Dropdown/index.tsx | 151 +- client/src/components/FileMenu/index.tsx | 56 - client/src/components/Filters/FilterItem.tsx | 64 - .../src/components/Filters/FilterSection.tsx | 150 -- client/src/components/Filters/FilterTitle.tsx | 37 - client/src/components/Filters/index.tsx | 237 --- .../FloatingToolbar.stories.tsx | 14 - .../FloatingToolbar/ToolbarButton.tsx | 28 - .../src/components/FloatingToolbar/index.tsx | 89 - .../src/components/Header/HeaderRightPart.tsx | 44 + .../components/Header/ProjectsDropdown.tsx | 82 + client/src/components/Header/UserDropdown.tsx | 105 ++ client/src/components/Header/index.tsx | 92 + .../src/components/IdeNavigation/DirEntry.tsx | 210 --- .../ListNavigation/ListNavigation.stories.tsx | 89 - .../IdeNavigation/ListNavigation/index.tsx | 68 - .../IdeNavigation/NavigationItem/index.tsx | 45 - .../NavigationItemChevron/index.tsx | 31 - .../IdeNavigation/NavigationPanel/index.tsx | 30 - client/src/components/IdeNavigation/index.tsx | 101 -- .../components/IpynbRenderer/IpynbCell.tsx | 4 +- client/src/components/KeyboardHint/index.tsx | 17 + .../src/components/LanguageSelector/index.tsx | 44 - client/src/components/LeftSidebar/index.tsx | 32 - client/src/components/Loaders/BarLoader.tsx | 12 - .../Loaders/CircleProgressLoader.tsx | 16 - client/src/components/Loaders/LiteLoader.tsx | 4 +- .../components/Loaders/Loaders.stories.tsx | 60 - .../src/components/Loaders/SkeletonLoader.tsx | 9 - client/src/components/Loaders/SpinLoader.tsx | 130 -- .../src/components/Loaders/SpinnerLoader.tsx | 20 + .../components/Loaders/ThreeDotsLoader.tsx | 9 - .../MarkdownWithCode/CodeRenderer.tsx | 105 +- .../MarkdownWithCode/CodeWithBreadcrumbs.tsx | 44 +- .../MarkdownWithCode/CopyButton.tsx | 23 +- .../components/MarkdownWithCode/DiffCode.tsx | 37 +- .../MarkdownWithCode/FolderChip.tsx | 52 +- .../MarkdownWithCode/LinkRenderer.tsx | 86 +- .../components/MarkdownWithCode/NewCode.tsx | 30 +- .../src/components/MarkdownWithCode/index.tsx | 42 +- .../index.tsx | 51 +- .../src/components/ModalOrSidebar/index.tsx | 158 -- client/src/components/NavBar/Tab.tsx | 82 - client/src/components/NavBar/index.tsx | 162 -- .../components/PageTemplate/BranchItem.tsx | 93 - .../PageTemplate/BranchSelector.tsx | 216 --- .../components/PageTemplate/HomeSubheader.tsx | 60 - .../components/PageTemplate/RepoHomeBtn.tsx | 26 - .../src/components/PageTemplate/Separator.tsx | 5 - .../src/components/PageTemplate/Subheader.tsx | 220 --- .../src/components/PageTemplate/TabButton.tsx | 26 - client/src/components/PageTemplate/index.tsx | 45 - .../Pagination/PaginationButton/index.tsx | 22 - client/src/components/Pagination/index.tsx | 75 - .../src/components/PortalContainer/index.tsx | 18 - .../ProgressBar/ProgressBar.stories.tsx | 54 - client/src/components/ProgressBar/index.tsx | 22 - .../components/PromptGuidePopup/PromptSvg.tsx | 502 ------ .../src/components/PromptGuidePopup/index.tsx | 99 -- client/src/components/RadioButton/index.tsx | 58 - client/src/components/RefsDefsPopup/Badge.tsx | 67 + .../RefsDefsPopup/RefDefFileItem.tsx | 89 + .../RefsDefsPopup/RefDefFileLine.tsx | 67 + client/src/components/RefsDefsPopup/index.tsx | 165 ++ .../components/RepoList/RepoList.stories.tsx | 118 -- .../RepoList/SearchableRepoList.tsx | 89 - client/src/components/RepoList/index.tsx | 204 --- .../components/ReportBugModal/ConfirmImg.tsx | 122 -- .../src/components/ReportBugModal/index.tsx | 225 +-- .../components/RepositoryFiles/FileRow.tsx | 90 - .../src/components/RepositoryFiles/index.tsx | 131 -- .../components/ResultsPageHeader/index.tsx | 67 - .../components/ScrollToBottom/Composer.tsx | 363 ++++ .../src/components/ScrollToBottom/EventSpy.ts | 48 + .../ScrollToBottom/FunctionContext.ts | 9 + .../ScrollToBottom/InternalContext.ts | 13 + .../src/components/ScrollToBottom/Panel.tsx | 27 + .../src/components/ScrollToBottom/SpineTo.tsx | 115 ++ .../src/components/ScrollToBottom/debounce.ts | 31 + .../src/components/ScrollToBottom/index.tsx | 39 + .../SearchInput/AutocompleteMenu.tsx | 99 -- .../SearchInput/AutocompleteMenuItem.tsx | 64 - .../SearchInput/SearchTextInput.tsx | 124 -- client/src/components/SearchInput/index.tsx | 230 --- client/src/components/SearchOnPage/index.tsx | 12 +- .../components/SectionsNav/SectionButton.tsx | 34 + client/src/components/SectionsNav/index.tsx | 57 + .../SelectToggleButton.stories.tsx | 40 - .../components/SelectToggleButton/index.tsx | 67 - .../DialogText/index.tsx | 26 - .../src/components/Settings/Badge/index.tsx | 17 - .../Settings/General/Password/index.tsx | 53 - .../Settings/General/Profile/index.tsx | 136 -- .../src/components/Settings/General/index.tsx | 22 - .../components/Settings/Preferences/index.tsx | 116 -- .../components/Settings/SettingsRow/index.tsx | 11 - .../Settings/SettingsText/index.tsx | 19 - client/src/components/Settings/index.tsx | 80 - .../ShareButton/ContextMenu/index.tsx | 54 - .../components/ShareButton/Counter/index.tsx | 32 - .../ShareButton/ShareButton.stories.tsx | 62 - client/src/components/ShareButton/index.tsx | 82 - .../src/components/ShareFileModal/index.tsx | 89 - .../components/Skeleton/Skeleton.stories.tsx | 20 - client/src/components/Skeleton/index.tsx | 194 -- .../SkeletonItem/SkeletonItem.stories.tsx | 21 - client/src/components/SkeletonItem/index.tsx | 13 - .../StatusBar/StatusBar.stories.tsx | 14 - .../components/StatusBar/StatusItem/index.tsx | 38 - client/src/components/StatusBar/index.tsx | 161 -- .../components/StudioGuidePopup/PromptSvg.tsx | 502 ------ .../src/components/StudioGuidePopup/index.tsx | 86 - client/src/components/Tabs/Tabs.stories.tsx | 162 -- client/src/components/Tabs/index.tsx | 91 - .../TextField/TextField.stories.tsx | 24 - .../{ => TextInput}/ClearButton/index.tsx | 4 +- .../{ => TextInput}/RegexButton/index.tsx | 4 +- .../TextInput/TextInput.stories.tsx | 153 -- client/src/components/TextInput/index.tsx | 110 +- .../components/Tooltip/Tooltip.stories.tsx | 120 -- client/src/components/Tooltip/index.tsx | 98 +- client/src/components/TooltipCode/Badge.tsx | 64 - .../src/components/TooltipCode/RefDefItem.tsx | 112 -- .../components/TooltipCode/RefsDefsPopup.tsx | 210 --- .../TooltipCode/TooltipCode.stories.tsx | 330 ---- client/src/components/TooltipCode/index.tsx | 62 - .../TooltipCommit/TooltipCommit.stories.tsx | 42 - client/src/components/TooltipCommit/index.tsx | 96 - .../Typography/Typography.stories.tsx | 25 - .../UpgradePopup/ConversationSvg.tsx | 1579 ----------------- .../src/components/UpgradePopup/Countdown.tsx | 42 - .../WaitingUpgradePopup/index.tsx | 139 -- client/src/components/UpgradePopup/index.tsx | 96 - .../BranchesSvg.tsx | 0 .../index.tsx | 65 +- client/src/consts/animations.ts | 32 +- client/src/consts/commandBar.ts | 7 + client/src/consts/general.ts | 29 + client/src/consts/shortcuts.ts | 1 + client/src/context/appNavigationContext.ts | 50 - client/src/context/chatContext.ts | 45 - client/src/context/chatsContext.tsx | 35 + client/src/context/commandBarContext.ts | 56 + client/src/context/deviceContext.ts | 7 +- client/src/context/envContext.ts | 12 + client/src/context/fileModalContext.ts | 24 - client/src/context/projectContext.ts | 40 + .../providers/AnalyticsContextProvider.tsx | 10 +- .../providers/AppNavigationProvider.tsx | 298 ---- .../context/providers/ChatContextProvider.tsx | 75 - .../providers/ChatsContextProvider.tsx | 23 + .../providers/CommandBarContextProvider.tsx | 71 + .../providers/DeviceContextProvider.tsx | 13 +- .../providers/FileModalContextProvider.tsx | 119 -- .../PersonalQuotaContextProvider.tsx | 4 +- .../providers/ProjectContextProvider.tsx | 195 ++ .../providers/RepositoriesContextProvider.tsx | 130 ++ .../providers/SearchContextProvider.tsx | 73 - .../providers/StudioContextProvider.tsx | 61 - .../providers/TabUiContextProvider.tsx | 65 - .../context/providers/TabsContextProvider.tsx | 348 ++++ ...textProvider.tsx => UIContextProvider.tsx} | 152 +- client/src/context/repositoriesContext.ts | 12 +- client/src/context/searchContext.ts | 30 - client/src/context/studioContext.ts | 29 - client/src/context/tabsContext.ts | 43 - client/src/context/tabsContext.tsx | 49 + client/src/context/uiContext.ts | 71 +- client/src/file-icons.css | 12 +- client/src/fonts/FiraCode/FiraCode-Bold.ttf | Bin 0 -> 189152 bytes client/src/fonts/FiraCode/FiraCode-Light.ttf | Bin 0 -> 188708 bytes client/src/fonts/FiraCode/FiraCode-Medium.ttf | Bin 0 -> 188492 bytes .../src/fonts/FiraCode/FiraCode-Regular.ttf | Bin 0 -> 188504 bytes .../src/fonts/FiraCode/FiraCode-SemiBold.ttf | Bin 0 -> 188848 bytes client/src/hooks/useAppNavigation.tsx | 6 - client/src/hooks/useCodeSearch.ts | 100 ++ client/src/hooks/useDiffLines.ts | 35 + client/src/hooks/useGlobalShortcuts.ts | 221 +++ client/src/hooks/useIsOnScreen.ts | 22 - client/src/hooks/useKeyboardNavigation.ts | 7 +- client/src/hooks/useOnClickOutsideHook.ts | 9 +- client/src/hooks/useOnScrollHook.ts | 13 - client/src/hooks/useRelatedFiles.tsx | 140 -- client/src/hooks/useResizeableSplitPanel.ts | 72 - client/src/hooks/useResizeableWidth.ts | 9 +- client/src/hooks/useSearch.tsx | 54 - client/src/hooks/useSearchState.tsx | 19 - client/src/hooks/useShortcuts.ts | 15 + client/src/hooks/useSignOut.ts | 22 + client/src/hooks/useStateRef.ts | 28 + client/src/icons/AIAnswerLong.tsx | 51 - client/src/icons/ArrowBoxOut.tsx | 31 - client/src/icons/ArrowDown.tsx | 27 - client/src/icons/ArrowJumpLeft.tsx | 31 - client/src/icons/ArrowLeft.tsx | 14 +- client/src/icons/ArrowOut.tsx | 27 +- client/src/icons/ArrowPushBottom.tsx | 45 - client/src/icons/ArrowPushLeft.tsx | 47 - client/src/icons/ArrowPushRight.tsx | 45 - client/src/icons/ArrowPushTop.tsx | 47 - client/src/icons/ArrowRefresh.tsx | 71 - client/src/icons/ArrowRevert.tsx | 55 - client/src/icons/ArrowRight.tsx | 27 - client/src/icons/ArrowRotate.tsx | 75 - client/src/icons/ArrowTriangleBottom.tsx | 12 + client/src/icons/ArrowUp.tsx | 27 - client/src/icons/Branch.tsx | 61 +- client/src/icons/BranchMerged.tsx | 31 - client/src/icons/Bug.tsx | 83 +- client/src/icons/Calendar.tsx | 39 - client/src/icons/Card.tsx | 39 - client/src/icons/ChatBubble.tsx | 23 - client/src/icons/ChatBubbles.tsx | 14 + client/src/icons/Check.tsx | 14 + client/src/icons/CheckIcon.tsx | 32 - client/src/icons/CheckList.tsx | 14 + client/src/icons/Checkmark.tsx | 39 - client/src/icons/CheckmarkInSquare.tsx | 14 + client/src/icons/ChevronDoubleIntersected.tsx | 29 - client/src/icons/ChevronDown.tsx | 17 +- client/src/icons/ChevronDownFilled.tsx | 27 - client/src/icons/ChevronDownIcon.tsx | 18 - client/src/icons/ChevronFoldIn.tsx | 41 - client/src/icons/ChevronFoldOut.tsx | 41 - client/src/icons/ChevronLeft.tsx | 21 - client/src/icons/ChevronLeftFilled.tsx | 27 - client/src/icons/ChevronRight.tsx | 17 +- client/src/icons/ChevronRightFilled.tsx | 21 - client/src/icons/ChevronRightIcon.tsx | 19 - client/src/icons/ChevronUp.tsx | 11 +- client/src/icons/ChevronUpFilled.tsx | 27 - client/src/icons/Chronometer.tsx | 73 - client/src/icons/ClearIcon.tsx | 26 - client/src/icons/Clipboard.tsx | 24 +- client/src/icons/Clock.tsx | 31 - client/src/icons/CloseSign.tsx | 21 +- client/src/icons/CloseSignInCircle.tsx | 14 + client/src/icons/Code.tsx | 23 +- client/src/icons/CodeLanguage.tsx | 45 - client/src/icons/CodeLineWithSparkle.tsx | 18 + client/src/icons/CodeStudio.tsx | 21 +- client/src/icons/CodeStudioColored.tsx | 147 -- client/src/icons/CodeStudioToken.tsx | 43 - client/src/icons/Codebase.tsx | 59 - client/src/icons/Cog.tsx | 23 +- client/src/icons/Collapsed.tsx | 61 - client/src/icons/Collections.tsx | 41 - client/src/icons/ColorSwitch.tsx | 14 + client/src/icons/Commit.tsx | 63 - client/src/icons/Conversation.tsx | 29 +- client/src/icons/CopyMD.tsx | 51 - client/src/icons/CopyText.tsx | 14 + client/src/icons/CornerArrow.tsx | 31 - client/src/icons/CursorSelection.tsx | 46 - client/src/icons/Def.tsx | 21 +- client/src/icons/DiscordLogo.tsx | 27 - client/src/icons/DocsSection.tsx | 47 - client/src/icons/Documents.tsx | 18 + client/src/icons/DoorLeft.tsx | 87 - client/src/icons/DoorOut.tsx | 14 + client/src/icons/DoorRight.tsx | 75 - client/src/icons/DoubleChevronLeft.tsx | 29 - client/src/icons/DoubleChevronRight.tsx | 29 - client/src/icons/DragVertical.tsx | 109 -- client/src/icons/Expanded.tsx | 69 - client/src/icons/Eye.tsx | 31 - client/src/icons/EyeCut.tsx | 29 +- client/src/icons/Feather.tsx | 31 - client/src/icons/FeatherSelected.tsx | 47 - client/src/icons/FileWithSparks.tsx | 18 + client/src/icons/Filter.tsx | 14 +- client/src/icons/Fire.tsx | 44 - client/src/icons/FloppyDisk.tsx | 59 - client/src/icons/Folder.tsx | 12 + client/src/icons/FolderClosed.tsx | 27 - client/src/icons/FolderFilled.tsx | 21 - client/src/icons/GitHubIcon.tsx | 2 +- client/src/icons/GitHubLogo.tsx | 25 - client/src/icons/Globe.tsx | 42 +- client/src/icons/Globe2.tsx | 69 - client/src/icons/HardDrive.tsx | 31 +- client/src/icons/Home.tsx | 33 - client/src/icons/Icons.stories.tsx | 22 - client/src/icons/Info.tsx | 31 - client/src/icons/Intelisence.tsx | 37 - client/src/icons/KLetter.tsx | 14 + client/src/icons/Letter.tsx | 173 -- client/src/icons/Like.tsx | 16 + client/src/icons/LinkChain.tsx | 14 + client/src/icons/List.tsx | 91 - client/src/icons/LiteLoader.tsx | 27 +- client/src/icons/Lock.tsx | 47 - client/src/icons/LockFilled.tsx | 45 - client/src/icons/LogoSmall.tsx | 27 - client/src/icons/Macintosh.tsx | 14 + client/src/icons/Magazine.tsx | 19 +- client/src/icons/MagnifyTool.tsx | 14 +- client/src/icons/MailIcon.tsx | 87 +- client/src/icons/MinusSignInCircle.tsx | 31 - client/src/icons/Modal.tsx | 79 - client/src/icons/MoreHorizontal.tsx | 12 +- client/src/icons/MoreVertical.tsx | 29 - client/src/icons/MultiShare.tsx | 51 - client/src/icons/NaturalLanguage.tsx | 85 - client/src/icons/NewTab.tsx | 43 - client/src/icons/Paper.tsx | 36 - client/src/icons/Papers.tsx | 82 - client/src/icons/Pen.tsx | 35 - client/src/icons/PenUnderline.tsx | 47 - client/src/icons/Pencil.tsx | 16 + client/src/icons/Person.tsx | 13 +- client/src/icons/Persons.tsx | 39 - client/src/icons/PlusSign.tsx | 14 + client/src/icons/PlusSignInBubble.tsx | 55 - client/src/icons/PlusSignInCircle.tsx | 31 - client/src/icons/PointClick.tsx | 65 - client/src/icons/PowerPlug.tsx | 54 - client/src/icons/Quill.tsx | 31 - client/src/icons/Ref.tsx | 17 +- client/src/icons/Refresh.tsx | 16 + client/src/icons/Regex.tsx | 67 +- client/src/icons/RegexSearch.tsx | 34 + client/src/icons/Repository.tsx | 37 +- client/src/icons/RepositoryFilled.tsx | 35 - client/src/icons/ReturnKey.tsx | 27 - client/src/icons/Run.tsx | 17 +- client/src/icons/Send.tsx | 11 +- client/src/icons/Shapes.tsx | 14 + client/src/icons/Sidebar.tsx | 87 - client/src/icons/SortAlphabetical.tsx | 59 - client/src/icons/Sparkle.tsx | 33 - client/src/icons/Sparkles.tsx | 43 - client/src/icons/SpinLoader.tsx | 126 ++ client/src/icons/SplitView.tsx | 14 + client/src/icons/Star.tsx | 23 - client/src/icons/Tab.tsx | 83 - client/src/icons/Template.tsx | 19 +- client/src/icons/TemplateAdd.tsx | 31 - client/src/icons/ThemeBlack.tsx | 25 + client/src/icons/ThemeDark.tsx | 25 + client/src/icons/ThemeLight.tsx | 25 + client/src/icons/ThumbsDown.tsx | 57 - client/src/icons/ThumbsUp.tsx | 57 - client/src/icons/Thunder.tsx | 31 - client/src/icons/TooltipTailBottom.tsx | 17 +- client/src/icons/TooltipTailLeft.tsx | 17 +- client/src/icons/TooltipTailRight.tsx | 17 +- client/src/icons/TooltipTailTop.tsx | 17 +- client/src/icons/TrashCan.tsx | 81 +- client/src/icons/TrashCanFilled.tsx | 31 - client/src/icons/TuneControls.tsx | 93 - client/src/icons/Unlike.tsx | 27 +- client/src/icons/Version.tsx | 41 - client/src/icons/Walk.tsx | 17 +- client/src/icons/Wallet.tsx | 14 + client/src/icons/WarningSign.tsx | 23 +- client/src/icons/Wrapper.tsx | 28 +- client/src/icons/WrenchAndScrewdriver.tsx | 51 - client/src/icons/index.ts | 191 +- client/src/index.css | 1481 ++++++---------- client/src/locales/en.json | 176 +- client/src/locales/es.json | 170 +- client/src/locales/it.json | 154 +- client/src/locales/ja.json | 161 +- client/src/locales/zh-CN.json | 163 +- client/src/mappers/conversation.ts | 20 +- client/src/mappers/filter.tsx | 31 - client/src/mappers/results.ts | 60 +- client/src/mocks/api_mocks.ts | 114 -- client/src/mocks/index.tsx | 349 ---- client/src/pages/HomeTab/AddRepoCard.tsx | 66 - .../HomeTab/AddRepos/AddCodeStudio/index.tsx | 71 - .../AddRepos/GithubReposStep/index.tsx | 103 -- .../HomeTab/AddRepos/GoBackButton/index.tsx | 18 - .../HomeTab/AddRepos/LocalReposStep/index.tsx | 145 -- .../AddRepos/PublicGithubReposStep/index.tsx | 145 -- client/src/pages/HomeTab/AddRepos/index.tsx | 50 - .../CodeStudiosSection/CodeStudioCard.tsx | 131 -- .../HomeTab/CodeStudiosSection/index.tsx | 86 - client/src/pages/HomeTab/Content.tsx | 252 --- .../HomeTab/ReposSection/RepoCard/NoRepos.tsx | 113 -- .../RepoCard/RepoCardSkeleton.tsx | 21 - .../HomeTab/ReposSection/RepoCard/index.tsx | 240 --- .../src/pages/HomeTab/ReposSection/index.tsx | 164 -- client/src/pages/HomeTab/index.tsx | 30 - client/src/pages/RepoTab/Content.tsx | 228 --- .../RepoTab/Repository/RepositoryOverview.tsx | 149 -- client/src/pages/RepoTab/Repository/index.tsx | 138 -- client/src/pages/RepoTab/ResultFull/index.tsx | 352 ---- .../ResultModal/FileModalContainer.tsx | 99 -- .../pages/RepoTab/ResultModal/ModeToggle.tsx | 78 - .../pages/RepoTab/ResultModal/Subheader.tsx | 40 - .../src/pages/RepoTab/ResultModal/index.tsx | 148 -- .../src/pages/RepoTab/Results/NoResults.tsx | 97 - .../pages/RepoTab/Results/ResultPreview.tsx | 69 - .../src/pages/RepoTab/Results/ResultsList.tsx | 49 - client/src/pages/RepoTab/Results/index.tsx | 112 -- client/src/pages/RepoTab/index.tsx | 47 - .../SelectBranch/BranchItem.tsx | 65 - .../AddContextModal/SelectBranch/index.tsx | 71 - .../AddContextModal/SelectFile/FileItem.tsx | 72 - .../AddContextModal/SelectFile/index.tsx | 82 - .../AddContextModal/SelectRepo/RepoItem.tsx | 93 - .../AddContextModal/SelectRepo/index.tsx | 94 - .../StudioTab/AddContextModal/StepItem.tsx | 21 - .../pages/StudioTab/AddContextModal/index.tsx | 211 --- .../AddDocsModal/CommandIndicator.tsx | 20 - .../IndexedDocsList/IndexedDocRow.tsx | 104 -- .../AddDocsModal/IndexedDocsList/index.tsx | 81 - .../PagesWithPreview/IndexedPage.tsx | 92 - .../AddDocsModal/PagesWithPreview/index.tsx | 208 --- .../AddDocsModal/Sections/RenderedSection.tsx | 56 - .../AddDocsModal/Sections/SectionItem.tsx | 65 - .../StudioTab/AddDocsModal/Sections/index.tsx | 71 - .../pages/StudioTab/AddDocsModal/index.tsx | 392 ---- client/src/pages/StudioTab/BlueChip.tsx | 15 - client/src/pages/StudioTab/Content.tsx | 546 ------ .../StudioTab/ContextPanel/ContextDocRow.tsx | 174 -- .../StudioTab/ContextPanel/ContextFileRow.tsx | 262 --- .../pages/StudioTab/ContextPanel/index.tsx | 273 --- .../src/pages/StudioTab/DiffPanel/index.tsx | 179 -- .../pages/StudioTab/DocPanel/DocSection.tsx | 76 - .../StudioTab/DocPanel/SectionsBadge.tsx | 19 - client/src/pages/StudioTab/DocPanel/index.tsx | 230 --- .../src/pages/StudioTab/FilePanel/index.tsx | 295 --- .../HistoryPanel/ConversationDay.tsx | 53 - .../HistoryPanel/ConversationTurn.tsx | 76 - .../pages/StudioTab/HistoryPanel/index.tsx | 95 - client/src/pages/StudioTab/KeyboardChip.tsx | 48 - client/src/pages/StudioTab/LinesBadge.tsx | 30 - .../src/pages/StudioTab/RelatedFilesBadge.tsx | 76 - .../pages/StudioTab/RelatedFilesDropdown.tsx | 259 --- .../RightPanel/Conversation/GeneratedDiff.tsx | 104 -- .../RightPanel/Conversation/Input.tsx | 274 --- .../RightPanel/Conversation/index.tsx | 696 -------- .../src/pages/StudioTab/RightPanel/index.tsx | 76 - .../StudioTab/TemplatesPanel/TemplateCard.tsx | 225 --- .../pages/StudioTab/TemplatesPanel/index.tsx | 57 - .../src/pages/StudioTab/TokensUsageBadge.tsx | 22 - .../pages/StudioTab/TokensUsageProgress.tsx | 90 - client/src/pages/StudioTab/index.tsx | 146 -- client/src/services/api.ts | 118 +- client/src/services/cache.ts | 6 +- client/src/services/storage.ts | 26 +- client/src/themes/abyss.css | 125 -- client/src/themes/atom-one-dark-pro.css | 156 -- client/src/themes/darcula.css | 155 -- client/src/themes/default-dark.css | 117 +- client/src/themes/default-light.css | 60 +- client/src/themes/dracula.css | 156 -- client/src/themes/github-dark.css | 135 -- client/src/themes/github-light.css | 135 -- client/src/themes/groovebox-dark.css | 166 -- client/src/themes/groovebox-light.css | 175 -- client/src/themes/gruvbox-dark.css | 166 -- client/src/themes/gruvbox-light.css | 175 -- client/src/themes/kimbie-dark.css | 151 -- client/src/themes/material.css | 155 -- client/src/themes/monokai.css | 131 -- client/src/themes/night-owl.css | 133 -- client/src/themes/quiet-light.css | 160 -- client/src/themes/solarized-dark.css | 140 -- client/src/themes/solarized-light.css | 112 -- client/src/themes/tomorrow-night-blue.css | 146 -- client/src/themes/vscode-default-dark.css | 139 -- client/src/themes/vscode-default-light.css | 145 -- client/src/types/api.ts | 40 +- client/src/types/general.ts | 226 ++- client/src/types/index.ts | 23 +- client/src/types/results.ts | 18 +- client/src/utils/commandBarUtils.test.ts | 202 +++ client/src/utils/commandBarUtils.ts | 44 + client/src/utils/domUtils.ts | 22 +- client/src/utils/index.ts | 41 +- client/src/utils/keyboardUtils.ts | 65 + client/src/utils/mappers.ts | 62 + client/src/utils/navigationUtils.ts | 9 +- client/tailwind.config.cjs | 91 +- package-lock.json | 737 +++----- package.json | 8 +- 658 files changed, 18002 insertions(+), 40925 deletions(-) create mode 100644 client/public/bloopHeadMascotLight.png create mode 100644 client/public/stripe_logo.png create mode 100644 client/src/CommandBar/Body/Item.tsx create mode 100644 client/src/CommandBar/Body/Section.tsx create mode 100644 client/src/CommandBar/Body/SectionDivider.tsx create mode 100644 client/src/CommandBar/Body/index.tsx create mode 100644 client/src/CommandBar/Footer/HintButton.tsx create mode 100644 client/src/CommandBar/Footer/index.tsx create mode 100644 client/src/CommandBar/Header/ChipItem.tsx create mode 100644 client/src/CommandBar/Header/index.tsx create mode 100644 client/src/CommandBar/index.tsx create mode 100644 client/src/CommandBar/steps/AddNewRepo.tsx create mode 100644 client/src/CommandBar/steps/CreateProject.tsx create mode 100644 client/src/CommandBar/steps/Documentation.tsx create mode 100644 client/src/CommandBar/steps/Initial.tsx create mode 100644 client/src/CommandBar/steps/LocalRepos.tsx create mode 100644 client/src/CommandBar/steps/ManageRepos/ActionsDropdown.tsx create mode 100644 client/src/CommandBar/steps/ManageRepos/index.tsx create mode 100644 client/src/CommandBar/steps/PrivateRepos/ActionsDropdown.tsx create mode 100644 client/src/CommandBar/steps/PrivateRepos/index.tsx create mode 100644 client/src/CommandBar/steps/PublicRepos.tsx create mode 100644 client/src/CommandBar/steps/SeachFiles.tsx create mode 100644 client/src/CommandBar/steps/ToggleTheme.tsx create mode 100644 client/src/CommandBar/steps/items/DocItem.tsx create mode 100644 client/src/CommandBar/steps/items/RepoItem.tsx rename client/src/{pages => }/Onboarding/Desktop/FeaturesStep/Feature.tsx (84%) rename client/src/{pages => }/Onboarding/Desktop/FeaturesStep/index.tsx (66%) rename client/src/{pages => }/Onboarding/Desktop/UserForm/Step1.tsx (69%) rename client/src/{pages => }/Onboarding/Desktop/UserForm/Step2.tsx (88%) rename client/src/{pages => }/Onboarding/Desktop/UserForm/index.tsx (65%) rename client/src/{pages => }/Onboarding/Desktop/index.tsx (78%) rename client/src/{pages => }/Onboarding/SelfServe/index.tsx (66%) rename client/src/{pages => }/Onboarding/index.tsx (78%) create mode 100644 client/src/Project/CurrentTabContent/ChatTab/ActionsDropdown.tsx create mode 100644 client/src/Project/CurrentTabContent/ChatTab/ChatPersistentState.tsx create mode 100644 client/src/Project/CurrentTabContent/ChatTab/Conversation.tsx create mode 100644 client/src/Project/CurrentTabContent/ChatTab/DeprecatedClientModal.tsx rename client/src/{components/Chat/ChatFooter => Project/CurrentTabContent/ChatTab}/Input/InputCore.tsx (63%) create mode 100644 client/src/Project/CurrentTabContent/ChatTab/Input/index.tsx rename client/src/{components/Chat/ChatFooter => Project/CurrentTabContent/ChatTab}/Input/mentionPlugin.ts (93%) rename client/src/{components/Chat/ChatFooter => Project/CurrentTabContent/ChatTab}/Input/nodes.ts (64%) rename client/src/{components/Chat/ChatFooter => Project/CurrentTabContent/ChatTab}/Input/placeholderPlugin.ts (100%) rename client/src/{components/Chat/ChatFooter => Project/CurrentTabContent/ChatTab}/Input/utils.ts (78%) create mode 100644 client/src/Project/CurrentTabContent/ChatTab/Message/LoadingStep.tsx rename client/src/{components/Chat/ChatBody/ConversationMessage => Project/CurrentTabContent/ChatTab/Message}/UserParsedQuery/LangChip.tsx (76%) rename client/src/{components/Chat/ChatBody/ConversationMessage => Project/CurrentTabContent/ChatTab/Message}/UserParsedQuery/PathChip.tsx (71%) create mode 100644 client/src/Project/CurrentTabContent/ChatTab/Message/UserParsedQuery/RepoChip.tsx rename client/src/{components/Chat/ChatBody/ConversationMessage => Project/CurrentTabContent/ChatTab/Message}/UserParsedQuery/index.tsx (81%) create mode 100644 client/src/Project/CurrentTabContent/ChatTab/Message/index.tsx create mode 100644 client/src/Project/CurrentTabContent/ChatTab/ScrollableContent.tsx create mode 100644 client/src/Project/CurrentTabContent/ChatTab/StarterMessage.tsx create mode 100644 client/src/Project/CurrentTabContent/ChatTab/index.tsx create mode 100644 client/src/Project/CurrentTabContent/DropTarget.tsx create mode 100644 client/src/Project/CurrentTabContent/EmptyTab.tsx create mode 100644 client/src/Project/CurrentTabContent/FileTab/ActionsDropdown.tsx create mode 100644 client/src/Project/CurrentTabContent/FileTab/index.tsx create mode 100644 client/src/Project/CurrentTabContent/Header/AddTabButton.tsx create mode 100644 client/src/Project/CurrentTabContent/Header/AddTabDropdown.tsx create mode 100644 client/src/Project/CurrentTabContent/Header/TabButton.tsx create mode 100644 client/src/Project/CurrentTabContent/Header/index.tsx create mode 100644 client/src/Project/CurrentTabContent/index.tsx create mode 100644 client/src/Project/EmptyProject.tsx create mode 100644 client/src/Project/LeftSidebar/NavPanel/Conversations.tsx create mode 100644 client/src/Project/LeftSidebar/NavPanel/ConversationsDropdown.tsx create mode 100644 client/src/Project/LeftSidebar/NavPanel/CoversationEntry.tsx create mode 100644 client/src/Project/LeftSidebar/NavPanel/Repo.tsx create mode 100644 client/src/Project/LeftSidebar/NavPanel/RepoDropdown.tsx create mode 100644 client/src/Project/LeftSidebar/NavPanel/RepoEntry.tsx create mode 100644 client/src/Project/LeftSidebar/NavPanel/index.tsx create mode 100644 client/src/Project/LeftSidebar/RegexSearchPanel/AutocompleteMenu.tsx create mode 100644 client/src/Project/LeftSidebar/RegexSearchPanel/AutocompleteMenuItem.tsx rename client/src/{components/CodeBlock/Code/index.tsx => Project/LeftSidebar/RegexSearchPanel/Results/CodeLine.tsx} (54%) create mode 100644 client/src/Project/LeftSidebar/RegexSearchPanel/Results/CodeResult.tsx create mode 100644 client/src/Project/LeftSidebar/RegexSearchPanel/Results/FileResult.tsx create mode 100644 client/src/Project/LeftSidebar/RegexSearchPanel/Results/RepoResult.tsx create mode 100644 client/src/Project/LeftSidebar/RegexSearchPanel/index.tsx create mode 100644 client/src/Project/LeftSidebar/index.tsx create mode 100644 client/src/Project/RightTab.tsx create mode 100644 client/src/Project/index.tsx create mode 100644 client/src/ProjectSettings/General.tsx create mode 100644 client/src/ProjectSettings/index.tsx create mode 100644 client/src/Settings/General/AnswerSpeedDropdown.tsx create mode 100644 client/src/Settings/General/index.tsx create mode 100644 client/src/Settings/Preferences/LanguageDropdown.tsx create mode 100644 client/src/Settings/Preferences/ThemeDropdown.tsx create mode 100644 client/src/Settings/Preferences/index.tsx create mode 100644 client/src/Settings/Subscription/BenefitItem.tsx create mode 100644 client/src/Settings/Subscription/CardFree.tsx create mode 100644 client/src/Settings/Subscription/CardPaid.tsx create mode 100644 client/src/Settings/Subscription/Confetti.tsx create mode 100644 client/src/Settings/Subscription/index.tsx create mode 100644 client/src/Settings/index.tsx delete mode 100644 client/src/circleProgress.css delete mode 100644 client/src/components/Accordion/Accordion.stories.tsx delete mode 100644 client/src/components/Accordion/index.tsx delete mode 100644 client/src/components/AddStudioContext/index.tsx delete mode 100644 client/src/components/Breadcrumbs/Breadcrumbs.stories.tsx rename client/src/components/{BreadcrumbsPath/index.tsx => Breadcrumbs/PathContainer.tsx} (70%) delete mode 100644 client/src/components/Button/Button.stories.tsx delete mode 100644 client/src/components/Chat/ChatBody/AllCoversations/ConversationListItem.tsx delete mode 100644 client/src/components/Chat/ChatBody/AllCoversations/index.tsx delete mode 100644 client/src/components/Chat/ChatBody/Conversation.tsx delete mode 100644 client/src/components/Chat/ChatBody/ConversationMessage/MessageFeedback.tsx delete mode 100644 client/src/components/Chat/ChatBody/ConversationMessage/index.tsx delete mode 100644 client/src/components/Chat/ChatBody/FirstMessage.tsx delete mode 100644 client/src/components/Chat/ChatBody/index.tsx delete mode 100644 client/src/components/Chat/ChatFooter/DeprecatedClientModal.tsx delete mode 100644 client/src/components/Chat/ChatFooter/InputLoader.tsx delete mode 100644 client/src/components/Chat/ChatFooter/NLInput.tsx delete mode 100644 client/src/components/Chat/ChatFooter/SuggestionItem.tsx delete mode 100644 client/src/components/Chat/ChatFooter/Suggestions.tsx delete mode 100644 client/src/components/Chat/ChatFooter/index.tsx delete mode 100644 client/src/components/Chat/ChatHeader/AnswerSpeedSelector/SelectionItem.tsx delete mode 100644 client/src/components/Chat/ChatHeader/AnswerSpeedSelector/index.tsx delete mode 100644 client/src/components/Chat/ChatHeader/index.tsx delete mode 100644 client/src/components/Chat/ChipButton.tsx delete mode 100644 client/src/components/Chat/FeedbackBtns/Downvote.tsx delete mode 100644 client/src/components/Chat/FeedbackBtns/Upvote.tsx delete mode 100644 client/src/components/Chat/index.tsx rename client/src/components/{Chat/ChatBody/ConversationMessage => Chips}/FileChip.tsx (65%) delete mode 100644 client/src/components/ClearButton/ClearButton.stories.tsx rename client/src/components/{CodeBlock/Search => Code/CodeBlockSearch}/index.tsx (50%) create mode 100644 client/src/components/Code/CodeFragment/index.tsx create mode 100644 client/src/components/Code/CodeFull/SelectionPopup.tsx rename client/src/components/{CodeBlock => Code}/CodeFull/Token.tsx (63%) create mode 100644 client/src/components/Code/CodeFull/VirtualizedCode.tsx create mode 100644 client/src/components/Code/CodeFull/index.tsx create mode 100644 client/src/components/Code/CodeLine.tsx create mode 100644 client/src/components/Code/CodeToken.tsx delete mode 100644 client/src/components/CodeBlock/Code/CodeContainer.tsx delete mode 100644 client/src/components/CodeBlock/Code/CodeLine.tsx delete mode 100644 client/src/components/CodeBlock/Code/CodeToken.tsx delete mode 100644 client/src/components/CodeBlock/CodeBlock.stories.tsx delete mode 100644 client/src/components/CodeBlock/CodeDiff/index.tsx delete mode 100644 client/src/components/CodeBlock/CodeFull/CodeContainer.tsx delete mode 100644 client/src/components/CodeBlock/CodeFull/CodeContainerFull.tsx delete mode 100644 client/src/components/CodeBlock/CodeFull/CodeContainerVirtualized.tsx delete mode 100644 client/src/components/CodeBlock/CodeFull/ExplainButton.tsx delete mode 100644 client/src/components/CodeBlock/CodeFull/FoldButton.tsx delete mode 100644 client/src/components/CodeBlock/CodeFull/index.tsx delete mode 100644 client/src/components/CodeBlock/CodeFullSelectable/CodeContainer.tsx delete mode 100644 client/src/components/CodeBlock/CodeFullSelectable/CodeLine.tsx delete mode 100644 client/src/components/CodeBlock/CodeFullSelectable/FoldButton.tsx delete mode 100644 client/src/components/CodeBlock/CodeFullSelectable/LazyLinesContainer.tsx delete mode 100644 client/src/components/CodeBlock/CodeFullSelectable/SelectionHandler.tsx delete mode 100644 client/src/components/CodeBlock/CodeFullSelectable/SelectionRect.tsx delete mode 100644 client/src/components/CodeBlock/CodeFullSelectable/index.tsx delete mode 100644 client/src/components/CodeBlock/MiniMap/index.tsx delete mode 100644 client/src/components/CodeBlock/SearchFile/index.tsx delete mode 100644 client/src/components/CodeBlock/SearchRepo/index.tsx delete mode 100644 client/src/components/CodeSymbolIcon/CodeSymbolIcons.stories.tsx delete mode 100644 client/src/components/CodeSymbolIcon/index.tsx delete mode 100644 client/src/components/Comment/Comment.stories.tsx delete mode 100644 client/src/components/Comment/CommentItemExpanded.tsx delete mode 100644 client/src/components/Comment/index.tsx delete mode 100644 client/src/components/ConfirmationPopup/index.tsx delete mode 100644 client/src/components/ContextMenu/ContextMenuItem/Item.tsx delete mode 100644 client/src/components/ContextMenu/ContextMenuItem/ItemSelectable.tsx delete mode 100644 client/src/components/ContextMenu/ContextMenuItem/ItemShared.tsx delete mode 100644 client/src/components/ContextMenu/index.tsx delete mode 100644 client/src/components/Dropdown/Dropdown.stories.tsx delete mode 100644 client/src/components/Dropdown/Normal/index.tsx create mode 100644 client/src/components/Dropdown/Section/SectionItem.tsx create mode 100644 client/src/components/Dropdown/Section/SectionLabel.tsx create mode 100644 client/src/components/Dropdown/Section/index.tsx delete mode 100644 client/src/components/Dropdown/WithIcon/index.tsx delete mode 100644 client/src/components/FileMenu/index.tsx delete mode 100644 client/src/components/Filters/FilterItem.tsx delete mode 100644 client/src/components/Filters/FilterSection.tsx delete mode 100644 client/src/components/Filters/FilterTitle.tsx delete mode 100644 client/src/components/Filters/index.tsx delete mode 100644 client/src/components/FloatingToolbar/FloatingToolbar.stories.tsx delete mode 100644 client/src/components/FloatingToolbar/ToolbarButton.tsx delete mode 100644 client/src/components/FloatingToolbar/index.tsx create mode 100644 client/src/components/Header/HeaderRightPart.tsx create mode 100644 client/src/components/Header/ProjectsDropdown.tsx create mode 100644 client/src/components/Header/UserDropdown.tsx create mode 100644 client/src/components/Header/index.tsx delete mode 100644 client/src/components/IdeNavigation/DirEntry.tsx delete mode 100644 client/src/components/IdeNavigation/ListNavigation/ListNavigation.stories.tsx delete mode 100644 client/src/components/IdeNavigation/ListNavigation/index.tsx delete mode 100644 client/src/components/IdeNavigation/NavigationItem/index.tsx delete mode 100644 client/src/components/IdeNavigation/NavigationItemChevron/index.tsx delete mode 100644 client/src/components/IdeNavigation/NavigationPanel/index.tsx delete mode 100644 client/src/components/IdeNavigation/index.tsx create mode 100644 client/src/components/KeyboardHint/index.tsx delete mode 100644 client/src/components/LanguageSelector/index.tsx delete mode 100644 client/src/components/LeftSidebar/index.tsx delete mode 100644 client/src/components/Loaders/BarLoader.tsx delete mode 100644 client/src/components/Loaders/CircleProgressLoader.tsx delete mode 100644 client/src/components/Loaders/Loaders.stories.tsx delete mode 100644 client/src/components/Loaders/SkeletonLoader.tsx delete mode 100644 client/src/components/Loaders/SpinLoader.tsx create mode 100644 client/src/components/Loaders/SpinnerLoader.tsx delete mode 100644 client/src/components/Loaders/ThreeDotsLoader.tsx rename client/src/components/{SeparateOnboardingStep => Modal}/index.tsx (56%) delete mode 100644 client/src/components/ModalOrSidebar/index.tsx delete mode 100644 client/src/components/NavBar/Tab.tsx delete mode 100644 client/src/components/NavBar/index.tsx delete mode 100644 client/src/components/PageTemplate/BranchItem.tsx delete mode 100644 client/src/components/PageTemplate/BranchSelector.tsx delete mode 100644 client/src/components/PageTemplate/HomeSubheader.tsx delete mode 100644 client/src/components/PageTemplate/RepoHomeBtn.tsx delete mode 100644 client/src/components/PageTemplate/Separator.tsx delete mode 100644 client/src/components/PageTemplate/Subheader.tsx delete mode 100644 client/src/components/PageTemplate/TabButton.tsx delete mode 100644 client/src/components/PageTemplate/index.tsx delete mode 100644 client/src/components/Pagination/PaginationButton/index.tsx delete mode 100644 client/src/components/Pagination/index.tsx delete mode 100644 client/src/components/PortalContainer/index.tsx delete mode 100644 client/src/components/ProgressBar/ProgressBar.stories.tsx delete mode 100644 client/src/components/ProgressBar/index.tsx delete mode 100644 client/src/components/PromptGuidePopup/PromptSvg.tsx delete mode 100644 client/src/components/PromptGuidePopup/index.tsx delete mode 100644 client/src/components/RadioButton/index.tsx create mode 100644 client/src/components/RefsDefsPopup/Badge.tsx create mode 100644 client/src/components/RefsDefsPopup/RefDefFileItem.tsx create mode 100644 client/src/components/RefsDefsPopup/RefDefFileLine.tsx create mode 100644 client/src/components/RefsDefsPopup/index.tsx delete mode 100644 client/src/components/RepoList/RepoList.stories.tsx delete mode 100644 client/src/components/RepoList/SearchableRepoList.tsx delete mode 100644 client/src/components/RepoList/index.tsx delete mode 100644 client/src/components/ReportBugModal/ConfirmImg.tsx delete mode 100644 client/src/components/RepositoryFiles/FileRow.tsx delete mode 100644 client/src/components/RepositoryFiles/index.tsx delete mode 100644 client/src/components/ResultsPageHeader/index.tsx create mode 100644 client/src/components/ScrollToBottom/Composer.tsx create mode 100644 client/src/components/ScrollToBottom/EventSpy.ts create mode 100644 client/src/components/ScrollToBottom/FunctionContext.ts create mode 100644 client/src/components/ScrollToBottom/InternalContext.ts create mode 100644 client/src/components/ScrollToBottom/Panel.tsx create mode 100644 client/src/components/ScrollToBottom/SpineTo.tsx create mode 100644 client/src/components/ScrollToBottom/debounce.ts create mode 100644 client/src/components/ScrollToBottom/index.tsx delete mode 100644 client/src/components/SearchInput/AutocompleteMenu.tsx delete mode 100644 client/src/components/SearchInput/AutocompleteMenuItem.tsx delete mode 100644 client/src/components/SearchInput/SearchTextInput.tsx delete mode 100644 client/src/components/SearchInput/index.tsx create mode 100644 client/src/components/SectionsNav/SectionButton.tsx create mode 100644 client/src/components/SectionsNav/index.tsx delete mode 100644 client/src/components/SelectToggleButton/SelectToggleButton.stories.tsx delete mode 100644 client/src/components/SelectToggleButton/index.tsx delete mode 100644 client/src/components/SeparateOnboardingStep/DialogText/index.tsx delete mode 100644 client/src/components/Settings/Badge/index.tsx delete mode 100644 client/src/components/Settings/General/Password/index.tsx delete mode 100644 client/src/components/Settings/General/Profile/index.tsx delete mode 100644 client/src/components/Settings/General/index.tsx delete mode 100644 client/src/components/Settings/Preferences/index.tsx delete mode 100644 client/src/components/Settings/SettingsRow/index.tsx delete mode 100644 client/src/components/Settings/SettingsText/index.tsx delete mode 100644 client/src/components/Settings/index.tsx delete mode 100644 client/src/components/ShareButton/ContextMenu/index.tsx delete mode 100644 client/src/components/ShareButton/Counter/index.tsx delete mode 100644 client/src/components/ShareButton/ShareButton.stories.tsx delete mode 100644 client/src/components/ShareButton/index.tsx delete mode 100644 client/src/components/ShareFileModal/index.tsx delete mode 100644 client/src/components/Skeleton/Skeleton.stories.tsx delete mode 100644 client/src/components/Skeleton/index.tsx delete mode 100644 client/src/components/SkeletonItem/SkeletonItem.stories.tsx delete mode 100644 client/src/components/SkeletonItem/index.tsx delete mode 100644 client/src/components/StatusBar/StatusBar.stories.tsx delete mode 100644 client/src/components/StatusBar/StatusItem/index.tsx delete mode 100644 client/src/components/StatusBar/index.tsx delete mode 100644 client/src/components/StudioGuidePopup/PromptSvg.tsx delete mode 100644 client/src/components/StudioGuidePopup/index.tsx delete mode 100644 client/src/components/Tabs/Tabs.stories.tsx delete mode 100644 client/src/components/Tabs/index.tsx delete mode 100644 client/src/components/TextField/TextField.stories.tsx rename client/src/components/{ => TextInput}/ClearButton/index.tsx (85%) rename client/src/components/{ => TextInput}/RegexButton/index.tsx (91%) delete mode 100644 client/src/components/TextInput/TextInput.stories.tsx delete mode 100644 client/src/components/Tooltip/Tooltip.stories.tsx delete mode 100644 client/src/components/TooltipCode/Badge.tsx delete mode 100644 client/src/components/TooltipCode/RefDefItem.tsx delete mode 100644 client/src/components/TooltipCode/RefsDefsPopup.tsx delete mode 100644 client/src/components/TooltipCode/TooltipCode.stories.tsx delete mode 100644 client/src/components/TooltipCode/index.tsx delete mode 100644 client/src/components/TooltipCommit/TooltipCommit.stories.tsx delete mode 100644 client/src/components/TooltipCommit/index.tsx delete mode 100644 client/src/components/Typography/Typography.stories.tsx delete mode 100644 client/src/components/UpgradePopup/ConversationSvg.tsx delete mode 100644 client/src/components/UpgradePopup/Countdown.tsx delete mode 100644 client/src/components/UpgradePopup/WaitingUpgradePopup/index.tsx delete mode 100644 client/src/components/UpgradePopup/index.tsx rename client/src/components/{CloudFeaturePopup => UpgradeRequiredPopup}/BranchesSvg.tsx (100%) rename client/src/components/{CloudFeaturePopup => UpgradeRequiredPopup}/index.tsx (50%) create mode 100644 client/src/consts/commandBar.ts create mode 100644 client/src/consts/general.ts create mode 100644 client/src/consts/shortcuts.ts delete mode 100644 client/src/context/appNavigationContext.ts delete mode 100644 client/src/context/chatContext.ts create mode 100644 client/src/context/chatsContext.tsx create mode 100644 client/src/context/commandBarContext.ts create mode 100644 client/src/context/envContext.ts delete mode 100644 client/src/context/fileModalContext.ts create mode 100644 client/src/context/projectContext.ts delete mode 100644 client/src/context/providers/AppNavigationProvider.tsx delete mode 100644 client/src/context/providers/ChatContextProvider.tsx create mode 100644 client/src/context/providers/ChatsContextProvider.tsx create mode 100644 client/src/context/providers/CommandBarContextProvider.tsx delete mode 100644 client/src/context/providers/FileModalContextProvider.tsx create mode 100644 client/src/context/providers/ProjectContextProvider.tsx create mode 100644 client/src/context/providers/RepositoriesContextProvider.tsx delete mode 100644 client/src/context/providers/SearchContextProvider.tsx delete mode 100644 client/src/context/providers/StudioContextProvider.tsx delete mode 100644 client/src/context/providers/TabUiContextProvider.tsx create mode 100644 client/src/context/providers/TabsContextProvider.tsx rename client/src/context/providers/{GeneralUiContextProvider.tsx => UIContextProvider.tsx} (53%) delete mode 100644 client/src/context/searchContext.ts delete mode 100644 client/src/context/studioContext.ts delete mode 100644 client/src/context/tabsContext.ts create mode 100644 client/src/context/tabsContext.tsx create mode 100644 client/src/fonts/FiraCode/FiraCode-Bold.ttf create mode 100644 client/src/fonts/FiraCode/FiraCode-Light.ttf create mode 100644 client/src/fonts/FiraCode/FiraCode-Medium.ttf create mode 100644 client/src/fonts/FiraCode/FiraCode-Regular.ttf create mode 100644 client/src/fonts/FiraCode/FiraCode-SemiBold.ttf delete mode 100644 client/src/hooks/useAppNavigation.tsx create mode 100644 client/src/hooks/useCodeSearch.ts create mode 100644 client/src/hooks/useDiffLines.ts create mode 100644 client/src/hooks/useGlobalShortcuts.ts delete mode 100644 client/src/hooks/useIsOnScreen.ts delete mode 100644 client/src/hooks/useOnScrollHook.ts delete mode 100644 client/src/hooks/useRelatedFiles.tsx delete mode 100644 client/src/hooks/useResizeableSplitPanel.ts delete mode 100644 client/src/hooks/useSearch.tsx delete mode 100644 client/src/hooks/useSearchState.tsx create mode 100644 client/src/hooks/useShortcuts.ts create mode 100644 client/src/hooks/useSignOut.ts create mode 100644 client/src/hooks/useStateRef.ts delete mode 100644 client/src/icons/AIAnswerLong.tsx delete mode 100644 client/src/icons/ArrowBoxOut.tsx delete mode 100644 client/src/icons/ArrowDown.tsx delete mode 100644 client/src/icons/ArrowJumpLeft.tsx delete mode 100644 client/src/icons/ArrowPushBottom.tsx delete mode 100644 client/src/icons/ArrowPushLeft.tsx delete mode 100644 client/src/icons/ArrowPushRight.tsx delete mode 100644 client/src/icons/ArrowPushTop.tsx delete mode 100644 client/src/icons/ArrowRefresh.tsx delete mode 100644 client/src/icons/ArrowRevert.tsx delete mode 100644 client/src/icons/ArrowRight.tsx delete mode 100644 client/src/icons/ArrowRotate.tsx create mode 100644 client/src/icons/ArrowTriangleBottom.tsx delete mode 100644 client/src/icons/ArrowUp.tsx delete mode 100644 client/src/icons/BranchMerged.tsx delete mode 100644 client/src/icons/Calendar.tsx delete mode 100644 client/src/icons/Card.tsx delete mode 100644 client/src/icons/ChatBubble.tsx create mode 100644 client/src/icons/ChatBubbles.tsx create mode 100644 client/src/icons/Check.tsx delete mode 100644 client/src/icons/CheckIcon.tsx create mode 100644 client/src/icons/CheckList.tsx delete mode 100644 client/src/icons/Checkmark.tsx create mode 100644 client/src/icons/CheckmarkInSquare.tsx delete mode 100644 client/src/icons/ChevronDoubleIntersected.tsx delete mode 100644 client/src/icons/ChevronDownFilled.tsx delete mode 100644 client/src/icons/ChevronDownIcon.tsx delete mode 100644 client/src/icons/ChevronFoldIn.tsx delete mode 100644 client/src/icons/ChevronFoldOut.tsx delete mode 100644 client/src/icons/ChevronLeft.tsx delete mode 100644 client/src/icons/ChevronLeftFilled.tsx delete mode 100644 client/src/icons/ChevronRightFilled.tsx delete mode 100644 client/src/icons/ChevronRightIcon.tsx delete mode 100644 client/src/icons/ChevronUpFilled.tsx delete mode 100644 client/src/icons/Chronometer.tsx delete mode 100644 client/src/icons/ClearIcon.tsx delete mode 100644 client/src/icons/Clock.tsx create mode 100644 client/src/icons/CloseSignInCircle.tsx delete mode 100644 client/src/icons/CodeLanguage.tsx create mode 100644 client/src/icons/CodeLineWithSparkle.tsx delete mode 100644 client/src/icons/CodeStudioColored.tsx delete mode 100644 client/src/icons/CodeStudioToken.tsx delete mode 100644 client/src/icons/Codebase.tsx delete mode 100644 client/src/icons/Collapsed.tsx delete mode 100644 client/src/icons/Collections.tsx create mode 100644 client/src/icons/ColorSwitch.tsx delete mode 100644 client/src/icons/Commit.tsx delete mode 100644 client/src/icons/CopyMD.tsx create mode 100644 client/src/icons/CopyText.tsx delete mode 100644 client/src/icons/CornerArrow.tsx delete mode 100644 client/src/icons/CursorSelection.tsx delete mode 100644 client/src/icons/DiscordLogo.tsx delete mode 100644 client/src/icons/DocsSection.tsx create mode 100644 client/src/icons/Documents.tsx delete mode 100644 client/src/icons/DoorLeft.tsx create mode 100644 client/src/icons/DoorOut.tsx delete mode 100644 client/src/icons/DoorRight.tsx delete mode 100644 client/src/icons/DoubleChevronLeft.tsx delete mode 100644 client/src/icons/DoubleChevronRight.tsx delete mode 100644 client/src/icons/DragVertical.tsx delete mode 100644 client/src/icons/Expanded.tsx delete mode 100644 client/src/icons/Eye.tsx delete mode 100644 client/src/icons/Feather.tsx delete mode 100644 client/src/icons/FeatherSelected.tsx create mode 100644 client/src/icons/FileWithSparks.tsx delete mode 100644 client/src/icons/Fire.tsx delete mode 100644 client/src/icons/FloppyDisk.tsx create mode 100644 client/src/icons/Folder.tsx delete mode 100644 client/src/icons/FolderClosed.tsx delete mode 100644 client/src/icons/FolderFilled.tsx delete mode 100644 client/src/icons/GitHubLogo.tsx delete mode 100644 client/src/icons/Globe2.tsx delete mode 100644 client/src/icons/Home.tsx delete mode 100644 client/src/icons/Icons.stories.tsx delete mode 100644 client/src/icons/Info.tsx delete mode 100644 client/src/icons/Intelisence.tsx create mode 100644 client/src/icons/KLetter.tsx delete mode 100644 client/src/icons/Letter.tsx create mode 100644 client/src/icons/Like.tsx create mode 100644 client/src/icons/LinkChain.tsx delete mode 100644 client/src/icons/List.tsx delete mode 100644 client/src/icons/Lock.tsx delete mode 100644 client/src/icons/LockFilled.tsx delete mode 100644 client/src/icons/LogoSmall.tsx create mode 100644 client/src/icons/Macintosh.tsx delete mode 100644 client/src/icons/MinusSignInCircle.tsx delete mode 100644 client/src/icons/Modal.tsx delete mode 100644 client/src/icons/MoreVertical.tsx delete mode 100644 client/src/icons/MultiShare.tsx delete mode 100644 client/src/icons/NaturalLanguage.tsx delete mode 100644 client/src/icons/NewTab.tsx delete mode 100644 client/src/icons/Paper.tsx delete mode 100644 client/src/icons/Papers.tsx delete mode 100644 client/src/icons/Pen.tsx delete mode 100644 client/src/icons/PenUnderline.tsx create mode 100644 client/src/icons/Pencil.tsx delete mode 100644 client/src/icons/Persons.tsx create mode 100644 client/src/icons/PlusSign.tsx delete mode 100644 client/src/icons/PlusSignInBubble.tsx delete mode 100644 client/src/icons/PlusSignInCircle.tsx delete mode 100644 client/src/icons/PointClick.tsx delete mode 100644 client/src/icons/PowerPlug.tsx delete mode 100644 client/src/icons/Quill.tsx create mode 100644 client/src/icons/Refresh.tsx create mode 100644 client/src/icons/RegexSearch.tsx delete mode 100644 client/src/icons/RepositoryFilled.tsx delete mode 100644 client/src/icons/ReturnKey.tsx create mode 100644 client/src/icons/Shapes.tsx delete mode 100644 client/src/icons/Sidebar.tsx delete mode 100644 client/src/icons/SortAlphabetical.tsx delete mode 100644 client/src/icons/Sparkle.tsx delete mode 100644 client/src/icons/Sparkles.tsx create mode 100644 client/src/icons/SpinLoader.tsx create mode 100644 client/src/icons/SplitView.tsx delete mode 100644 client/src/icons/Star.tsx delete mode 100644 client/src/icons/Tab.tsx delete mode 100644 client/src/icons/TemplateAdd.tsx create mode 100644 client/src/icons/ThemeBlack.tsx create mode 100644 client/src/icons/ThemeDark.tsx create mode 100644 client/src/icons/ThemeLight.tsx delete mode 100644 client/src/icons/ThumbsDown.tsx delete mode 100644 client/src/icons/ThumbsUp.tsx delete mode 100644 client/src/icons/Thunder.tsx delete mode 100644 client/src/icons/TrashCanFilled.tsx delete mode 100644 client/src/icons/TuneControls.tsx delete mode 100644 client/src/icons/Version.tsx create mode 100644 client/src/icons/Wallet.tsx delete mode 100644 client/src/icons/WrenchAndScrewdriver.tsx delete mode 100644 client/src/mappers/filter.tsx delete mode 100644 client/src/mocks/api_mocks.ts delete mode 100644 client/src/mocks/index.tsx delete mode 100644 client/src/pages/HomeTab/AddRepoCard.tsx delete mode 100644 client/src/pages/HomeTab/AddRepos/AddCodeStudio/index.tsx delete mode 100644 client/src/pages/HomeTab/AddRepos/GithubReposStep/index.tsx delete mode 100644 client/src/pages/HomeTab/AddRepos/GoBackButton/index.tsx delete mode 100644 client/src/pages/HomeTab/AddRepos/LocalReposStep/index.tsx delete mode 100644 client/src/pages/HomeTab/AddRepos/PublicGithubReposStep/index.tsx delete mode 100644 client/src/pages/HomeTab/AddRepos/index.tsx delete mode 100644 client/src/pages/HomeTab/CodeStudiosSection/CodeStudioCard.tsx delete mode 100644 client/src/pages/HomeTab/CodeStudiosSection/index.tsx delete mode 100644 client/src/pages/HomeTab/Content.tsx delete mode 100644 client/src/pages/HomeTab/ReposSection/RepoCard/NoRepos.tsx delete mode 100644 client/src/pages/HomeTab/ReposSection/RepoCard/RepoCardSkeleton.tsx delete mode 100644 client/src/pages/HomeTab/ReposSection/RepoCard/index.tsx delete mode 100644 client/src/pages/HomeTab/ReposSection/index.tsx delete mode 100644 client/src/pages/HomeTab/index.tsx delete mode 100644 client/src/pages/RepoTab/Content.tsx delete mode 100644 client/src/pages/RepoTab/Repository/RepositoryOverview.tsx delete mode 100644 client/src/pages/RepoTab/Repository/index.tsx delete mode 100644 client/src/pages/RepoTab/ResultFull/index.tsx delete mode 100644 client/src/pages/RepoTab/ResultModal/FileModalContainer.tsx delete mode 100644 client/src/pages/RepoTab/ResultModal/ModeToggle.tsx delete mode 100644 client/src/pages/RepoTab/ResultModal/Subheader.tsx delete mode 100644 client/src/pages/RepoTab/ResultModal/index.tsx delete mode 100644 client/src/pages/RepoTab/Results/NoResults.tsx delete mode 100644 client/src/pages/RepoTab/Results/ResultPreview.tsx delete mode 100644 client/src/pages/RepoTab/Results/ResultsList.tsx delete mode 100644 client/src/pages/RepoTab/Results/index.tsx delete mode 100644 client/src/pages/RepoTab/index.tsx delete mode 100644 client/src/pages/StudioTab/AddContextModal/SelectBranch/BranchItem.tsx delete mode 100644 client/src/pages/StudioTab/AddContextModal/SelectBranch/index.tsx delete mode 100644 client/src/pages/StudioTab/AddContextModal/SelectFile/FileItem.tsx delete mode 100644 client/src/pages/StudioTab/AddContextModal/SelectFile/index.tsx delete mode 100644 client/src/pages/StudioTab/AddContextModal/SelectRepo/RepoItem.tsx delete mode 100644 client/src/pages/StudioTab/AddContextModal/SelectRepo/index.tsx delete mode 100644 client/src/pages/StudioTab/AddContextModal/StepItem.tsx delete mode 100644 client/src/pages/StudioTab/AddContextModal/index.tsx delete mode 100644 client/src/pages/StudioTab/AddDocsModal/CommandIndicator.tsx delete mode 100644 client/src/pages/StudioTab/AddDocsModal/IndexedDocsList/IndexedDocRow.tsx delete mode 100644 client/src/pages/StudioTab/AddDocsModal/IndexedDocsList/index.tsx delete mode 100644 client/src/pages/StudioTab/AddDocsModal/PagesWithPreview/IndexedPage.tsx delete mode 100644 client/src/pages/StudioTab/AddDocsModal/PagesWithPreview/index.tsx delete mode 100644 client/src/pages/StudioTab/AddDocsModal/Sections/RenderedSection.tsx delete mode 100644 client/src/pages/StudioTab/AddDocsModal/Sections/SectionItem.tsx delete mode 100644 client/src/pages/StudioTab/AddDocsModal/Sections/index.tsx delete mode 100644 client/src/pages/StudioTab/AddDocsModal/index.tsx delete mode 100644 client/src/pages/StudioTab/BlueChip.tsx delete mode 100644 client/src/pages/StudioTab/Content.tsx delete mode 100644 client/src/pages/StudioTab/ContextPanel/ContextDocRow.tsx delete mode 100644 client/src/pages/StudioTab/ContextPanel/ContextFileRow.tsx delete mode 100644 client/src/pages/StudioTab/ContextPanel/index.tsx delete mode 100644 client/src/pages/StudioTab/DiffPanel/index.tsx delete mode 100644 client/src/pages/StudioTab/DocPanel/DocSection.tsx delete mode 100644 client/src/pages/StudioTab/DocPanel/SectionsBadge.tsx delete mode 100644 client/src/pages/StudioTab/DocPanel/index.tsx delete mode 100644 client/src/pages/StudioTab/FilePanel/index.tsx delete mode 100644 client/src/pages/StudioTab/HistoryPanel/ConversationDay.tsx delete mode 100644 client/src/pages/StudioTab/HistoryPanel/ConversationTurn.tsx delete mode 100644 client/src/pages/StudioTab/HistoryPanel/index.tsx delete mode 100644 client/src/pages/StudioTab/KeyboardChip.tsx delete mode 100644 client/src/pages/StudioTab/LinesBadge.tsx delete mode 100644 client/src/pages/StudioTab/RelatedFilesBadge.tsx delete mode 100644 client/src/pages/StudioTab/RelatedFilesDropdown.tsx delete mode 100644 client/src/pages/StudioTab/RightPanel/Conversation/GeneratedDiff.tsx delete mode 100644 client/src/pages/StudioTab/RightPanel/Conversation/Input.tsx delete mode 100644 client/src/pages/StudioTab/RightPanel/Conversation/index.tsx delete mode 100644 client/src/pages/StudioTab/RightPanel/index.tsx delete mode 100644 client/src/pages/StudioTab/TemplatesPanel/TemplateCard.tsx delete mode 100644 client/src/pages/StudioTab/TemplatesPanel/index.tsx delete mode 100644 client/src/pages/StudioTab/TokensUsageBadge.tsx delete mode 100644 client/src/pages/StudioTab/TokensUsageProgress.tsx delete mode 100644 client/src/pages/StudioTab/index.tsx delete mode 100644 client/src/themes/abyss.css delete mode 100644 client/src/themes/atom-one-dark-pro.css delete mode 100644 client/src/themes/darcula.css delete mode 100644 client/src/themes/dracula.css delete mode 100644 client/src/themes/github-dark.css delete mode 100644 client/src/themes/github-light.css delete mode 100644 client/src/themes/groovebox-dark.css delete mode 100644 client/src/themes/groovebox-light.css delete mode 100644 client/src/themes/gruvbox-dark.css delete mode 100644 client/src/themes/gruvbox-light.css delete mode 100644 client/src/themes/kimbie-dark.css delete mode 100644 client/src/themes/material.css delete mode 100644 client/src/themes/monokai.css delete mode 100644 client/src/themes/night-owl.css delete mode 100644 client/src/themes/quiet-light.css delete mode 100644 client/src/themes/solarized-dark.css delete mode 100644 client/src/themes/solarized-light.css delete mode 100644 client/src/themes/tomorrow-night-blue.css delete mode 100644 client/src/themes/vscode-default-dark.css delete mode 100644 client/src/themes/vscode-default-light.css create mode 100644 client/src/utils/commandBarUtils.test.ts create mode 100644 client/src/utils/commandBarUtils.ts create mode 100644 client/src/utils/keyboardUtils.ts create mode 100644 client/src/utils/mappers.ts diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index 6919c06b6e..b1717c92ec 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -103,8 +103,9 @@ "hiddenTitle": true, "titleBarStyle": "Overlay", "minHeight": 700, - "minWidth": 1000 + "minWidth": 1000, + "fileDropEnabled": false } ] } -} \ No newline at end of file +} diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 75750a765d..cbd59fd812 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -32,6 +32,7 @@ import { polling } from '../../../client/src/utils/requestUtils'; import ReportBugModal from '../../../client/src/components/ReportBugModal'; import { UIContext } from '../../../client/src/context/uiContext'; import { DeviceContextProvider } from '../../../client/src/context/providers/DeviceContextProvider'; +import { EnvContext } from '../../../client/src/context/envContext'; import TextSearch from './TextSearch'; import SplashScreen from './SplashScreen'; @@ -226,12 +227,18 @@ function App() { isRepoManagementAllowed: true, forceAnalytics: false, isSelfServe: false, - envConfig, - setEnvConfig, showNativeMessage: message, relaunch, }), - [homeDirectory, indexFolder, os, release, envConfig], + [homeDirectory, indexFolder, os, release], + ); + + const envContextValue = useMemo( + () => ({ + envConfig, + setEnvConfig, + }), + [envConfig], ); const bugReportContextValue = useMemo( @@ -244,26 +251,32 @@ function App() { ); return ( - - - {shouldShowSplashScreen && } - - {shouldShowSplashScreen && ( - - - - - - )} - -
- - {!shouldShowSplashScreen && ( - + + + + + {shouldShowSplashScreen && } + + {shouldShowSplashScreen && ( + + + )} - -
-
+ +
+ + {!shouldShowSplashScreen && } + +
+ + + ); } diff --git a/apps/desktop/src/SplashScreen.tsx b/apps/desktop/src/SplashScreen.tsx index 4ba612fd6d..705d26fc12 100644 --- a/apps/desktop/src/SplashScreen.tsx +++ b/apps/desktop/src/SplashScreen.tsx @@ -26,7 +26,7 @@ const SplashScreen = ({}: Props) => {
-
+
Loading...
diff --git a/apps/desktop/src/TextSearch.tsx b/apps/desktop/src/TextSearch.tsx index 8f9f687fd7..d08e3a3625 100644 --- a/apps/desktop/src/TextSearch.tsx +++ b/apps/desktop/src/TextSearch.tsx @@ -142,7 +142,7 @@ const TextSearch = ({ currentResult={currentResult} setCurrentResult={setCurrentResult} searchValue={searchValue} - containerClassName="fixed top-[100px] right-[5px]" + containerClassName="fixed top-[100px] right-[5px] w-80" /> ); }; diff --git a/client/public/bloopHeadMascot.png b/client/public/bloopHeadMascot.png index 84c7bd386395bfabf85f12dd0a74aac20cc31545..d68483d28a82fa2b814d771d665683d33de55a02 100644 GIT binary patch literal 308958 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
  • j$-^?FMdTEw=WD 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

    h281Kq<-JMB!D| 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 z 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

    A$`k)5dvK}v= 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 z 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≫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

    P*1VEr4a_PCy^UtVq`~m{~ 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 IM6N<$f~vCk^#A|> literal 0 HcmV?d00001 diff --git a/client/src/App.tsx b/client/src/App.tsx index e4dc87e09f..dead9e8902 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,409 +1,68 @@ -import React, { - useCallback, - useEffect, - useMemo, - useState, - useTransition, -} from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; -import * as Sentry from '@sentry/react'; -import { DeviceContextType } from './context/deviceContext'; -import './index.css'; -import RepoTab from './pages/RepoTab'; -import { TabsContext } from './context/tabsContext'; -import { - NavigationItem, - RepoProvider, - RepoTabType, - RepoType, - StudioTabType, - TabType, - UITabType, -} from './types/general'; -import { - LAST_ACTIVE_TAB_KEY, - saveJsonToStorage, - savePlainToStorage, - TABS_KEY, -} from './services/storage'; -import { getRepos, initApi } from './services/api'; -import { useComponentWillMount } from './hooks/useComponentWillMount'; -import { RepoSource } from './types'; -import { RepositoriesContext } from './context/repositoriesContext'; +import React, { memo } from 'react'; +import { DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; +import { Toaster } from 'sonner'; import { AnalyticsContextProvider } from './context/providers/AnalyticsContextProvider'; -import { buildURLPart, getNavItemFromURL } from './utils/navigationUtils'; -import { DeviceContextProvider } from './context/providers/DeviceContextProvider'; -import useKeyboardNavigation from './hooks/useKeyboardNavigation'; -import StudioTab from './pages/StudioTab'; -import HomeTab from './pages/HomeTab'; -import Settings from './components/Settings'; -import ReportBugModal from './components/ReportBugModal'; -import { GeneralUiContextProvider } from './context/providers/GeneralUiContextProvider'; -import PromptGuidePopup from './components/PromptGuidePopup'; -import Onboarding from './pages/Onboarding'; -import NavBar from './components/NavBar'; -import StatusBar from './components/StatusBar'; -import CloudFeaturePopup from './components/CloudFeaturePopup'; -import ErrorFallback from './components/ErrorFallback'; import { PersonalQuotaContextProvider } from './context/providers/PersonalQuotaContextProvider'; -import UpgradePopup from './components/UpgradePopup'; -import StudioGuidePopup from './components/StudioGuidePopup'; -import WaitingUpgradePopup from './components/UpgradePopup/WaitingUpgradePopup'; -import { polling } from './utils/requestUtils'; - -type Props = { - deviceContextValue: DeviceContextType; +import ReportBugModal from './components/ReportBugModal'; +import Onboarding from './Onboarding'; +import Project from './Project'; +import CommandBar from './CommandBar'; +import ProjectContextProvider from './context/providers/ProjectContextProvider'; +import CommandBarContextProvider from './context/providers/CommandBarContextProvider'; +import { UIContextProvider } from './context/providers/UIContextProvider'; +import Settings from './Settings'; +import ProjectSettings from './ProjectSettings'; +import TabsContextProvider from './context/providers/TabsContextProvider'; +import { FileHighlightsContextProvider } from './context/providers/FileHighlightsContextProvider'; +import RepositoriesContextProvider from './context/providers/RepositoriesContextProvider'; +import UpgradeRequiredPopup from './components/UpgradeRequiredPopup'; + +const toastOptions = { + unStyled: true, + classNames: { + toast: + 'w-[20.75rem] p-4 pl-5 flex items-start gap-3 rounded-md border border-bg-border bg-bg-base shadow-high', + error: 'text-red', + info: 'text-label-title', + title: 'body-s-b', + description: '!text-label-muted body-s mt-1.5', + actionButton: 'bg-zinc-400', + cancelButton: 'bg-orange-400', + closeButton: + '!bg-bg-base !text-label-muted !border-none !left-[unset] !right-2 !top-6 !w-6 !h-6', + }, }; -function App({ deviceContextValue }: Props) { - useComponentWillMount(() => - initApi(deviceContextValue.apiUrl, deviceContextValue.isSelfServe), - ); - - const [tabs, setTabs] = useState([ - { - key: 'initial', - name: 'Home', - type: TabType.HOME, - }, - ]); - const location = useLocation(); - const [activeTab, setActiveTab] = useState('initial'); - const [repositories, setRepositories] = useState(); - const [isLoading, setLoading] = useState(true); - const navigate = useNavigate(); - const [isTransitioning, startTransition] = useTransition(); - - useEffect(() => { - if (isLoading) { - return; - } - const tab = tabs.find((t) => t.key === activeTab); - if (tab && tab.type === TabType.HOME) { - navigate('/'); - return; - } else if (tab && tab.type === TabType.REPO) { - const lastNav = tab.navigationHistory[tab.navigationHistory.length - 1]; - navigate( - `/${encodeURIComponent(tab.repoRef)}/${encodeURIComponent( - tab.branch || 'all', - )}/${lastNav ? buildURLPart(lastNav) : ''}`, - ); - } else if (tab && tab.type === TabType.STUDIO) { - navigate( - `/studio/${encodeURIComponent(tab.key)}/${encodeURIComponent( - tab.name, - )}`, - ); - } - }, [activeTab, tabs]); - - const handleAddRepoTab = useCallback( - ( - repoRef: string, - repoName: string, - name: string, - source: RepoSource, - branch?: string | null, - navHistory?: NavigationItem[], - ) => { - const newTab = { - key: repoRef + '#' + Date.now(), - name, - repoName, - repoRef, - source, - branch, - navigationHistory: navHistory || [], - type: TabType.REPO, - }; - setTabs((prev) => [...prev, newTab]); - setActiveTab(newTab.key); - }, - [], - ); - - const handleAddStudioTab = useCallback((name: string, id: string) => { - const newTab: StudioTabType = { - key: id.toString(), - name, - type: TabType.STUDIO, - }; - setTabs((prev: UITabType[]) => { - const existing = prev.find((t) => t.key === newTab.key); - if (existing) { - setActiveTab(existing.key); - return prev; - } - return [...prev, newTab]; - }); - setActiveTab(newTab.key); - }, []); - - useEffect(() => { - if (location.pathname === '/') { - setLoading(false); - return; - } - if (isLoading && repositories?.length) { - const firstPart = decodeURIComponent( - location.pathname.slice(1).split('/')[0], - ); - const repo = repositories.find((r) => r.ref === firstPart); - if (firstPart === 'studio') { - handleAddStudioTab( - decodeURIComponent(location.pathname.slice(1).split('/')[2]), - decodeURIComponent(location.pathname.slice(1).split('/')[1]), - ); - } else if (repo) { - const urlBranch = decodeURIComponent(location.pathname.split('/')[2]); - handleAddRepoTab( - repo.ref, - repo.provider === RepoProvider.GitHub ? repo.ref : repo.name, - repo.name, - repo.provider === RepoProvider.GitHub - ? RepoSource.GH - : RepoSource.LOCAL, - urlBranch === 'all' ? null : urlBranch, - getNavItemFromURL( - location, - repo.provider === RepoProvider.GitHub ? repo.ref : repo.name, - ), - ); - } - setLoading(false); - } - }, [repositories, isLoading]); - - useEffect(() => { - saveJsonToStorage(TABS_KEY, tabs); - }, [tabs]); - - useEffect(() => { - savePlainToStorage(LAST_ACTIVE_TAB_KEY, activeTab); - }, [activeTab]); - - useEffect(() => { - if (!tabs.find((t) => t.key === activeTab)) { - setActiveTab('initial'); - } - }, [activeTab, tabs]); - - const handleRemoveTab = useCallback( - (tabKey: string) => { - setActiveTab((prev) => { - const prevIndex = tabs.findIndex((t) => t.key === prev); - if (tabKey === prev) { - return prevIndex > 0 - ? tabs[prevIndex - 1].key - : tabs[prevIndex + 1].key; - } - return prev; - }); - setTabs((prev) => prev.filter((t) => t.key !== tabKey)); - }, - [tabs], - ); - - const updateTabNavHistory = useCallback( - (tabKey: string, history: (prev: NavigationItem[]) => NavigationItem[]) => { - setTabs((prev) => { - const tabIndex = prev.findIndex((t) => t.key === tabKey); - if (tabIndex < 0 || prev[tabIndex].type !== TabType.REPO) { - return prev; - } - const newTab = { - ...prev[tabIndex], - navigationHistory: history( - (prev[tabIndex] as RepoTabType).navigationHistory, - ), - }; - const newTabs = [...prev]; - newTabs[tabIndex] = newTab; - return newTabs; - }); - }, - [], - ); - - const updateTabBranch = useCallback( - (tabKey: string, branch: null | string) => { - setTabs((prev) => { - const tabIndex = prev.findIndex((t) => t.key === tabKey); - if (tabIndex < 0) { - return prev; - } - const newTab = { - ...prev[tabIndex], - branch, - }; - const newTabs = [...prev]; - newTabs[tabIndex] = newTab; - return newTabs; - }); - }, - [], - ); - - const updateTabName = useCallback((tabKey: string, name: string) => { - setTabs((prev) => { - const tabIndex = prev.findIndex((t) => t.key === tabKey); - if (tabIndex < 0) { - return prev; - } - const newTab = { - ...prev[tabIndex], - name, - }; - const newTabs = [...prev]; - newTabs[tabIndex] = newTab; - return newTabs; - }); - }, []); - - const handleKeyEvent = useCallback( - (e: KeyboardEvent) => { - if (e.metaKey || e.ctrlKey) { - const num = Number(e.key); - if (Object.keys(tabs).includes((num - 1).toString())) { - const newTab = tabs[num - 1]?.key; - if (newTab) { - e.preventDefault(); - setActiveTab(newTab); - } - } else if (e.key === 'w' && activeTab !== 'initial') { - e.preventDefault(); - e.stopPropagation(); - handleRemoveTab(activeTab); - return true; - } - } - }, - [tabs, activeTab], - ); - useKeyboardNavigation(handleKeyEvent); - - const handleChangeActiveTab = useCallback((t: string) => { - startTransition(() => { - setActiveTab(t); - }); - }, []); - - const handleReorderTabs = useCallback((newTabs: UITabType[]) => { - setTabs((prev) => { - return [prev[0], ...newTabs]; - }); - }, []); - - const contextValue = useMemo( - () => ({ - tabs, - handleAddRepoTab, - handleAddStudioTab, - handleRemoveTab, - setActiveTab: handleChangeActiveTab, - updateTabNavHistory, - updateTabBranch, - updateTabName, - handleReorderTabs, - }), - [ - tabs, - handleAddRepoTab, - handleAddStudioTab, - handleRemoveTab, - updateTabNavHistory, - updateTabBranch, - updateTabName, - handleReorderTabs, - ], - ); - - const fetchRepos = useCallback(() => { - return getRepos().then((data) => { - const list = data?.list?.sort((a, b) => (a.name < b.name ? -1 : 1)) || []; - setRepositories((prev) => { - if (JSON.stringify(prev) === JSON.stringify(list)) { - return prev; - } - return list; - }); - }); - }, []); - - useEffect(() => { - const intervalId = polling(fetchRepos, 5000); - return () => { - clearInterval(intervalId); - }; - }, []); - - const reposContextValue = useMemo( - () => ({ - repositories, - setRepositories, - localSyncError: false, - githubSyncError: false, - fetchRepos, - }), - [repositories], - ); - +const App = () => { return ( - - - - - - - -

    - {tabs.map((t) => - t.type === TabType.STUDIO ? ( - - ) : t.type === TabType.REPO ? ( - - ) : ( - - ), - )} - + + + + + + + - - - - - - - - - - - + + + + + + + + + + + + + + + + - + ); -} +}; -export default Sentry.withErrorBoundary(App, { - fallback: (props) => , -}); +export default memo(App); diff --git a/client/src/CloudApp.tsx b/client/src/CloudApp.tsx index f86d3d055f..a3a475dc34 100644 --- a/client/src/CloudApp.tsx +++ b/client/src/CloudApp.tsx @@ -1,18 +1,23 @@ import React, { useEffect, useMemo, useState } from 'react'; import { BrowserRouter } from 'react-router-dom'; import packageJson from '../package.json'; -import { getConfig } from './services/api'; import App from './App'; import { LocaleContext } from './context/localeContext'; import i18n from './i18n'; +import './index.css'; import { getPlainFromStorage, LANGUAGE_KEY, savePlainToStorage, } from './services/storage'; import { LocaleType } from './types/general'; +import { DeviceContextProvider } from './context/providers/DeviceContextProvider'; +import { EnvContext } from './context/envContext'; +import { getConfig, initApi } from './services/api'; +import { useComponentWillMount } from './hooks/useComponentWillMount'; const CloudApp = () => { + useComponentWillMount(() => initApi('/api', true)); const [envConfig, setEnvConfig] = useState({}); const [locale, setLocale] = useState( (getPlainFromStorage(LANGUAGE_KEY) as LocaleType | null) || 'en', @@ -43,9 +48,14 @@ const CloudApp = () => { isSelfServe: true, forceAnalytics: true, showNativeMessage: alert, + relaunch: () => {}, + }), + [], + ); + const envContextValue = useMemo( + () => ({ envConfig, setEnvConfig, - relaunch: () => {}, }), [envConfig], ); @@ -64,11 +74,18 @@ const CloudApp = () => { ); return ( - - - - - + + + + + + + + + ); }; diff --git a/client/src/CommandBar/Body/Item.tsx b/client/src/CommandBar/Body/Item.tsx new file mode 100644 index 0000000000..e7b2657bff --- /dev/null +++ b/client/src/CommandBar/Body/Item.tsx @@ -0,0 +1,153 @@ +import { + Dispatch, + memo, + ReactElement, + SetStateAction, + useCallback, + useContext, + useEffect, + useRef, +} from 'react'; +import { + CommandBarItemGeneralType, + CommandBarStepEnum, +} from '../../types/general'; +import useShortcuts from '../../hooks/useShortcuts'; +import useKeyboardNavigation from '../../hooks/useKeyboardNavigation'; +import { checkEventKeys } from '../../utils/keyboardUtils'; +import { CommandBarContext } from '../../context/commandBarContext'; +import { CheckmarkInSquareIcon } from '../../icons'; +import { + RECENT_COMMANDS_KEY, + updateArrayInStorage, +} from '../../services/storage'; + +type Props = CommandBarItemGeneralType & { + isFocused?: boolean; + i: number; + isFirst?: boolean; + isWithCheckmark?: boolean; + setFocusedIndex: Dispatch>; + customRightElement?: ReactElement; + focusedItemProps?: Record; + disableKeyNav?: boolean; + itemKey: string; +}; + +const CommandBarItem = ({ + isFocused, + Icon, + label, + shortcut, + i, + setFocusedIndex, + id, + footerBtns, + isFirst, + iconContainerClassName, + footerHint, + customRightElement, + onClick, + focusedItemProps, + disableKeyNav, + isWithCheckmark, + closeOnClick, + itemKey, +}: Props) => { + const ref = useRef(null); + const shortcutKeys = useShortcuts(shortcut); + const { setFocusedItem, setChosenStep, setIsVisible } = useContext( + CommandBarContext.Handlers, + ); + + useEffect(() => { + if (isFocused) { + setFocusedItem({ + footerHint, + footerBtns, + focusedItemProps, + }); + ref.current?.scrollIntoView({ block: 'nearest' }); + } + }, [isFocused, footerBtns, footerHint, focusedItemProps]); + + const handleMouseOver = useCallback(() => { + setFocusedIndex(i); + }, [i, setFocusedIndex]); + + const handleClick = useCallback(() => { + if (onClick) { + onClick(); + if (closeOnClick) { + setIsVisible(false); + } + } else { + setChosenStep({ id: id as CommandBarStepEnum }); + } + updateArrayInStorage(RECENT_COMMANDS_KEY, itemKey); + }, [id, onClick, closeOnClick, itemKey]); + + const handleKeyEvent = useCallback( + (e: KeyboardEvent) => { + const shortAction = footerBtns.find((b) => checkEventKeys(e, b.shortcut)); + if ( + (isFocused && shortAction && !shortAction.action) || + checkEventKeys(e, shortcut) + ) { + e.preventDefault(); + e.stopPropagation(); + handleClick(); + return; + } + if (isFocused && shortAction?.action) { + e.preventDefault(); + e.stopPropagation(); + shortAction.action(); + } + }, + [isFocused, shortcut, footerBtns, handleClick], + ); + useKeyboardNavigation(handleKeyEvent, disableKeyNav); + + return ( + + ); +}; + +export default memo(CommandBarItem); diff --git a/client/src/CommandBar/Body/Section.tsx b/client/src/CommandBar/Body/Section.tsx new file mode 100644 index 0000000000..4c2d189067 --- /dev/null +++ b/client/src/CommandBar/Body/Section.tsx @@ -0,0 +1,57 @@ +import { Dispatch, memo, SetStateAction } from 'react'; +import { + CommandBarItemCustomType, + CommandBarItemGeneralType, +} from '../../types/general'; +import SectionDivider from './SectionDivider'; +import Item from './Item'; + +type Props = { + title?: string; + items: (CommandBarItemCustomType | CommandBarItemGeneralType)[]; + focusedIndex: number; + setFocusedIndex: Dispatch>; + offset: number; + disableKeyNav?: boolean; +}; + +const CommandBarBodySection = ({ + title, + items, + setFocusedIndex, + offset, + focusedIndex, + disableKeyNav, +}: Props) => { + return ( +
    + {!!title && } + {items.map(({ key, ...Rest }, i) => + 'Component' in Rest ? ( + + ) : ( + + ), + )} +
    + ); +}; + +export default memo(CommandBarBodySection); diff --git a/client/src/CommandBar/Body/SectionDivider.tsx b/client/src/CommandBar/Body/SectionDivider.tsx new file mode 100644 index 0000000000..3c4e7f4dd5 --- /dev/null +++ b/client/src/CommandBar/Body/SectionDivider.tsx @@ -0,0 +1,15 @@ +import { memo } from 'react'; + +type Props = { + text: string; +}; + +const SectionDivider = ({ text }: Props) => { + return ( +
    + {text} +
    + ); +}; + +export default memo(SectionDivider); diff --git a/client/src/CommandBar/Body/index.tsx b/client/src/CommandBar/Body/index.tsx new file mode 100644 index 0000000000..fbbebacd77 --- /dev/null +++ b/client/src/CommandBar/Body/index.tsx @@ -0,0 +1,60 @@ +import { memo, useCallback, useEffect, useState } from 'react'; +import { CommandBarSectionType } from '../../types/general'; +import useKeyboardNavigation from '../../hooks/useKeyboardNavigation'; +import Section from './Section'; + +type Props = { + sections: CommandBarSectionType[]; + disableKeyNav?: boolean; +}; + +const CommandBarBody = ({ sections, disableKeyNav }: Props) => { + const [focusedIndex, setFocusedIndex] = useState(0); + + useEffect(() => { + setFocusedIndex(0); + }, [sections]); + + const handleKeyEvent = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'ArrowDown') { + e.preventDefault(); + e.stopPropagation(); + setFocusedIndex((prev) => + prev < + sections.reduce((prev, curr) => prev + curr.items.length, 0) - 1 + ? prev + 1 + : 0, + ); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + e.stopPropagation(); + setFocusedIndex((prev) => + prev > 0 + ? prev - 1 + : sections.reduce((prev, curr) => prev + curr.items.length, 0) - 1, + ); + } + }, + [sections], + ); + useKeyboardNavigation(handleKeyEvent, disableKeyNav); + + return ( +
    + {sections.map((s) => ( +
    + ))} +
    + ); +}; + +export default memo(CommandBarBody); diff --git a/client/src/CommandBar/Footer/HintButton.tsx b/client/src/CommandBar/Footer/HintButton.tsx new file mode 100644 index 0000000000..e617e496a1 --- /dev/null +++ b/client/src/CommandBar/Footer/HintButton.tsx @@ -0,0 +1,33 @@ +import { ForwardedRef, forwardRef, memo } from 'react'; +import useShortcuts from '../../hooks/useShortcuts'; + +type Props = { + label: string; + shortcut?: string[]; +}; + +const HintButton = forwardRef( + ({ label, shortcut }: Props, ref: ForwardedRef) => { + const shortcutKeys = useShortcuts(shortcut); + return ( +
    + {label} + {shortcutKeys?.map((k) => ( +
    + {k} +
    + ))} +
    + ); + }, +); + +HintButton.displayName = 'HintButtonWithRef'; + +export default memo(HintButton); diff --git a/client/src/CommandBar/Footer/index.tsx b/client/src/CommandBar/Footer/index.tsx new file mode 100644 index 0000000000..9bfe07f541 --- /dev/null +++ b/client/src/CommandBar/Footer/index.tsx @@ -0,0 +1,58 @@ +import { memo, useCallback, useContext, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { CommandBarContext } from '../../context/commandBarContext'; +import Dropdown from '../../components/Dropdown'; +import useKeyboardNavigation from '../../hooks/useKeyboardNavigation'; +import HintButton from './HintButton'; + +type Props = { + ActionsDropdown?: (props: any) => JSX.Element | null; + actionsDropdownProps?: Record; + onDropdownVisibilityChange?: (isVisible: boolean) => void; +}; + +const CommandBarFooter = ({ + ActionsDropdown, + actionsDropdownProps, + onDropdownVisibilityChange, +}: Props) => { + const { t } = useTranslation(); + const { focusedItem } = useContext(CommandBarContext.FooterValues); + const actionsBtn = useRef(null); + + const handleKeyEvent = useCallback((e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === 'k') { + e.preventDefault(); + e.stopPropagation(); + actionsBtn.current?.click(); + } + }, []); + useKeyboardNavigation(handleKeyEvent, !ActionsDropdown); + + return ( +
    +

    + {focusedItem?.footerHint} +

    + {focusedItem?.footerBtns?.map((b) => )} + {!!ActionsDropdown && ( + + + + )} +
    + ); +}; + +export default memo(CommandBarFooter); diff --git a/client/src/CommandBar/Header/ChipItem.tsx b/client/src/CommandBar/Header/ChipItem.tsx new file mode 100644 index 0000000000..93c8331b6a --- /dev/null +++ b/client/src/CommandBar/Header/ChipItem.tsx @@ -0,0 +1,13 @@ +import { memo } from 'react'; + +type Props = { text: string }; + +const CommandBarChipItem = ({ text }: Props) => { + return ( +
    + {text} +
    + ); +}; + +export default memo(CommandBarChipItem); diff --git a/client/src/CommandBar/Header/index.tsx b/client/src/CommandBar/Header/index.tsx new file mode 100644 index 0000000000..b7656573d0 --- /dev/null +++ b/client/src/CommandBar/Header/index.tsx @@ -0,0 +1,126 @@ +import { + ChangeEvent, + memo, + ReactElement, + useCallback, + useContext, + useState, +} from 'react'; +import { useTranslation } from 'react-i18next'; +import Tooltip from '../../components/Tooltip'; +import useKeyboardNavigation from '../../hooks/useKeyboardNavigation'; +import { CommandBarContext } from '../../context/commandBarContext'; +import ChipItem from './ChipItem'; + +type PropsWithoutInput = { + noInput: true; + customSubmitHandler?: never; + onChange?: never; + value?: never; + placeholder?: never; +}; + +type PropsWithInput = { + noInput?: false; + value: string; + onChange: (e: ChangeEvent) => void; + customSubmitHandler?: (value: string) => void; + placeholder?: string; +}; + +type GeneralProps = { + handleBack?: () => void; + breadcrumbs?: string[]; + customRightComponent?: ReactElement; + disableKeyNav?: boolean; +}; + +type Props = GeneralProps & (PropsWithInput | PropsWithoutInput); + +const CommandBarHeader = ({ + handleBack, + breadcrumbs, + customRightComponent, + customSubmitHandler, + onChange, + value, + placeholder, + noInput, + disableKeyNav, +}: Props) => { + const { t } = useTranslation(); + const { isVisible } = useContext(CommandBarContext.General); + const { setIsVisible } = useContext(CommandBarContext.Handlers); + const [isComposing, setIsComposing] = useState(false); + + const onCompositionStart = useCallback(() => { + setIsComposing(true); + }, []); + + const onCompositionEnd = useCallback(() => { + // this event comes before keydown and sets state faster causing unintentional submit + setTimeout(() => setIsComposing(false), 10); + }, []); + + const handleKeyEvent = useCallback( + (e: KeyboardEvent) => { + if ( + e.key === 'Escape' || + (e.key === 'Backspace' && !value && !isComposing) + ) { + e.stopPropagation(); + e.preventDefault(); + if (handleBack) { + handleBack(); + } else { + setIsVisible(false); + } + } else if (e.key === 'Enter' && customSubmitHandler && !isComposing) { + e.stopPropagation(); + e.preventDefault(); + customSubmitHandler(value); + } + }, + [setIsVisible, handleBack, customSubmitHandler, value, isComposing], + ); + useKeyboardNavigation(handleKeyEvent, !isVisible || disableKeyNav); + + return ( +
    +
    +
    + {!!handleBack && ( + + + + )} + {breadcrumbs?.map((b) => )} +
    + {customRightComponent} +
    + {!noInput && ( + + )} +
    + ); +}; + +export default memo(CommandBarHeader); diff --git a/client/src/CommandBar/index.tsx b/client/src/CommandBar/index.tsx new file mode 100644 index 0000000000..b5dbb8889e --- /dev/null +++ b/client/src/CommandBar/index.tsx @@ -0,0 +1,86 @@ +import { memo, useCallback, useContext } from 'react'; +import Modal from '../components/Modal'; +import useKeyboardNavigation from '../hooks/useKeyboardNavigation'; +import { CommandBarStepEnum } from '../types/general'; +import { CommandBarContext } from '../context/commandBarContext'; +import { useGlobalShortcuts } from '../hooks/useGlobalShortcuts'; +import { checkEventKeys } from '../utils/keyboardUtils'; +import Initial from './steps/Initial'; +import PrivateRepos from './steps/PrivateRepos'; +import PublicRepos from './steps/PublicRepos'; +import LocalRepos from './steps/LocalRepos'; +import Documentation from './steps/Documentation'; +import CreateProject from './steps/CreateProject'; +import ManageRepos from './steps/ManageRepos'; +import AddNewRepo from './steps/AddNewRepo'; +import ToggleTheme from './steps/ToggleTheme'; +import SearchFiles from './steps/SeachFiles'; + +type Props = {}; + +const CommandBar = ({}: Props) => { + const { chosenStep } = useContext(CommandBarContext.CurrentStep); + const { isVisible } = useContext(CommandBarContext.General); + const { setChosenStep, setIsVisible } = useContext( + CommandBarContext.Handlers, + ); + const globalShortcuts = useGlobalShortcuts(); + + const handleClose = useCallback(() => { + setIsVisible(false); + setChosenStep({ + id: CommandBarStepEnum.INITIAL, + }); + }, []); + + const handleKeyEvent = useCallback( + (e: KeyboardEvent) => { + if (checkEventKeys(e, ['cmd', 'K'])) { + e.stopPropagation(); + e.preventDefault(); + setIsVisible(true); + } + Object.values(globalShortcuts).forEach((s) => { + if (checkEventKeys(e, s.shortcut)) { + e.stopPropagation(); + e.preventDefault(); + s.action(); + } + }); + }, + [isVisible, globalShortcuts], + ); + useKeyboardNavigation(handleKeyEvent); + + return ( + + {chosenStep.id === CommandBarStepEnum.INITIAL ? ( + + ) : chosenStep.id === CommandBarStepEnum.PRIVATE_REPOS ? ( + + ) : chosenStep.id === CommandBarStepEnum.PUBLIC_REPOS ? ( + + ) : chosenStep.id === CommandBarStepEnum.LOCAL_REPOS ? ( + + ) : chosenStep.id === CommandBarStepEnum.DOCS ? ( + + ) : chosenStep.id === CommandBarStepEnum.CREATE_PROJECT ? ( + + ) : chosenStep.id === CommandBarStepEnum.MANAGE_REPOS ? ( + + ) : chosenStep.id === CommandBarStepEnum.ADD_NEW_REPO ? ( + + ) : chosenStep.id === CommandBarStepEnum.TOGGLE_THEME ? ( + + ) : chosenStep.id === CommandBarStepEnum.SEARCH_FILES ? ( + + ) : null} + + ); +}; + +export default memo(CommandBar); diff --git a/client/src/CommandBar/steps/AddNewRepo.tsx b/client/src/CommandBar/steps/AddNewRepo.tsx new file mode 100644 index 0000000000..0088c231d8 --- /dev/null +++ b/client/src/CommandBar/steps/AddNewRepo.tsx @@ -0,0 +1,132 @@ +import { memo, useCallback, useContext, useMemo } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { toast } from 'sonner'; +import { useGlobalShortcuts } from '../../hooks/useGlobalShortcuts'; +import { + CommandBarItemGeneralType, + CommandBarStepEnum, +} from '../../types/general'; +import { GlobeIcon, HardDriveIcon, RepositoryIcon } from '../../icons'; +import Header from '../Header'; +import Body from '../Body'; +import Footer from '../Footer'; +import { CommandBarContext } from '../../context/commandBarContext'; +import { DeviceContext } from '../../context/deviceContext'; +import { scanLocalRepos, syncRepo } from '../../services/api'; +import SpinLoaderContainer from '../../components/Loaders/SpinnerLoader'; + +type Props = {}; + +const AddNewRepo = ({}: Props) => { + const { t } = useTranslation(); + const globalShortcuts = useGlobalShortcuts(); + const { setChosenStep } = useContext(CommandBarContext.Handlers); + const { homeDir, chooseFolder } = useContext(DeviceContext); + + const handleBack = useCallback(() => { + setChosenStep({ id: CommandBarStepEnum.MANAGE_REPOS }); + }, []); + + const handleChooseFolder = useCallback(async () => { + let folder: string | string[] | null; + if (chooseFolder) { + try { + folder = await chooseFolder({ + directory: true, + defaultPath: homeDir, + }); + } catch (err) { + console.log(err); + } + } + // @ts-ignore + if (typeof folder === 'string') { + scanLocalRepos(folder).then((data) => { + if (data.list.length === 1) { + syncRepo(data.list[0].ref); + toast(t('Indexing repository'), { + description: ( + + repoName has + started indexing. You’ll receive a notification as soon as this + process completes. + + ), + icon: , + unstyled: true, + }); + handleBack(); + return; + } else if (!data.list.length) { + toast.error(t('Not a git repository'), { + description: t('The folder you selected is not a git repository.'), + icon: , + unstyled: true, + }); + } else if (data.list.length > 1) { + toast.error(t('Folder too large'), { + description: t( + 'The folder you selected has multiple git repositories nested inside.', + ), + icon: , + unstyled: true, + }); + } + }); + } + }, [chooseFolder, homeDir, handleBack]); + + const initialSections = useMemo(() => { + const contextItems: CommandBarItemGeneralType[] = [ + { + label: t('Private repository'), + Icon: RepositoryIcon, + id: CommandBarStepEnum.PRIVATE_REPOS, + key: 'private', + shortcut: globalShortcuts.openPrivateRepos.shortcut, + footerHint: '', + footerBtns: [{ label: t('Next'), shortcut: ['entr'] }], + }, + { + label: t('Public repository'), + Icon: GlobeIcon, + id: CommandBarStepEnum.PUBLIC_REPOS, + key: 'public', + shortcut: globalShortcuts.openPublicRepos.shortcut, + footerHint: '', + footerBtns: [{ label: t('Next'), shortcut: ['entr'] }], + }, + { + label: t('Local repository'), + Icon: HardDriveIcon, + id: CommandBarStepEnum.LOCAL_REPOS, + onClick: handleChooseFolder, + key: 'local', + shortcut: globalShortcuts.openLocalRepos.shortcut, + footerHint: '', + footerBtns: [{ label: t('Next'), shortcut: ['entr'] }], + }, + ]; + return [ + { + items: contextItems, + itemsOffset: 0, + key: 'context-items', + }, + ]; + }, [t, globalShortcuts]); + + return ( +
    +
    + +
    +
    + ); +}; + +export default memo(AddNewRepo); diff --git a/client/src/CommandBar/steps/CreateProject.tsx b/client/src/CommandBar/steps/CreateProject.tsx new file mode 100644 index 0000000000..d83482a624 --- /dev/null +++ b/client/src/CommandBar/steps/CreateProject.tsx @@ -0,0 +1,82 @@ +import { + ChangeEvent, + memo, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; +import { useTranslation } from 'react-i18next'; +import Header from '../Header'; +import Footer from '../Footer'; +import { CommandBarStepEnum } from '../../types/general'; +import { CommandBarContext } from '../../context/commandBarContext'; +import { createProject } from '../../services/api'; +import { ProjectContext } from '../../context/projectContext'; + +type Props = {}; + +const CreateProject = ({}: Props) => { + const { t } = useTranslation(); + const [inputValue, setInputValue] = useState(''); + const { setChosenStep, setFocusedItem, setIsVisible } = useContext( + CommandBarContext.Handlers, + ); + const { setCurrentProjectId } = useContext(ProjectContext.Current); + const { refreshAllProjects } = useContext(ProjectContext.All); + + const handleInputChange = useCallback((e: ChangeEvent) => { + setInputValue(e.target.value); + }, []); + + useEffect(() => { + setFocusedItem({ + footerHint: t('Provide a short, concise title for your project'), + footerBtns: [{ label: t('Create project'), shortcut: ['entr'] }], + }); + }, [t]); + + const switchProject = useCallback((id: string) => { + setCurrentProjectId(id); + setIsVisible(false); + refreshAllProjects(); + setChosenStep({ + id: CommandBarStepEnum.INITIAL, + }); + }, []); + + const breadcrumbs = useMemo(() => { + return [t('Create project')]; + }, [t]); + + const handleBack = useCallback(() => { + setChosenStep({ id: CommandBarStepEnum.INITIAL }); + }, []); + + const submitHandler = useCallback( + async (value: string) => { + setInputValue(''); + const newId = await createProject(value); + switchProject(newId); + }, + [switchProject], + ); + + return ( +
    +
    +
    +
    +
    + ); +}; + +export default memo(CreateProject); diff --git a/client/src/CommandBar/steps/Documentation.tsx b/client/src/CommandBar/steps/Documentation.tsx new file mode 100644 index 0000000000..f9281ac6d4 --- /dev/null +++ b/client/src/CommandBar/steps/Documentation.tsx @@ -0,0 +1,200 @@ +import { + ChangeEvent, + memo, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; +import { useTranslation } from 'react-i18next'; +import { CommandBarSectionType, CommandBarStepEnum } from '../../types/general'; +import { CommandBarContext } from '../../context/commandBarContext'; +import { getIndexedDocs, verifyDocsUrl } from '../../services/api'; +import { PlusSignIcon } from '../../icons'; +import { DocShortType } from '../../types/api'; +import Header from '../Header'; +import Body from '../Body'; +import Footer from '../Footer'; +import DocItem from './items/DocItem'; + +type Props = {}; + +const Documentation = ({}: Props) => { + const { t } = useTranslation(); + const [isAddMode, setIsAddMode] = useState(false); + const [hasFetched, setHasFetched] = useState(false); + const [indexedDocs, setIndexedDocs] = useState([]); + const [addedDoc, setAddedDoc] = useState(''); + const { setChosenStep, setFocusedItem } = useContext( + CommandBarContext.Handlers, + ); + const [inputValue, setInputValue] = useState(''); + + const handleInputChange = useCallback((e: ChangeEvent) => { + setInputValue(e.target.value); + }, []); + + const enterAddMode = useCallback(() => { + setFocusedItem({ + footerHint: t('Paste a link to any documentation web page'), + footerBtns: [{ label: t('Sync'), shortcut: ['entr'] }], + }); + setIsAddMode(true); + }, []); + + const addItem = useMemo(() => { + return { + itemsOffset: 0, + key: 'add-docs', + items: [ + { + label: 'Add documentation', + Icon: PlusSignIcon, + footerHint: t('Add any library documentation'), + footerBtns: [ + { + label: t('Add'), + shortcut: ['entr'], + }, + ], + key: 'add', + id: 'Add', + onClick: enterAddMode, + }, + ], + }; + }, []); + const [sections, setSections] = useState([addItem]); + + const breadcrumbs = useMemo(() => { + const arr = [t('Docs')]; + if (isAddMode) { + arr.push(t('Add docs')); + } + return arr; + }, [t, isAddMode]); + + const handleBack = useCallback(() => { + if (isAddMode && (addedDoc || indexedDocs.length)) { + setIsAddMode(false); + } else { + setChosenStep({ id: CommandBarStepEnum.INITIAL }); + } + }, [isAddMode, addedDoc, indexedDocs]); + + const refetchDocs = useCallback(() => { + getIndexedDocs().then((data) => { + setIndexedDocs(data); + setHasFetched(true); + }); + }, []); + + useEffect(() => { + const mapped = indexedDocs.map((d) => ({ + Component: DocItem, + componentProps: { doc: d, isIndexed: !!d.id, refetchDocs }, + key: d.id, + })); + if (!mapped.length && hasFetched && !addedDoc) { + enterAddMode(); + } + if (addedDoc) { + mapped.unshift({ + Component: DocItem, + componentProps: { + doc: { + url: addedDoc, + id: '', + name: '', + favicon: '', + index_status: 'indexing', + }, + isIndexed: false, + refetchDocs: () => { + refetchDocs(); + setAddedDoc(''); + }, + }, + key: addedDoc, + }); + } + setSections([ + addItem, + { + itemsOffset: 1, + key: 'indexed-docs', + label: t('Indexed documentation web pages'), + items: mapped, + }, + ]); + }, [indexedDocs, addedDoc, hasFetched]); + + useEffect(() => { + if (!isAddMode || !hasFetched) { + refetchDocs(); + } + }, [isAddMode]); + + const handleAddSubmit = useCallback((inputValue: string) => { + setFocusedItem({ + footerHint: t('Verifying access...'), + footerBtns: [], + }); + setInputValue(''); + verifyDocsUrl(inputValue.trim()) + .then(() => { + setIsAddMode(false); + setAddedDoc(inputValue); + }) + .catch(() => { + setFocusedItem({ + footerHint: t( + "We couldn't find any docs at that link. Try again or make sure the link is correct!", + ), + footerBtns: [], + }); + }); + }, []); + + const sectionsToShow = useMemo(() => { + if (!inputValue) { + return sections; + } + const newSections: CommandBarSectionType[] = []; + sections.forEach((s) => { + const newItems = s.items.filter((i) => + ('label' in i ? i.label : i.componentProps.doc.name) + .toLowerCase() + .includes(inputValue.toLowerCase()), + ); + if (newItems.length) { + newSections.push({ ...s, items: newItems }); + } + }); + return newSections; + }, [inputValue, sections]); + + return ( +
    +
    + {isAddMode ? ( +
    + ) : ( + + )} +
    +
    + ); +}; + +export default memo(Documentation); diff --git a/client/src/CommandBar/steps/Initial.tsx b/client/src/CommandBar/steps/Initial.tsx new file mode 100644 index 0000000000..5ae2105694 --- /dev/null +++ b/client/src/CommandBar/steps/Initial.tsx @@ -0,0 +1,412 @@ +import { + ChangeEvent, + memo, + useCallback, + useContext, + useMemo, + useState, +} from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { ProjectContext } from '../../context/projectContext'; +import { + BugIcon, + ChatBubblesIcon, + CloseSignInCircleIcon, + CogIcon, + ColorSwitchIcon, + DocumentsIcon, + DoorOutIcon, + MagazineIcon, + MagnifyToolIcon, + PlusSignIcon, + RegexIcon, + RepositoryIcon, + WalletIcon, +} from '../../icons'; +import { CommandBarContext } from '../../context/commandBarContext'; +import Header from '../Header'; +import Body from '../Body'; +import Footer from '../Footer'; +import { + CommandBarItemGeneralType, + CommandBarSectionType, + CommandBarStepEnum, + TabTypesEnum, +} from '../../types/general'; +import { UIContext } from '../../context/uiContext'; +import { useGlobalShortcuts } from '../../hooks/useGlobalShortcuts'; +import { + getJsonFromStorage, + RECENT_COMMANDS_KEY, +} from '../../services/storage'; +import { bubbleUpRecentItems } from '../../utils/commandBarUtils'; +import { TabsContext } from '../../context/tabsContext'; + +type Props = {}; + +const InitialCommandBar = ({}: Props) => { + const { t } = useTranslation(); + const { setIsVisible } = useContext(CommandBarContext.Handlers); + const { tabItems } = useContext(CommandBarContext.FocusedTab); + const { openNewTab } = useContext(TabsContext.Handlers); + const { projects } = useContext(ProjectContext.All); + const { setCurrentProjectId, project } = useContext(ProjectContext.Current); + const { theme } = useContext(UIContext.Theme); + const [inputValue, setInputValue] = useState(''); + const globalShortcuts = useGlobalShortcuts(); + + const handleInputChange = useCallback((e: ChangeEvent) => { + setInputValue(e.target.value); + }, []); + + const switchProject = useCallback((id: string) => { + setCurrentProjectId(id); + setIsVisible(false); + }, []); + + const initialSections = useMemo(() => { + const recentKeys = getJsonFromStorage(RECENT_COMMANDS_KEY); + const contextItems: CommandBarItemGeneralType[] = [ + { + label: t('Manage repositories'), + Icon: RepositoryIcon, + id: CommandBarStepEnum.MANAGE_REPOS, + key: CommandBarStepEnum.MANAGE_REPOS, + shortcut: globalShortcuts.openManageRepos.shortcut, + footerHint: '', + footerBtns: [{ label: t('Manage'), shortcut: ['entr'] }], + }, + { + label: t('Add new repository'), + Icon: PlusSignIcon, + id: CommandBarStepEnum.ADD_NEW_REPO, + key: CommandBarStepEnum.ADD_NEW_REPO, + shortcut: ['cmd', 'A'], + footerHint: '', + footerBtns: [ + { + label: t('Add'), + shortcut: ['entr'], + }, + ], + }, + ]; + const projectItems: CommandBarItemGeneralType[] = projects + .map( + (p): CommandBarItemGeneralType => ({ + label: p.name, + Icon: MagazineIcon, + id: `project-${p.id}`, + key: `project-${p.id}`, + onClick: () => switchProject(p.id), + footerHint: + project?.id === p.id + ? t('Manage project') + : t(`Switch to`) + ' ' + p.name, + footerBtns: + project?.id === p.id + ? [{ label: t('Manage'), shortcut: ['entr'] }] + : [ + { + label: t('Open'), + shortcut: ['entr'], + }, + ], + }), + ) + .concat({ + label: t('New project'), + Icon: MagazineIcon, + id: CommandBarStepEnum.CREATE_PROJECT, + key: CommandBarStepEnum.CREATE_PROJECT, + shortcut: globalShortcuts.createNewProject.shortcut, + footerHint: t('Create new project'), + footerBtns: [ + { + label: t('Manage'), + shortcut: ['entr'], + }, + ], + }); + const themeItems: CommandBarItemGeneralType[] = [ + { + label: t(`Theme`), + Icon: ColorSwitchIcon, + id: CommandBarStepEnum.TOGGLE_THEME, + key: CommandBarStepEnum.TOGGLE_THEME, + shortcut: globalShortcuts.toggleTheme.shortcut, + footerHint: t(`Change application colour theme`), + footerBtns: [ + { + label: t('Select'), + shortcut: ['entr'], + }, + ], + }, + ]; + const otherCommands: CommandBarItemGeneralType[] = [ + { + label: t(`Close all tabs`), + Icon: CloseSignInCircleIcon, + id: `close-tabs`, + key: `close-tabs`, + onClick: globalShortcuts.closeAllTabs.action, + shortcut: globalShortcuts.closeAllTabs.shortcut, + footerHint: t(`Close all open tabs`), + footerBtns: [ + { + label: t('Close'), + shortcut: ['entr'], + }, + ], + }, + { + label: t(`Account settings`), + Icon: CogIcon, + id: `account-settings`, + key: `account-settings`, + onClick: globalShortcuts.openSettings.action, + shortcut: globalShortcuts.openSettings.shortcut, + footerHint: t(`Open account settings`), + footerBtns: [ + { + label: t('Open'), + shortcut: ['entr'], + }, + ], + }, + { + label: t(`Subscription`), + Icon: WalletIcon, + id: `subscription-settings`, + key: `subscription-settings`, + onClick: globalShortcuts.openSubscriptionSettings.action, + shortcut: globalShortcuts.openSubscriptionSettings.shortcut, + footerHint: t(`Open subscription settings`), + footerBtns: [ + { + label: t('Open'), + shortcut: ['entr'], + }, + ], + }, + { + label: t(`Documentation`), + Icon: DocumentsIcon, + id: `app-docs`, + key: `app-docs`, + onClick: globalShortcuts.openAppDocs.action, + shortcut: globalShortcuts.openAppDocs.shortcut, + footerHint: t(`View bloop app documentation on our website`), + footerBtns: [ + { + label: t('Open'), + shortcut: ['entr'], + }, + ], + }, + { + label: t(`Report a bug`), + Icon: BugIcon, + id: `bug`, + key: `bug`, + onClick: globalShortcuts.reportABug.action, + shortcut: globalShortcuts.reportABug.shortcut, + footerHint: t(`Report a bug`), + footerBtns: [ + { + label: t('Open'), + shortcut: ['entr'], + }, + ], + }, + { + label: t(`Sign out`), + Icon: DoorOutIcon, + id: `sign-out`, + key: `sign-out`, + onClick: globalShortcuts.signOut.action, + shortcut: globalShortcuts.signOut.shortcut, + footerHint: t(`Sign out`), + footerBtns: [ + { + label: t('Sign out'), + shortcut: ['entr'], + }, + ], + }, + { + label: t(`Regex search`), + Icon: RegexIcon, + id: `toggle-regex`, + key: `toggle-regex`, + onClick: globalShortcuts.toggleRegex.action, + shortcut: globalShortcuts.toggleRegex.shortcut, + footerHint: t(`Search your repositories using RegExp`), + footerBtns: [ + { + label: t('Toggle'), + shortcut: ['entr'], + }, + ], + }, + { + label: t(`Search files`), + Icon: MagnifyToolIcon, + id: CommandBarStepEnum.SEARCH_FILES, + key: CommandBarStepEnum.SEARCH_FILES, + shortcut: globalShortcuts.openSearchFiles.shortcut, + footerHint: t(`Search your files in this project`), + footerBtns: [ + { + label: t('Search'), + shortcut: ['entr'], + }, + ], + }, + ]; + const commandsItems = [...themeItems, ...otherCommands]; + const newTabItems = [ + { + label: t('New chat'), + Icon: ChatBubblesIcon, + id: 'new chat', + key: 'new_chat', + onClick: () => openNewTab({ type: TabTypesEnum.CHAT }), + shortcut: ['option', 'N'], + closeOnClick: true, + footerHint: '', + footerBtns: [ + { + label: t('Open'), + shortcut: ['entr'], + }, + ], + }, + ]; + const chatItems: CommandBarItemGeneralType[] = ( + project?.conversations || [] + ) + .slice(-5) + .map((c) => ({ + label: c.title, + id: `chat-${c.id}`, + key: `chat-${c.id}`, + Icon: ChatBubblesIcon, + closeOnClick: true, + onClick: () => { + openNewTab({ + type: TabTypesEnum.CHAT, + title: c.title, + conversationId: c.id, + }); + }, + footerHint: '', + footerBtns: [{ label: t('Open'), shortcut: ['entr'] }], + })); + return bubbleUpRecentItems( + [ + ...(newTabItems.length + ? [ + { + items: newTabItems, + itemsOffset: 0, + key: 'new-tab-items', + }, + ] + : []), + ...(tabItems.length + ? [ + { + items: tabItems, + itemsOffset: newTabItems.length, + key: 'tab-items', + }, + ] + : []), + ...(chatItems.length + ? [ + { + label: t('Recent conversations'), + items: chatItems, + itemsOffset: newTabItems.length + tabItems.length, + key: 'chat-items', + }, + ] + : []), + { + items: contextItems, + itemsOffset: newTabItems.length + tabItems.length + chatItems.length, + label: t('Manage context'), + key: 'context-items', + }, + { + items: projectItems, + itemsOffset: + newTabItems.length + + tabItems.length + + chatItems.length + + contextItems.length, + label: t('Recent projects'), + key: 'recent-projects', + }, + { + items: commandsItems, + itemsOffset: + newTabItems.length + + tabItems.length + + chatItems.length + + contextItems.length + + projectItems.length, + label: t('Commands'), + key: 'general-commands', + }, + ], + recentKeys || [], + t('Recently used'), + ); + }, [t, projects, project, theme, globalShortcuts, tabItems, openNewTab]); + + const sectionsToShow = useMemo(() => { + if (!inputValue) { + return initialSections; + } + const newSections: CommandBarSectionType[] = []; + initialSections.forEach((s) => { + const newItems = (s.items as CommandBarItemGeneralType[]).filter((i) => + i.label.toLowerCase().includes(inputValue.toLowerCase()), + ); + if (newItems.length) { + newSections.push({ + ...s, + items: newItems, + itemsOffset: newSections[newSections.length - 1] + ? newSections[newSections.length - 1].items.length + + newSections[newSections.length - 1].itemsOffset + : 0, + }); + } + }); + return newSections; + }, [inputValue, initialSections]); + + return ( +
    +
    + {!!sectionsToShow.length ? ( + + ) : ( +
    + No commands found... +
    + )} +
    +
    + ); +}; + +export default memo(InitialCommandBar); diff --git a/client/src/CommandBar/steps/LocalRepos.tsx b/client/src/CommandBar/steps/LocalRepos.tsx new file mode 100644 index 0000000000..37a2e3465f --- /dev/null +++ b/client/src/CommandBar/steps/LocalRepos.tsx @@ -0,0 +1,171 @@ +import { + ChangeEvent, + memo, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; +import { useTranslation } from 'react-i18next'; +import { CommandBarContext } from '../../context/commandBarContext'; +import { PlusSignIcon } from '../../icons'; +import { + CommandBarSectionType, + CommandBarStepEnum, + RepoProvider, +} from '../../types/general'; +import { getIndexedRepos, scanLocalRepos, syncRepo } from '../../services/api'; +import { DeviceContext } from '../../context/deviceContext'; +import Footer from '../Footer'; +import Body from '../Body'; +import Header from '../Header'; +import RepoItem from './items/RepoItem'; + +type Props = {}; + +const LocalRepos = ({}: Props) => { + const { t } = useTranslation(); + const [chosenFolder, setChosenFolder] = useState(null); + const [inputValue, setInputValue] = useState(''); + const { homeDir, chooseFolder } = useContext(DeviceContext); + const { setChosenStep, setFocusedItem } = useContext( + CommandBarContext.Handlers, + ); + + const handleInputChange = useCallback((e: ChangeEvent) => { + setInputValue(e.target.value); + }, []); + + const handleChooseFolder = useCallback(async () => { + let folder: string | string[] | null; + if (chooseFolder) { + try { + folder = await chooseFolder({ + directory: true, + defaultPath: homeDir, + }); + } catch (err) { + console.log(err); + } + } + // @ts-ignore + if (typeof folder === 'string') { + setChosenFolder(folder); + } + }, [chooseFolder, homeDir]); + + const enterAddMode = useCallback(async () => { + setFocusedItem({ + footerHint: t('Select a folder containing a git repository'), + footerBtns: [{ label: t('Start indexing'), shortcut: ['entr'] }], + }); + await handleChooseFolder(); + }, []); + + useEffect(() => { + if (chosenFolder) { + scanLocalRepos(chosenFolder).then((data) => { + if (data.list.length === 1) { + syncRepo(data.list[0].ref); + refetchRepos(); + return; + } + }); + } + }, [chosenFolder]); + + const addItem = useMemo(() => { + return { + itemsOffset: 0, + key: 'add', + items: [ + { + label: t('Add local repository'), + Icon: PlusSignIcon, + footerHint: t('Add a repository from your local machine'), + footerBtns: [ + { + label: t('Select folder'), + shortcut: ['entr'], + }, + ], + key: 'add', + id: 'Add', + onClick: enterAddMode, + }, + ], + }; + }, []); + const [sections, setSections] = useState([addItem]); + + const breadcrumbs = useMemo(() => { + return [t('Local repositories')]; + }, [t]); + + const handleBack = useCallback(() => { + setChosenStep({ id: CommandBarStepEnum.INITIAL }); + }, []); + + const refetchRepos = useCallback(() => { + getIndexedRepos().then((data) => { + const mapped = data.list + .filter((r) => r.provider === RepoProvider.Local) + .map((r) => ({ + Component: RepoItem, + componentProps: { repo: { ...r, shortName: r.name }, refetchRepos }, + key: r.ref, + })); + if (!mapped.length) { + enterAddMode(); + } + setSections([ + addItem, + { + itemsOffset: 1, + key: 'indexed-repos', + label: t('Indexed local repositories'), + items: mapped, + }, + ]); + }); + }, []); + + useEffect(() => { + refetchRepos(); + }, []); + + const sectionsToShow = useMemo(() => { + if (!inputValue) { + return sections; + } + const newSections: CommandBarSectionType[] = []; + sections.forEach((s) => { + const newItems = s.items.filter((i) => + ('label' in i ? i.label : i.componentProps.repo.shortName) + .toLowerCase() + .includes(inputValue.toLowerCase()), + ); + if (newItems.length) { + newSections.push({ ...s, items: newItems }); + } + }); + return newSections; + }, [inputValue, sections]); + + return ( +
    +
    + +
    +
    + ); +}; + +export default memo(LocalRepos); diff --git a/client/src/CommandBar/steps/ManageRepos/ActionsDropdown.tsx b/client/src/CommandBar/steps/ManageRepos/ActionsDropdown.tsx new file mode 100644 index 0000000000..fd4e29d32c --- /dev/null +++ b/client/src/CommandBar/steps/ManageRepos/ActionsDropdown.tsx @@ -0,0 +1,203 @@ +import { + Dispatch, + memo, + SetStateAction, + useCallback, + useContext, + useMemo, + useState, +} from 'react'; +import { useTranslation } from 'react-i18next'; +import SectionLabel from '../../../components/Dropdown/Section/SectionLabel'; +import SectionItem from '../../../components/Dropdown/Section/SectionItem'; +import DropdownSection from '../../../components/Dropdown/Section'; +import { CommandBarContext } from '../../../context/commandBarContext'; +import useKeyboardNavigation from '../../../hooks/useKeyboardNavigation'; +import { HardDriveIcon, ShapesIcon } from '../../../icons'; +import GitHubIcon from '../../../icons/GitHubIcon'; +import { Filter, Provider } from './index'; + +type Props = { + setRepoType: Dispatch>; + repoType: Provider; + setFilter: Dispatch>; + filter: Filter; + handleClose: () => void; +}; + +const ActionsDropDown = ({ + setRepoType, + repoType, + setFilter, + filter, + handleClose, +}: Props) => { + const { t } = useTranslation(); + const { focusedItem } = useContext(CommandBarContext.FooterValues); + const [focusedIndex, setFocusedIndex] = useState(0); + + const providerIconMap = useMemo( + () => ({ + [Provider.All]: ShapesIcon, + [Provider.GitHub]: GitHubIcon, + [Provider.Local]: HardDriveIcon, + }), + [], + ); + + const providerOptions = useMemo( + () => [Provider.All, Provider.GitHub, Provider.Local], + [], + ); + const filterOptions = useMemo( + () => [Filter.All, Filter.Indexed, Filter.Indexing, Filter.InThisProject], + [], + ); + + const focusedDropdownItems = useMemo(() => { + return ( + (focusedItem && + 'focusedItemProps' in focusedItem && + focusedItem.focusedItemProps?.dropdownItems) || + [] + ); + }, [focusedItem]); + + const focusedDropdownItemsLength = useMemo(() => { + return focusedDropdownItems.reduce( + (prev: number, curr: { items: Record[]; key: string }) => + prev + curr.items.length, + 0, + ); + }, [focusedDropdownItems]); + + const handleKeyEvent = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'ArrowDown') { + e.preventDefault(); + e.stopPropagation(); + setFocusedIndex((prev) => + prev < + providerOptions.length + + filterOptions.length + + focusedDropdownItemsLength - + 1 + ? prev + 1 + : 0, + ); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + e.stopPropagation(); + setFocusedIndex((prev) => + prev > 0 + ? prev - 1 + : providerOptions.length + + filterOptions.length + + focusedDropdownItemsLength - + 1, + ); + } else if (e.key === 'Enter') { + if (focusedIndex < focusedDropdownItemsLength) { + let currentIndex = 0; + + for (let i = 0; i < focusedDropdownItems.length; i++) { + for (let j = 0; j < focusedDropdownItems[i].items.length; j++) { + if (currentIndex === focusedIndex) { + handleClose(); + return focusedDropdownItems[i].items[j].onClick(); + } + currentIndex++; + } + } + } else if ( + focusedIndex < + focusedDropdownItemsLength + providerOptions.length + ) { + setRepoType( + providerOptions[focusedIndex - focusedDropdownItemsLength], + ); + } else { + setFilter( + filterOptions[ + focusedIndex - focusedDropdownItemsLength - providerOptions.length + ], + ); + } + } + }, + [focusedIndex, focusedDropdownItems, focusedDropdownItemsLength], + ); + useKeyboardNavigation(handleKeyEvent); + + return ( +
    + {!!focusedDropdownItems.length && + focusedDropdownItems.map( + (section: { + items: Record[]; + key: string; + itemsOffset: number; + }) => ( + + {section.items.map((item: Record, i: number) => ( + { + item.onClick(); + handleClose(); + }} + label={item.label} + icon={item.icon} + /> + ))} + + ), + )} + + + {providerOptions.map((type, i) => { + const Icon = providerIconMap[type]; + return ( + { + setRepoType(type); + handleClose(); + }} + label={t(type)} + icon={} + /> + ); + })} + + + + {filterOptions.map((type, i) => ( + { + setFilter(type); + handleClose(); + }} + label={t(type)} + /> + ))} + +
    + ); +}; + +export default memo(ActionsDropDown); diff --git a/client/src/CommandBar/steps/ManageRepos/index.tsx b/client/src/CommandBar/steps/ManageRepos/index.tsx new file mode 100644 index 0000000000..6d46dd782b --- /dev/null +++ b/client/src/CommandBar/steps/ManageRepos/index.tsx @@ -0,0 +1,217 @@ +import { + ChangeEvent, + memo, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { + CommandBarItemCustomType, + CommandBarItemGeneralType, + CommandBarItemType, + CommandBarSectionType, + CommandBarStepEnum, + RepoProvider, + SyncStatus, +} from '../../../types/general'; +import { PlusSignIcon } from '../../../icons'; +import Header from '../../Header'; +import Body from '../../Body'; +import Footer from '../../Footer'; +import { getIndexedRepos } from '../../../services/api'; +import { mapReposBySections } from '../../../utils/mappers'; +import { ProjectContext } from '../../../context/projectContext'; +import { CommandBarContext } from '../../../context/commandBarContext'; +import RepoItem from '../items/RepoItem'; +import ActionsDropdown from './ActionsDropdown'; + +type Props = {}; + +export enum Filter { + All = 'All', + Indexed = 'Indexed', + Indexing = 'Indexing', + InThisProject = 'In this project', +} + +export enum Provider { + All = 'All', + GitHub = 'GitHub', + Local = 'Local', +} + +const ManageRepos = ({}: Props) => { + const { t } = useTranslation(); + const { project } = useContext(ProjectContext.Current); + const { setChosenStep } = useContext(CommandBarContext.Handlers); + const [sections, setSections] = useState([]); + const [sectionsToShow, setSectionsToShow] = useState( + [], + ); + const [isDropdownVisible, setIsDropdownVisible] = useState(false); + const [filter, setFilter] = useState(Filter.All); + const [repoType, setRepoType] = useState(Provider.All); + const [inputValue, setInputValue] = useState(''); + + const handleInputChange = useCallback((e: ChangeEvent) => { + setInputValue(e.target.value); + }, []); + + const addItem = useMemo(() => { + return { + itemsOffset: 0, + key: 'add', + items: [ + { + label: t('Add new repository'), + Icon: PlusSignIcon, + id: CommandBarStepEnum.ADD_NEW_REPO, + shortcut: ['cmd', 'A'], + footerHint: '', + footerBtns: [ + { + label: t('Add'), + shortcut: ['entr'], + }, + ], + key: 'add', + }, + ], + }; + }, []); + + const refetchRepos = useCallback(() => { + getIndexedRepos().then((data) => { + const mapped = mapReposBySections(data.list).map((o) => ({ + items: o.items.map((r) => ({ + Component: RepoItem, + componentProps: { repo: r, refetchRepos }, + key: r.ref, + })), + itemsOffset: o.offset + 1, + label: o.org === 'Local' ? t('Local') : o.org, + key: o.org, + })); + setSections([addItem, ...mapped]); + }); + }, []); + + useEffect(() => { + if (filter === Filter.All && !inputValue && repoType === Provider.All) { + setSectionsToShow(sections); + return; + } + const newSectionsToShow: CommandBarSectionType[] = []; + const filterByStatus = (item: CommandBarItemType) => { + if ('componentProps' in item) { + switch (filter) { + case Filter.Indexing: + return [ + SyncStatus.Syncing, + SyncStatus.Indexing, + SyncStatus.Queued, + ].includes(item.componentProps.repo.sync_status); + case Filter.Indexed: + return item.componentProps.repo.sync_status === SyncStatus.Done; + case Filter.InThisProject: + return !!project?.repos.find( + (r) => r.repo.ref === item.componentProps.repo.ref, + ); + default: + return true; + } + } + return false; + }; + + const filterByProvider = (item: CommandBarItemType) => { + if ('componentProps' in item) { + switch (repoType) { + case Provider.GitHub: + return item.componentProps.repo.provider === RepoProvider.GitHub; + case Provider.Local: + return item.componentProps.repo.provider === RepoProvider.Local; + default: + return true; + } + } + return false; + }; + + const filterByName = ( + item: CommandBarItemGeneralType | CommandBarItemCustomType, + ) => { + return 'componentProps' in item + ? item.componentProps.repo.shortName + .toLowerCase() + .includes(inputValue.toLowerCase()) + : item.label.toLowerCase().includes(inputValue.toLowerCase()); + }; + + sections.forEach((s) => { + const items = s.items.filter( + (item) => + filterByProvider(item) && filterByStatus(item) && filterByName(item), + ); + + if (items.length) { + newSectionsToShow.push({ + ...s, + items, + itemsOffset: newSectionsToShow[newSectionsToShow.length - 1] + ? newSectionsToShow[newSectionsToShow.length - 1].itemsOffset + + newSectionsToShow[newSectionsToShow.length - 1].items.length + : 0, + }); + } + }); + setSectionsToShow(newSectionsToShow); + }, [sections, filter, inputValue, project?.repos, repoType]); + + useEffect(() => { + refetchRepos(); + }, []); + + const handleBack = useCallback(() => { + setChosenStep({ id: CommandBarStepEnum.INITIAL }); + }, []); + + const actionsDropdownProps = useMemo(() => { + return { + repoType, + setRepoType, + filter, + setFilter, + }; + }, [repoType, filter]); + + return ( +
    +
    + {sectionsToShow.length ? ( + + ) : ( +
    + No repositories found... +
    + )} +
    +
    + ); +}; + +export default memo(ManageRepos); diff --git a/client/src/CommandBar/steps/PrivateRepos/ActionsDropdown.tsx b/client/src/CommandBar/steps/PrivateRepos/ActionsDropdown.tsx new file mode 100644 index 0000000000..9db4637fa8 --- /dev/null +++ b/client/src/CommandBar/steps/PrivateRepos/ActionsDropdown.tsx @@ -0,0 +1,89 @@ +import { memo, useCallback, useContext, useMemo, useState } from 'react'; +import SectionItem from '../../../components/Dropdown/Section/SectionItem'; +import DropdownSection from '../../../components/Dropdown/Section'; +import { CommandBarContext } from '../../../context/commandBarContext'; +import useKeyboardNavigation from '../../../hooks/useKeyboardNavigation'; + +type Props = {}; + +const ActionsDropDown = ({}: Props) => { + const { focusedItem } = useContext(CommandBarContext.FooterValues); + const [focusedIndex, setFocusedIndex] = useState(0); + + const focusedDropdownItems = useMemo(() => { + return ( + (focusedItem && + 'focusedItemProps' in focusedItem && + focusedItem.focusedItemProps?.dropdownItems) || + [] + ); + }, [focusedItem]); + + const focusedDropdownItemsLength = useMemo(() => { + return focusedDropdownItems.reduce( + (prev: number, curr: { items: Record[]; key: string }) => + prev + curr.items.length, + 0, + ); + }, [focusedDropdownItems]); + + const handleKeyEvent = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'ArrowDown') { + e.preventDefault(); + e.stopPropagation(); + setFocusedIndex((prev) => + prev < focusedDropdownItemsLength - 1 ? prev + 1 : 0, + ); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + e.stopPropagation(); + setFocusedIndex((prev) => + prev > 0 ? prev - 1 : focusedDropdownItemsLength - 1, + ); + } else if (e.key === 'Enter') { + let currentIndex = 0; + + for (let i = 0; i < focusedDropdownItems.length; i++) { + for (let j = 0; j < focusedDropdownItems[i].items.length; j++) { + if (currentIndex === focusedIndex) { + return focusedDropdownItems[i].items[j].onClick(); + } + currentIndex++; + } + } + } + }, + [focusedIndex, focusedDropdownItems, focusedDropdownItemsLength], + ); + useKeyboardNavigation(handleKeyEvent); + + return ( +
    + {!!focusedDropdownItems.length && + focusedDropdownItems.map( + (section: { + items: Record[]; + key: string; + itemsOffset: number; + }) => ( + + {section.items.map((item: Record, i: number) => ( + + ))} + + ), + )} +
    + ); +}; + +export default memo(ActionsDropDown); diff --git a/client/src/CommandBar/steps/PrivateRepos/index.tsx b/client/src/CommandBar/steps/PrivateRepos/index.tsx new file mode 100644 index 0000000000..0c4eaf6059 --- /dev/null +++ b/client/src/CommandBar/steps/PrivateRepos/index.tsx @@ -0,0 +1,124 @@ +import { + ChangeEvent, + memo, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { CommandBarContext } from '../../../context/commandBarContext'; +import { + CommandBarItemCustomType, + CommandBarSectionType, + CommandBarStepEnum, + RepoProvider, + SyncStatus, +} from '../../../types/general'; +import { getRepos } from '../../../services/api'; +import { mapReposBySections } from '../../../utils/mappers'; +import Header from '../../Header'; +import Body from '../../Body'; +import Footer from '../../Footer'; +import RepoItem from '../items/RepoItem'; +import ActionsDropdown from './ActionsDropdown'; + +type Props = {}; + +const PrivateReposStep = ({}: Props) => { + const { t } = useTranslation(); + const [sections, setSections] = useState([]); + const [sectionsToShow, setSectionsToShow] = useState( + [], + ); + const { setChosenStep } = useContext(CommandBarContext.Handlers); + const [inputValue, setInputValue] = useState(''); + const [isDropdownVisible, setIsDropdownVisible] = useState(false); + + const handleInputChange = useCallback((e: ChangeEvent) => { + setInputValue(e.target.value); + }, []); + + const refetchRepos = useCallback(async () => { + const data = await getRepos(); + const mapped = mapReposBySections( + data.list.filter((r) => r.provider !== RepoProvider.Local), + ).map((o) => ({ + items: o.items.map((r) => ({ + Component: RepoItem, + componentProps: { repo: r, refetchRepos }, + key: r.ref, + })), + itemsOffset: o.offset, + label: o.org, + key: o.org, + })); + setSections(mapped); + }, []); + + useEffect(() => { + if (!inputValue) { + setSectionsToShow(sections); + return; + } + const newSectionsToShow: CommandBarSectionType[] = []; + sections.forEach((s) => { + const items = (s.items as CommandBarItemCustomType[]).filter((item) => { + return item.componentProps.repo.shortName + .toLowerCase() + .includes(inputValue.toLowerCase()); + }); + + if (items.length) { + newSectionsToShow.push({ + ...s, + items, + itemsOffset: newSectionsToShow[newSectionsToShow.length - 1] + ? newSectionsToShow[newSectionsToShow.length - 1].itemsOffset + + newSectionsToShow[newSectionsToShow.length - 1].items.length + : 0, + }); + } + }); + setSectionsToShow(newSectionsToShow); + }, [sections, inputValue]); + + useEffect(() => { + refetchRepos(); + }, []); + + const breadcrumbs = useMemo(() => { + return [t('Add private repository')]; + }, [t]); + + const handleBack = useCallback(() => { + setChosenStep({ id: CommandBarStepEnum.MANAGE_REPOS }); + }, []); + + return ( +
    +
    + {sectionsToShow.length ? ( + + ) : ( +
    + No repositories found... +
    + )} +
    +
    + ); +}; + +export default memo(PrivateReposStep); diff --git a/client/src/CommandBar/steps/PublicRepos.tsx b/client/src/CommandBar/steps/PublicRepos.tsx new file mode 100644 index 0000000000..44f647d86b --- /dev/null +++ b/client/src/CommandBar/steps/PublicRepos.tsx @@ -0,0 +1,102 @@ +import { + ChangeEvent, + memo, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; +import { useTranslation } from 'react-i18next'; +import axios from 'axios'; +import { CommandBarStepEnum } from '../../types/general'; +import { CommandBarContext } from '../../context/commandBarContext'; +import { syncRepo } from '../../services/api'; +import Header from '../Header'; +import Footer from '../Footer'; + +type Props = {}; + +const PublicRepos = ({}: Props) => { + const { t } = useTranslation(); + const { setChosenStep, setFocusedItem } = useContext( + CommandBarContext.Handlers, + ); + const [inputValue, setInputValue] = useState(''); + + const handleInputChange = useCallback((e: ChangeEvent) => { + setInputValue(e.target.value); + }, []); + + useEffect(() => { + setFocusedItem({ + footerHint: t('Paste a link to any public repository hosted on GitHub'), + footerBtns: [{ label: t('Start indexing'), shortcut: ['entr'] }], + }); + }, []); + + const breadcrumbs = useMemo(() => { + return [t('Add public repository')]; + }, [t]); + + const handleBack = useCallback(() => { + setChosenStep({ id: CommandBarStepEnum.MANAGE_REPOS }); + }, []); + + const handleAddSubmit = useCallback((inputValue: string) => { + setFocusedItem({ + footerHint: t('Verifying access...'), + footerBtns: [], + }); + let cleanRef = inputValue + .replace('https://', '') + .replace('github.com/', '') + .replace(/\.git$/, '') + .replace(/"$/, '') + .replace(/^"/, '') + .replace(/\/$/, ''); + if (inputValue.startsWith('git@github.com:')) { + cleanRef = inputValue.slice(15).replace(/\.git$/, ''); + } + axios(`https://api.github.com/repos/${cleanRef}`) + .then((resp) => { + if (resp?.data?.visibility === 'public') { + syncRepo(`github.com/${cleanRef}`); + handleBack(); + } else { + setFocusedItem({ + footerHint: t( + "This is not a public repository / We couldn't find this repository", + ), + footerBtns: [], + }); + } + }) + .catch((err) => { + console.log(err); + setFocusedItem({ + footerHint: t( + "This is not a public repository / We couldn't find this repository", + ), + footerBtns: [], + }); + }); + }, []); + + return ( +
    +
    +
    +
    +
    + ); +}; + +export default memo(PublicRepos); diff --git a/client/src/CommandBar/steps/SeachFiles.tsx b/client/src/CommandBar/steps/SeachFiles.tsx new file mode 100644 index 0000000000..0db638b670 --- /dev/null +++ b/client/src/CommandBar/steps/SeachFiles.tsx @@ -0,0 +1,103 @@ +import { + ChangeEvent, + memo, + useCallback, + useContext, + useDeferredValue, + useEffect, + useMemo, + useState, +} from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import Header from '../Header'; +import { CommandBarStepEnum, TabTypesEnum } from '../../types/general'; +import { CommandBarContext } from '../../context/commandBarContext'; +import { getAutocomplete } from '../../services/api'; +import { FileResItem } from '../../types/api'; +import Body from '../Body'; +import FileIcon from '../../components/FileIcon'; +import { TabsContext } from '../../context/tabsContext'; + +type Props = {}; + +const SearchFiles = ({}: Props) => { + const { t } = useTranslation(); + const [inputValue, setInputValue] = useState(''); + const { setChosenStep, setIsVisible } = useContext( + CommandBarContext.Handlers, + ); + const { openNewTab } = useContext(TabsContext.Handlers); + const [files, setFiles] = useState<{ path: string; repo: string }[]>([]); + const searchValue = useDeferredValue(inputValue); + + const handleInputChange = useCallback((e: ChangeEvent) => { + setInputValue(e.target.value); + }, []); + + useEffect(() => { + getAutocomplete(`path:${searchValue}&content=false`).then((respPath) => { + const fileResults = respPath.data + .filter( + (d): d is FileResItem => d.kind === 'file_result' && !d.data.is_dir, + ) + .map((d) => ({ + path: d.data.relative_path.text, + repo: d.data.repo_ref, + })); + setFiles(fileResults); + }); + }, [searchValue]); + + const breadcrumbs = useMemo(() => { + return [t('Search files')]; + }, [t]); + + const handleBack = useCallback(() => { + setChosenStep({ id: CommandBarStepEnum.INITIAL }); + }, []); + + const sections = useMemo(() => { + return [ + { + key: 'files', + items: files.map(({ path, repo }) => ({ + key: `${path}-${repo}`, + id: `${path}-${repo}`, + onClick: () => { + openNewTab({ type: TabTypesEnum.FILE, path, repoRef: repo }); + setIsVisible(false); + setChosenStep({ id: CommandBarStepEnum.INITIAL }); + }, + label: path, + footerHint: t('Open'), + footerBtns: [{ label: t('Open'), shortcut: ['entr'] }], + Icon: (props: { sizeClassName?: string }) => ( + + ), + })), + itemsOffset: 0, + }, + ]; + }, [files]); + + return ( +
    +
    + {files.length ? ( + + ) : ( +
    + No files found... +
    + )} +
    + ); +}; + +export default memo(SearchFiles); diff --git a/client/src/CommandBar/steps/ToggleTheme.tsx b/client/src/CommandBar/steps/ToggleTheme.tsx new file mode 100644 index 0000000000..84e4084e51 --- /dev/null +++ b/client/src/CommandBar/steps/ToggleTheme.tsx @@ -0,0 +1,75 @@ +import { memo, useCallback, useContext, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + CommandBarItemGeneralType, + CommandBarStepEnum, +} from '../../types/general'; +import { + MacintoshIcon, + ThemeBlackIcon, + ThemeDarkIcon, + ThemeLightIcon, +} from '../../icons'; +import Header from '../Header'; +import Body from '../Body'; +import Footer from '../Footer'; +import { CommandBarContext } from '../../context/commandBarContext'; +import { Theme } from '../../types'; +import { UIContext } from '../../context/uiContext'; + +type Props = {}; + +const ToggleTheme = ({}: Props) => { + const { t } = useTranslation(); + const { setChosenStep } = useContext(CommandBarContext.Handlers); + const { setTheme } = useContext(UIContext.Theme); + + const handleBack = useCallback(() => { + setChosenStep({ id: CommandBarStepEnum.INITIAL }); + }, []); + + const initialSections = useMemo(() => { + const themeOptions = ['light', 'dark', 'black', 'system'] as Theme[]; + const themeMap = { + light: ThemeLightIcon, + dark: ThemeDarkIcon, + black: ThemeBlackIcon, + system: MacintoshIcon, + }; + const themeItems: CommandBarItemGeneralType[] = themeOptions.map((th) => ({ + label: t(`Use ${th} theme`), + Icon: themeMap[th], + id: `${th}-theme`, + key: `${th}-theme`, + onClick: () => setTheme(th), + footerHint: t(`Use ${th} theme`), + footerBtns: [ + { + label: t('Toggle'), + shortcut: ['entr'], + }, + ], + })); + return [ + { + items: themeItems, + itemsOffset: 0, + key: 'theme-commands', + }, + ]; + }, [t]); + + return ( +
    +
    + +
    +
    + ); +}; + +export default memo(ToggleTheme); diff --git a/client/src/CommandBar/steps/items/DocItem.tsx b/client/src/CommandBar/steps/items/DocItem.tsx new file mode 100644 index 0000000000..f7256ade84 --- /dev/null +++ b/client/src/CommandBar/steps/items/DocItem.tsx @@ -0,0 +1,209 @@ +import { + Dispatch, + memo, + SetStateAction, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { format } from 'date-fns'; +import { LinkChainIcon, RepositoryIcon } from '../../../icons'; +import { DeviceContext } from '../../../context/deviceContext'; +import { getDateFnsLocale } from '../../../utils'; +import { LocaleContext } from '../../../context/localeContext'; +import { DocShortType } from '../../../types/api'; +import { deleteDocProvider, getIndexedDocs } from '../../../services/api'; +import Item from '../../Body/Item'; +import SpinLoaderContainer from '../../../components/Loaders/SpinnerLoader'; +import Button from '../../../components/Button'; + +type Props = { + doc: DocShortType; + i: number; + isFocused: boolean; + setFocusedIndex: Dispatch>; + isFirst: boolean; + isIndexed: boolean; + refetchDocs: () => {}; +}; + +const DocItem = ({ + doc, + isFirst, + setFocusedIndex, + isFocused, + i, + isIndexed, + refetchDocs, +}: Props) => { + const { t } = useTranslation(); + const { locale } = useContext(LocaleContext); + const [docToShow, setDocToShow] = useState(doc); + const [indexingStartedAt, setIndexingStartedAt] = useState(Date.now()); + const [isIndexingFinished, setIsIndexingFinished] = useState( + !!doc.id && doc.index_status === 'done', + ); + const { apiUrl, openLink } = useContext(DeviceContext); + const eventSourceRef = useRef(null); + + const refetchDoc = useCallback(() => { + getIndexedDocs().then((data) => { + const newDoc = data.find((d) => d.url === doc.url); + setDocToShow(newDoc || doc); + }); + }, []); + + const startEventSource = useCallback( + (isResync?: boolean) => { + setIsIndexingFinished(false); + setIndexingStartedAt(Date.now()); + eventSourceRef.current = new EventSource( + `${apiUrl.replace('https:', '')}/docs/${ + isResync ? `${docToShow.id}/resync` : `sync?url=${docToShow.url}` + }`, + ); + setTimeout(refetchDoc, 3000); + eventSourceRef.current.onmessage = (ev) => { + try { + const data = JSON.parse(ev.data); + console.log(data); + if (data.Ok.Done) { + eventSourceRef.current?.close(); + eventSourceRef.current = null; + setIsIndexingFinished(true); + refetchDoc(); + return; + } + } catch (err) { + console.log(err); + eventSourceRef.current?.close(); + eventSourceRef.current = null; + } + }; + eventSourceRef.current.onerror = (err) => { + console.log(err); + eventSourceRef.current?.close(); + eventSourceRef.current = null; + }; + }, + [docToShow], + ); + + useEffect(() => { + return () => { + eventSourceRef.current?.close(); + eventSourceRef.current = null; + }; + }, []); + + useEffect(() => { + if (!isIndexed && !eventSourceRef.current && !isIndexingFinished) { + startEventSource(); + } + }, [isIndexed]); + + const handleAddToProject = useCallback(() => { + console.log(docToShow); + }, [docToShow]); + + const handleCancelSync = useCallback(() => { + eventSourceRef.current?.close(); + eventSourceRef.current = null; + setIsIndexingFinished(true); + }, []); + + const isIndexing = useMemo(() => { + return !isIndexed && !isIndexingFinished; + }, [isIndexed, isIndexingFinished]); + + const handleRemove = useCallback(() => { + if (docToShow.id) { + deleteDocProvider(docToShow.id).then(() => { + refetchDocs(); + }); + } else { + refetchDocs(); + } + }, [docToShow.id]); + + return ( + + {t(`Indexed`)} + + + ) : ( + t('Index repository') + ) + } + onClick={isIndexing ? handleCancelSync : handleAddToProject} + iconContainerClassName={ + isIndexingFinished + ? 'bg-bg-contrast text-label-contrast' + : 'bg-bg-border' + } + footerBtns={ + isIndexingFinished + ? [ + { + label: t('Remove'), + shortcut: ['cmd', 'D'], + action: handleRemove, + }, + { + label: t('Re-sync'), + shortcut: ['cmd', 'R'], + action: () => startEventSource(true), + }, + { + label: t('Add to project'), + shortcut: ['entr'], + action: handleAddToProject, + }, + ] + : [ + { + label: t('Stop indexing'), + shortcut: ['entr'], + action: handleCancelSync, + }, + ] + } + customRightElement={ + isIndexing ? ( +

    {t('Indexing...')}

    + ) : undefined + } + /> + ); +}; + +export default memo(DocItem); diff --git a/client/src/CommandBar/steps/items/RepoItem.tsx b/client/src/CommandBar/steps/items/RepoItem.tsx new file mode 100644 index 0000000000..c20f986953 --- /dev/null +++ b/client/src/CommandBar/steps/items/RepoItem.tsx @@ -0,0 +1,314 @@ +import { + Dispatch, + memo, + SetStateAction, + useCallback, + useContext, + useMemo, +} from 'react'; +import { useTranslation } from 'react-i18next'; +import { + CommandBarStepEnum, + RepoIndexingStatusType, + RepoProvider, + RepoUi, + SyncStatus, +} from '../../../types/general'; +import { + addRepoToProject, + cancelSync, + deleteRepo, + removeRepoFromProject, + syncRepo, +} from '../../../services/api'; +import { + CloseSignInCircleIcon, + LinkChainIcon, + PlusSignIcon, + RepositoryIcon, + TrashCanIcon, +} from '../../../icons'; +import { DeviceContext } from '../../../context/deviceContext'; +import { getFileManagerName } from '../../../utils'; +import Item from '../../Body/Item'; +import SpinLoaderContainer from '../../../components/Loaders/SpinnerLoader'; +import { ProjectContext } from '../../../context/projectContext'; +import { repoStatusMap } from '../../../consts/general'; +import { RepositoriesContext } from '../../../context/repositoriesContext'; + +type Props = { + repo: RepoUi; + i: number; + isFocused: boolean; + setFocusedIndex: Dispatch>; + isFirst: boolean; + refetchRepos: () => void; + disableKeyNav?: boolean; + indexingStatus?: RepoIndexingStatusType; +}; + +const RepoItem = ({ + repo, + isFirst, + setFocusedIndex, + isFocused, + i, + refetchRepos, + disableKeyNav, + indexingStatus, +}: Props) => { + const { t } = useTranslation(); + const { project, refreshCurrentProjectRepos } = useContext( + ProjectContext.Current, + ); + const { openFolderInExplorer, os, openLink } = useContext(DeviceContext); + + const onRepoSync = useCallback( + async (e?: MouseEvent | KeyboardEvent) => { + e?.preventDefault(); + e?.stopPropagation(); + await syncRepo(repo.ref); + }, + [repo.ref], + ); + + const status = useMemo(() => { + return indexingStatus?.status || repo.sync_status; + }, [indexingStatus]); + + const handleAddToProject = useCallback(() => { + if (project?.id) { + return addRepoToProject( + project.id, + repo.ref, + repo.branch_filter?.select?.[0], + ).finally(() => { + refreshCurrentProjectRepos(); + }); + } + }, [repo]); + + const handleOpenInFinder = useCallback(() => { + openFolderInExplorer(repo.ref.slice(6)); + }, [openFolderInExplorer, repo.ref]); + + const handleOpenInGitHub = useCallback(() => { + openLink('https://' + repo.ref); + }, [openLink, repo.ref]); + + const handleRemoveFromProject = useCallback(() => { + if (project?.id) { + return removeRepoFromProject(project.id, repo.ref).finally(() => { + refreshCurrentProjectRepos(); + }); + } + }, [repo]); + + const handleCancelSync = useCallback(() => { + cancelSync(repo.ref); + }, [repo.ref]); + + const isIndexing = useMemo(() => { + return [ + SyncStatus.Indexing, + SyncStatus.Syncing, + SyncStatus.Queued, + ].includes(status); + }, [status]); + + const onRepoRemove = useCallback(async () => { + await deleteRepo(repo.ref); + refetchRepos(); + }, [repo.ref]); + + const isInProject = useMemo(() => { + return project?.repos.find((r) => r.repo.ref === repo.ref); + }, [project, repo.ref]); + + const focusedItemProps = useMemo(() => { + const dropdownItems1 = []; + if (isIndexing) { + dropdownItems1.push({ + onClick: handleCancelSync, + label: t('Stop indexing'), + icon: ( + + + + ), + key: 'stop_indexing', + }); + } + if (status === SyncStatus.Done || status === SyncStatus.Cancelled) { + dropdownItems1.push( + isInProject + ? { + onClick: handleRemoveFromProject, + label: t('Remove from project'), + icon: ( + + + + ), + key: 'remove_from_project', + } + : { + onClick: handleAddToProject, + label: t('Add to project'), + icon: ( + + + + ), + key: 'add_to_project', + }, + ); + dropdownItems1.push({ + onClick: onRepoSync, + label: t('Re-sync'), + shortcut: ['cmd', 'R'], + key: 'resync', + }); + dropdownItems1.push({ + onClick: onRepoRemove, + label: t('Remove'), + shortcut: ['cmd', 'D'], + key: 'remove', + }); + } + const dropdownItems2 = [ + repo.provider === RepoProvider.Local + ? { + onClick: handleOpenInFinder, + label: t(`Open in {{viewer}}`, { + viewer: getFileManagerName(os.type), + }), + key: 'openInFinder', + } + : { + onClick: handleOpenInGitHub, + label: t(`Open in GitHub`), + icon: ( + + + + ), + key: 'openInGitHub', + }, + ]; + const dropdownItems = []; + if (dropdownItems1.length) { + dropdownItems.push({ items: dropdownItems1, key: '1', itemsOffset: 0 }); + } + if (dropdownItems2.length) { + dropdownItems.push({ + items: dropdownItems2, + key: '2', + itemsOffset: dropdownItems1.length, + }); + } + return { + dropdownItems, + }; + }, [ + t, + isInProject, + handleAddToProject, + handleRemoveFromProject, + handleCancelSync, + status, + repo.provider, + isIndexing, + handleOpenInFinder, + handleOpenInGitHub, + onRepoRemove, + ]); + + return ( + + {t(repoStatusMap[status].text)} + {indexingStatus?.percentage !== null && + indexingStatus?.percentage !== undefined && + `· ${indexingStatus?.percentage}%`} +

    + ) : undefined + } + focusedItemProps={focusedItemProps} + disableKeyNav={disableKeyNav} + /> + ); +}; + +const WithIndexingStatus = (props: Omit) => { + const { indexingStatus } = useContext(RepositoriesContext); + const repoIndexingStatus = useMemo(() => { + return indexingStatus[props.repo.ref]; + }, [indexingStatus[props.repo.ref]]); + + return ; +}; + +export default memo(WithIndexingStatus); diff --git a/client/src/pages/Onboarding/Desktop/FeaturesStep/Feature.tsx b/client/src/Onboarding/Desktop/FeaturesStep/Feature.tsx similarity index 84% rename from client/src/pages/Onboarding/Desktop/FeaturesStep/Feature.tsx rename to client/src/Onboarding/Desktop/FeaturesStep/Feature.tsx index fba0e9f23c..47829c4e66 100644 --- a/client/src/pages/Onboarding/Desktop/FeaturesStep/Feature.tsx +++ b/client/src/Onboarding/Desktop/FeaturesStep/Feature.tsx @@ -13,7 +13,7 @@ const Feature = ({ icon, description, title }: Props) => { {icon} {title}
    -
    {description}
    +
    {description}
    ); }; diff --git a/client/src/pages/Onboarding/Desktop/FeaturesStep/index.tsx b/client/src/Onboarding/Desktop/FeaturesStep/index.tsx similarity index 66% rename from client/src/pages/Onboarding/Desktop/FeaturesStep/index.tsx rename to client/src/Onboarding/Desktop/FeaturesStep/index.tsx index cb4a73e16c..604e5fa0cc 100644 --- a/client/src/pages/Onboarding/Desktop/FeaturesStep/index.tsx +++ b/client/src/Onboarding/Desktop/FeaturesStep/index.tsx @@ -1,17 +1,14 @@ import React, { useCallback } from 'react'; import { Trans, useTranslation } from 'react-i18next'; -import Button from '../../../../components/Button'; -import { ChatBubble, PointClick, CodeStudioIcon } from '../../../../icons'; -import DialogText from '../../../../components/SeparateOnboardingStep/DialogText'; -import GoBackButton from '../../../HomeTab/AddRepos/GoBackButton'; +import Button from '../../../components/Button'; +import { ChatBubblesIcon, CodeStudioIcon } from '../../../icons'; import Feature from './Feature'; type Props = { handleNext: (e?: any) => void; - handleBack?: (e?: any) => void; }; -const FeaturesStep = ({ handleNext, handleBack }: Props) => { +const FeaturesStep = ({ handleNext }: Props) => { const { t } = useTranslation(); const handleSubmit = useCallback( (e: React.MouseEvent) => { @@ -23,20 +20,24 @@ const FeaturesStep = ({ handleNext, handleBack }: Props) => { return ( <> - +
    +

    + {t('Welcome to bloop')} +

    +

    + {t('Unlock the value of your existing code, using AI')} +

    +
    } + icon={} title={t('Search code in natural language')} description={t( 'Ask questions about your codebases in natural language, just like you’d speak to ChatGPT. Get started by syncing a repo, then open the repo and start chatting.', )} /> } + icon={} title={t('Generate code using AI')} description={t( 'Code studio helps you write scripts, create unit tests, debug issues or generate anything else you can think of using AI! Sync a repo, then create a code studio project.', @@ -48,7 +49,6 @@ const FeaturesStep = ({ handleNext, handleBack }: Props) => {
    - {handleBack ? : null} ); }; diff --git a/client/src/pages/Onboarding/Desktop/UserForm/Step1.tsx b/client/src/Onboarding/Desktop/UserForm/Step1.tsx similarity index 69% rename from client/src/pages/Onboarding/Desktop/UserForm/Step1.tsx rename to client/src/Onboarding/Desktop/UserForm/Step1.tsx index adba3e4627..0aed99f170 100644 --- a/client/src/pages/Onboarding/Desktop/UserForm/Step1.tsx +++ b/client/src/Onboarding/Desktop/UserForm/Step1.tsx @@ -7,17 +7,17 @@ import React, { useState, } from 'react'; import { Trans, useTranslation } from 'react-i18next'; -import TextInput from '../../../../components/TextInput'; -import { EMAIL_REGEX } from '../../../../consts/validations'; -import Dropdown from '../../../../components/Dropdown/Normal'; -import { themesMap } from '../../../../components/Settings/Preferences'; -import { MenuItemType } from '../../../../types/general'; -import { Theme } from '../../../../types'; -import { previewTheme } from '../../../../utils'; -import Button from '../../../../components/Button'; -import { UIContext } from '../../../../context/uiContext'; +import TextInput from '../../../components/TextInput'; +import { EMAIL_REGEX } from '../../../consts/validations'; +import { themesMap } from '../../../consts/general'; +import Button from '../../../components/Button'; +import { UIContext } from '../../../context/uiContext'; import { Form } from '../../index'; -import { DeviceContext } from '../../../../context/deviceContext'; +import { DeviceContext } from '../../../context/deviceContext'; +import Dropdown from '../../../components/Dropdown'; +import ThemeDropdown from '../../../Settings/Preferences/ThemeDropdown'; +import { themeIconsMap } from '../../../Settings/Preferences'; +import { ChevronDownIcon } from '../../../icons'; type Props = { form: Form; @@ -27,7 +27,7 @@ type Props = { const UserFormStep1 = ({ form, setForm, onContinue }: Props) => { const { t } = useTranslation(); - const { theme, setTheme } = useContext(UIContext.Theme); + const { theme } = useContext(UIContext.Theme); const { openLink } = useContext(DeviceContext); const [showErrors, setShowErrors] = useState(false); @@ -65,7 +65,6 @@ const UserFormStep1 = ({ form, setForm, onContinue }: Props) => { value={form.firstName} name="firstName" placeholder={t('First name')} - variant="filled" onChange={(e) => setForm((prev) => ({ ...prev, firstName: e.target.value })) } @@ -80,7 +79,6 @@ const UserFormStep1 = ({ form, setForm, onContinue }: Props) => { value={form.lastName} name="lastName" placeholder={t('Last name')} - variant="filled" onChange={(e) => setForm((prev) => ({ ...prev, lastName: e.target.value })) } @@ -90,7 +88,6 @@ const UserFormStep1 = ({ form, setForm, onContinue }: Props) => { /> setForm((prev) => ({ ...prev, @@ -98,7 +95,7 @@ const UserFormStep1 = ({ form, setForm, onContinue }: Props) => { emailError: null, })) } - validate={() => { + onBlur={() => { if (form.email && !EMAIL_REGEX.test(form.email)) { setForm((prev) => ({ ...prev, @@ -113,26 +110,21 @@ const UserFormStep1 = ({ form, setForm, onContinue }: Props) => { name="email" placeholder={t('Email address')} /> -
    +
    + + Select color theme: + - Select color theme: - - } - btnClassName="w-full border-transparent" - items={Object.entries(themesMap).map(([key, name]) => ({ - type: MenuItemType.DEFAULT, - text: t(name), - onClick: () => setTheme(key as Theme), - onMouseOver: () => previewTheme(key), - }))} - onClose={() => previewTheme(theme)} - selected={{ - type: MenuItemType.DEFAULT, - text: t(themesMap[theme]), - }} - /> + DropdownComponent={ThemeDropdown} + size="small" + dropdownPlacement="bottom-end" + > + +
    )} - + + +
    @@ -40,14 +54,14 @@ const UserForm = ({ form, setForm, onContinue }: Props) => { Setup bloop {envConfig.credentials_upgrade && ( -

    +

    We’ve updated our auth service to make bloop more secure, please reauthorise your client with GitHub

    )} -

    +

    {step === 0 ? ( Let’s get you started with bloop! ) : ( diff --git a/client/src/pages/Onboarding/Desktop/index.tsx b/client/src/Onboarding/Desktop/index.tsx similarity index 78% rename from client/src/pages/Onboarding/Desktop/index.tsx rename to client/src/Onboarding/Desktop/index.tsx index 085ea867aa..bb41f85300 100644 --- a/client/src/pages/Onboarding/Desktop/index.tsx +++ b/client/src/Onboarding/Desktop/index.tsx @@ -1,24 +1,24 @@ import React, { memo, useCallback, useContext, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import NavBar from '../../../components/NavBar'; -import { DeviceContext } from '../../../context/deviceContext'; +import NavBar from '../../components/Header'; +import { DeviceContext } from '../../context/deviceContext'; import { getJsonFromStorage, saveJsonToStorage, USER_DATA_FORM, -} from '../../../services/storage'; +} from '../../services/storage'; import { Form } from '../index'; -import { saveUserData } from '../../../services/api'; -import SeparateOnboardingStep from '../../../components/SeparateOnboardingStep'; +import { saveUserData } from '../../services/api'; +import Modal from '../../components/Modal'; +import { EnvContext } from '../../context/envContext'; import FeaturesStep from './FeaturesStep'; import UserForm from './UserForm'; type Props = { - activeTab: string; closeOnboarding: () => void; }; -const Desktop = ({ activeTab, closeOnboarding }: Props) => { +const Desktop = ({ closeOnboarding }: Props) => { const { t } = useTranslation(); const [shouldShowPopup, setShouldShowPopup] = useState(false); const [form, setForm] = useState

    ({ @@ -28,7 +28,8 @@ const Desktop = ({ activeTab, closeOnboarding }: Props) => { emailError: null, ...getJsonFromStorage(USER_DATA_FORM), }); - const { os, envConfig } = useContext(DeviceContext); + const { os } = useContext(DeviceContext); + const { envConfig } = useContext(EnvContext); const onSubmit = useCallback(() => { saveUserData({ @@ -44,7 +45,12 @@ const Desktop = ({ activeTab, closeOnboarding }: Props) => { return (
    - {os.type === 'Darwin' && } + {os.type === 'Darwin' && ( +
    + )} {
    - setShouldShowPopup(false)} > setShouldShowPopup(false)} /> - +
    ); }; diff --git a/client/src/pages/Onboarding/SelfServe/index.tsx b/client/src/Onboarding/SelfServe/index.tsx similarity index 66% rename from client/src/pages/Onboarding/SelfServe/index.tsx rename to client/src/Onboarding/SelfServe/index.tsx index 810db2ef07..e27aea57cf 100644 --- a/client/src/pages/Onboarding/SelfServe/index.tsx +++ b/client/src/Onboarding/SelfServe/index.tsx @@ -1,18 +1,11 @@ import React, { memo, useEffect, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; -import NavBar from '../../../components/NavBar'; -import StatusBar from '../../../components/StatusBar'; -import { githubLogin } from '../../../services/api'; -import DialogText from '../../../components/SeparateOnboardingStep/DialogText'; -import Button from '../../../components/Button'; -import { GitHubLogo } from '../../../icons'; +import { githubLogin } from '../../services/api'; +import Button from '../../components/Button'; +import { GitHubLogo } from '../../icons'; -type Props = { - activeTab: string; -}; - -const SelfServe = ({ activeTab }: Props) => { +const SelfServe = () => { const { t } = useTranslation(); const [loginUrl, setLoginUrl] = useState(''); const location = useLocation(); @@ -30,8 +23,7 @@ const SelfServe = ({ activeTab }: Props) => { }, []); return ( -
    ' @@ -108,8 +120,23 @@ const InputCore = ({ const plugins = useMemo(() => { return [ + placeholderPlugin(placeholder), + react(), + mentionPlugin, keymap({ ...baseKeymap, + Escape: (state) => { + const key = Object.keys(state).find((k) => + k.startsWith('autosuggestions'), + ); + + // @ts-ignore + if (key && state[key]?.active) { + return true; + } + blurInput(); + return true; + }, Enter: (state) => { const key = Object.keys(state).find((k) => k.startsWith('autosuggestions'), @@ -118,7 +145,7 @@ const InputCore = ({ if (key && state[key]?.active) { return false; } - const parts = state.toJSON().doc.content[0]?.content; + const parts = state.toJSON().doc?.content?.[0]?.content; // trying to submit with no text if (!parts) { return false; @@ -130,9 +157,6 @@ const InputCore = ({ 'Cmd-Enter': baseKeymap.Enter, 'Shift-Enter': baseKeymap.Enter, }), - placeholderPlugin(placeholder), - react(), - mentionPlugin, ]; }, [onSubmit]); @@ -150,17 +174,16 @@ const InputCore = ({ useEffect(() => { if (mount) { - setState( - EditorState.create({ - schema, - plugins, - doc: initialValue - ? schema.topNodeType.create(null, [ - schema.nodeFromJSON(initialValue), - ]) - : undefined, - }), - ); + const newState = EditorState.create({ + schema, + plugins, + doc: initialValue + ? schema.topNodeType.create(null, [schema.nodeFromJSON(initialValue)]) + : undefined, + }); + const endPos = newState.selection.$to.after() - 1; + newState.selection = new TextSelection(newState.doc.resolve(endPos)); + setState(newState); } }, [mount, initialValue, plugins]); @@ -170,12 +193,12 @@ const InputCore = ({ ); useEffect(() => { - const newValue = state.toJSON().doc.content[0]?.content; + const newValue = state.toJSON().doc?.content?.[0]?.content || ''; onChange(newValue || []); }, [state]); return ( -
    +
    | null; + generationInProgress?: boolean; + isStoppable?: boolean; + onStop?: () => void; + setInputValue: Dispatch< + SetStateAction<{ parsed: ParsedQueryType[]; plain: string }> + >; + selectedLines?: [number, number] | null; + setSelectedLines?: (l: [number, number] | null) => void; + queryIdToEdit?: string; + onMessageEditCancel?: () => void; + conversation: ChatMessage[]; + hideMessagesFrom: number | null; + setConversation: Dispatch>; + setSubmittedQuery: Dispatch< + SetStateAction<{ parsed: ParsedQueryType[]; plain: string }> + >; + submittedQuery: { parsed: ParsedQueryType[]; plain: string }; + isInputAtBottom?: boolean; +}; + +const ConversationInput = ({ + value, + valueToEdit, + setInputValue, + generationInProgress, + isStoppable, + onStop, + selectedLines, + setSelectedLines, + queryIdToEdit, + onMessageEditCancel, + conversation, + hideMessagesFrom, + setConversation, + setSubmittedQuery, + submittedQuery, + isInputAtBottom, +}: Props) => { + const { t } = useTranslation(); + const { envConfig } = useContext(EnvContext); + const [initialValue, setInitialValue] = useState< + Record | null | undefined + >({ + type: 'paragraph', + content: value?.parsed + .filter((pq) => ['path', 'lang', 'text'].includes(pq.type)) + .map((pq) => + pq.type === 'text' + ? { type: 'text', text: pq.text } + : { + type: 'mention', + attrs: { + id: pq.text, + display: pq.text, + type: pq.type, + isFirst: false, + }, + }, + ), + }); + const [hasRendered, setHasRendered] = useState(false); + const containerRef = useRef(null); + + useEffect(() => { + setHasRendered(true); + setTimeout(focusInput, 500); + }, []); + + useEffect(() => { + if (hasRendered) { + setInitialValue(valueToEdit); + } + }, [valueToEdit]); + + // useEffect(() => { + // if (containerRef.current) { + // setIsInputAtBottom(containerRef.current) + // } + // }, [conversation]); + + const onSubmit = useCallback( + (value: { parsed: ParsedQueryType[]; plain: string }) => { + if ( + (conversation[conversation.length - 1] as ChatMessageServer) + ?.isLoading || + !value.plain.trim() + ) { + return; + } + if (hideMessagesFrom !== null) { + setConversation((prev) => prev.slice(0, hideMessagesFrom)); + } + setSubmittedQuery(value); + }, + [conversation, submittedQuery, hideMessagesFrom], + ); + + const onChangeInput = useCallback((inputState: InputEditorContent[]) => { + setInputValue(mapEditorContentToInputValue(inputState)); + }, []); + + const onSubmitButtonClicked = useCallback(() => { + if (value && onSubmit) { + onSubmit(value); + } + }, [value, onSubmit]); + + const getDataPath = useCallback(async (search: string) => { + const respPath = await getAutocomplete(`path:${search}&content=false`); + const fileResults = respPath.data.filter( + (d): d is FileResItem => d.kind === 'file_result', + ); + const dirResults = fileResults + .filter((d) => d.data.is_dir) + .map((d) => ({ + path: d.data.relative_path.text, + repo: d.data.repo_ref, + })); + const filesResults = openTabsCache.tabs + .filter( + (t): t is FileTabType => + t.type === TabTypesEnum.FILE && + (!search || t.path.toLowerCase().includes(search.toLowerCase())), + ) + .map((t) => ({ path: t.path, repo: t.repoRef })); + filesResults.push( + ...fileResults + .filter( + (d) => + !d.data.is_dir && + !filesResults.find( + (f) => + f.path === d.data.relative_path.text && + f.repo === d.data.repo_ref, + ), + ) + .map((d) => ({ + path: d.data.relative_path.text, + repo: d.data.repo_ref, + })), + ); + const results: MentionOptionType[] = []; + filesResults.forEach((fr, i) => { + results.push({ + id: fr.path, + display: fr.path, + type: 'file', + isFirst: i === 0, + hint: splitPath(fr.repo).pop(), + }); + }); + dirResults.forEach((fr, i) => { + results.push({ + id: fr.path, + display: fr.path, + type: 'dir', + isFirst: i === 0, + hint: splitPath(fr.repo).pop(), + }); + }); + return results; + }, []); + + const getDataLang = useCallback( + async ( + search: string, + // callback: (a: { id: string; display: string }[]) => void, + ) => { + const respLang = await getAutocomplete(`lang:${search}&content=false`); + const langResults = respLang.data + .filter((d): d is LangItem => d.kind === 'lang') + .map((d) => d.data); + const results: MentionOptionType[] = []; + langResults.forEach((fr, i) => { + results.push({ id: fr, display: fr, type: 'lang', isFirst: i === 0 }); + }); + return results; + }, + [], + ); + + const getDataRepo = useCallback( + async ( + search: string, + // callback: (a: { id: string; display: string }[]) => void, + ) => { + const respRepo = await getAutocomplete( + `repo:${search}&content=false&path=false&file=false`, + ); + const repoResults = respRepo.data + .filter((d): d is RepoItem => d.kind === 'repository_result') + .map((d) => d.data); + const results: MentionOptionType[] = []; + repoResults.forEach((rr, i) => { + results.push({ + id: rr.name.text, + display: rr.name.text.replace('github.com/', ''), + type: 'repo', + isFirst: i === 0, + }); + }); + return results; + }, + [], + ); + + const handleKeyEvent = useCallback( + (e: KeyboardEvent) => { + if ( + e.key === 'Escape' && + ((onMessageEditCancel && queryIdToEdit) || (isStoppable && onStop)) + ) { + e.preventDefault(); + e.stopPropagation(); + onMessageEditCancel?.(); + onStop?.(); + } + }, + [onMessageEditCancel, isStoppable, onStop], + ); + useKeyboardNavigation(handleKeyEvent, !queryIdToEdit && !isStoppable); + + return ( +
    +
    + {t('avatar')} +
    +
    +

    + You +

    + {generationInProgress ? ( +
    + Generating answer... +
    + ) : ( + + )} +
    + {isStoppable && ( + + )} + {!!queryIdToEdit && ( + + )} + +
    +
    +
    + ); +}; + +export default memo(ConversationInput); diff --git a/client/src/components/Chat/ChatFooter/Input/mentionPlugin.ts b/client/src/Project/CurrentTabContent/ChatTab/Input/mentionPlugin.ts similarity index 93% rename from client/src/components/Chat/ChatFooter/Input/mentionPlugin.ts rename to client/src/Project/CurrentTabContent/ChatTab/Input/mentionPlugin.ts index 9ab55e592f..3809854f28 100644 --- a/client/src/components/Chat/ChatFooter/Input/mentionPlugin.ts +++ b/client/src/Project/CurrentTabContent/ChatTab/Input/mentionPlugin.ts @@ -1,6 +1,7 @@ import { Plugin, PluginKey } from 'prosemirror-state'; import { Decoration, DecorationSet, EditorView } from 'prosemirror-view'; import { ResolvedPos } from 'prosemirror-model'; +import { MentionOptionType } from '../../../../types/results'; export function getRegexp(mentionTrigger: string, allowSpace?: boolean) { return allowSpace @@ -115,17 +116,17 @@ type Options = { getSuggestions: ( type: string, text: string, - done: (s: Record[]) => void, + done: (s: MentionOptionType[]) => void, ) => void; delay: number; - getSuggestionsHTML: (items: Record[], type: string) => string; + getSuggestionsHTML: (items: MentionOptionType[], type: string) => string; }; export function getMentionsPlugin(opts: Partial) { // default options const defaultOpts = { mentionTrigger: '@', - allowSpace: true, + allowSpace: false, getSuggestions: ( type: string, text: string, @@ -156,7 +157,7 @@ export function getMentionsPlugin(opts: Partial) { const showList = function ( view: EditorView, state: State, - suggestions: Record[], + suggestions: MentionOptionType[], opts: Options, ) { try { @@ -198,6 +199,7 @@ export function getMentionsPlugin(opts: Partial) { el.style.position = 'fixed'; el.style.left = -9999 + 'px'; const offsetLeft = offset?.left || 0; + const offsetTop = offset?.top || 0; setTimeout(() => { el.style.left = offsetLeft + el.clientWidth < window.innerWidth @@ -205,12 +207,14 @@ export function getMentionsPlugin(opts: Partial) { : offsetLeft + (window.innerWidth - (offsetLeft + el.clientWidth) - 10) + 'px'; + el.style.bottom = + window.innerHeight - offsetTop + el.clientHeight > window.innerHeight + ? window.innerHeight - offsetTop - el.clientHeight - 20 + 'px' + : window.innerHeight - offsetTop + 'px'; }, 10); - const bottom = window.innerHeight - (offset?.top || 0); - el.style.bottom = bottom + 'px'; el.style.display = 'block'; - el.style.zIndex = '999999'; + el.style.zIndex = '80'; } catch (e) { console.log(e); } @@ -305,6 +309,7 @@ export function getMentionsPlugin(opts: Partial) { return newState; } catch (e) { + console.log(e); return state; } }, @@ -321,15 +326,19 @@ export function getMentionsPlugin(opts: Partial) { } if (e.key === 'ArrowDown') { + e.stopPropagation(); goNext(view, state, options); return true; } else if (e.key === 'ArrowUp') { + e.stopPropagation(); goPrev(view, state, options); return true; } else if (e.key === 'Enter') { + e.stopPropagation(); select(view, state, options); return true; } else if (e.key === 'Escape') { + e.stopPropagation(); clearTimeout(showListTimeoutId); hideList(); // @ts-ignore @@ -384,6 +393,9 @@ export function getMentionsPlugin(opts: Partial) { this, ); }, + destroy: () => { + hideList(); + }, }; }, }); diff --git a/client/src/components/Chat/ChatFooter/Input/nodes.ts b/client/src/Project/CurrentTabContent/ChatTab/Input/nodes.ts similarity index 64% rename from client/src/components/Chat/ChatFooter/Input/nodes.ts rename to client/src/Project/CurrentTabContent/ChatTab/Input/nodes.ts index 6fb169d44e..b78a04715a 100644 --- a/client/src/components/Chat/ChatFooter/Input/nodes.ts +++ b/client/src/Project/CurrentTabContent/ChatTab/Input/nodes.ts @@ -1,4 +1,3 @@ -// @ts-ignore import * as icons from 'file-icons-js'; import { type AttributeSpec, type NodeSpec } from 'prosemirror-model'; import { getFileExtensionForLang, splitPath } from '../../../../utils'; @@ -31,6 +30,18 @@ export const mentionNode: NodeSpec = { /> `; folderIcon.className = 'w-4 h-4 flex-shrink-0'; + + const repoIcon = document.createElement('span'); + repoIcon.innerHTML = ` + + `; + repoIcon.className = 'w-4 h-4 flex-shrink-0'; + return [ 'span', { @@ -39,20 +50,24 @@ export const mentionNode: NodeSpec = { 'data-first': node.attrs.isFirst, 'data-display': node.attrs.display, class: - 'prosemirror-tag-node inline-flex gap-1.5 items-center align-bottom bg-chat-bg-border-hover rounded px-1', + 'prosemirror-tag-node inline-flex gap-1 h-[22px] items-center align-bottom bg-bg-base border border-bg-border rounded px-1', }, isDir ? folderIcon + : node.attrs.type === 'repo' + ? repoIcon : [ 'span', { - class: `text-left w-4 h-4 file-icon flex-shrink-0 inline-flex items-center ${icons.getClassWithColor( - (node.attrs.type === 'lang' - ? node.attrs.display.includes(' ') - ? '.txt' - : getFileExtensionForLang(node.attrs.display, true) - : node.attrs.display) || '.txt', - )}`, + class: `text-left w-4 h-4 file-icon flex-shrink-0 inline-flex items-center ${ + icons.getClassWithColor( + (node.attrs.type === 'lang' + ? node.attrs.display.includes(' ') + ? '.txt' + : getFileExtensionForLang(node.attrs.display, true) + : node.attrs.display) || '.txt', + ) || icons.getClassWithColor('index.txt') + }`, }, '', ], diff --git a/client/src/components/Chat/ChatFooter/Input/placeholderPlugin.ts b/client/src/Project/CurrentTabContent/ChatTab/Input/placeholderPlugin.ts similarity index 100% rename from client/src/components/Chat/ChatFooter/Input/placeholderPlugin.ts rename to client/src/Project/CurrentTabContent/ChatTab/Input/placeholderPlugin.ts diff --git a/client/src/components/Chat/ChatFooter/Input/utils.ts b/client/src/Project/CurrentTabContent/ChatTab/Input/utils.ts similarity index 78% rename from client/src/components/Chat/ChatFooter/Input/utils.ts rename to client/src/Project/CurrentTabContent/ChatTab/Input/utils.ts index 19dd2d439d..1d3667045a 100644 --- a/client/src/components/Chat/ChatFooter/Input/utils.ts +++ b/client/src/Project/CurrentTabContent/ChatTab/Input/utils.ts @@ -1,7 +1,9 @@ import OrderedMap from 'orderedmap'; import { type NodeSpec } from 'prosemirror-model'; -import { InputEditorContent } from '../../../../utils'; -import { ParsedQueryTypeEnum } from '../../../../types/general'; +import { + InputEditorContent, + ParsedQueryTypeEnum, +} from '../../../../types/general'; import { mentionNode } from './nodes'; export function addMentionNodes(nodes: OrderedMap) { @@ -9,10 +11,12 @@ export function addMentionNodes(nodes: OrderedMap) { mention: mentionNode, }); } + export const mapEditorContentToInputValue = ( inputState: InputEditorContent[], ) => { - const getType = (type: string) => (type === 'lang' ? 'lang' : 'path'); + const getType = (type: string) => + type === 'lang' || type === 'repo' ? type : 'path'; const newValue = inputState .map((s) => s.type === 'mention' @@ -28,6 +32,8 @@ export const mapEditorContentToInputValue = ( type: s.attrs.type === 'lang' ? ParsedQueryTypeEnum.LANG + : s.attrs.type === 'repo' + ? ParsedQueryTypeEnum.REPO : ParsedQueryTypeEnum.PATH, text: s.attrs.id, } diff --git a/client/src/Project/CurrentTabContent/ChatTab/Message/LoadingStep.tsx b/client/src/Project/CurrentTabContent/ChatTab/Message/LoadingStep.tsx new file mode 100644 index 0000000000..67e2468a04 --- /dev/null +++ b/client/src/Project/CurrentTabContent/ChatTab/Message/LoadingStep.tsx @@ -0,0 +1,43 @@ +import { memo, useCallback, useContext } from 'react'; +import { useTranslation } from 'react-i18next'; +import FileChip from '../../../../components/Chips/FileChip'; +import { ChatLoadingStep, TabTypesEnum } from '../../../../types/general'; +import { TabsContext } from '../../../../context/tabsContext'; + +type Props = ChatLoadingStep & { + side: 'left' | 'right'; + repo?: string; +}; + +const LoadingStep = ({ type, path, displayText, side, repo }: Props) => { + const { t } = useTranslation(); + const { openNewTab } = useContext(TabsContext.Handlers); + + const handleClickFile = useCallback(() => { + if (type === 'proc' && repo && path) { + openNewTab( + { + type: TabTypesEnum.FILE, + repoRef: repo, + path, + }, + side === 'left' ? 'right' : 'left', + ); + } + }, [path, repo, side]); + + return ( +
    + {type === 'proc' ? t('Reading ') : displayText} + {type === 'proc' ? ( + + ) : null} +
    + ); +}; + +export default memo(LoadingStep); diff --git a/client/src/components/Chat/ChatBody/ConversationMessage/UserParsedQuery/LangChip.tsx b/client/src/Project/CurrentTabContent/ChatTab/Message/UserParsedQuery/LangChip.tsx similarity index 76% rename from client/src/components/Chat/ChatBody/ConversationMessage/UserParsedQuery/LangChip.tsx rename to client/src/Project/CurrentTabContent/ChatTab/Message/UserParsedQuery/LangChip.tsx index 9890d58315..102ded050a 100644 --- a/client/src/components/Chat/ChatBody/ConversationMessage/UserParsedQuery/LangChip.tsx +++ b/client/src/Project/CurrentTabContent/ChatTab/Message/UserParsedQuery/LangChip.tsx @@ -1,5 +1,5 @@ -import FileIcon from '../../../../FileIcon'; import { getFileExtensionForLang } from '../../../../../utils'; +import FileIcon from '../../../../../components/FileIcon'; type Props = { lang: string; @@ -8,7 +8,7 @@ type Props = { const LangChip = ({ lang }: Props) => { return ( diff --git a/client/src/components/Chat/ChatBody/ConversationMessage/UserParsedQuery/PathChip.tsx b/client/src/Project/CurrentTabContent/ChatTab/Message/UserParsedQuery/PathChip.tsx similarity index 71% rename from client/src/components/Chat/ChatBody/ConversationMessage/UserParsedQuery/PathChip.tsx rename to client/src/Project/CurrentTabContent/ChatTab/Message/UserParsedQuery/PathChip.tsx index f34ccfe25e..ff337ccea4 100644 --- a/client/src/components/Chat/ChatBody/ConversationMessage/UserParsedQuery/PathChip.tsx +++ b/client/src/Project/CurrentTabContent/ChatTab/Message/UserParsedQuery/PathChip.tsx @@ -1,6 +1,6 @@ import { useMemo } from 'react'; -import { FolderClosed, ArrowOut } from '../../../../../icons'; -import FileIcon from '../../../../FileIcon'; +import { FolderIcon } from '../../../../../icons'; +import FileIcon from '../../../../../components/FileIcon'; import { splitPath } from '../../../../../utils'; type Props = { @@ -11,12 +11,12 @@ const PathChip = ({ path }: Props) => { const isFolder = useMemo(() => path.endsWith('/'), [path]); return ( {isFolder ? ( - + ) : ( )} diff --git a/client/src/Project/CurrentTabContent/ChatTab/Message/UserParsedQuery/RepoChip.tsx b/client/src/Project/CurrentTabContent/ChatTab/Message/UserParsedQuery/RepoChip.tsx new file mode 100644 index 0000000000..01fb16764e --- /dev/null +++ b/client/src/Project/CurrentTabContent/ChatTab/Message/UserParsedQuery/RepoChip.tsx @@ -0,0 +1,22 @@ +import { RepositoryIcon } from '../../../../../icons'; +import { splitPath } from '../../../../../utils'; + +type Props = { + name: string; +}; + +const RepoChip = ({ name }: Props) => { + return ( + + + + {splitPath(name).pop()} + + + ); +}; + +export default RepoChip; diff --git a/client/src/components/Chat/ChatBody/ConversationMessage/UserParsedQuery/index.tsx b/client/src/Project/CurrentTabContent/ChatTab/Message/UserParsedQuery/index.tsx similarity index 81% rename from client/src/components/Chat/ChatBody/ConversationMessage/UserParsedQuery/index.tsx rename to client/src/Project/CurrentTabContent/ChatTab/Message/UserParsedQuery/index.tsx index 17a671b16d..20355a8ffd 100644 --- a/client/src/components/Chat/ChatBody/ConversationMessage/UserParsedQuery/index.tsx +++ b/client/src/Project/CurrentTabContent/ChatTab/Message/UserParsedQuery/index.tsx @@ -5,6 +5,7 @@ import { } from '../../../../../types/general'; import PathChip from './PathChip'; import LangChip from './LangChip'; +import RepoChip from './RepoChip'; type Props = { textQuery: string; @@ -13,7 +14,7 @@ type Props = { const UserParsedQuery = ({ textQuery, parsedQuery }: Props) => { return ( -
    + {parsedQuery ? parsedQuery.map((p, i) => p.type === ParsedQueryTypeEnum.TEXT ? ( @@ -22,10 +23,12 @@ const UserParsedQuery = ({ textQuery, parsedQuery }: Props) => { ) : p.type === ParsedQueryTypeEnum.LANG ? ( + ) : p.type === ParsedQueryTypeEnum.REPO ? ( + ) : null, ) : textQuery} -
    +
    ); }; diff --git a/client/src/Project/CurrentTabContent/ChatTab/Message/index.tsx b/client/src/Project/CurrentTabContent/ChatTab/Message/index.tsx new file mode 100644 index 0000000000..5df2e3aae2 --- /dev/null +++ b/client/src/Project/CurrentTabContent/ChatTab/Message/index.tsx @@ -0,0 +1,259 @@ +import { memo, useCallback, useContext, useEffect, useState } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { format } from 'date-fns'; +import { + ChatLoadingStep, + ChatMessageAuthor, + ParsedQueryType, +} from '../../../../types/general'; +import { EnvContext } from '../../../../context/envContext'; +import MarkdownWithCode from '../../../../components/MarkdownWithCode'; +import Button from '../../../../components/Button'; +import { + CheckListIcon, + LikeIcon, + PencilIcon, + UnlikeIcon, + WarningSignIcon, +} from '../../../../icons'; +import { getDateFnsLocale } from '../../../../utils'; +import SpinLoaderContainer from '../../../../components/Loaders/SpinnerLoader'; +import { + getPlainFromStorage, + LOADING_STEPS_SHOWN_KEY, + savePlainToStorage, +} from '../../../../services/storage'; +import { LocaleContext } from '../../../../context/localeContext'; +import { upvoteAnswer } from '../../../../services/api'; +import CopyButton from '../../../../components/MarkdownWithCode/CopyButton'; +import UserParsedQuery from './UserParsedQuery'; +import LoadingStep from './LoadingStep'; + +type Props = { + author: ChatMessageAuthor; + text: string; + parsedQuery?: ParsedQueryType[]; + error?: string; + threadId: string; + queryId: string; + responseTimestamp: string | null; + showInlineFeedback: boolean; + isLoading?: boolean; + loadingSteps?: ChatLoadingStep[]; + i: number; + onMessageEdit: (queryId: string, i: number) => void; + singleFileExplanation?: boolean; + side: 'left' | 'right'; + projectId: string; +}; + +const ConversationMessage = ({ + author, + text, + parsedQuery, + i, + queryId, + onMessageEdit, + singleFileExplanation, + threadId, + isLoading, + loadingSteps, + showInlineFeedback, + responseTimestamp, + error, + side, + projectId, +}: Props) => { + const { t } = useTranslation(); + const { envConfig } = useContext(EnvContext); + const { locale } = useContext(LocaleContext); + const [isUpvote, setIsUpvote] = useState(false); + const [isDownvote, setIsDownvote] = useState(false); + const [isLoadingStepsShown, setLoadingStepsShown] = useState( + getPlainFromStorage(LOADING_STEPS_SHOWN_KEY) + ? !!Number(getPlainFromStorage(LOADING_STEPS_SHOWN_KEY)) + : true, + ); + + useEffect(() => { + savePlainToStorage( + LOADING_STEPS_SHOWN_KEY, + isLoadingStepsShown ? '1' : '0', + ); + }, [isLoadingStepsShown]); + + const toggleStepsShown = useCallback(() => { + setLoadingStepsShown((prev) => !prev); + }, []); + + const handleEdit = useCallback(() => { + onMessageEdit(queryId, i); + }, [onMessageEdit, queryId, i]); + + const handleUpvote = useCallback(() => { + setIsUpvote(true); + setIsDownvote(false); + return upvoteAnswer(projectId, threadId, queryId, { type: 'positive' }); + }, [showInlineFeedback, envConfig.tracking_id, threadId, queryId, projectId]); + + const handleDownvote = useCallback(() => { + setIsUpvote(false); + setIsDownvote(true); + return upvoteAnswer(projectId, threadId, queryId, { + type: 'negative', + feedback: '', + }); + }, [showInlineFeedback, envConfig.tracking_id, threadId, queryId, projectId]); + + return ( +
    + {error ? ( +
    +
    + +
    +

    {error}

    +
    + ) : ( + <> +
    +
    + {author === ChatMessageAuthor.User ? ( + {t('avatar')} + ) : isLoading ? ( + + ) : ( + bloop + )} +
    + {(isUpvote || isDownvote) && ( +
    + {isUpvote ? ( + + ) : ( + + )} +
    + )} +
    +
    +
    + {author === ChatMessageAuthor.User ? You : 'bloop'} + {author === ChatMessageAuthor.Server && ( +

    + ·{' '} + {isLoading ? ( + Streaming response... + ) : responseTimestamp ? ( + format( + new Date(responseTimestamp), + 'hh:mm aa', + getDateFnsLocale(locale), + ) + ) : null} +

    + )} + {author === ChatMessageAuthor.Server && ( + + )} +
    + {!!loadingSteps?.length && ( +
    + {loadingSteps.map((s, i) => ( + + ))} +
    + )} +
    + {author === ChatMessageAuthor.Server ? ( + + ) : ( + + )} +
    +
    +
    + {author === ChatMessageAuthor.User ? ( + + ) : ( + !isLoading && ( + <> + + + + ) + )} + +
    + + )} +
    + ); +}; + +export default memo(ConversationMessage); diff --git a/client/src/Project/CurrentTabContent/ChatTab/ScrollableContent.tsx b/client/src/Project/CurrentTabContent/ChatTab/ScrollableContent.tsx new file mode 100644 index 0000000000..10507c02db --- /dev/null +++ b/client/src/Project/CurrentTabContent/ChatTab/ScrollableContent.tsx @@ -0,0 +1,90 @@ +import { Fragment, memo, useContext, useEffect } from 'react'; +import { Trans } from 'react-i18next'; +import { ChatMessageAuthor, ChatMessageServer } from '../../../types/general'; +import { WarningSignIcon } from '../../../icons'; +import { ChatContext } from '../../../context/chatsContext'; +import FunctionContext from '../../../components/ScrollToBottom/FunctionContext'; +import StarterMessage from './StarterMessage'; +import Message from './Message'; + +type Props = { + chatData: ChatContext; + side: 'left' | 'right'; + projectId: string; +}; + +const ScrollableContent = ({ chatData, side, projectId }: Props) => { + const { scrollToBottom } = useContext(FunctionContext); + + useEffect(() => { + if (chatData.submittedQuery.plain) { + scrollToBottom({ behavior: 'smooth' }); + } + }, [chatData.submittedQuery]); + + return ( + + + {(chatData.hideMessagesFrom === null + ? chatData.conversation + : chatData.conversation.slice(0, chatData.hideMessagesFrom + 1) + ).map((m, i) => ( + + ))} + {chatData.hideMessagesFrom !== null && ( +
    +
    + +
    +

    + + Editing previously submitted questions will discard all answers + and questions following it + +

    +
    + )} +
    + ); +}; + +export default memo(ScrollableContent); diff --git a/client/src/Project/CurrentTabContent/ChatTab/StarterMessage.tsx b/client/src/Project/CurrentTabContent/ChatTab/StarterMessage.tsx new file mode 100644 index 0000000000..f1cb45b8bb --- /dev/null +++ b/client/src/Project/CurrentTabContent/ChatTab/StarterMessage.tsx @@ -0,0 +1,100 @@ +import { memo, useCallback, useContext, useEffect, useState } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { ChatBubblesIcon } from '../../../icons'; +import { TutorialQuestionType } from '../../../types/api'; +import { getTutorialQuestions } from '../../../services/api'; +import { ProjectContext } from '../../../context/projectContext'; + +type Props = { + isEmptyConversation: boolean; + setInputValueImperatively: (v: string) => void; +}; + +const StarterMessage = ({ + isEmptyConversation, + setInputValueImperatively, +}: Props) => { + useTranslation(); + const [tutorials, setTutorials] = useState([]); + const { project } = useContext(ProjectContext.Current); + + const getDiverseTutorials = useCallback(async () => { + if (project?.repos.length) { + const tutorials = []; + let tutorialsPerRepo = Math.floor(10 / project.repos.length); + let remainingTutorials = 10; + + for (const repo of project.repos) { + const repoTutorials = await getTutorialQuestions(repo.repo.ref); + + const tutorialsToAdd = Math.min( + tutorialsPerRepo, + repoTutorials.questions.length, + remainingTutorials, + ); + + tutorials.push(...repoTutorials.questions.slice(0, tutorialsToAdd)); + + remainingTutorials -= tutorialsToAdd; + + if (remainingTutorials <= 0) { + break; + } + } + + setTutorials(tutorials); + } + }, [project?.repos]); + + useEffect(() => { + getDiverseTutorials(); + }, [getDiverseTutorials]); + + return ( +
    +
    + bloop +
    +
    +

    bloop

    +

    + + Hi, I am bloop! In{' '} + + + Chat mode + {' '} + I can answer any questions related to any of your repositories. + +

    + {isEmptyConversation && !!tutorials.length && ( +

    + + Below are a few suggestions you can ask me to get started: + +

    + )} + {isEmptyConversation && !!tutorials.length && ( +
    + {tutorials.map((t, i) => ( + + ))} +
    + )} +
    +
    + ); +}; + +export default memo(StarterMessage); diff --git a/client/src/Project/CurrentTabContent/ChatTab/index.tsx b/client/src/Project/CurrentTabContent/ChatTab/index.tsx new file mode 100644 index 0000000000..d223c000d1 --- /dev/null +++ b/client/src/Project/CurrentTabContent/ChatTab/index.tsx @@ -0,0 +1,141 @@ +import React, { + memo, + useCallback, + useContext, + useEffect, + useMemo, +} from 'react'; +import { useTranslation } from 'react-i18next'; +import Button from '../../../components/Button'; +import { + ChatBubblesIcon, + MoreHorizontalIcon, + SplitViewIcon, +} from '../../../icons'; +import Dropdown from '../../../components/Dropdown'; +import { checkEventKeys } from '../../../utils/keyboardUtils'; +import useKeyboardNavigation from '../../../hooks/useKeyboardNavigation'; +import { TabsContext } from '../../../context/tabsContext'; +import { ChatTabType } from '../../../types/general'; +import { ProjectContext } from '../../../context/projectContext'; +import { CommandBarContext } from '../../../context/commandBarContext'; +import { openInSplitViewShortcut } from '../../../consts/commandBar'; +import { UIContext } from '../../../context/uiContext'; +import Conversation from './Conversation'; +import ActionsDropdown from './ActionsDropdown'; + +type Props = ChatTabType & { + noBorder?: boolean; + side: 'left' | 'right'; + tabKey: string; + handleMoveToAnotherSide: () => void; +}; + +const ChatTab = ({ + noBorder, + side, + title, + conversationId, + tabKey, + handleMoveToAnotherSide, +}: Props) => { + const { t } = useTranslation(); + const { focusedPanel } = useContext(TabsContext.All); + const { closeTab } = useContext(TabsContext.Handlers); + const { isLeftSidebarFocused } = useContext(UIContext.Focus); + const { setFocusedTabItems } = useContext(CommandBarContext.Handlers); + const { project, refreshCurrentProjectConversations } = useContext( + ProjectContext.Current, + ); + + const dropdownComponentProps = useMemo(() => { + return { + handleMoveToAnotherSide, + conversationId, + projectId: project?.id, + tabKey, + closeTab, + refreshCurrentProjectConversations, + side, + }; + }, [ + handleMoveToAnotherSide, + conversationId, + closeTab, + project?.id, + tabKey, + refreshCurrentProjectConversations, + side, + ]); + + const handleKeyEvent = useCallback( + (e: KeyboardEvent) => { + if (checkEventKeys(e, ['cmd', ']'])) { + handleMoveToAnotherSide(); + } + }, + [handleMoveToAnotherSide], + ); + useKeyboardNavigation( + handleKeyEvent, + focusedPanel !== side || isLeftSidebarFocused, + ); + + useEffect(() => { + if (focusedPanel === side) { + setFocusedTabItems([ + { + label: t('Open in split view'), + Icon: SplitViewIcon, + id: 'split_view', + key: 'split_view', + onClick: handleMoveToAnotherSide, + closeOnClick: true, + shortcut: openInSplitViewShortcut, + footerHint: '', + footerBtns: [{ label: t('Move'), shortcut: ['entr'] }], + }, + ]); + } + }, [focusedPanel, side, handleMoveToAnotherSide]); + + return ( +
    +
    +
    + + {title || t('New chat')} +
    + {focusedPanel === side && ( + + + + )} +
    +
    + +
    +
    + ); +}; + +export default memo(ChatTab); diff --git a/client/src/Project/CurrentTabContent/DropTarget.tsx b/client/src/Project/CurrentTabContent/DropTarget.tsx new file mode 100644 index 0000000000..8a86901a90 --- /dev/null +++ b/client/src/Project/CurrentTabContent/DropTarget.tsx @@ -0,0 +1,53 @@ +import { memo } from 'react'; +import { useDrop } from 'react-dnd'; +import { Trans, useTranslation } from 'react-i18next'; +import { TabType } from '../../types/general'; +import { SplitViewIcon } from '../../icons'; + +type Props = { + onDrop: (t: TabType) => void; +}; + +const DropTarget = ({ onDrop }: Props) => { + useTranslation(); + const [{ isOver, canDrop }, drop] = useDrop( + () => ({ + accept: 'tab-left', + drop: (item: { t: TabType }, monitor) => { + onDrop(item.t); + }, + collect: (monitor) => { + return { + isOver: !!monitor.isOver(), + canDrop: !!monitor.canDrop(), + }; + }, + }), + [onDrop], + ); + + return ( +
    + {isOver && canDrop && ( +
    +
    +
    +
    + +

    + Release to open in split view +

    +
    +
    +
    + )} +
    + ); +}; + +export default memo(DropTarget); diff --git a/client/src/Project/CurrentTabContent/EmptyTab.tsx b/client/src/Project/CurrentTabContent/EmptyTab.tsx new file mode 100644 index 0000000000..b9dc87d1ef --- /dev/null +++ b/client/src/Project/CurrentTabContent/EmptyTab.tsx @@ -0,0 +1,37 @@ +import { memo } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import useShortcuts from '../../hooks/useShortcuts'; + +type Props = {}; + +const EmptyTab = ({}: Props) => { + useTranslation(); + const shortcut = useShortcuts(['cmd']); + return ( +
    +
    + bloop +
    +
    +

    + No file selected +

    +

    + Select a file or open a new tab to display it here.{' '} + + Press{' '} + + cmdKey + {' '} + + K + {' '} + on your keyboard to open the Command bar. + +

    +
    +
    + ); +}; + +export default memo(EmptyTab); diff --git a/client/src/Project/CurrentTabContent/FileTab/ActionsDropdown.tsx b/client/src/Project/CurrentTabContent/FileTab/ActionsDropdown.tsx new file mode 100644 index 0000000000..d885f0a85a --- /dev/null +++ b/client/src/Project/CurrentTabContent/FileTab/ActionsDropdown.tsx @@ -0,0 +1,39 @@ +import { memo, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import DropdownSection from '../../../components/Dropdown/Section'; +import SectionItem from '../../../components/Dropdown/Section/SectionItem'; +import { SplitViewIcon, FileWithSparksIcon } from '../../../icons'; +import { openInSplitViewShortcut } from '../../../consts/commandBar'; +import { explainFileShortcut } from './index'; + +type Props = { + handleExplain: () => void; + handleMoveToAnotherSide: () => void; +}; + +const ActionsDropdown = ({ handleExplain, handleMoveToAnotherSide }: Props) => { + const { t } = useTranslation(); + + return ( +
    + + } + /> + } + /> + +
    + ); +}; + +export default memo(ActionsDropdown); diff --git a/client/src/Project/CurrentTabContent/FileTab/index.tsx b/client/src/Project/CurrentTabContent/FileTab/index.tsx new file mode 100644 index 0000000000..2942e376d0 --- /dev/null +++ b/client/src/Project/CurrentTabContent/FileTab/index.tsx @@ -0,0 +1,306 @@ +import React, { + memo, + useCallback, + useContext, + useEffect, + useMemo, + useState, + useTransition, +} from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { + forceFileToBeIndexed, + getFileContent, + getHoverables, +} from '../../../services/api'; +import FileIcon from '../../../components/FileIcon'; +import Button from '../../../components/Button'; +import { + EyeCutIcon, + FileWithSparksIcon, + MoreHorizontalIcon, + SplitViewIcon, +} from '../../../icons'; +import { FileResponse } from '../../../types/api'; +import { mapRanges } from '../../../mappers/results'; +import { Range } from '../../../types/results'; +import CodeFull from '../../../components/Code/CodeFull'; +import IpynbRenderer from '../../../components/IpynbRenderer'; +import SpinLoaderContainer from '../../../components/Loaders/SpinnerLoader'; +import { FileTabType, SyncStatus, TabTypesEnum } from '../../../types/general'; +import { FileHighlightsContext } from '../../../context/fileHighlightsContext'; +import Dropdown from '../../../components/Dropdown'; +import { TabsContext } from '../../../context/tabsContext'; +import { checkEventKeys } from '../../../utils/keyboardUtils'; +import useKeyboardNavigation from '../../../hooks/useKeyboardNavigation'; +import { CommandBarContext } from '../../../context/commandBarContext'; +import { openInSplitViewShortcut } from '../../../consts/commandBar'; +import BreadcrumbsPathContainer from '../../../components/Breadcrumbs/PathContainer'; +import { RepositoriesContext } from '../../../context/repositoriesContext'; +import { UIContext } from '../../../context/uiContext'; +import ActionsDropdown from './ActionsDropdown'; + +type Props = { + tabKey: string; + repoRef: string; + path: string; + scrollToLine?: string; + tokenRange?: string; + noBorder?: boolean; + branch?: string | null; + side: 'left' | 'right'; + handleMoveToAnotherSide: () => void; +}; + +export const explainFileShortcut = ['cmd', 'E']; + +const FileTab = ({ + path, + noBorder, + repoRef, + scrollToLine, + branch, + side, + tokenRange, + handleMoveToAnotherSide, + tabKey, +}: Props) => { + const { t } = useTranslation(); + const [file, setFile] = useState(null); + const [hoverableRanges, setHoverableRanges] = useState< + Record | undefined + >(undefined); + const [indexRequested, setIndexRequested] = useState(false); + const [isFetched, setIsFetched] = useState(false); + const { setFocusedTabItems } = useContext(CommandBarContext.Handlers); + const [isPending, startTransition] = useTransition(); + const { openNewTab, updateTabProperty } = useContext(TabsContext.Handlers); + const { focusedPanel } = useContext(TabsContext.All); + const { isLeftSidebarFocused } = useContext(UIContext.Focus); + const { fileHighlights, hoveredLines } = useContext( + FileHighlightsContext.Values, + ); + const { indexingStatus } = useContext(RepositoriesContext); + + const highlights = useMemo(() => { + return fileHighlights[path]?.sort((a, b) => + a && b && a?.lines?.[1] - a?.lines?.[0] < b?.lines?.[1] - b?.lines?.[0] + ? -1 + : 1, + ); + }, [path, fileHighlights]); + + useEffect(() => { + setIndexRequested(false); + setIsFetched(false); + }, [path, repoRef]); + + const refetchFile = useCallback(async () => { + try { + const resp = await getFileContent(repoRef, path, branch); + if (!resp) { + setIsFetched(true); + return; + } + startTransition(() => { + setFile(resp); + setIsFetched(true); + }); + // if (item.indexed) { + const data = await getHoverables(path, repoRef, branch); + setHoverableRanges(mapRanges(data.ranges)); + // } + } catch (err) { + setIsFetched(true); + } + }, [repoRef, path, branch]); + + useEffect(() => { + refetchFile(); + }, [refetchFile]); + + useEffect(() => { + if (indexingStatus[repoRef]?.status === SyncStatus.Done) { + setTimeout(refetchFile, 2000); + } + }, [indexingStatus[repoRef]?.status]); + + const onIndexRequested = useCallback(async () => { + if (path) { + setIndexRequested(true); + await forceFileToBeIndexed(repoRef, path); + setTimeout(() => refetchFile(), 1000); + } + }, [repoRef, path]); + + const handleClick = useCallback(() => { + updateTabProperty(tabKey, 'isTemp', false, side); + }, [updateTabProperty, tabKey, side]); + + const linesNumber = useMemo(() => { + return file?.contents?.split(/\n(?!$)/g).length || 0; + }, [file?.contents]); + + const handleExplain = useCallback(() => { + openNewTab( + { + type: TabTypesEnum.CHAT, + initialQuery: { + path, + repoRef, + branch, + lines: [0, linesNumber - 1], + }, + }, + side === 'left' ? 'right' : 'left', + ); + }, [path, repoRef, branch, linesNumber, side, openNewTab]); + const handleKeyEvent = useCallback( + (e: KeyboardEvent) => { + if (checkEventKeys(e, explainFileShortcut)) { + handleExplain(); + } else if (checkEventKeys(e, openInSplitViewShortcut)) { + handleMoveToAnotherSide(); + } + }, + [handleExplain, handleMoveToAnotherSide], + ); + useKeyboardNavigation( + handleKeyEvent, + !file?.contents || focusedPanel !== side || isLeftSidebarFocused, + ); + + useEffect(() => { + if (focusedPanel === side && file?.contents) { + setFocusedTabItems([ + { + label: t('Explain file'), + Icon: FileWithSparksIcon, + id: 'explain_file', + key: 'explain_file', + onClick: handleExplain, + closeOnClick: true, + shortcut: explainFileShortcut, + footerHint: '', + footerBtns: [{ label: t('Explain'), shortcut: ['entr'] }], + }, + { + label: t('Open in split view'), + Icon: SplitViewIcon, + id: 'split_view', + key: 'split_view', + onClick: handleMoveToAnotherSide, + closeOnClick: true, + shortcut: openInSplitViewShortcut, + footerHint: '', + footerBtns: [{ label: t('Move'), shortcut: ['entr'] }], + }, + ]); + } + }, [ + focusedPanel, + side, + file?.contents, + handleExplain, + handleMoveToAnotherSide, + ]); + + const dropdownComponentProps = useMemo(() => { + return { + handleExplain, + handleMoveToAnotherSide, + }; + }, [handleExplain, handleMoveToAnotherSide]); + + return ( +
    +
    +
    + + +
    + {focusedPanel === side && ( + + + + )} +
    +
    + {file?.lang === 'jupyter notebook' ? ( + + ) : file ? ( + + {({ width, height }) => ( + + )} + + ) : isFetched && !file ? ( +
    +
    + +
    +
    +

    + File not indexed +

    +

    + + This might be because the file is too big or it has one of + bloop's excluded file types. + +

    +
    + {!indexRequested ? ( + + ) : ( +
    + +
    + )} +
    + ) : null} +
    +
    + ); +}; + +export default memo(FileTab); diff --git a/client/src/Project/CurrentTabContent/Header/AddTabButton.tsx b/client/src/Project/CurrentTabContent/Header/AddTabButton.tsx new file mode 100644 index 0000000000..f78e183073 --- /dev/null +++ b/client/src/Project/CurrentTabContent/Header/AddTabButton.tsx @@ -0,0 +1,64 @@ +import React, { memo, useCallback, useContext } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PlusSignIcon } from '../../../icons'; +import Button from '../../../components/Button'; +import { checkEventKeys } from '../../../utils/keyboardUtils'; +import { TabTypesEnum } from '../../../types/general'; +import useKeyboardNavigation from '../../../hooks/useKeyboardNavigation'; +import { TabsContext } from '../../../context/tabsContext'; + +type Props = { + tabsLength: number; + side: 'left' | 'right'; + focusedPanel: 'left' | 'right'; +}; + +const newTabShortcut = ['option', 'N']; + +const AddTabButton = ({ side, focusedPanel }: Props) => { + const { t } = useTranslation(); + const { openNewTab } = useContext(TabsContext.Handlers); + + // const dropdownComponentProps = useMemo(() => { + // return { side }; + // }, [side]); + + const openChatTab = useCallback(() => { + openNewTab({ type: TabTypesEnum.CHAT }, side); + }, [openNewTab, side]); + + const handleKeyEvent = useCallback( + (e: KeyboardEvent) => { + if (checkEventKeys(e, newTabShortcut)) { + e.stopPropagation(); + e.preventDefault(); + openChatTab(); + } + }, + [openNewTab], + ); + useKeyboardNavigation(handleKeyEvent, side !== focusedPanel); + + return ( + // 1 ? 'bottom-end' : 'bottom-start'} + // > + + // + ); +}; + +export default memo(AddTabButton); diff --git a/client/src/Project/CurrentTabContent/Header/AddTabDropdown.tsx b/client/src/Project/CurrentTabContent/Header/AddTabDropdown.tsx new file mode 100644 index 0000000000..eaf05b698b --- /dev/null +++ b/client/src/Project/CurrentTabContent/Header/AddTabDropdown.tsx @@ -0,0 +1,78 @@ +import React, { + memo, + useCallback, + useContext, + MouseEvent, + useMemo, +} from 'react'; +import { useTranslation } from 'react-i18next'; +import SectionItem from '../../../components/Dropdown/Section/SectionItem'; +import { ChatBubblesIcon, CodeStudioIcon } from '../../../icons'; +import { TabsContext } from '../../../context/tabsContext'; +import { TabTypesEnum } from '../../../types/general'; +import { checkEventKeys } from '../../../utils/keyboardUtils'; +import useKeyboardNavigation from '../../../hooks/useKeyboardNavigation'; + +type Props = { + side: 'left' | 'right'; +}; + +const AddTabDropdown = ({ side }: Props) => { + const { t } = useTranslation(); + const { openNewTab } = useContext(TabsContext.Handlers); + + const openChatTab = useCallback(() => { + openNewTab({ type: TabTypesEnum.CHAT }, side); + }, [openNewTab, side]); + + const shortcuts = useMemo(() => { + return { newChat: ['option', 'N'], newStudio: ['option', 'shift', 'N'] }; + }, []); + + const handleKeyEvent = useCallback( + (e: KeyboardEvent) => { + if (checkEventKeys(e, shortcuts.newChat)) { + e.stopPropagation(); + e.preventDefault(); + openNewTab({ type: TabTypesEnum.CHAT }); + } + }, + [openNewTab], + ); + useKeyboardNavigation(handleKeyEvent, side !== 'left'); + + const noPropagate = useCallback((e?: MouseEvent) => { + e?.stopPropagation(); + }, []); + + return ( +
    +
    + + } + label={t('New Chat')} + shortcut={shortcuts.newChat} + onClick={openChatTab} + /> + + } + label={t('New Code Studio')} + shortcut={shortcuts.newStudio} + onClick={noPropagate} + /> +
    +
    + ); +}; + +export default memo(AddTabDropdown); diff --git a/client/src/Project/CurrentTabContent/Header/TabButton.tsx b/client/src/Project/CurrentTabContent/Header/TabButton.tsx new file mode 100644 index 0000000000..a0d8895994 --- /dev/null +++ b/client/src/Project/CurrentTabContent/Header/TabButton.tsx @@ -0,0 +1,229 @@ +import React, { + memo, + MouseEvent, + useCallback, + useContext, + useRef, +} from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDrag, useDrop } from 'react-dnd'; +import { + DraggableTabItem, + TabType, + TabTypesEnum, +} from '../../../types/general'; +import FileIcon from '../../../components/FileIcon'; +import { splitPath } from '../../../utils'; +import Button from '../../../components/Button'; +import { ChatBubblesIcon, CloseSignIcon } from '../../../icons'; +import { TabsContext } from '../../../context/tabsContext'; + +type Props = TabType & { + tabKey: string; + isActive: boolean; + side: 'left' | 'right'; + isOnlyTab: boolean; + moveTab: ( + i: number, + j: number, + sourceSide: 'left' | 'right', + targetSide: 'left' | 'right', + ) => void; + i: number; + repoRef?: string; + path?: string; + title?: string; + branch?: string | null; + scrollToLine?: string; + tokenRange?: string; + focusedPanel: 'left' | 'right'; + isTemp?: boolean; +}; + +const closeTabShortcut = ['cmd', 'W']; + +const TabButton = ({ + isActive, + tabKey, + repoRef, + path, + type, + title, + side, + moveTab, + isOnlyTab, + i, + branch, + scrollToLine, + tokenRange, + focusedPanel, + isTemp, +}: Props) => { + const { t } = useTranslation(); + const { closeTab, setActiveLeftTab, setActiveRightTab, setFocusedPanel } = + useContext(TabsContext.Handlers); + const ref = useRef(null); + const [{ handlerId }, drop] = useDrop({ + accept: [`tab-left`, `tab-right`], + canDrop: (item: DraggableTabItem) => true, + collect(monitor) { + return { + handlerId: monitor.getHandlerId(), + }; + }, + hover(item: DraggableTabItem, monitor) { + if (!ref.current) { + return; + } + const dragIndex = item.index; + const hoverIndex = i; + const sourceSide = item.side as 'left' | 'right'; + const targetSide = side as 'left' | 'right'; + // Don't replace items with themselves + if (dragIndex === hoverIndex && sourceSide === targetSide) { + return; + } + // Determine rectangle on screen + const hoverBoundingRect = ref.current?.getBoundingClientRect(); + // Get vertical middle + const hoverMiddleX = + (hoverBoundingRect.right - hoverBoundingRect.left) / 2; + // Determine mouse position + const clientOffset = monitor.getClientOffset(); + // Get pixels to the left + const hoverClientX = (clientOffset?.x || 0) - hoverBoundingRect.left; + // Only perform the move when the mouse has crossed half of the items width + // When dragging left, only move when the cursor is below 50% + // When dragging right, only move when the cursor is above 50% + // Dragging left + if ( + dragIndex < hoverIndex && + hoverClientX < hoverMiddleX && + sourceSide === targetSide + ) { + return; + } + // Dragging right + if ( + dragIndex > hoverIndex && + hoverClientX > hoverMiddleX && + sourceSide === targetSide + ) { + return; + } + // Time to actually perform the action + moveTab(dragIndex, hoverIndex, sourceSide, targetSide); + // Note: we're mutating the monitor item here! + // Generally it's better to avoid mutations, + // but it's good here for the sake of performance + // to avoid expensive index searches. + item.index = hoverIndex; + if (sourceSide !== targetSide) { + item.side = targetSide; + } + }, + }); + const [{ isDragging }, drag] = useDrag({ + type: `tab-${side}`, + canDrag: side !== 'left' || !isOnlyTab, + item: (): DraggableTabItem => { + return { + id: tabKey, + index: i, + // @ts-ignore + t: { + key: tabKey, + repoRef: repoRef!, + path: path!, + type, + title, + branch, + scrollToLine, + tokenRange, + }, + side, + }; + }, + collect: (monitor) => ({ + isDragging: monitor.isDragging(), + }), + }); + drag(drop(ref)); + + const handleClose = useCallback( + (e: MouseEvent) => { + e.stopPropagation(); + closeTab(tabKey, side); + }, + [tabKey, side], + ); + + const handleClick = useCallback(() => { + const setAction = side === 'left' ? setActiveLeftTab : setActiveRightTab; + // @ts-ignore + setAction({ + path, + repoRef, + key: tabKey, + type, + title, + branch, + scrollToLine, + tokenRange, + }); + setFocusedPanel(side); + }, [path, repoRef, tabKey, side, branch, scrollToLine, tokenRange, title]); + + return ( + + {type === TabTypesEnum.FILE ? ( + + ) : ( + + )} +

    + {type === TabTypesEnum.FILE + ? splitPath(path).pop() + : title || t('New chat')} +

    + +
    + ); +}; + +export default memo(TabButton); diff --git a/client/src/Project/CurrentTabContent/Header/index.tsx b/client/src/Project/CurrentTabContent/Header/index.tsx new file mode 100644 index 0000000000..af024fa5fa --- /dev/null +++ b/client/src/Project/CurrentTabContent/Header/index.tsx @@ -0,0 +1,117 @@ +import React, { memo, useCallback, useContext, useMemo } from 'react'; +import HeaderRightPart from '../../../components/Header/HeaderRightPart'; +import { TabsContext } from '../../../context/tabsContext'; +import { TabType, TabTypesEnum } from '../../../types/general'; +import AddTabButton from './AddTabButton'; +import TabButton from './TabButton'; + +type Props = { + side: 'left' | 'right'; +}; + +const ProjectHeader = ({ side }: Props) => { + const { leftTabs, rightTabs, focusedPanel } = useContext(TabsContext.All); + const { tab } = useContext( + TabsContext[side === 'left' ? 'CurrentLeft' : 'CurrentRight'], + ); + const { setLeftTabs, setRightTabs, setActiveRightTab, setActiveLeftTab } = + useContext(TabsContext.Handlers); + const tabs = useMemo(() => { + return side === 'left' ? leftTabs : rightTabs; + }, [side, rightTabs, leftTabs]); + + const moveTab = useCallback( + ( + dragIndex: number, + hoverIndex: number, + sourceSide: 'left' | 'right', + targetSide: 'left' | 'right', + ) => { + if (sourceSide === targetSide) { + const action = side === 'left' ? setLeftTabs : setRightTabs; + action((prevTabs) => { + const newTabs = JSON.parse(JSON.stringify(prevTabs)); + newTabs.splice(dragIndex, 1); + const newTab = prevTabs[dragIndex]; + newTabs.splice( + hoverIndex, + 0, + newTab.type === TabTypesEnum.FILE && newTab.isTemp + ? { ...newTab, isTemp: false } + : newTab, + ); + return newTabs; + }); + } else { + const sourceAction = sourceSide === 'left' ? setLeftTabs : setRightTabs; + const sourceTabAction = + sourceSide === 'left' ? setActiveLeftTab : setActiveRightTab; + const targetAction = targetSide === 'left' ? setLeftTabs : setRightTabs; + const targetTabAction = + targetSide === 'left' ? setActiveLeftTab : setActiveRightTab; + + sourceAction((prevSourceTabs) => { + const newSourceTabs = JSON.parse(JSON.stringify(prevSourceTabs)); + const [movedTab] = newSourceTabs.splice(dragIndex, 1); + sourceTabAction( + newSourceTabs.length + ? newSourceTabs[dragIndex - 1] || newSourceTabs[0] + : null, + ); + + targetAction((prevTargetTabs) => { + const newTargetTabs = JSON.parse(JSON.stringify(prevTargetTabs)); + if (!newTargetTabs.find((t: TabType) => t.key === movedTab.key)) { + newTargetTabs.splice(hoverIndex, 0, movedTab); + } + targetTabAction(movedTab); + return newTargetTabs; + }); + + return newSourceTabs; + }); + } + }, + [side], + ); + + return ( +
    +
    + {tabs.map(({ key, ...t }, i) => ( + + ))} + {!!tabs.length &&
    } + +
    + {(side === 'right' || !rightTabs.length) && ( +
    + +
    + )} +
    + ); +}; + +export default memo(ProjectHeader); diff --git a/client/src/Project/CurrentTabContent/index.tsx b/client/src/Project/CurrentTabContent/index.tsx new file mode 100644 index 0000000000..88ba20755e --- /dev/null +++ b/client/src/Project/CurrentTabContent/index.tsx @@ -0,0 +1,112 @@ +import React, { memo, useCallback, useContext } from 'react'; +import { useDrop } from 'react-dnd'; +import { Trans } from 'react-i18next'; +import { TabsContext } from '../../context/tabsContext'; +import { DraggableTabItem, TabType, TabTypesEnum } from '../../types/general'; +import { SplitViewIcon } from '../../icons'; +import { UIContext } from '../../context/uiContext'; +import EmptyTab from './EmptyTab'; +import FileTab from './FileTab'; +import Header from './Header'; +import ChatTab from './ChatTab'; + +type Props = { + side: 'left' | 'right'; + onDrop: (t: TabType) => void; + moveToAnotherSide: (t: TabType) => void; + shouldStretch?: boolean; +}; + +const CurrentTabContent = ({ + side, + onDrop, + shouldStretch, + moveToAnotherSide, +}: Props) => { + const { tab } = useContext( + TabsContext[side === 'left' ? 'CurrentLeft' : 'CurrentRight'], + ); + const { setFocusedPanel } = useContext(TabsContext.Handlers); + const { setIsLeftSidebarFocused } = useContext(UIContext.Focus); + + const [{ isOver, canDrop }, drop] = useDrop( + () => ({ + accept: side === 'right' ? 'tab-left' : 'tab-right', + canDrop: (i: DraggableTabItem) => i.side !== side, + drop: (item: DraggableTabItem) => { + onDrop(item.t); + }, + collect: (monitor) => ({ + isOver: monitor.isOver(), + canDrop: monitor.canDrop(), + }), + }), + [onDrop], + ); + + const focusPanel = useCallback(() => { + setFocusedPanel(side); + setIsLeftSidebarFocused(false); + }, [side]); + + const handleMoveToAnotherSide = useCallback(() => { + if (tab) { + moveToAnotherSide(tab); + } + }, [moveToAnotherSide, tab]); + + return ( +
    +
    +
    + {tab?.type === TabTypesEnum.FILE ? ( + + ) : tab?.type === TabTypesEnum.CHAT ? ( + + ) : ( + + )} + {isOver && canDrop && ( +
    +
    +
    +
    +
    + +

    + Release to open in split view +

    +
    +
    +
    +
    + )} +
    +
    + ); +}; + +export default memo(CurrentTabContent); diff --git a/client/src/Project/EmptyProject.tsx b/client/src/Project/EmptyProject.tsx new file mode 100644 index 0000000000..1218ad5bf5 --- /dev/null +++ b/client/src/Project/EmptyProject.tsx @@ -0,0 +1,60 @@ +import React, { memo, useCallback, useContext } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import Header from '../components/Header'; +import { PlusSignIcon, ShapesIcon } from '../icons'; +import Button from '../components/Button'; +import { CommandBarContext } from '../context/commandBarContext'; +import { CommandBarStepEnum } from '../types/general'; +import useShortcuts from '../hooks/useShortcuts'; + +type Props = {}; + +const EmptyProject = ({}: Props) => { + useTranslation(); + const shortcut = useShortcuts(['cmd']); + const { setIsVisible, setChosenStep } = useContext( + CommandBarContext.Handlers, + ); + + const openCommandBar = useCallback(() => { + setChosenStep({ id: CommandBarStepEnum.MANAGE_REPOS }); + setIsVisible(true); + }, []); + + return ( +
    +
    +
    +
    +
    +
    + +
    +
    +

    + This project is empty +

    +

    + + Press{' '} + + cmdKey + {' '} + + K + {' '} + on your keyboard to open the Command bar and add a repository. + +

    +
    + +
    +
    +
    +
    + ); +}; + +export default memo(EmptyProject); diff --git a/client/src/Project/LeftSidebar/NavPanel/Conversations.tsx b/client/src/Project/LeftSidebar/NavPanel/Conversations.tsx new file mode 100644 index 0000000000..bf55777d39 --- /dev/null +++ b/client/src/Project/LeftSidebar/NavPanel/Conversations.tsx @@ -0,0 +1,127 @@ +import React, { + Dispatch, + memo, + SetStateAction, + useCallback, + useEffect, + useRef, + MouseEvent, + useContext, +} from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import Dropdown from '../../../components/Dropdown'; +import { + ArrowTriangleBottomIcon, + ChatBubblesIcon, + MoreHorizontalIcon, +} from '../../../icons'; +import Button from '../../../components/Button'; +import { ProjectContext } from '../../../context/projectContext'; +import ConversationsDropdown from './ConversationsDropdown'; +import ConversationEntry from './CoversationEntry'; + +type Props = { + setExpanded: Dispatch>; + isExpanded: boolean; + focusedIndex: string; + index: number; +}; + +const reactRoot = document.getElementById('root')!; + +const ConversationsNav = ({ + isExpanded, + setExpanded, + focusedIndex, + index, +}: Props) => { + const { t } = useTranslation(); + const { project } = useContext(ProjectContext.Current); + const containerRef = useRef(null); + + const toggleExpanded = useCallback(() => { + setExpanded((prev) => (prev === 0 ? -1 : 0)); + }, []); + + useEffect(() => { + if (isExpanded) { + // containerRef.current?.scrollIntoView({ block: 'nearest' }); + } + }, [isExpanded]); + + const noPropagate = useCallback((e?: MouseEvent) => { + e?.stopPropagation(); + }, []); + + useEffect(() => { + if (focusedIndex === index.toString() && containerRef.current) { + containerRef.current.scrollIntoView({ block: 'nearest' }); + } + }, [focusedIndex, index]); + + return ( +
    + + +

    + + Chat conversations + + {isExpanded && ( + + )} +

    + {isExpanded && ( +
    + + + +
    + )} +
    + {isExpanded && ( +
    + {project?.conversations.map((c, ci) => ( + + ))} +
    + )} +
    + ); +}; + +export default memo(ConversationsNav); diff --git a/client/src/Project/LeftSidebar/NavPanel/ConversationsDropdown.tsx b/client/src/Project/LeftSidebar/NavPanel/ConversationsDropdown.tsx new file mode 100644 index 0000000000..f6e0cd7061 --- /dev/null +++ b/client/src/Project/LeftSidebar/NavPanel/ConversationsDropdown.tsx @@ -0,0 +1,39 @@ +import { memo, useCallback, useContext } from 'react'; +import { useTranslation } from 'react-i18next'; +import DropdownSection from '../../../components/Dropdown/Section'; +import SectionItem from '../../../components/Dropdown/Section/SectionItem'; +import { TrashCanIcon } from '../../../icons'; +import { deleteConversation } from '../../../services/api'; +import { ProjectContext } from '../../../context/projectContext'; + +type Props = {}; + +const ConversationsDropdown = ({}: Props) => { + const { t } = useTranslation(); + const { project, refreshCurrentProjectConversations } = useContext( + ProjectContext.Current, + ); + + const handleRemoveAllConversations = useCallback(async () => { + if (project?.id && project.conversations.length) { + await Promise.allSettled( + project.conversations.map((c) => deleteConversation(project.id, c.id)), + ); + refreshCurrentProjectConversations(); + } + }, [project?.id, project?.conversations]); + + return ( +
    + + } + /> + +
    + ); +}; + +export default memo(ConversationsDropdown); diff --git a/client/src/Project/LeftSidebar/NavPanel/CoversationEntry.tsx b/client/src/Project/LeftSidebar/NavPanel/CoversationEntry.tsx new file mode 100644 index 0000000000..1586f043bd --- /dev/null +++ b/client/src/Project/LeftSidebar/NavPanel/CoversationEntry.tsx @@ -0,0 +1,36 @@ +import { memo, useCallback, useContext } from 'react'; +import { ConversationShortType } from '../../../types/api'; +import { TabsContext } from '../../../context/tabsContext'; +import { TabTypesEnum } from '../../../types/general'; + +type Props = ConversationShortType & { + index: string; + focusedIndex: string; +}; + +const ConversationEntry = ({ title, id, index, focusedIndex }: Props) => { + const { openNewTab } = useContext(TabsContext.Handlers); + + const handleClick = useCallback(() => { + openNewTab({ type: TabTypesEnum.CHAT, conversationId: id, title }); + }, [openNewTab, id, title]); + + return ( + + {title} + + ); +}; + +export default memo(ConversationEntry); diff --git a/client/src/Project/LeftSidebar/NavPanel/Repo.tsx b/client/src/Project/LeftSidebar/NavPanel/Repo.tsx new file mode 100644 index 0000000000..ce3c61bb95 --- /dev/null +++ b/client/src/Project/LeftSidebar/NavPanel/Repo.tsx @@ -0,0 +1,226 @@ +import React, { + Dispatch, + memo, + SetStateAction, + useCallback, + useEffect, + useMemo, + useRef, + useState, + MouseEvent, +} from 'react'; +import { useTranslation } from 'react-i18next'; +import { DirectoryEntry } from '../../../types/api'; +import { getFolderContent } from '../../../services/api'; +import { splitPath } from '../../../utils'; +import GitHubIcon from '../../../icons/GitHubIcon'; +import Dropdown from '../../../components/Dropdown'; +import { + ArrowTriangleBottomIcon, + HardDriveIcon, + MoreHorizontalIcon, +} from '../../../icons'; +import Button from '../../../components/Button'; +import { RepoIndexingStatusType, SyncStatus } from '../../../types/general'; +import SpinLoaderContainer from '../../../components/Loaders/SpinnerLoader'; +import Tooltip from '../../../components/Tooltip'; +import { repoStatusMap } from '../../../consts/general'; +import RepoEntry from './RepoEntry'; +import RepoDropdown from './RepoDropdown'; + +type Props = { + repoRef: string; + setExpanded: Dispatch>; + isExpanded: boolean; + i: number; + projectId: string; + lastIndex: string; + currentPath?: string; + branch: string; + allBranches: { name: string; last_commit_unix_secs: number }[]; + indexedBranches: string[]; + indexingData?: RepoIndexingStatusType; + focusedIndex: string; + index: number; +}; + +const reactRoot = document.getElementById('root')!; + +const RepoNav = ({ + repoRef, + i, + isExpanded, + setExpanded, + branch, + indexedBranches, + allBranches, + projectId, + lastIndex, + currentPath, + indexingData, + focusedIndex, + index, +}: Props) => { + const { t } = useTranslation(); + const [files, setFiles] = useState([]); + const containerRef = useRef(null); + + const fetchFiles = useCallback( + async (path?: string) => { + const resp = await getFolderContent(repoRef, path, branch); + if (!resp.entries) { + return []; + } + return resp?.entries.sort((a, b) => { + if ((a.entry_data === 'Directory') === (b.entry_data === 'Directory')) { + return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1; + } else { + return a.entry_data === 'Directory' ? -1 : 1; + } + }); + }, + [repoRef, branch, indexingData?.status], + ); + + const refetchParentFolder = useCallback(() => { + fetchFiles().then(setFiles); + }, [fetchFiles]); + + useEffect(() => { + refetchParentFolder(); + }, [refetchParentFolder]); + + const toggleExpanded = useCallback(() => { + setExpanded((prev) => (prev === i ? -1 : i)); + }, [i]); + + useEffect(() => { + if (isExpanded) { + // containerRef.current?.scrollIntoView({ block: 'nearest' }); + } + }, [isExpanded]); + + const dropdownComponentProps = useMemo(() => { + return { + key: repoRef, + projectId, + repoRef, + selectedBranch: branch, + indexedBranches, + allBranches, + }; + }, [projectId, repoRef, branch, indexedBranches, allBranches]); + + const noPropagate = useCallback((e?: MouseEvent) => { + e?.stopPropagation(); + }, []); + + const isIndexing = useMemo(() => { + if (!indexingData) { + return false; + } + return [ + SyncStatus.Indexing, + SyncStatus.Syncing, + SyncStatus.Queued, + ].includes(indexingData.status); + }, [indexingData]); + + useEffect(() => { + if (focusedIndex === index.toString() && containerRef.current) { + containerRef.current.scrollIntoView({ block: 'nearest' }); + } + }, [focusedIndex, index]); + + return ( +
    + + {isIndexing && indexingData ? ( + + + + ) : repoRef.startsWith('github.com') ? ( + + ) : ( + + )} +

    + {splitPath(repoRef).pop()} + {isExpanded && ( + <> + / + + {branch?.replace(/^origin\//, '')}{' '} + + + + )} +

    + {isExpanded && ( +
    + + + +
    + )} +
    + {isExpanded && ( +
    + {files.map((f, fi) => ( + + ))} +
    + )} +
    + ); +}; + +export default memo(RepoNav); diff --git a/client/src/Project/LeftSidebar/NavPanel/RepoDropdown.tsx b/client/src/Project/LeftSidebar/NavPanel/RepoDropdown.tsx new file mode 100644 index 0000000000..69e7be9941 --- /dev/null +++ b/client/src/Project/LeftSidebar/NavPanel/RepoDropdown.tsx @@ -0,0 +1,295 @@ +import { + memo, + useCallback, + useContext, + useState, + MouseEvent, + ChangeEvent, + useMemo, + useEffect, +} from 'react'; +import { useTranslation } from 'react-i18next'; +import DropdownSection from '../../../components/Dropdown/Section'; +import SectionItem from '../../../components/Dropdown/Section/SectionItem'; +import { + ArrowTriangleBottomIcon, + BranchIcon, + RefreshIcon, + TrashCanIcon, +} from '../../../icons'; +import { + changeRepoBranch, + indexRepoBranch, + removeRepoFromProject, + syncRepo, +} from '../../../services/api'; +import { DeviceContext } from '../../../context/deviceContext'; +import SpinLoaderContainer from '../../../components/Loaders/SpinnerLoader'; +import SectionLabel from '../../../components/Dropdown/Section/SectionLabel'; +import Button from '../../../components/Button'; +import { ProjectContext } from '../../../context/projectContext'; +import { PersonalQuotaContext } from '../../../context/personalQuotaContext'; +import { RepoIndexingStatusType } from '../../../types/general'; +import { RepositoriesContext } from '../../../context/repositoriesContext'; +import { UIContext } from '../../../context/uiContext'; + +type Props = { + repoRef: string; + projectId: string; + selectedBranch?: string | null; + allBranches: { name: string; last_commit_unix_secs: number }[]; + indexedBranches: string[]; + indexingStatus?: RepoIndexingStatusType; + handleClose: () => void; +}; + +const RepoDropdown = ({ + repoRef, + selectedBranch, + indexedBranches, + allBranches, + projectId, + indexingStatus, + handleClose, +}: Props) => { + const { t } = useTranslation(); + const [isBranchesOpen, setIsBranchesOpen] = useState(false); + const [isSyncing, setIsSyncing] = useState(false); + const [search, setSearch] = useState(''); + const [branchesToSync, setBranchesToSync] = useState([]); + const { isSelfServe } = useContext(DeviceContext); + const { refreshCurrentProjectRepos } = useContext(ProjectContext.Current); + const { isSubscribed } = useContext(PersonalQuotaContext.Values); + const { setIsUpgradeRequiredPopupOpen } = useContext( + UIContext.UpgradeRequiredPopup, + ); + + const onRepoSync = useCallback( + async (e?: MouseEvent) => { + e?.stopPropagation(); + await syncRepo(repoRef); + setIsSyncing(true); + }, + [repoRef], + ); + + const toggleBranches = useCallback((e?: MouseEvent) => { + e?.stopPropagation(); + setIsBranchesOpen((prev) => !prev); + }, []); + + const handleInputChange = useCallback((e: ChangeEvent) => { + setSearch(e.target.value); + }, []); + + const noPropagate = useCallback((e?: MouseEvent) => { + e?.stopPropagation(); + }, []); + + useEffect(() => { + setBranchesToSync((prevState) => + prevState.filter((p) => !indexedBranches.includes(p)), + ); + }, [indexedBranches]); + + const notSyncedBranches = useMemo(() => { + return [...allBranches] + .reverse() + .filter( + (b) => + !indexedBranches.includes(b.name) && !branchesToSync.includes(b.name), + ) + .map((b) => b.name); + }, [indexedBranches, allBranches, branchesToSync]); + + const handleRemoveFromProject = useCallback(async () => { + if (projectId) { + await removeRepoFromProject(projectId, repoRef); + refreshCurrentProjectRepos(); + } + }, [projectId, repoRef]); + + const indexedBranchesToShow = useMemo(() => { + if (!search) { + return indexedBranches; + } + return indexedBranches.filter((b) => + b + .replace(/^origin\//, '') + .toLowerCase() + .includes(search.toLowerCase()), + ); + }, [indexedBranches, search]); + + const indexingBranchesToShow = useMemo(() => { + if (!search) { + return branchesToSync; + } + return branchesToSync.filter((b) => + b + .replace(/^origin\//, '') + .toLowerCase() + .includes(search.toLowerCase()), + ); + }, [branchesToSync, search]); + + const notIndexedBranchesToShow = useMemo(() => { + if (!search) { + return notSyncedBranches; + } + return notSyncedBranches.filter((b) => + b + .replace(/^origin\//, '') + .toLowerCase() + .includes(search.toLowerCase()), + ); + }, [notSyncedBranches, search]); + + const switchToBranch = useCallback( + async (branch: string, e?: MouseEvent) => { + e?.stopPropagation(); + if (isSubscribed || isSelfServe) { + await changeRepoBranch(projectId, repoRef, branch); + refreshCurrentProjectRepos(); + } else { + setIsUpgradeRequiredPopupOpen(true); + handleClose(); + } + }, + [projectId, repoRef, isSubscribed, isSelfServe], + ); + + return ( +
    + + + ) : ( + + ) + } + /> + } + customRightElement={ + + {selectedBranch} + + + } + /> + +
    + + + + {!!indexedBranchesToShow.length && ( + + + {indexedBranchesToShow.map((b) => ( + switchToBranch(b, e)} + label={b.replace(/^origin\//, '')} + icon={} + key={b} + isSelected={selectedBranch === b} + /> + ))} + + )} + {!!indexingBranchesToShow.length && ( + + + {indexingBranchesToShow.map((b) => ( + } + label={b.replace(/^origin\//, '')} + key={b} + customRightElement={ + + {indexingStatus?.branch === b + ? indexingStatus?.percentage + '%' + : t('Queued...')} + + } + /> + ))} + + )} + {!!notIndexedBranchesToShow.length && ( + + + {notIndexedBranchesToShow.map((b) => ( + } + customRightElement={ + + } + /> + ))} + + )} +
    + + } + /> + +
    + ); +}; +const WithIndexingStatus = (props: Omit) => { + const { indexingStatus } = useContext(RepositoriesContext); + const repoIndexingStatus = useMemo(() => { + return indexingStatus[props.repoRef]; + }, [indexingStatus[props.repoRef]]); + + return ; +}; + +export default memo(WithIndexingStatus); diff --git a/client/src/Project/LeftSidebar/NavPanel/RepoEntry.tsx b/client/src/Project/LeftSidebar/NavPanel/RepoEntry.tsx new file mode 100644 index 0000000000..9b4415e921 --- /dev/null +++ b/client/src/Project/LeftSidebar/NavPanel/RepoEntry.tsx @@ -0,0 +1,231 @@ +import React, { + memo, + useCallback, + useContext, + useEffect, + useRef, + useState, +} from 'react'; +import { ChevronRightIcon, EyeCutIcon, FolderIcon } from '../../../icons'; +import FileIcon from '../../../components/FileIcon'; +import { DirectoryEntry } from '../../../types/api'; +import { TabsContext } from '../../../context/tabsContext'; +import { + RepoIndexingStatusType, + SyncStatus, + TabTypesEnum, +} from '../../../types/general'; +import SpinLoaderContainer from '../../../components/Loaders/SpinnerLoader'; +import { UIContext } from '../../../context/uiContext'; +import useKeyboardNavigation from '../../../hooks/useKeyboardNavigation'; + +type Props = { + name: string; + isDirectory: boolean; + level: number; + fullPath: string; + fetchFiles: (path: string) => Promise; + defaultOpen?: boolean; + indexed: boolean; + repoRef: string; + lastIndex: string; + currentPath?: string; + branch?: string | null; + indexingData?: RepoIndexingStatusType; + focusedIndex: string; + index: string; +}; + +const RepoEntry = ({ + name, + level, + isDirectory, + currentPath, + fullPath, + fetchFiles, + defaultOpen, + indexed, + repoRef, + lastIndex, + branch, + indexingData, + focusedIndex, + index, +}: Props) => { + const { openNewTab } = useContext(TabsContext.Handlers); + const { isLeftSidebarFocused } = useContext(UIContext.Focus); + const [isOpen, setOpen] = useState( + defaultOpen || (currentPath && currentPath.startsWith(fullPath)), + ); + const [subItems, setSubItems] = useState(null); + const ref = useRef(null); + const [isMounted, setIsMounted] = useState(false); + + const refetchFolderFiles = useCallback(() => { + fetchFiles(fullPath).then(setSubItems); + }, [fullPath, fetchFiles]); + + useEffect(() => { + if (indexingData?.status === SyncStatus.Done && isDirectory) { + refetchFolderFiles(); + } + }, [indexingData?.status]); + + useEffect(() => { + if (currentPath && currentPath.startsWith(fullPath)) { + setOpen(true); + } + }, [currentPath, fullPath]); + + useEffect(() => { + if ( + subItems?.length && + subItems.find( + (si) => si.entry_data !== 'Directory' && !si.entry_data.File.indexed, + ) && + isMounted + ) { + refetchFolderFiles(); + } else { + setIsMounted(true); + } + }, [lastIndex]); + + useEffect(() => { + if (isDirectory && isOpen) { + refetchFolderFiles(); + } + }, [isOpen, isDirectory, refetchFolderFiles]); + + const handleClick = useCallback(() => { + if (isDirectory) { + setOpen((prev) => !prev); + } else { + openNewTab({ + type: TabTypesEnum.FILE, + path: fullPath, + repoRef, + branch, + }); + } + }, [isDirectory, fullPath, openNewTab, repoRef, branch]); + + const handleKeyEvent = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Enter') { + handleClick(); + } + }, + [handleClick], + ); + useKeyboardNavigation(handleKeyEvent, focusedIndex !== index); + + useEffect(() => { + if (focusedIndex === index && ref.current) { + ref.current.scrollIntoView({ block: 'nearest' }); + } + }, [focusedIndex, index]); + + return ( +
    + + {isDirectory ? ( +
    + +
    + ) : null} + {isDirectory ? ( + + ) : !indexed ? ( + indexingData?.status === SyncStatus.Indexing ? ( + + ) : ( + + ) + ) : ( + + )} + {isDirectory ? name.slice(0, -1) : name} + {/*{!indexed && !indexRequested && (*/} + {/* */} + {/* Index*/} + {/* */} + {/*)}*/} + {/*{!indexed && indexRequested && isIndexing && (*/} + {/*
    */} + {/* */} + {/*
    */} + {/*)}*/} +
    + {isOpen && subItems?.length ? ( +
    +
    + {subItems.map((si, sii) => ( + + ))} +
    + ) : null} +
    + ); +}; + +export default memo(RepoEntry); diff --git a/client/src/Project/LeftSidebar/NavPanel/index.tsx b/client/src/Project/LeftSidebar/NavPanel/index.tsx new file mode 100644 index 0000000000..b9eb19b328 --- /dev/null +++ b/client/src/Project/LeftSidebar/NavPanel/index.tsx @@ -0,0 +1,91 @@ +import { memo, useContext, useEffect, useMemo, useState } from 'react'; +import { ProjectContext } from '../../../context/projectContext'; +import { TabTypesEnum } from '../../../types/general'; +import { TabsContext } from '../../../context/tabsContext'; +import { RepositoriesContext } from '../../../context/repositoriesContext'; +import RepoNav from './Repo'; +import ConversationsNav from './Conversations'; + +type Props = { + focusedIndex: string; +}; + +const NavPanel = ({ focusedIndex }: Props) => { + const [expanded, setExpanded] = useState(-1); + const { project } = useContext(ProjectContext.Current); + const { focusedPanel } = useContext(TabsContext.All); + const { tab: leftTab } = useContext(TabsContext.CurrentLeft); + const { tab: rightTab } = useContext(TabsContext.CurrentRight); + const { indexingStatus } = useContext(RepositoriesContext); + + const currentlyFocusedTab = useMemo(() => { + const focusedTab = focusedPanel === 'left' ? leftTab : rightTab; + if (focusedTab?.type === TabTypesEnum.FILE) { + return focusedTab; + } + return null; + }, [focusedPanel, leftTab, rightTab]); + + useEffect(() => { + if (project?.repos.length === 1) { + setExpanded(!!project?.conversations.length ? 1 : 0); + } + }, [project?.repos]); + + useEffect(() => { + if (currentlyFocusedTab?.repoRef) { + const repoIndex = project?.repos.findIndex( + (r) => r.repo.ref === currentlyFocusedTab.repoRef, + ); + if (repoIndex !== undefined && repoIndex > -1) { + setExpanded(repoIndex + (!!project?.conversations.length ? 1 : 0)); + } + } + }, [currentlyFocusedTab]); + + return ( +
    + {!!project?.conversations.length && ( + + )} + {project?.repos.map((r, i) => ( + + ))} +
    + ); +}; + +export default memo(NavPanel); diff --git a/client/src/Project/LeftSidebar/RegexSearchPanel/AutocompleteMenu.tsx b/client/src/Project/LeftSidebar/RegexSearchPanel/AutocompleteMenu.tsx new file mode 100644 index 0000000000..f795db35d6 --- /dev/null +++ b/client/src/Project/LeftSidebar/RegexSearchPanel/AutocompleteMenu.tsx @@ -0,0 +1,85 @@ +import React, { memo, useMemo } from 'react'; +import { Trans } from 'react-i18next'; +import { ResultItemType, SuggestionType } from '../../../types/results'; +import AutocompleteMenuItem from './AutocompleteMenuItem'; + +type Props = { + getMenuProps: () => any; + getItemProps: ({ + item, + index, + }: { + item: SuggestionType; + index: number; + }) => any; + isOpen: boolean; + options: SuggestionType[]; + highlightedIndex: number; +}; + +const AutocompleteMenu = ({ + getMenuProps, + isOpen, + options, + getItemProps, + highlightedIndex, +}: Props) => { + const queryOptions = useMemo( + () => + options.filter( + (o) => + o.type === ResultItemType.FLAG || + o.type === ResultItemType.LANG || + o.type === ResultItemType.FILE || + o.type === ResultItemType.REPO, + ), + [options], + ); + const resultOptions = useMemo( + () => options.filter((o) => o.type === ResultItemType.CODE), + [options], + ); + + return ( +
    +
      + {isOpen && ( + <> + {queryOptions.length ? ( + + Query suggestions + + ) : null} + {queryOptions.map((item, index) => ( + + ))} + {resultOptions.length ? ( + + Result suggestions + + ) : null} + {resultOptions.map((item, index) => ( + + ))} + + )} +
    +
    + ); +}; + +export default memo(AutocompleteMenu); diff --git a/client/src/Project/LeftSidebar/RegexSearchPanel/AutocompleteMenuItem.tsx b/client/src/Project/LeftSidebar/RegexSearchPanel/AutocompleteMenuItem.tsx new file mode 100644 index 0000000000..ae9f836b6e --- /dev/null +++ b/client/src/Project/LeftSidebar/RegexSearchPanel/AutocompleteMenuItem.tsx @@ -0,0 +1,100 @@ +import React, { memo, useEffect, useMemo, useRef } from 'react'; +import { Trans } from 'react-i18next'; +import { ResultItemType, SuggestionType } from '../../../types/results'; +import CodeBlockSearch from '../../../components/Code/CodeBlockSearch'; +import CodeResult from './Results/CodeResult'; + +type Props = { + item: SuggestionType; + index: number; + getItemProps: ({ + item, + index, + }: { + item: SuggestionType; + index: number; + }) => any; + isFocused: boolean; + isFirst: boolean; +}; + +const AutocompleteMenuItem = ({ + getItemProps, + item, + index, + isFocused, + isFirst, +}: Props) => { + const ref = useRef(null); + + useEffect(() => { + if (isFocused) { + ref.current?.scrollIntoView({ block: 'nearest' }); + } + }, [isFocused]); + + const snippets = useMemo(() => { + if (item.type === ResultItemType.CODE) { + return item.snippets?.slice(0, 1).map((s) => ({ + ...s, + line_range: { + start: s.lineStart || 0, + end: (s.lineStart || 0) + s.code.split('\n').length, + }, + highlights: s.highlights || [], + symbols: [], + data: s.code.split('\n').slice(0, 5).join('\n'), // don't render big snippets that have over 5 lines + })); + } + return []; + }, [item]); + + return ( +
  • + {item.type === ResultItemType.FLAG || + item.type === ResultItemType.LANG ? ( + {item.data} + ) : item.type === ResultItemType.CODE ? ( + + + + ) : item.type === ResultItemType.FILE ? ( + <> + {item.relativePath} + + File + + + ) : item.type === ResultItemType.REPO ? ( + <> + {item.repoName} + + Repository + + + ) : null} +
  • + ); +}; + +export default memo(AutocompleteMenuItem); diff --git a/client/src/components/CodeBlock/Code/index.tsx b/client/src/Project/LeftSidebar/RegexSearchPanel/Results/CodeLine.tsx similarity index 54% rename from client/src/components/CodeBlock/Code/index.tsx rename to client/src/Project/LeftSidebar/RegexSearchPanel/Results/CodeLine.tsx index b5ba0625f0..8468ad0808 100644 --- a/client/src/components/CodeBlock/Code/index.tsx +++ b/client/src/Project/LeftSidebar/RegexSearchPanel/Results/CodeLine.tsx @@ -1,45 +1,44 @@ -import React, { useEffect, useMemo } from 'react'; -import { getPrismLanguage, tokenizeCode } from '../../../utils/prism'; -import { - HighlightMap, - Range, - SnippetSymbol, - TokensLine, -} from '../../../types/results'; -import { Token } from '../../../types/prism'; -import CodeContainer from './CodeContainer'; +import React, { memo, useCallback, useContext, useMemo } from 'react'; +import { TabTypesEnum } from '../../../../types/general'; +import { CodeIcon } from '../../../../icons'; +import { TabsContext } from '../../../../context/tabsContext'; +import { getPrismLanguage, tokenizeCode } from '../../../../utils/prism'; +import { HighlightMap, Range, TokensLine } from '../../../../types/results'; +import { Token } from '../../../../types/prism'; +import CodeToken from '../../../../components/Code/CodeToken'; type Props = { + path: string; + repoRef: string; + lineStart: number; + lineEnd: number; code: string; language: string; - lineStart?: number; - highlights?: Range[]; - showLines?: boolean; - symbols?: SnippetSymbol[]; - onlySymbolLines?: boolean; - removePaddings?: boolean; - lineHoverEffect?: boolean; - isDiff?: boolean; - canWrap?: boolean; - highlightColor?: string; - onTokensLoaded?: () => void; + highlights: Range[]; }; -const Code = ({ +const noOp = () => {}; + +const CodeLine = ({ + path, + repoRef, + lineStart, + lineEnd, code, language, - lineStart = 0, - showLines = true, highlights, - symbols, - onlySymbolLines, - removePaddings, - lineHoverEffect, - highlightColor, - isDiff, - onTokensLoaded, - canWrap, }: Props) => { + const { openNewTab } = useContext(TabsContext.Handlers); + + const onClick = useCallback(() => { + openNewTab({ + type: TabTypesEnum.FILE, + path, + repoRef, + scrollToLine: `${lineStart}_${lineEnd}`, + }); + }, [path, lineEnd, lineStart, repoRef, openNewTab]); + const lang = useMemo( () => getPrismLanguage(language) || 'plaintext', [language], @@ -58,7 +57,7 @@ const Code = ({ const getMap = (tokens: Token[]): HighlightMap[] => { const highlightMaps: HighlightMap[] = []; - tokens.forEach((token, index) => { + tokens.forEach((token) => { highlightMaps.push(...getToken(token)); }); @@ -120,41 +119,41 @@ const Code = ({ .map((l): TokensLine => ({ tokens: l, lineNumber: null })); let currentLine = lineStart; for (let i = 0; i < lines.length; i++) { - if ( - isDiff && - (lines[i].tokens[0]?.token.content === '-' || - lines[i].tokens[1]?.token.content === '-') - ) { - continue; - } lines[i].lineNumber = currentLine + 1; currentLine++; } return lines; - }, [tokens, lineStart, isDiff]); + }, [tokens, lineStart]); - useEffect(() => { - if (tokensMap.length && onTokensLoaded) { - onTokensLoaded(); + const lineToRender = useMemo(() => { + const ltr = tokensMap.find((l) => !!l.tokens.find((t) => t.highlight)); + const firstHighlightIndex = ltr?.tokens.findIndex((t) => t.highlight) || 0; + if (ltr) { + ltr.tokens = ltr.tokens.slice(Math.max(firstHighlightIndex - 2, 0)); } + return ltr; }, [tokensMap]); return ( - +
  • + +

    + {(lineToRender || tokensMap[0]).tokens.map((token, index) => ( + + ))} +

    +
  • ); }; -export default Code; +export default memo(CodeLine); diff --git a/client/src/Project/LeftSidebar/RegexSearchPanel/Results/CodeResult.tsx b/client/src/Project/LeftSidebar/RegexSearchPanel/Results/CodeResult.tsx new file mode 100644 index 0000000000..8594b2c141 --- /dev/null +++ b/client/src/Project/LeftSidebar/RegexSearchPanel/Results/CodeResult.tsx @@ -0,0 +1,124 @@ +import { + memo, + useCallback, + useContext, + useEffect, + useRef, + useState, +} from 'react'; +import { SnippetItem } from '../../../../types/api'; +import { ChevronRightIcon } from '../../../../icons'; +import FileIcon from '../../../../components/FileIcon'; +import { TabsContext } from '../../../../context/tabsContext'; +import { TabTypesEnum } from '../../../../types/general'; +import useKeyboardNavigation from '../../../../hooks/useKeyboardNavigation'; +import { UIContext } from '../../../../context/uiContext'; +import CodeLine from './CodeLine'; + +type Props = { + relative_path: string; + repo_ref: string; + lang: string; + snippets: SnippetItem[]; + index: string; + focusedIndex: string; + isFirst: boolean; +}; + +const CodeResult = ({ + relative_path, + repo_ref, + lang, + snippets, + index, + focusedIndex, + isFirst, +}: Props) => { + const { openNewTab } = useContext(TabsContext.Handlers); + const { isLeftSidebarFocused } = useContext(UIContext.Focus); + const ref = useRef(null); + const [isExpanded, setIsExpanded] = useState(true); + const toggleExpanded = useCallback(() => { + setIsExpanded((prev) => !prev); + }, []); + + useEffect(() => { + if (focusedIndex === index) { + ref.current?.scrollIntoView({ block: 'nearest' }); + } + }, [focusedIndex, index]); + + const handleKeyEvent = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Enter') { + e.stopPropagation(); + e.preventDefault(); + openNewTab({ + type: TabTypesEnum.FILE, + path: relative_path, + repoRef: repo_ref, + scrollToLine: `${snippets[0].line_range.start}_${snippets[0].line_range.end}`, + }); + } + }, + [repo_ref, relative_path, openNewTab], + ); + useKeyboardNavigation( + handleKeyEvent, + focusedIndex !== index || !isLeftSidebarFocused, + ); + + const handleClick = useCallback(() => { + openNewTab({ + type: TabTypesEnum.FILE, + path: relative_path, + repoRef: repo_ref, + }); + }, [repo_ref, relative_path, openNewTab]); + + return ( +
    + +
    + + + {/**/} +
    {relative_path}
    +
    +
      + {isExpanded + ? snippets.map((s, i) => ( + + )) + : null} +
    +
    + ); +}; + +export default memo(CodeResult); diff --git a/client/src/Project/LeftSidebar/RegexSearchPanel/Results/FileResult.tsx b/client/src/Project/LeftSidebar/RegexSearchPanel/Results/FileResult.tsx new file mode 100644 index 0000000000..500a430eeb --- /dev/null +++ b/client/src/Project/LeftSidebar/RegexSearchPanel/Results/FileResult.tsx @@ -0,0 +1,156 @@ +import React, { + memo, + useCallback, + useContext, + useEffect, + useRef, + useState, +} from 'react'; +import FileIcon from '../../../../components/FileIcon'; +import { TabTypesEnum } from '../../../../types/general'; +import { TabsContext } from '../../../../context/tabsContext'; +import { DirectoryEntry, RepoFileNameItem } from '../../../../types/api'; +import { FolderIcon } from '../../../../icons'; +import useKeyboardNavigation from '../../../../hooks/useKeyboardNavigation'; +import { UIContext } from '../../../../context/uiContext'; +import { getFolderContent } from '../../../../services/api'; +import RepoEntry from '../../NavPanel/RepoEntry'; + +type Props = { + relative_path: RepoFileNameItem; + repo_ref: string; + is_dir: boolean; + index: string; + focusedIndex: string; + isFirst: boolean; +}; + +const FileResult = ({ + relative_path, + repo_ref, + is_dir, + index, + focusedIndex, + isFirst, +}: Props) => { + const { openNewTab } = useContext(TabsContext.Handlers); + const { isLeftSidebarFocused } = useContext(UIContext.Focus); + const ref = useRef(null); + const [isExpanded, setIsExpanded] = useState(false); + const [files, setFiles] = useState([]); + + useEffect(() => { + if (focusedIndex === index) { + ref.current?.scrollIntoView({ block: 'nearest' }); + } + }, [focusedIndex, index]); + + const fetchFiles = useCallback( + async (path?: string) => { + const resp = await getFolderContent(repo_ref, path); + if (!resp.entries) { + return []; + } + return resp?.entries.sort((a, b) => { + if ((a.entry_data === 'Directory') === (b.entry_data === 'Directory')) { + return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1; + } else { + return a.entry_data === 'Directory' ? -1 : 1; + } + }); + }, + [repo_ref], + ); + + useEffect(() => { + if (isExpanded && !files.length) { + fetchFiles(relative_path.text).then(setFiles); + } + }, [fetchFiles, files, isExpanded, relative_path.text]); + + const handleClick = useCallback(() => { + if (is_dir) { + setIsExpanded((prev) => !prev); + } else { + openNewTab({ + type: TabTypesEnum.FILE, + path: relative_path.text, + repoRef: repo_ref, + }); + } + }, [relative_path, repo_ref, is_dir, openNewTab]); + + const handleKeyEvent = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Enter') { + e.stopPropagation(); + e.preventDefault(); + handleClick(); + } + }, + [handleClick], + ); + useKeyboardNavigation( + handleKeyEvent, + focusedIndex !== index || !isLeftSidebarFocused, + ); + + return ( + + + {is_dir ? ( + + ) : ( + + )} + {/**/} + + {relative_path.text} + + + {isExpanded && ( +
    + {files.map((f, fi) => ( + + ))} +
    + )} +
    + ); +}; + +export default memo(FileResult); diff --git a/client/src/Project/LeftSidebar/RegexSearchPanel/Results/RepoResult.tsx b/client/src/Project/LeftSidebar/RegexSearchPanel/Results/RepoResult.tsx new file mode 100644 index 0000000000..79bf033aae --- /dev/null +++ b/client/src/Project/LeftSidebar/RegexSearchPanel/Results/RepoResult.tsx @@ -0,0 +1,125 @@ +import React, { + memo, + useCallback, + useContext, + useEffect, + useState, +} from 'react'; +import GitHubIcon from '../../../../icons/GitHubIcon'; +import { HardDriveIcon } from '../../../../icons'; +import { splitPath } from '../../../../utils'; +import { DirectoryEntry } from '../../../../types/api'; +import { getFolderContent } from '../../../../services/api'; +import RepoEntry from '../../NavPanel/RepoEntry'; +import useKeyboardNavigation from '../../../../hooks/useKeyboardNavigation'; +import { UIContext } from '../../../../context/uiContext'; + +type Props = { + repoRef: string; + index: number; + isExpandable?: boolean; + focusedIndex: string; +}; + +const RepoResult = ({ repoRef, isExpandable, index, focusedIndex }: Props) => { + const { isLeftSidebarFocused } = useContext(UIContext.Focus); + const [isExpanded, setIsExpanded] = useState(false); + const [files, setFiles] = useState([]); + + const fetchFiles = useCallback( + async (path?: string) => { + const resp = await getFolderContent(repoRef, path); + if (!resp.entries) { + return []; + } + return resp?.entries.sort((a, b) => { + if ((a.entry_data === 'Directory') === (b.entry_data === 'Directory')) { + return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1; + } else { + return a.entry_data === 'Directory' ? -1 : 1; + } + }); + }, + [repoRef], + ); + + useEffect(() => { + if (isExpanded && !files.length) { + fetchFiles().then(setFiles); + } + }, [fetchFiles, files, isExpanded]); + + const onClick = useCallback(() => { + if (isExpandable) { + setIsExpanded((prev) => !prev); + } + }, [isExpandable]); + + const handleKeyEvent = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Enter') { + e.stopPropagation(); + e.preventDefault(); + onClick(); + } + }, + [onClick], + ); + useKeyboardNavigation( + handleKeyEvent, + focusedIndex !== index.toString() || !isExpandable || !isLeftSidebarFocused, + ); + + return ( + + + {repoRef.startsWith('github.com/') ? ( + + ) : ( + + )} + {splitPath(repoRef) + .slice(repoRef.startsWith('github.com/') ? -2 : -1) + .join('/')} + + {isExpanded && ( +
    + {files.map((f, fi) => ( + + ))} +
    + )} +
    + ); +}; + +export default memo(RepoResult); diff --git a/client/src/Project/LeftSidebar/RegexSearchPanel/index.tsx b/client/src/Project/LeftSidebar/RegexSearchPanel/index.tsx new file mode 100644 index 0000000000..20f76524de --- /dev/null +++ b/client/src/Project/LeftSidebar/RegexSearchPanel/index.tsx @@ -0,0 +1,284 @@ +import React, { + ChangeEvent, + FormEvent, + memo, + useCallback, + useContext, + useEffect, + useRef, + useState, +} from 'react'; +import { useTranslation } from 'react-i18next'; +import { CloseSignIcon, RegexSearchIcon } from '../../../icons'; +import Button from '../../../components/Button'; +import useKeyboardNavigation from '../../../hooks/useKeyboardNavigation'; +import { search } from '../../../services/api'; +import { + CodeItem, + DirectoryItem, + FileItem, + FileResItem, + RepoItem, +} from '../../../types/api'; +import { ProjectContext } from '../../../context/projectContext'; +import { CommandBarContext } from '../../../context/commandBarContext'; +import { regexToggleShortcut } from '../../../consts/shortcuts'; +import { UIContext } from '../../../context/uiContext'; +import CodeResult from './Results/CodeResult'; +import RepoResult from './Results/RepoResult'; +import FileResult from './Results/FileResult'; + +type Props = { + projectId?: string; + isRegexEnabled?: boolean; + focusedIndex: string; + setFocusedIndex: (i: number) => void; +}; + +// const getAutocompleteThrottled = throttle( +// async ( +// query: string, +// setOptions: (o: SuggestionType[]) => void, +// ): Promise => { +// const newOptions = await getAutocomplete(query); +// setOptions(mapResults(newOptions)); +// }, +// 100, +// { trailing: true, leading: false }, +// ); + +type ResultType = CodeItem | RepoItem | FileResItem | DirectoryItem | FileItem; + +const RegexSearchPanel = ({ + projectId, + isRegexEnabled, + focusedIndex, + setFocusedIndex, +}: Props) => { + const { t } = useTranslation(); + // const { openNewTab } = useContext(TabsContext.Handlers); + const [inputValue, setInputValue] = useState(''); + // const [options, setOptions] = useState([]); + const [results, setResults] = useState>({}); + const [resultsRaw, setResultsRaw] = useState([]); + const inputRef = useRef(null); + const { setIsRegexSearchEnabled } = useContext(ProjectContext.RegexSearch); + const { isVisible } = useContext(CommandBarContext.General); + const { isLeftSidebarFocused, setIsLeftSidebarFocused } = useContext( + UIContext.Focus, + ); + + const onChange = useCallback((e: ChangeEvent) => { + setInputValue(e.target.value); + setFocusedIndex(0); + }, []); + + const onClear = useCallback(() => { + setInputValue(''); + if (!inputValue) { + setIsRegexSearchEnabled(false); + } + }, [inputValue]); + + // const { + // isOpen, + // getMenuProps, + // getInputProps, + // getItemProps, + // closeMenu, + // highlightedIndex, + // } = useCombobox({ + // inputValue, + // onStateChange: async (state) => { + // if ( + // state.type === useCombobox.stateChangeTypes.ItemClick || + // state.type === useCombobox.stateChangeTypes.InputKeyDownEnter + // ) { + // if (state.selectedItem?.type === ResultItemType.FLAG) { + // const words = inputValue.split(' '); + // words[words.length - 1] = + // state.selectedItem?.data || words[words.length - 1]; + // const newInputValue = words.join(' ') + ':'; + // setInputValue(newInputValue); + // } else if (state.selectedItem?.type === ResultItemType.LANG) { + // setInputValue( + // (prev) => + // prev.split(':').slice(0, -1).join(':') + + // ':' + + // (state.selectedItem as LangResult)?.data, + // ); + // } else { + // if ( + // state.selectedItem?.type === ResultItemType.FILE || + // state.selectedItem?.type === ResultItemType.CODE + // ) { + // openNewTab({ + // type: TabTypesEnum.FILE, + // branch: null, + // repoRef: state.selectedItem.repoRef, + // path: state.selectedItem.relativePath, + // }); + // } + // } + // inputRef.current?.focus(); + // } else if (state.type === useCombobox.stateChangeTypes.InputChange) { + // if (state.inputValue === '') { + // setInputValue(state.inputValue); + // setOptions([]); + // return; + // } + // if (!state.inputValue) { + // return; + // } + // let autocompleteQuery = state.inputValue; + // getAutocompleteThrottled(autocompleteQuery, setOptions); + // } + // }, + // items: options, + // itemToString(item) { + // return ( + // (item?.type === ResultItemType.FLAG || + // item?.type === ResultItemType.LANG + // ? item?.data + // : '') || '' + // ); + // }, + // }); + + useEffect(() => { + if (focusedIndex === 'input') { + inputRef.current?.focus(); + } + }, [focusedIndex]); + + const onSubmit = useCallback( + async (e: FormEvent) => { + e.preventDefault(); + if (focusedIndex === 'input') { + if (projectId) { + const data = await search(projectId, inputValue); + const newResults: Record = {}; + data.data.forEach((d) => { + if (!newResults[d.data.repo_ref]) { + newResults[d.data.repo_ref] = [d]; + } else { + newResults[d.data.repo_ref].push(d); + } + }); + setResults(newResults); + setResultsRaw(data.data); + // closeMenu(); + } + } + }, + [inputValue, focusedIndex], + ); + + const handleKeyEvent = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Escape') { + e.preventDefault(); + e.stopPropagation(); + if (focusedIndex === 'input') { + onClear(); + } else { + setFocusedIndex(0); + } + } + }, + [focusedIndex, onClear], + ); + useKeyboardNavigation( + handleKeyEvent, + isVisible || !isRegexEnabled || !isLeftSidebarFocused, + ); + + useEffect(() => { + if (isRegexEnabled) { + setIsLeftSidebarFocused(true); + } + }, [isRegexEnabled]); + + return !isRegexEnabled ? null : ( +
    + + + + + + {/**/} + {!!Object.keys(results).length && ( +
      + {Object.keys(results).map((repoRef, repoIndex) => ( +
    • + + +
        + {results[repoRef].map((r, i) => ( +
      • + {r.kind === 'snippets' ? ( + + ) : r.kind === 'file_result' ? ( + + ) : null} +
      • + ))} +
      +
    • + ))} +
    + )} +
    + ); +}; + +export default memo(RegexSearchPanel); diff --git a/client/src/Project/LeftSidebar/index.tsx b/client/src/Project/LeftSidebar/index.tsx new file mode 100644 index 0000000000..6079465301 --- /dev/null +++ b/client/src/Project/LeftSidebar/index.tsx @@ -0,0 +1,127 @@ +import React, { + memo, + useCallback, + useContext, + MouseEvent, + useRef, + useState, + useEffect, +} from 'react'; +import useResizeableWidth from '../../hooks/useResizeableWidth'; +import { LEFT_SIDEBAR_WIDTH_KEY } from '../../services/storage'; +import ProjectsDropdown from '../../components/Header/ProjectsDropdown'; +import { ChevronDownIcon } from '../../icons'; +import Dropdown from '../../components/Dropdown'; +import { DeviceContext } from '../../context/deviceContext'; +import { ProjectContext } from '../../context/projectContext'; +import { UIContext } from '../../context/uiContext'; +import { checkEventKeys } from '../../utils/keyboardUtils'; +import useKeyboardNavigation from '../../hooks/useKeyboardNavigation'; +import RegexSearchPanel from './RegexSearchPanel'; +import NavPanel from './NavPanel'; + +type Props = {}; + +const LeftSidebar = ({}: Props) => { + const { os } = useContext(DeviceContext); + const { project } = useContext(ProjectContext.Current); + const { isRegexSearchEnabled } = useContext(ProjectContext.RegexSearch); + const { setIsLeftSidebarFocused, isLeftSidebarFocused } = useContext( + UIContext.Focus, + ); + const ref = useRef(null); + const [focusedIndex, setFocusedIndex] = useState(-1); + const [focusedIndexFull, setFocusedIndexFull] = useState(''); + + const { panelRef, dividerRef } = useResizeableWidth( + true, + LEFT_SIDEBAR_WIDTH_KEY, + 20, + 40, + ); + + const handleKeyEvent = useCallback( + (e: KeyboardEvent) => { + if (checkEventKeys(e, ['cmd', '0'])) { + e.preventDefault(); + e.stopPropagation(); + setIsLeftSidebarFocused(true); + } + if (isLeftSidebarFocused) { + if (ref.current) { + if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { + e.preventDefault(); + e.stopPropagation(); + const nodes = ref.current.querySelectorAll('[data-node-index]'); + setFocusedIndex((prev) => { + return e.key === 'ArrowDown' + ? prev < nodes.length - 1 + ? prev + 1 + : 0 + : prev > 0 + ? prev - 1 + : nodes.length - 1; + }); + } + } + } + }, + [isLeftSidebarFocused], + ); + useKeyboardNavigation(handleKeyEvent); + + useEffect(() => { + if (ref.current) { + const nodes = ref.current.querySelectorAll('[data-node-index]'); + setFocusedIndexFull( + (nodes[focusedIndex] as HTMLElement)?.dataset?.nodeIndex || '', + ); + } + }, [focusedIndex]); + + const handleClick = useCallback((e: MouseEvent) => { + e.stopPropagation(); + setIsLeftSidebarFocused(true); + }, []); + + return ( +
    +
    + {os.type === 'Darwin' ? : ''} + +
    +

    + {project?.name || 'Default project'} +

    + +
    +
    +
    +
    + + {!isRegexSearchEnabled && } +
    +
    +
    +
    +
    + ); +}; + +export default memo(LeftSidebar); diff --git a/client/src/Project/RightTab.tsx b/client/src/Project/RightTab.tsx new file mode 100644 index 0000000000..b6ba7ed0e5 --- /dev/null +++ b/client/src/Project/RightTab.tsx @@ -0,0 +1,38 @@ +import React, { memo } from 'react'; +import useResizeableWidth from '../hooks/useResizeableWidth'; +import { RIGHT_SIDEBAR_WIDTH_KEY } from '../services/storage'; +import { TabType } from '../types/general'; +import CurrentTabContent from './CurrentTabContent'; + +type Props = { + onDropToRight: (tab: TabType) => void; + moveToAnotherSide: (tab: TabType) => void; +}; + +const RightTab = ({ onDropToRight, moveToAnotherSide }: Props) => { + const { panelRef, dividerRef } = useResizeableWidth( + false, + RIGHT_SIDEBAR_WIDTH_KEY, + 40, + 60, + 15, + ); + + return ( +
    +
    +
    +
    + +
    + ); +}; + +export default memo(RightTab); diff --git a/client/src/Project/index.tsx b/client/src/Project/index.tsx new file mode 100644 index 0000000000..69618a4323 --- /dev/null +++ b/client/src/Project/index.tsx @@ -0,0 +1,134 @@ +import React, { memo, useCallback, useContext } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDragLayer } from 'react-dnd'; +import { ProjectContext } from '../context/projectContext'; +import { TabsContext } from '../context/tabsContext'; +import { TabType, TabTypesEnum } from '../types/general'; +import ChatsContextProvider from '../context/providers/ChatsContextProvider'; +import { UIContext } from '../context/uiContext'; +import { checkEventKeys } from '../utils/keyboardUtils'; +import useKeyboardNavigation from '../hooks/useKeyboardNavigation'; +import LeftSidebar from './LeftSidebar'; +import CurrentTabContent from './CurrentTabContent'; +import EmptyProject from './EmptyProject'; +import DropTarget from './CurrentTabContent/DropTarget'; +import RightTab from './RightTab'; +import ChatPersistentState from './CurrentTabContent/ChatTab/ChatPersistentState'; + +type Props = {}; + +const Project = ({}: Props) => { + useTranslation(); + const { project } = useContext(ProjectContext.Current); + const { rightTabs, leftTabs } = useContext(TabsContext.All); + const { setIsLeftSidebarFocused } = useContext(UIContext.Focus); + const { + setActiveRightTab, + setActiveLeftTab, + setLeftTabs, + setFocusedPanel, + setRightTabs, + } = useContext(TabsContext.Handlers); + const { isDragging } = useDragLayer((monitor) => ({ + isDragging: monitor.isDragging(), + })); + + const onDropToRight = useCallback((tab: TabType) => { + setRightTabs((prev) => + prev.find((t) => t.key === tab.key) + ? prev + : [ + ...prev, + tab.type === TabTypesEnum.FILE && tab.isTemp + ? { ...tab, isTemp: false } + : tab, + ], + ); + setLeftTabs((prev) => { + const newTabs = prev.filter((s) => s.key !== tab.key); + setActiveLeftTab(newTabs[newTabs.length - 1]); + return newTabs; + }); + setActiveRightTab(tab); + setFocusedPanel('right'); + }, []); + + const onDropToLeft = useCallback((tab: TabType) => { + setLeftTabs((prev) => + prev.find((t) => t.key === tab.key) + ? prev + : [ + ...prev, + tab.type === TabTypesEnum.FILE && tab.isTemp + ? { ...tab, isTemp: false } + : tab, + ], + ); + setRightTabs((prev) => { + const newTabs = prev.filter((s) => s.key !== tab.key); + setActiveRightTab(newTabs[newTabs.length - 1]); + return newTabs; + }); + setActiveLeftTab(tab); + setFocusedPanel('left'); + }, []); + + const handleKeyEvent = useCallback( + (e: KeyboardEvent) => { + if (checkEventKeys(e, ['cmd', '1'])) { + e.preventDefault(); + e.stopPropagation(); + setFocusedPanel('left'); + setIsLeftSidebarFocused(false); + } else if (checkEventKeys(e, ['cmd', '2']) && rightTabs.length) { + e.preventDefault(); + e.stopPropagation(); + setFocusedPanel('right'); + setIsLeftSidebarFocused(false); + } + }, + [rightTabs.length], + ); + useKeyboardNavigation(handleKeyEvent); + + return !project?.repos?.length ? ( + + ) : ( +
    + + +
    + + {!rightTabs.length && isDragging ? ( + + ) : null} +
    + {!!rightTabs.length && ( + + )} + {[...leftTabs, ...rightTabs].map((t, i) => + t.type === TabTypesEnum.CHAT ? ( + + ) : null, + )} +
    +
    + ); +}; + +export default memo(Project); diff --git a/client/src/ProjectSettings/General.tsx b/client/src/ProjectSettings/General.tsx new file mode 100644 index 0000000000..f2963e8d3e --- /dev/null +++ b/client/src/ProjectSettings/General.tsx @@ -0,0 +1,96 @@ +import React, { + ChangeEvent, + memo, + useCallback, + useContext, + useEffect, + useState, +} from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import TextInput from '../components/TextInput'; +import { ProjectContext } from '../context/projectContext'; +import Button from '../components/Button'; +import { deleteProject, updateProject } from '../services/api'; +import { UIContext } from '../context/uiContext'; +import Dropdown from '../components/Dropdown'; +import { ChevronDownIcon, RunIcon, WalkIcon } from '../icons'; +import AnswerSpeedDropdown from '../Settings/General/AnswerSpeedDropdown'; + +type Props = {}; + +const General = ({}: Props) => { + const { t } = useTranslation(); + const { project, refreshCurrentProject, setCurrentProjectId } = useContext( + ProjectContext.Current, + ); + const { refreshAllProjects, projects } = useContext(ProjectContext.All); + const { setProjectSettingsOpen } = useContext(UIContext.ProjectSettings); + const [name, setName] = useState(project?.name || ''); + + useEffect(() => { + setName(project?.name || ''); + }, [project?.name]); + + const handleChange = useCallback((e: ChangeEvent) => { + setName(e.target.value); + }, []); + + const handleSubmit = useCallback(async () => { + if (project?.id && name) { + await updateProject(project?.id, { name }); + refreshCurrentProject(); + } + }, [project?.id, name, refreshCurrentProject]); + + const handleDelete = useCallback(async () => { + if (project?.id) { + await deleteProject(project?.id); + if (projects.length > 1) { + setCurrentProjectId( + projects.find((p) => p.id !== project.id)?.id || '', + ); + } + refreshAllProjects(); + refreshCurrentProject(); + setProjectSettingsOpen(false); + } + }, [project?.id, projects, refreshCurrentProject]); + + return ( +
    +
    +

    + General +

    +

    + Manage your general project settings +

    +
    +
    +
    + +
    +
    +
    +

    + Permanently delete{' '} + {project?.name} and + remove all the data associated to it. Repositories will remain + accessible in your GitHub account. +

    + +
    +
    + ); +}; + +export default memo(General); diff --git a/client/src/ProjectSettings/index.tsx b/client/src/ProjectSettings/index.tsx new file mode 100644 index 0000000000..ac0404547c --- /dev/null +++ b/client/src/ProjectSettings/index.tsx @@ -0,0 +1,60 @@ +import React, { memo, useCallback, useContext } from 'react'; +import { useTranslation } from 'react-i18next'; +import { UIContext } from '../context/uiContext'; +import Header from '../components/Header'; +import useKeyboardNavigation from '../hooks/useKeyboardNavigation'; +import { ProjectSettingSections } from '../types/general'; +import SectionsNav from '../components/SectionsNav'; +import { ShapesIcon } from '../icons'; +import General from './General'; + +type Props = {}; + +const ProjectSettings = ({}: Props) => { + const { t } = useTranslation(); + const { + isProjectSettingsOpen, + setProjectSettingsOpen, + projectSettingsSection, + setProjectSettingsSection, + } = useContext(UIContext.ProjectSettings); + + const handleKeyEvent = useCallback((e: KeyboardEvent) => { + if (e.key === 'Escape') { + e.preventDefault(); + e.stopPropagation(); + setProjectSettingsOpen(false); + } + }, []); + useKeyboardNavigation(handleKeyEvent, !isProjectSettingsOpen); + + return isProjectSettingsOpen ? ( +
    +
    +
    + + activeItem={projectSettingsSection} + sections={[ + { + title: t('Project'), + Icon: ShapesIcon, + items: [ + { + type: ProjectSettingSections.GENERAL, + onClick: setProjectSettingsSection, + label: t('General'), + }, + ], + }, + ]} + /> + {projectSettingsSection === ProjectSettingSections.GENERAL ? ( + + ) : null} +
    +
    +
    + ) : null; +}; + +export default memo(ProjectSettings); diff --git a/client/src/Settings/General/AnswerSpeedDropdown.tsx b/client/src/Settings/General/AnswerSpeedDropdown.tsx new file mode 100644 index 0000000000..8b9c192dfd --- /dev/null +++ b/client/src/Settings/General/AnswerSpeedDropdown.tsx @@ -0,0 +1,39 @@ +import { memo, useContext } from 'react'; +import { useTranslation } from 'react-i18next'; +import DropdownSection from '../../components/Dropdown/Section'; +import SectionItem from '../../components/Dropdown/Section/SectionItem'; +import { ProjectContext } from '../../context/projectContext'; +import { RunIcon, WalkIcon } from '../../icons'; + +type Props = {}; + +const AnswerSpeedDropdown = ({}: Props) => { + const { t } = useTranslation(); + const { preferredAnswerSpeed, setPreferredAnswerSpeed } = useContext( + ProjectContext.AnswerSpeed, + ); + return ( +
    + + setPreferredAnswerSpeed('normal')} + icon={} + description={t('Recommended: The classic response type')} + /> + + + setPreferredAnswerSpeed('fast')} + icon={} + description={t('Experimental: Faster but less accurate')} + /> + +
    + ); +}; + +export default memo(AnswerSpeedDropdown); diff --git a/client/src/Settings/General/index.tsx b/client/src/Settings/General/index.tsx new file mode 100644 index 0000000000..805e762d25 --- /dev/null +++ b/client/src/Settings/General/index.tsx @@ -0,0 +1,132 @@ +import React, { + ChangeEvent, + memo, + useCallback, + useContext, + useMemo, + useState, +} from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import TextInput from '../../components/TextInput'; +import { + getJsonFromStorage, + saveJsonToStorage, + USER_DATA_FORM, +} from '../../services/storage'; +import { EMAIL_REGEX } from '../../consts/validations'; +import Dropdown from '../../components/Dropdown'; +import Button from '../../components/Button'; +import { ChevronDownIcon, RunIcon, WalkIcon } from '../../icons'; +import { ProjectContext } from '../../context/projectContext'; +import AnswerSpeedDropdown from './AnswerSpeedDropdown'; + +type Props = {}; + +type Form = { + firstName: string; + lastName: string; + email: string; + emailError?: string; +}; + +const GeneralSettings = ({}: Props) => { + const { t } = useTranslation(); + const { preferredAnswerSpeed } = useContext(ProjectContext.AnswerSpeed); + const savedForm: Form | null = useMemo( + () => getJsonFromStorage(USER_DATA_FORM), + [], + ); + const [form, setForm] = useState
    ({ + firstName: savedForm?.firstName || '', + lastName: savedForm?.lastName || '', + email: savedForm?.email || '', + emailError: '', + }); + + const onChange = useCallback((e: ChangeEvent) => { + setForm((prev) => { + const newForm = { + ...prev, + [e.target.name]: e.target.value, + emailError: e.target.name === 'email' ? '' : prev.emailError, + }; + saveJsonToStorage(USER_DATA_FORM, newForm); + return newForm; + }); + }, []); + + return ( +
    +
    +

    + General +

    +

    + Manage your general account settings +

    +
    +
    +
    + + +
    +
    +
    + { + if (!EMAIL_REGEX.test(form.email)) { + setForm((prev) => ({ + ...prev, + emailError: t('Email is not valid'), + })); + } + }} + error={form.emailError} + /> +
    +
    +
    +
    +

    + Answer speed +

    +

    + How fast or precise bloop's answers will be. +

    +
    + + + +
    +
    + ); +}; + +export default memo(GeneralSettings); diff --git a/client/src/Settings/Preferences/LanguageDropdown.tsx b/client/src/Settings/Preferences/LanguageDropdown.tsx new file mode 100644 index 0000000000..6994a68536 --- /dev/null +++ b/client/src/Settings/Preferences/LanguageDropdown.tsx @@ -0,0 +1,32 @@ +import { memo, useContext } from 'react'; +import { useTranslation } from 'react-i18next'; +import SectionItem from '../../components/Dropdown/Section/SectionItem'; +import { MacintoshIcon } from '../../icons'; +import { LocaleContext } from '../../context/localeContext'; +import { localesMap } from '../../consts/general'; +import { LocaleType } from '../../types/general'; + +type Props = {}; + +const LanguageDropdown = ({}: Props) => { + const { t } = useTranslation(); + const { locale, setLocale } = useContext(LocaleContext); + + return ( +
    +
    + {(Object.keys(localesMap) as LocaleType[]).map((k) => ( + setLocale(k)} + label={localesMap[k].name} + icon={{localesMap[k].icon}} + /> + ))} +
    +
    + ); +}; + +export default memo(LanguageDropdown); diff --git a/client/src/Settings/Preferences/ThemeDropdown.tsx b/client/src/Settings/Preferences/ThemeDropdown.tsx new file mode 100644 index 0000000000..988ec25b1b --- /dev/null +++ b/client/src/Settings/Preferences/ThemeDropdown.tsx @@ -0,0 +1,52 @@ +import { memo, useContext } from 'react'; +import { useTranslation } from 'react-i18next'; +import SectionItem from '../../components/Dropdown/Section/SectionItem'; +import { + MacintoshIcon, + ThemeBlackIcon, + ThemeDarkIcon, + ThemeLightIcon, +} from '../../icons'; +import { UIContext } from '../../context/uiContext'; + +type Props = {}; + +const ThemeDropdown = ({}: Props) => { + const { t } = useTranslation(); + const { theme, setTheme } = useContext(UIContext.Theme); + + return ( +
    +
    + setTheme('system')} + label={t('System preferences')} + icon={} + /> +
    +
    + setTheme('light')} + label={t('Light')} + icon={} + /> + setTheme('dark')} + label={t('Dark')} + icon={} + /> + setTheme('black')} + label={t('Black')} + icon={} + /> +
    +
    + ); +}; + +export default memo(ThemeDropdown); diff --git a/client/src/Settings/Preferences/index.tsx b/client/src/Settings/Preferences/index.tsx new file mode 100644 index 0000000000..2d2f221efd --- /dev/null +++ b/client/src/Settings/Preferences/index.tsx @@ -0,0 +1,89 @@ +import React, { memo, useContext } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import Dropdown from '../../components/Dropdown'; +import Button from '../../components/Button'; +import { + ChevronDownIcon, + MacintoshIcon, + ThemeBlackIcon, + ThemeDarkIcon, + ThemeLightIcon, +} from '../../icons'; +import { UIContext } from '../../context/uiContext'; +import { localesMap, themesMap } from '../../consts/general'; +import { LocaleContext } from '../../context/localeContext'; +import ThemeDropdown from './ThemeDropdown'; +import LanguageDropdown from './LanguageDropdown'; + +type Props = {}; + +export const themeIconsMap = { + light: , + dark: , + black: , + system: , +}; +const Preferences = ({}: Props) => { + useTranslation(); + const { theme } = useContext(UIContext.Theme); + const { locale } = useContext(LocaleContext); + + return ( +
    +
    +

    + Preferences +

    +

    + Manage your preferences +

    +
    +
    +
    +
    +

    + Theme +

    +

    + Select the interface colour scheme +

    +
    + + + +
    +
    +
    +
    +

    + Language +

    +

    + Select the interface language +

    +
    + + + +
    +
    + ); +}; + +export default memo(Preferences); diff --git a/client/src/Settings/Subscription/BenefitItem.tsx b/client/src/Settings/Subscription/BenefitItem.tsx new file mode 100644 index 0000000000..40df778c5d --- /dev/null +++ b/client/src/Settings/Subscription/BenefitItem.tsx @@ -0,0 +1,17 @@ +import { memo } from 'react'; +import { CheckIcon } from '../../icons'; + +type Props = { + text: string; +}; + +const BenefitItem = ({ text }: Props) => { + return ( +
    + +

    {text}

    +
    + ); +}; + +export default memo(BenefitItem); diff --git a/client/src/Settings/Subscription/CardFree.tsx b/client/src/Settings/Subscription/CardFree.tsx new file mode 100644 index 0000000000..400ab4b646 --- /dev/null +++ b/client/src/Settings/Subscription/CardFree.tsx @@ -0,0 +1,50 @@ +import React, { memo } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import Badge from '../../components/Badge'; +import Button from '../../components/Button'; +import LiteLoaderContainer from '../../components/Loaders/LiteLoader'; +import BenefitItem from './BenefitItem'; + +type Props = { + isActive: boolean; + isFetchingLink: boolean; + onManage: () => void; +}; + +const CardFree = ({ isActive, onManage, isFetchingLink }: Props) => { + const { t } = useTranslation(); + return ( +
    +
    +

    + Individual +

    +

    + Free +

    +
    +
    + + + + + +
    + {isActive ? ( +
    + +
    + ) : ( + + )} +
    + ); +}; + +export default memo(CardFree); diff --git a/client/src/Settings/Subscription/CardPaid.tsx b/client/src/Settings/Subscription/CardPaid.tsx new file mode 100644 index 0000000000..b467eb8663 --- /dev/null +++ b/client/src/Settings/Subscription/CardPaid.tsx @@ -0,0 +1,76 @@ +import React, { + memo, + useCallback, + useContext, + useEffect, + useRef, + useState, +} from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import Button from '../../components/Button'; +import Badge from '../../components/Badge'; +import { getSubscriptionLink } from '../../services/api'; +import { polling } from '../../utils/requestUtils'; +import { DeviceContext } from '../../context/deviceContext'; +import { UIContext } from '../../context/uiContext'; +import { PersonalQuotaContext } from '../../context/personalQuotaContext'; +import LiteLoaderContainer from '../../components/Loaders/LiteLoader'; +import BenefitItem from './BenefitItem'; +import Confetti from './Confetti'; + +type Props = { + isActive: boolean; + onUpgrade: () => void; + hasUpgraded: boolean; + isFetchingLink: boolean; +}; + +const CardPaid = ({ + isActive, + hasUpgraded, + onUpgrade, + isFetchingLink, +}: Props) => { + const { t } = useTranslation(); + + return ( +
    + {hasUpgraded && } +
    +

    + Personal +

    +

    + $20{' '} + + / billed monthly + +

    +
    +
    + + + + + + + +
    + {isActive ? ( +
    + +
    + ) : ( + + )} +
    + ); +}; + +export default memo(CardPaid); diff --git a/client/src/Settings/Subscription/Confetti.tsx b/client/src/Settings/Subscription/Confetti.tsx new file mode 100644 index 0000000000..0802489683 --- /dev/null +++ b/client/src/Settings/Subscription/Confetti.tsx @@ -0,0 +1,154 @@ +import { memo } from 'react'; + +type Props = {}; + +const Confetti = ({}: Props) => { + return ( +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + ); +}; + +export default memo(Confetti); diff --git a/client/src/Settings/Subscription/index.tsx b/client/src/Settings/Subscription/index.tsx new file mode 100644 index 0000000000..50c338a16d --- /dev/null +++ b/client/src/Settings/Subscription/index.tsx @@ -0,0 +1,201 @@ +import React, { + memo, + useCallback, + useContext, + useEffect, + useRef, + useState, +} from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { PersonalQuotaContext } from '../../context/personalQuotaContext'; +import { getSubscriptionLink } from '../../services/api'; +import { DeviceContext } from '../../context/deviceContext'; +import { UIContext } from '../../context/uiContext'; +import LiteLoaderContainer from '../../components/Loaders/LiteLoader'; +import { polling } from '../../utils/requestUtils'; +import Button from '../../components/Button'; +import Badge from '../../components/Badge'; +import SpinLoaderContainer from '../../components/Loaders/SpinnerLoader'; +import CardFree from './CardFree'; +import CardPaid from './CardPaid'; + +type Props = {}; + +const SubscriptionSettings = ({}: Props) => { + useTranslation(); + const [isFetchingLink, setIsFetchingLink] = useState(false); + const [isUpgradeRequested, setIsUpgradeRequested] = useState(false); + const { isSubscribed } = useContext(PersonalQuotaContext.Values); + const { refetchQuota } = useContext(PersonalQuotaContext.Handlers); + const { openLink } = useContext(DeviceContext); + const { setBugReportModalOpen } = useContext(UIContext.BugReport); + const [hasUpgraded, setHasUpgraded] = useState(false); + const [hasChecked, setHasChecked] = useState(false); + const intervalId = useRef(0); + + const handleUpgrade = useCallback(() => { + setIsFetchingLink(true); + getSubscriptionLink() + .then((resp) => { + if (resp.url) { + openLink(resp.url); + clearInterval(intervalId.current); + if (!isSubscribed) { + setIsUpgradeRequested(true); + intervalId.current = polling(() => refetchQuota(), 2000); + setTimeout(() => clearInterval(intervalId.current), 10 * 60 * 1000); + } + } else { + setBugReportModalOpen(true); + } + }) + .catch(() => { + setBugReportModalOpen(true); + }) + .finally(() => setIsFetchingLink(false)); + }, [openLink, isSubscribed]); + + useEffect(() => { + if (!hasUpgraded && isSubscribed && isUpgradeRequested) { + clearInterval(intervalId.current); + setHasUpgraded(true); + setIsUpgradeRequested(false); + } + }, [isSubscribed, hasUpgraded, isUpgradeRequested]); + + const handleCancel = useCallback(() => { + setIsUpgradeRequested(false); + clearInterval(intervalId.current); + }, []); + + const onManualOpenClick = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + getSubscriptionLink() + .then((resp) => { + if (resp.url) { + openLink(resp.url); + } else { + setBugReportModalOpen(true); + } + }) + .catch(() => { + setBugReportModalOpen(true); + }); + }, + [openLink], + ); + + const checkStatus = useCallback(() => { + refetchQuota().then(() => setHasChecked(true)); + }, []); + + return ( +
    +
    +
    +

    + {isUpgradeRequested ? ( + Upgrade to Personal plan + ) : ( + Plans + )} +

    +

    + {isUpgradeRequested ? ( + $20 / billed monthly + ) : ( + Manage your subscription plan + )} +

    +
    + {isUpgradeRequested && ( + + )} +
    +
    + {isUpgradeRequested ? ( +
    + +
    +

    + Complete your transaction in Stripe... +

    +

    + + We've redirected you to Stripe to complete your + transaction.{' '} + + Launch manually + {' '} + if it didn't work. + +

    +
    + {hasChecked && ( + + )} + +
    + ) : ( + <> +
    + + +
    +
    +
    +
    + Stripe +
    +

    + + All payments, invoices and billing information are managed in + Stripe. + +

    + {isSubscribed && ( + + )} +
    + + )} +
    + ); +}; + +export default memo(SubscriptionSettings); diff --git a/client/src/Settings/index.tsx b/client/src/Settings/index.tsx new file mode 100644 index 0000000000..9098e72f98 --- /dev/null +++ b/client/src/Settings/index.tsx @@ -0,0 +1,80 @@ +import React, { memo, useCallback, useContext, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { UIContext } from '../context/uiContext'; +import Header from '../components/Header'; +import useKeyboardNavigation from '../hooks/useKeyboardNavigation'; +import { SettingSections } from '../types/general'; +import SectionsNav from '../components/SectionsNav'; +import { CogIcon } from '../icons'; +import General from './General'; +import Preferences from './Preferences'; +import SubscriptionSettings from './Subscription'; + +type Props = {}; + +const Settings = ({}: Props) => { + const { t } = useTranslation(); + const { + isSettingsOpen, + setSettingsOpen, + settingsSection, + setSettingsSection, + } = useContext(UIContext.Settings); + + const handleKeyEvent = useCallback((e: KeyboardEvent) => { + if (e.key === 'Escape') { + e.preventDefault(); + e.stopPropagation(); + setSettingsOpen(false); + } + }, []); + useKeyboardNavigation(handleKeyEvent, !isSettingsOpen); + + const settingsSections = useMemo(() => { + return [ + { + title: t('Account settings'), + Icon: CogIcon, + items: [ + { + type: SettingSections.GENERAL, + label: t('General'), + onClick: setSettingsSection, + }, + { + type: SettingSections.PREFERENCES, + label: t('Preferences'), + onClick: setSettingsSection, + }, + { + type: SettingSections.SUBSCRIPTION, + label: t('Subscription'), + onClick: setSettingsSection, + }, + ], + }, + ]; + }, [t]); + + return isSettingsOpen ? ( +
    +
    +
    + + sections={settingsSections} + activeItem={settingsSection} + /> + {settingsSection === SettingSections.GENERAL ? ( + + ) : settingsSection === SettingSections.PREFERENCES ? ( + + ) : ( + + )} +
    +
    +
    + ) : null; +}; + +export default memo(Settings); diff --git a/client/src/circleProgress.css b/client/src/circleProgress.css deleted file mode 100644 index b5c1e1b6cf..0000000000 --- a/client/src/circleProgress.css +++ /dev/null @@ -1,234 +0,0 @@ -.progress-circle { - font-size: 4px; - position: relative; /* so that children can be absolutely positioned */ - padding: 0; - width: 5em; - height: 5em; - background-color: var(--bg-shade); - border-radius: 50%; - line-height: 5em; -} - -.progress-circle:after{ - border: none; - position: absolute; - top: 0.35em; - left: 0.35em; - text-align: center; - display: block; - border-radius: 50%; - width: 4.3em; - height: 4.3em; - background-color: var(--bg-sub); - content: " "; -} -/* Text inside the control */ -.progress-circle span { - position: absolute; - line-height: 5em; - width: 5em; - text-align: center; - display: block; - color: var(--label-base); - z-index: 2; -} -.left-half-clipper { - /* a round circle */ - border-radius: 50%; - width: 5em; - height: 5em; - position: absolute; /* needed for clipping */ - clip: rect(0, 5em, 5em, 2.5em); /* clips the whole left half*/ -} -/* when p>50, don't clip left half*/ -.progress-circle.over50 .left-half-clipper { - clip: rect(auto,auto,auto,auto); -} -.value-bar { - /*This is an overlayed square, that is made round with the border radius, - then it is cut to display only the left half, then rotated clockwise - to escape the outer clipping path.*/ - position: absolute; /*needed for clipping*/ - clip: rect(0, 2.5em, 5em, 0); - width: 5em; - height: 5em; - border-radius: 50%; - border: 0.45em solid var(--label-base); /*The border is 0.35 but making it larger removes visual artifacts */ - /*background-color: #4D642D;*/ /* for debug */ - box-sizing: border-box; - -} -/* Progress bar filling the whole right half for values above 50% */ -.progress-circle.over50 .first50-bar { - /*Progress bar for the first 50%, filling the whole right half*/ - position: absolute; /*needed for clipping*/ - clip: rect(0, 5em, 5em, 2.5em); - background-color: var(--label-base); - border-radius: 50%; - width: 5em; - height: 5em; -} -.progress-circle:not(.over50) .first50-bar{ display: none; } - - -/* Progress bar rotation position */ -.progress-circle.p0 .value-bar { display: none; } -.progress-circle.p1 .value-bar { transform: rotate(4deg); } -.progress-circle.p2 .value-bar { transform: rotate(7deg); } -.progress-circle.p3 .value-bar { transform: rotate(11deg); } -.progress-circle.p4 .value-bar { transform: rotate(14deg); } -.progress-circle.p5 .value-bar { transform: rotate(18deg); } -.progress-circle.p6 .value-bar { transform: rotate(22deg); } -.progress-circle.p7 .value-bar { transform: rotate(25deg); } -.progress-circle.p8 .value-bar { transform: rotate(29deg); } -.progress-circle.p9 .value-bar { transform: rotate(32deg); } -.progress-circle.p10 .value-bar { transform: rotate(36deg); } -.progress-circle.p11 .value-bar { transform: rotate(40deg); } -.progress-circle.p12 .value-bar { transform: rotate(43deg); } -.progress-circle.p13 .value-bar { transform: rotate(47deg); } -.progress-circle.p14 .value-bar { transform: rotate(50deg); } -.progress-circle.p15 .value-bar { transform: rotate(54deg); } -.progress-circle.p16 .value-bar { transform: rotate(58deg); } -.progress-circle.p17 .value-bar { transform: rotate(61deg); } -.progress-circle.p18 .value-bar { transform: rotate(65deg); } -.progress-circle.p19 .value-bar { transform: rotate(68deg); } -.progress-circle.p20 .value-bar { transform: rotate(72deg); } -.progress-circle.p21 .value-bar { transform: rotate(76deg); } -.progress-circle.p22 .value-bar { transform: rotate(79deg); } -.progress-circle.p23 .value-bar { transform: rotate(83deg); } -.progress-circle.p24 .value-bar { transform: rotate(86deg); } -.progress-circle.p25 .value-bar { transform: rotate(90deg); } -.progress-circle.p26 .value-bar { transform: rotate(94deg); } -.progress-circle.p27 .value-bar { transform: rotate(97deg); } -.progress-circle.p28 .value-bar { transform: rotate(101deg); } -.progress-circle.p29 .value-bar { transform: rotate(104deg); } -.progress-circle.p30 .value-bar { transform: rotate(108deg); } -.progress-circle.p31 .value-bar { transform: rotate(112deg); } -.progress-circle.p32 .value-bar { transform: rotate(115deg); } -.progress-circle.p33 .value-bar { transform: rotate(119deg); } -.progress-circle.p34 .value-bar { transform: rotate(122deg); } -.progress-circle.p35 .value-bar { transform: rotate(126deg); } -.progress-circle.p36 .value-bar { transform: rotate(130deg); } -.progress-circle.p37 .value-bar { transform: rotate(133deg); } -.progress-circle.p38 .value-bar { transform: rotate(137deg); } -.progress-circle.p39 .value-bar { transform: rotate(140deg); } -.progress-circle.p40 .value-bar { transform: rotate(144deg); } -.progress-circle.p41 .value-bar { transform: rotate(148deg); } -.progress-circle.p42 .value-bar { transform: rotate(151deg); } -.progress-circle.p43 .value-bar { transform: rotate(155deg); } -.progress-circle.p44 .value-bar { transform: rotate(158deg); } -.progress-circle.p45 .value-bar { transform: rotate(162deg); } -.progress-circle.p46 .value-bar { transform: rotate(166deg); } -.progress-circle.p47 .value-bar { transform: rotate(169deg); } -.progress-circle.p48 .value-bar { transform: rotate(173deg); } -.progress-circle.p49 .value-bar { transform: rotate(176deg); } -.progress-circle.p50 .value-bar { transform: rotate(180deg); } -.progress-circle.p51 .value-bar { transform: rotate(184deg); } -.progress-circle.p52 .value-bar { transform: rotate(187deg); } -.progress-circle.p53 .value-bar { transform: rotate(191deg); } -.progress-circle.p54 .value-bar { transform: rotate(194deg); } -.progress-circle.p55 .value-bar { transform: rotate(198deg); } -.progress-circle.p56 .value-bar { transform: rotate(202deg); } -.progress-circle.p57 .value-bar { transform: rotate(205deg); } -.progress-circle.p58 .value-bar { transform: rotate(209deg); } -.progress-circle.p59 .value-bar { transform: rotate(212deg); } -.progress-circle.p60 .value-bar { transform: rotate(216deg); } -.progress-circle.p61 .value-bar { transform: rotate(220deg); } -.progress-circle.p62 .value-bar { transform: rotate(223deg); } -.progress-circle.p63 .value-bar { transform: rotate(227deg); } -.progress-circle.p64 .value-bar { transform: rotate(230deg); } -.progress-circle.p65 .value-bar { transform: rotate(234deg); } -.progress-circle.p66 .value-bar { transform: rotate(238deg); } -.progress-circle.p67 .value-bar { transform: rotate(241deg); } -.progress-circle.p68 .value-bar { transform: rotate(245deg); } -.progress-circle.p69 .value-bar { transform: rotate(248deg); } -.progress-circle.p70 .value-bar { transform: rotate(252deg); } -.progress-circle.p71 .value-bar { transform: rotate(256deg); } -.progress-circle.p72 .value-bar { transform: rotate(259deg); } -.progress-circle.p73 .value-bar { transform: rotate(263deg); } -.progress-circle.p74 .value-bar { transform: rotate(266deg); } -.progress-circle.p75 .value-bar { transform: rotate(270deg); } -.progress-circle.p76 .value-bar { transform: rotate(274deg); } -.progress-circle.p77 .value-bar { transform: rotate(277deg); } -.progress-circle.p78 .value-bar { transform: rotate(281deg); } -.progress-circle.p79 .value-bar { transform: rotate(284deg); } -.progress-circle.p80 .value-bar { transform: rotate(288deg); } -.progress-circle.p81 .value-bar { transform: rotate(292deg); } -.progress-circle.p82 .value-bar { transform: rotate(295deg); } -.progress-circle.p83 .value-bar { transform: rotate(299deg); } -.progress-circle.p84 .value-bar { transform: rotate(302deg); } -.progress-circle.p85 .value-bar { transform: rotate(306deg); } -.progress-circle.p86 .value-bar { transform: rotate(310deg); } -.progress-circle.p87 .value-bar { transform: rotate(313deg); } -.progress-circle.p88 .value-bar { transform: rotate(317deg); } -.progress-circle.p89 .value-bar { transform: rotate(320deg); } -.progress-circle.p90 .value-bar { transform: rotate(324deg); } -.progress-circle.p91 .value-bar { transform: rotate(328deg); } -.progress-circle.p92 .value-bar { transform: rotate(331deg); } -.progress-circle.p93 .value-bar { transform: rotate(335deg); } -.progress-circle.p94 .value-bar { transform: rotate(338deg); } -.progress-circle.p95 .value-bar { transform: rotate(342deg); } -.progress-circle.p96 .value-bar { transform: rotate(346deg); } -.progress-circle.p97 .value-bar { transform: rotate(349deg); } -.progress-circle.p98 .value-bar { transform: rotate(353deg); } -.progress-circle.p99 .value-bar { transform: rotate(356deg); } -.progress-circle.p100 .value-bar { transform: rotate(360deg); } - - -/* Three dots loader */ -.three-dots-loader, -.three-dots-loader:before, -.three-dots-loader:after { - border-radius: 50%; - width: 0.5em; - height: 0.5em; - -webkit-animation-fill-mode: both; - animation-fill-mode: both; - -webkit-animation: load7 1.8s infinite ease-in-out; - animation: load7 1.8s infinite ease-in-out; -} -.three-dots-loader { - color: currentColor; - margin: 0 0.7em; - position: relative; - text-indent: -9999em; - -webkit-transform: translateZ(0); - -ms-transform: translateZ(0); - transform: translateZ(0); - -webkit-animation-delay: -0.16s; - animation-delay: -0.16s; -} -.three-dots-loader:before, -.three-dots-loader:after { - content: ''; - position: absolute; - top: 0; -} -.three-dots-loader:before { - left: -0.7em; - -webkit-animation-delay: -0.32s; - animation-delay: -0.32s; -} -.three-dots-loader:after { - left: 0.7em; -} -@-webkit-keyframes load7 { - 0%, - 80%, - 100% { - box-shadow: 0 0.5em 0 -0.3em; - } - 40% { - box-shadow: 0 0.5em 0 0; - } -} -@keyframes load7 { - 0%, - 80%, - 100% { - box-shadow: 0 0.5em 0 -0.3em; - } - 40% { - box-shadow: 0 0.5em 0 0; - } -} diff --git a/client/src/components/Accordion/Accordion.stories.tsx b/client/src/components/Accordion/Accordion.stories.tsx deleted file mode 100644 index 41c6539d3d..0000000000 --- a/client/src/components/Accordion/Accordion.stories.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Papers } from '../../icons'; -import Accordion from './index'; - -export default { - title: 'components/Accordion', - component: Accordion, -}; - -export const Default = () => { - return ( - } - headerItems={['one', 'two', 'three'].map((i) => ( - {i} - ))} - > -

    Item 1

    -

    Item 2

    -
    - ); -}; diff --git a/client/src/components/Accordion/index.tsx b/client/src/components/Accordion/index.tsx deleted file mode 100644 index c459629230..0000000000 --- a/client/src/components/Accordion/index.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import React, { memo, useEffect, useState } from 'react'; -import { AnimatePresence, motion } from 'framer-motion'; -import { Trans } from 'react-i18next'; -import { ChevronDownFilled, ChevronUpFilled } from '../../icons'; -import { ACCORDION_CHILDREN_ANIMATION } from '../../consts/animations'; -import Button from '../Button'; - -type Props = { - title: string | React.ReactNode; - icon: React.ReactElement; - headerItems?: React.ReactNode; - children: React.ReactNode; - shownItems?: React.ReactNode; - defaultExpanded?: boolean; - onToggle?: (b: boolean) => void; -}; - -const zeroHeight = { height: 0 }; -const autoHeight = { height: 'auto' }; - -const Accordion = ({ - title, - icon, - children, - headerItems, - shownItems, - defaultExpanded = true, - onToggle, -}: Props) => { - const [expanded, setExpanded] = useState(defaultExpanded); - useEffect(() => { - setExpanded(defaultExpanded); - }, [defaultExpanded]); - return ( -
    - { - if (!document.getSelection()?.toString()) { - setExpanded(!expanded); - onToggle?.(!expanded); - } - }} - > - - {icon} - {title} - - - {headerItems} - - - {expanded ? : } - - - - - {!!shownItems && ( -
    - {shownItems} -
    - -
    -
    - )} - - {expanded && ( - - {children} - - )} - -
    - ); -}; - -export default memo(Accordion); diff --git a/client/src/components/AddStudioContext/index.tsx b/client/src/components/AddStudioContext/index.tsx deleted file mode 100644 index 6d5cf08872..0000000000 --- a/client/src/components/AddStudioContext/index.tsx +++ /dev/null @@ -1,188 +0,0 @@ -import React, { - memo, - useCallback, - useContext, - useEffect, - useState, -} from 'react'; -import { Trans, useTranslation } from 'react-i18next'; -import { CloseSign, PlusSignInCircle } from '../../icons'; -import SeparateOnboardingStep from '../SeparateOnboardingStep'; -import DialogText from '../SeparateOnboardingStep/DialogText'; -import SearchableRepoList from '../RepoList/SearchableRepoList'; -import Button from '../Button'; -import { - getCodeStudio, - getCodeStudios, - importCodeStudio, - patchCodeStudio, - postCodeStudio, -} from '../../services/api'; -import Tooltip from '../Tooltip'; -import { UIContext } from '../../context/uiContext'; -import { SearchContext } from '../../context/searchContext'; -import { TabsContext } from '../../context/tabsContext'; -import { CodeStudioShortType } from '../../types/general'; - -type Props = - | { - filePath: string; - threadId?: never; - name?: never; - } - | { threadId: string; filePath?: never; name: string }; - -const AddStudioContext = ({ filePath, threadId, name }: Props) => { - const { t } = useTranslation(); - const { tab } = useContext(UIContext.Tab); - const { handleAddStudioTab } = useContext(TabsContext); - const { selectedBranch } = useContext(SearchContext.SelectedBranch); - const [isSubmitting, setIsSubmitting] = useState(false); - const [isOpen, setIsOpen] = useState(false); - const [isLoading, setLoading] = useState(true); - const [studios, setStudios] = useState([]); - - const refetchStudios = useCallback(() => { - return getCodeStudios().then((list) => { - setStudios(list.sort((a, b) => (a.modified_at > b.modified_at ? -1 : 1))); - }); - }, []); - - useEffect(() => { - refetchStudios().finally(() => setLoading(false)); - }, [isOpen]); - - const onSubmit = useCallback( - async (studioId?: string) => { - setIsSubmitting(true); - if (filePath) { - if (studioId) { - try { - const studio = await getCodeStudio(studioId); - const exists = studio.context.find( - (f) => - f.path === filePath && - f.repo === tab.repoRef && - f.branch === selectedBranch, - ); - if (!exists) { - await patchCodeStudio(studioId, { - context: [ - ...studio.context, - { - path: filePath, - repo: tab.repoRef, - branch: selectedBranch, - ranges: [], - hidden: false, - }, - ], - }); - } - handleAddStudioTab(studio.name, studioId); - } catch (err) { - console.log(err); - } - } else { - const id = await postCodeStudio(); - await patchCodeStudio(id, { - context: [ - { - path: filePath, - repo: tab.repoRef, - branch: selectedBranch, - ranges: [], - hidden: false, - }, - ], - }); - handleAddStudioTab('New Studio', id); - refetchStudios(); - } - } else if (threadId) { - const id = await importCodeStudio(threadId, studioId); - let tabName = name; - if (studioId) { - const studio = studios.find((s) => s.id === studioId); - if (studio?.name) { - tabName = studio.name; - } - } - handleAddStudioTab(tabName, id); - refetchStudios(); - } - setIsSubmitting(false); - setIsOpen(false); - }, - [filePath, tab.repoRef, selectedBranch, studios, threadId, name], - ); - - return ( -
    - - - - setIsOpen(false)} - noWrapper - > -
    -
    - -
    - - {!!studios.length && ( -
    - -
    - )} - -
    -
    -
    - ); -}; - -export default memo(AddStudioContext); diff --git a/client/src/components/Badge/index.tsx b/client/src/components/Badge/index.tsx index eb0aea0c03..01dc031045 100644 --- a/client/src/components/Badge/index.tsx +++ b/client/src/components/Badge/index.tsx @@ -1,15 +1,57 @@ +import { memo } from 'react'; + type Props = { + type?: + | 'outlined' + | 'filled' + | 'green' + | 'green-subtle' + | 'red' + | 'red-subtle' + | 'yellow' + | 'yellow-subtle' + | 'blue' + | 'blue-subtle' + | 'studio'; + size?: 'large' | 'small' | 'mini'; + Icon?: (props: { + raw?: boolean | undefined; + sizeClassName?: string | undefined; + className?: string | undefined; + }) => JSX.Element; text: string; - active?: boolean; }; -const Badge = ({ text, active }: Props) => { +const typeMap = { + outlined: 'border border-bg-border text-label-base', + filled: 'bg-bg-border text-label-base', + green: 'bg-bg-green text-label-control', + 'green-subtle': 'bg-green-subtle text-green', + red: 'bg-bg-red text-label-control', + 'red-subtle': 'bg-red-subtle text-red', + yellow: 'bg-bg-yellow text-label-control', + 'yellow-subtle': 'bg-yellow-subtle text-yellow', + blue: 'bg-bg-blue text-label-control', + 'blue-subtle': 'bg-blue-subtle text-blue', + studio: + 'bg-[linear-gradient(110deg,#D92037_1.23%,#D9009D_77.32%)] text-label-control', +}; + +const sizeMap = { + large: { pill: 'h-7 px-2.5 gap-1.5 body-s', icon: 'w-4 h-4' }, + small: { pill: 'h-6 px-2 gap-1 body-mini', icon: 'w-3.5 h-3.5' }, + mini: { pill: 'h-5 px-1.5 gap-1 body-tiny', icon: 'w-3 h-3' }, +}; + +const Badge = ({ type = 'outlined', size = 'small', Icon, text }: Props) => { return ( - - {text} - + {!!Icon && } +

    {text}

    +
    ); }; -export default Badge; + +export default memo(Badge); diff --git a/client/src/components/Breadcrumbs/BreadcrumbSection.tsx b/client/src/components/Breadcrumbs/BreadcrumbSection.tsx index aa159ad025..2a62f08b09 100644 --- a/client/src/components/Breadcrumbs/BreadcrumbSection.tsx +++ b/client/src/components/Breadcrumbs/BreadcrumbSection.tsx @@ -1,4 +1,4 @@ -import { MouseEvent, ReactElement } from 'react'; +import { memo, MouseEvent, ReactElement, useCallback } from 'react'; import { Range } from '../../types/results'; type HighlightedString = { @@ -44,7 +44,7 @@ const BreadcrumbSection = ({ limitSectionWidth, nonInteractive, }: Props) => { - const getHighlight = () => { + const getHighlight = useCallback(() => { if (highlight) { const left = label.substring(0, highlight.start); const search = label.substring(highlight.start, highlight.end + 1); @@ -60,7 +60,7 @@ const BreadcrumbSection = ({ ); } return label; - }; + }, [highlight, label]); return ( - + ); }; -export default BreadcrumbsCollapsed; +export default memo(BreadcrumbsCollapsed); diff --git a/client/src/components/BreadcrumbsPath/index.tsx b/client/src/components/Breadcrumbs/PathContainer.tsx similarity index 70% rename from client/src/components/BreadcrumbsPath/index.tsx rename to client/src/components/Breadcrumbs/PathContainer.tsx index d76677b8a7..8c9fdf082a 100644 --- a/client/src/components/BreadcrumbsPath/index.tsx +++ b/client/src/components/Breadcrumbs/PathContainer.tsx @@ -1,18 +1,18 @@ -import React, { useMemo } from 'react'; -import Breadcrumbs, { PathParts } from '../Breadcrumbs'; +import React, { memo, useMemo } from 'react'; import { breadcrumbsItemPath, isWindowsPath, + splitPath, splitPathForBreadcrumbs, } from '../../utils'; import { FileTreeFileType } from '../../types'; -import useAppNavigation from '../../hooks/useAppNavigation'; +import Breadcrumbs, { PathParts } from './index'; type BProps = React.ComponentProps; type Props = { path: string; - repo: string; + repoRef?: string; onClick?: (path: string, fileType?: FileTreeFileType) => void; shouldGoToFile?: boolean; nonInteractive?: boolean; @@ -20,20 +20,19 @@ type Props = { scrollContainerRef?: React.MutableRefObject; } & Omit; -const BreadcrumbsPath = ({ +const BreadcrumbsPathContainer = ({ path, onClick, - repo, shouldGoToFile, allowOverflow, scrollContainerRef, + repoRef, ...rest }: Props) => { - const { navigateRepoPath, navigateFullResult } = useAppNavigation(); const pathParts: PathParts[] = useMemo(() => { - return splitPathForBreadcrumbs(path, (e, item, index, pParts) => { + const pieces = splitPathForBreadcrumbs(path, (e, item, index, pParts) => { if (onClick) { - e.stopPropagation(); + e?.stopPropagation(); } const isLastPart = index === pParts.length - 1; const newPath = breadcrumbsItemPath( @@ -47,13 +46,17 @@ const BreadcrumbsPath = ({ isLastPart ? FileTreeFileType.FILE : FileTreeFileType.DIR, ); if (!isLastPart) { - navigateRepoPath(repo, newPath); + // navigateRepoPath(repo, newPath); } if (shouldGoToFile && isLastPart) { - navigateFullResult(path); + // navigateFullResult(path); } }); - }, [path, shouldGoToFile, onClick, repo]); + if (repoRef) { + pieces.unshift({ label: splitPath(repoRef).pop() || '' }); + } + return pieces; + }, [path, shouldGoToFile, onClick, repoRef]); return (
    ); }; -export default BreadcrumbsPath; + +export default memo(BreadcrumbsPathContainer); diff --git a/client/src/components/Breadcrumbs/index.tsx b/client/src/components/Breadcrumbs/index.tsx index f7e49f4cee..f5206bd31b 100644 --- a/client/src/components/Breadcrumbs/index.tsx +++ b/client/src/components/Breadcrumbs/index.tsx @@ -7,6 +7,7 @@ import React, { useRef, useState, ClipboardEvent, + memo, } from 'react'; import { Range } from '../../types/results'; import { copyToClipboard, isWindowsPath } from '../../utils'; @@ -26,7 +27,7 @@ type ItemElement = { export type PathParts = { icon?: ReactElement; link?: string; - onClick?: (e: MouseEvent) => void; + onClick?: (e?: MouseEvent) => void; underline?: boolean; } & (HighlightedString | ItemElement); @@ -170,4 +171,4 @@ const Breadcrumbs = ({ ); }; -export default Breadcrumbs; +export default memo(Breadcrumbs); diff --git a/client/src/components/Button/Button.stories.tsx b/client/src/components/Button/Button.stories.tsx deleted file mode 100644 index 9e521e4eae..0000000000 --- a/client/src/components/Button/Button.stories.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import { Fragment } from 'react'; -import { MailIcon } from '../../icons'; -import Button from './index'; - -export default { - title: 'components/Button', - component: Button, -}; - -const sizes = ['large', 'medium', 'small', 'tiny'] as const; - -export const Primary = () => { - return ( -
    - {sizes.map((s) => ( - - {s} - - - - - ))} - {sizes.map((s) => ( - - {s} - - - - - ))} -
    - ); -}; - -export const Secondary = () => { - return ( -
    - {sizes.map((s) => ( - - {s} - - - - - ))} - {sizes.map((s) => ( - - {s} - - - - - ))} -
    - ); -}; - -export const Tertiary = () => { - return ( -
    - {sizes.map((s) => ( - - {s} - - - - - ))} - {sizes.map((s) => ( - - {s} - - - - - ))} -
    - ); -}; - -export const TertiaryOutlined = () => { - return ( -
    - {sizes.map((s) => ( - - {s} - - - - - ))} - {sizes.map((s) => ( - - {s} - - - - - ))} -
    - ); -}; diff --git a/client/src/components/Button/index.tsx b/client/src/components/Button/index.tsx index 873cfa2f10..e7c4cabdcd 100644 --- a/client/src/components/Button/index.tsx +++ b/client/src/components/Button/index.tsx @@ -12,20 +12,22 @@ import Tooltip from '../Tooltip'; type Props = { children: ReactNode; variant?: + | 'brand-default' | 'primary' | 'secondary' | 'tertiary' - | 'tertiary-outlined' - | 'tertiary-disabled' | 'tertiary-active' + | 'ghost' + | 'studio' | 'danger'; - size?: 'tiny' | 'small' | 'medium' | 'large'; + size?: 'mini' | 'small' | 'medium' | 'large'; className?: string; } & (OnlyIconProps | TextBtnProps); type OnlyIconProps = { onlyIcon: true; title: string; + shortcut?: string[]; tooltipPlacement?: TippyProps['placement']; }; @@ -33,40 +35,66 @@ type TextBtnProps = { onlyIcon?: false; tooltipPlacement?: never; title?: string; + shortcut?: string[]; }; const variantStylesMap = { + 'brand-default': + 'text-label-control border border-brand-default bg-brand-default shadow-low ' + + 'hover:bg-brand-default-hover ' + + 'focus:bg-brand-default-hover focus:shadow-rings-blue ' + + 'disabled:bg-bg-base disabled:border-none disabled:text-label-faint disabled:shadow-none ' + + 'disabled:hover:bg-bg-base', primary: - 'text-label-control bg-bg-main hover:bg-bg-main-hover focus:bg-bg-main-hover active:bg-bg-main active:shadow-rings-blue focus:shadow-rings-blue disabled:bg-bg-base disabled:text-label-muted disabled:hover:border-none disabled:hover:bg-bg-base disabled:active:shadow-none disabled:border-none', + 'text-label-contrast border border-bg-contrast bg-bg-contrast shadow-low ' + + 'hover:bg-bg-contrast-hover hover:border-bg-contrast-hover ' + + 'disabled:bg-bg-base disabled:border-none disabled:text-label-faint disabled:shadow-none ' + + 'disabled:hover:bg-bg-base', secondary: - 'text-label-title bg-bg-base border border-bg-border hover:border-bg-border-hover focus:border-bg-border-hover hover:bg-bg-base-hover focus:bg-bg-base-hover active:bg-bg-base disabled:bg-bg-base disabled:border-none disabled:text-label-muted shadow-low hover:shadow-none focus:shadow-none active:shadow-rings-gray disabled:shadow-none', + 'text-label-base border border-bg-border bg-bg-base shadow-low ' + + 'hover:text-label-title hover:bg-bg-base-hover hover:border-bg-border-hover ' + + 'disabled:text-label-faint disabled:shadow-none disabled:border-transparent' + + 'disabled:hover:text-label-faint disabled:hover:bg-bg-base disabled:hover:border-transparent', tertiary: - 'text-label-muted bg-transparent hover:text-label-title focus:text-label-title hover:bg-bg-base-hover focus:bg-bg-base-hover active:text-label-title active:bg-transparent disabled:bg-bg-base disabled:text-label-muted', - 'tertiary-active': 'text-label-title bg-bg-base-hover', - 'tertiary-outlined': - 'text-label-muted bg-transparent border border-bg-border hover:bg-bg-base-hover focus:bg-bg-base-hover active:bg-transparent hover:text-label-title focus:text-label-title active:text-label-title disabled:bg-bg-base disabled:text-label-muted disabled:border-transparent disabled:hover:border-transparent', - 'tertiary-disabled': - 'text-label-muted bg-transparent hover:text-label-title focus:text-label-title hover:bg-bg-base-hover focus:bg-bg-base-hover active:text-label-title active:bg-transparent disabled:opacity-50 disabled:text-label-muted disabled:hover:text-label-muted disabled:hover:bg-transparent', + 'text-label-muted bg-transparent ' + + 'hover:text-label-title hover:bg-bg-base-hover ' + + 'disabled:text-label-faint ' + + 'disabled:hover:text-label-faint disabled:hover:bg-transparent', + 'tertiary-active': + 'text-label-title bg-bg-base-hover disabled:text-label-faint', danger: - 'text-label-control bg-bg-danger hover:bg-bg-danger-hover focus:bg-bg-danger-hover active:bg-bg-danger active:shadow-low disabled:bg-bg-base disabled:text-label-muted disabled:hover:border-none disabled:hover:bg-bg-base disabled:active:shadow-none disabled:border-none', + 'text-red border border-bg-border bg-bg-base shadow-low ' + + 'hover:bg-bg-base-hover hover:border-bg-border-hover ' + + 'disabled:text-label-faint disabled:shadow-none disabled:bg-bg-base disabled:border-transparent' + + 'disabled:hover:text-label-faint disabled:hover:bg-bg-base disabled:hover:border-transparent', + ghost: + 'text-label-muted bg-transparent ' + + 'hover:text-label-title ' + + 'disabled:text-label-faint ' + + 'disabled:hover:text-label-faint', + studio: + 'text-label-control bg-brand-studio border border-brand-studio shadow-low ' + + 'hover:bg-brand-studio-hover ' + + 'disabled:text-label-faint disabled:bg-bg-base disabled:bg-transparent disabled:shadow-none' + + 'disabled:hover:bg-bg-base', }; const sizeMap = { - tiny: { - default: 'h-6 px-1 gap-1 caption-strong', - square: 'h-6 w-6 justify-center p-0', + mini: { + default: 'h-6 px-1.5 gap-1 body-mini-b rounded', + square: 'h-6 w-6 rounded', }, small: { - default: 'h-8 px-2 gap-1 caption-strong min-w-[70px]', - square: 'h-8 w-8 justify-center p-0', + default: 'h-7 px-2 gap-1 body-mini-b rounded', + square: 'h-7 w-8 rounded', }, medium: { - default: 'h-10 px-2.5 gap-2 callout min-w-[84px]', - square: 'h-10 w-10 justify-center p-0', + default: 'h-8 px-2.5 gap-1.5 body-s-b rounded-6', + square: 'h-8 w-10 rounded-6', }, large: { - default: 'h-11.5 px-3.5 gap-2 callout min-w-[84px]', - square: 'h-11.5 w-11.5 justify-center p-0', + default: 'h-9 px-3 gap-2 body-base-b rounded-6', + square: 'h-9 w-9 rounded-6', }, }; @@ -91,23 +119,22 @@ const Button = forwardRef< title, tooltipPlacement, type = 'button', + shortcut, ...rest }, ref, ) => { const buttonClassName = useMemo( () => - `py-0 rounded-4 focus:outline-none outline-none outline-0 flex items-center flex-grow-0 flex-shrink-0 ${ + `py-0 focus:outline-none outline-none outline-0 flex items-center justify-center flex-grow-0 flex-shrink-0 ${ variantStylesMap[variant] } ${onlyIcon ? sizeMap[size].square : sizeMap[size].default} ${ className || '' - } ${ - onlyIcon ? '' : 'justify-center' - } transition-all duration-300 ease-in-bounce select-none`, + } select-none transition-all duration-150 ease-in-out`, [variant, className, size, onlyIcon], ); return (onlyIcon && !rest.disabled) || title ? ( - + diff --git a/client/src/components/Chat/ChatBody/AllCoversations/ConversationListItem.tsx b/client/src/components/Chat/ChatBody/AllCoversations/ConversationListItem.tsx deleted file mode 100644 index 0f8286024a..0000000000 --- a/client/src/components/Chat/ChatBody/AllCoversations/ConversationListItem.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React from 'react'; -import { Trans } from 'react-i18next'; -import { QuillIcon } from '../../../../icons'; -import Button from '../../../Button'; - -type Props = { - onClick: () => void; - title: string; - subtitle: string; - onDelete: () => void; -}; - -const ConversationListItem = ({ - onClick, - title, - subtitle, - onDelete, -}: Props) => { - return ( -
    - - -
    -
    - {title} -
    -
    - {subtitle} -
    -
    -
    - -
    -
    -
    - ); -}; - -export default ConversationListItem; diff --git a/client/src/components/Chat/ChatBody/AllCoversations/index.tsx b/client/src/components/Chat/ChatBody/AllCoversations/index.tsx deleted file mode 100644 index e3b0a3cfb1..0000000000 --- a/client/src/components/Chat/ChatBody/AllCoversations/index.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import React, { - Dispatch, - SetStateAction, - useCallback, - useContext, - useEffect, - useState, -} from 'react'; -import { format } from 'date-fns'; -import { useTranslation } from 'react-i18next'; -import { - deleteConversation, - getAllConversations, - getConversation, -} from '../../../../services/api'; -import { AllConversationsResponse } from '../../../../types/api'; -import Conversation from '../Conversation'; -import { - ChatMessage, - ChatMessageAuthor, - OpenChatHistoryItem, -} from '../../../../types/general'; -import { conversationsCache } from '../../../../services/cache'; -import { - mapLoadingSteps, - mapUserQuery, -} from '../../../../mappers/conversation'; -import { LocaleContext } from '../../../../context/localeContext'; -import { getDateFnsLocale } from '../../../../utils'; -import ConversationListItem from './ConversationListItem'; - -type Props = { - repoRef: string; - repoName: string; - openItem: OpenChatHistoryItem | null; - setOpenItem: Dispatch>; -}; - -const AllConversations = ({ - repoRef, - repoName, - openItem, - setOpenItem, -}: Props) => { - const { t } = useTranslation(); - const [conversations, setConversations] = useState( - [], - ); - const { locale } = useContext(LocaleContext); - - const fetchConversations = useCallback(() => { - getAllConversations(repoRef).then(setConversations); - }, [repoRef]); - - useEffect(() => { - fetchConversations(); - }, [fetchConversations]); - - const onDelete = useCallback((threadId: string) => { - deleteConversation(threadId).then(fetchConversations); - }, []); - - const onClick = useCallback((threadId: string) => { - getConversation(threadId).then((resp) => { - const conv: ChatMessage[] = []; - resp.forEach((m) => { - // @ts-ignore - const userQuery = m.search_steps.find((s) => s.type === 'QUERY'); - const parsedQuery = mapUserQuery(m); - conv.push({ - author: ChatMessageAuthor.User, - text: m.query.raw_query || userQuery?.content?.query || '', - parsedQuery, - isFromHistory: true, - }); - conv.push({ - author: ChatMessageAuthor.Server, - isLoading: false, - loadingSteps: mapLoadingSteps(m.search_steps, t), - text: m.answer, - conclusion: m.conclusion, - isFromHistory: true, - queryId: m.id, - responseTimestamp: m.response_timestamp, - explainedFile: m.focused_chunk?.file_path, - }); - }); - setOpenItem({ conversation: conv, threadId }); - conversationsCache[threadId] = conv; - }); - }, []); - - return ( -
    - {!openItem && ( -
    - {conversations.map((c) => ( - onClick(c.thread_id)} - onDelete={() => onDelete(c.thread_id)} - /> - ))} -
    - )} - {!!openItem && ( -
    - {}} - setInputValueImperatively={() => {}} - /> -
    - )} -
    - ); -}; - -export default AllConversations; diff --git a/client/src/components/Chat/ChatBody/Conversation.tsx b/client/src/components/Chat/ChatBody/Conversation.tsx deleted file mode 100644 index ebb7e1e349..0000000000 --- a/client/src/components/Chat/ChatBody/Conversation.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React, { useContext } from 'react'; -import ScrollToBottom from 'react-scroll-to-bottom'; -import { - ChatMessage, - ChatMessageAuthor, - ChatMessageServer, -} from '../../../types/general'; -import { AppNavigationContext } from '../../../context/appNavigationContext'; -import Message from './ConversationMessage'; -import FirstMessage from './FirstMessage'; - -type Props = { - conversation: ChatMessage[]; - threadId: string; - repoRef: string; - repoName: string; - isLoading?: boolean; - isHistory?: boolean; - onMessageEdit: (queryId: string, i: number) => void; - setInputValueImperatively: (s: string) => void; -}; - -const Conversation = ({ - conversation, - threadId, - repoRef, - isLoading, - isHistory, - repoName, - onMessageEdit, - setInputValueImperatively, -}: Props) => { - const { navigatedItem } = useContext(AppNavigationContext); - - return ( - - {!isHistory && ( - - )} - {conversation.map((m, i) => ( - - ))} - - ); -}; - -export default Conversation; diff --git a/client/src/components/Chat/ChatBody/ConversationMessage/MessageFeedback.tsx b/client/src/components/Chat/ChatBody/ConversationMessage/MessageFeedback.tsx deleted file mode 100644 index fdc9e995da..0000000000 --- a/client/src/components/Chat/ChatBody/ConversationMessage/MessageFeedback.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import { AnimatePresence, motion } from 'framer-motion'; -import React, { useCallback, useContext, useState } from 'react'; -import { Trans, useTranslation } from 'react-i18next'; -import { Unlike } from '../../../../icons'; -import Button from '../../../Button'; -import { upvoteAnswer } from '../../../../services/api'; -import { DeviceContext } from '../../../../context/deviceContext'; -import UpvoteBtn from '../../FeedbackBtns/Upvote'; -import DownvoteBtn from '../../FeedbackBtns/Downvote'; - -type Props = { - showInlineFeedback: boolean; - isHistory?: boolean; - threadId: string; - queryId: string; - repoRef: string; - error: boolean; -}; - -const MessageFeedback = ({ - showInlineFeedback, - isHistory, - threadId, - queryId, - repoRef, - error, -}: Props) => { - const { t } = useTranslation(); - const [isUpvote, setIsUpvote] = useState(false); - const [isDownvote, setIsDownvote] = useState(false); - const [isSubmitted, setIsSubmitted] = useState(false); - const [showCommentInput, setShowCommentInput] = useState(false); - const [comment, setComment] = useState(''); - const { envConfig } = useContext(DeviceContext); - - const handleUpvote = useCallback( - (isUpvote: boolean) => { - setIsUpvote(isUpvote); - setIsDownvote(!isUpvote); - if (!isUpvote) { - setTimeout(() => { - setShowCommentInput(true); - }, 500); // to play animation - } - if (isUpvote) { - return upvoteAnswer(threadId, queryId, repoRef, { type: 'positive' }); - } - }, - [showInlineFeedback, envConfig.tracking_id, threadId, queryId, repoRef], - ); - - const handleSubmit = useCallback(() => { - setIsSubmitted(true); - return upvoteAnswer(threadId, queryId, repoRef, { - type: 'negative', - feedback: comment, - }); - }, [comment, isUpvote, threadId, queryId, repoRef]); - - return ( - <> - {showInlineFeedback && - !isHistory && - !error && - !isSubmitted && - !showCommentInput && ( -
    - {!isUpvote && !isDownvote && ( -

    - How would you rate this response? -

    - )} -
    - - -
    -
    - )} - - {showInlineFeedback && showCommentInput && !isSubmitted && ( - -
    -
    -
    -
    - -
    - - Bad response - -
    -
    - + ); }; diff --git a/client/src/pages/Onboarding/index.tsx b/client/src/Onboarding/index.tsx similarity index 78% rename from client/src/pages/Onboarding/index.tsx rename to client/src/Onboarding/index.tsx index 26ba24d1c5..ab1d64bbc9 100644 --- a/client/src/pages/Onboarding/index.tsx +++ b/client/src/Onboarding/index.tsx @@ -1,13 +1,14 @@ import React, { memo, useCallback, useContext, useEffect } from 'react'; -import { UIContext } from '../../context/uiContext'; -import { DeviceContext } from '../../context/deviceContext'; +import { UIContext } from '../context/uiContext'; +import { DeviceContext } from '../context/deviceContext'; import { getPlainFromStorage, ONBOARDING_DONE_KEY, REFRESH_TOKEN_KEY, savePlainToStorage, -} from '../../services/storage'; -import { getConfig, refreshToken } from '../../services/api'; +} from '../services/storage'; +import { getConfig, refreshToken } from '../services/api'; +import { EnvContext } from '../context/envContext'; import SelfServe from './SelfServe'; import Desktop from './Desktop'; @@ -18,11 +19,12 @@ export type Form = { emailError: string | null; }; -const Onboarding = ({ activeTab }: { activeTab: string }) => { +const Onboarding = () => { const { shouldShowWelcome, setShouldShowWelcome } = useContext( UIContext.Onboarding, ); - const { isSelfServe, setEnvConfig, envConfig } = useContext(DeviceContext); + const { isSelfServe } = useContext(DeviceContext); + const { setEnvConfig, envConfig } = useContext(EnvContext); const closeOnboarding = useCallback(() => { setShouldShowWelcome(false); @@ -71,9 +73,9 @@ const Onboarding = ({ activeTab }: { activeTab: string }) => { return shouldShowWelcome ? ( isSelfServe ? ( - + ) : ( - + ) ) : null; }; diff --git a/client/src/Project/CurrentTabContent/ChatTab/ActionsDropdown.tsx b/client/src/Project/CurrentTabContent/ChatTab/ActionsDropdown.tsx new file mode 100644 index 0000000000..4a99cce624 --- /dev/null +++ b/client/src/Project/CurrentTabContent/ChatTab/ActionsDropdown.tsx @@ -0,0 +1,74 @@ +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import DropdownSection from '../../../components/Dropdown/Section'; +import SectionItem from '../../../components/Dropdown/Section/SectionItem'; +import { SplitViewIcon, TrashCanIcon } from '../../../icons'; +import { deleteConversation } from '../../../services/api'; + +type Props = { + handleMoveToAnotherSide: () => void; + refreshCurrentProjectConversations: () => void; + closeTab: (tabKey: string, side: 'left' | 'right') => void; + conversationId?: string; + projectId?: string; + tabKey: string; + side: 'left' | 'right'; +}; + +const ActionsDropdown = ({ + handleMoveToAnotherSide, + refreshCurrentProjectConversations, + conversationId, + projectId, + closeTab, + tabKey, + side, +}: Props) => { + const { t } = useTranslation(); + + const removeConversation = useCallback(async () => { + if (projectId && conversationId) { + await deleteConversation(projectId, conversationId); + refreshCurrentProjectConversations(); + closeTab(tabKey, side); + } + }, [ + projectId, + conversationId, + closeTab, + refreshCurrentProjectConversations, + tabKey, + side, + ]); + + const shortcuts = useMemo(() => { + return { + splitView: ['cmd', ']'], + }; + }, []); + + return ( +
    + + } + /> + {conversationId && ( + } + /> + )} + +
    + ); +}; + +export default memo(ActionsDropdown); diff --git a/client/src/Project/CurrentTabContent/ChatTab/ChatPersistentState.tsx b/client/src/Project/CurrentTabContent/ChatTab/ChatPersistentState.tsx new file mode 100644 index 0000000000..2ffe8661f8 --- /dev/null +++ b/client/src/Project/CurrentTabContent/ChatTab/ChatPersistentState.tsx @@ -0,0 +1,636 @@ +import { + memo, + useCallback, + useContext, + useEffect, + useRef, + useState, +} from 'react'; +import { useTranslation } from 'react-i18next'; +import { DeviceContext } from '../../../context/deviceContext'; +import { ProjectContext } from '../../../context/projectContext'; +import { + ChatMessage, + ChatMessageAuthor, + ChatMessageServer, + ChatMessageUser, + ChatTabType, + InputValueType, + ParsedQueryType, + ParsedQueryTypeEnum, + TabTypesEnum, +} from '../../../types/general'; +import { conversationsCache } from '../../../services/cache'; +import { mapLoadingSteps, mapUserQuery } from '../../../mappers/conversation'; +import { focusInput } from '../../../utils/domUtils'; +import { ChatsContext } from '../../../context/chatsContext'; +import { TabsContext } from '../../../context/tabsContext'; +import { getConversation } from '../../../services/api'; + +type Options = { + path: string; + lines: [number, number]; + repoRef: string; + branch?: string | null; +}; + +type Props = { + tabKey: string; + tabTitle?: string; + conversationId?: string; + initialQuery?: Options; + side: 'left' | 'right'; +}; + +const ChatPersistentState = ({ + tabKey, + tabTitle, + side, + initialQuery, + conversationId: convId, +}: Props) => { + const { t } = useTranslation(); + const { apiUrl } = useContext(DeviceContext); + const { project, refreshCurrentProjectConversations } = useContext( + ProjectContext.Current, + ); + const { preferredAnswerSpeed } = useContext(ProjectContext.AnswerSpeed); + const { setChats } = useContext(ChatsContext); + const { openNewTab, updateTabProperty } = useContext(TabsContext.Handlers); + + const eventSource = useRef(null); + + const [conversation, setConversation] = useState([]); + useEffect(() => { + setChats((prev) => { + return { ...prev, [tabKey]: { ...prev[tabKey], conversation } }; + }); + }, [conversation]); + + const [selectedLines, setSelectedLines] = useState<[number, number] | null>( + null, + ); + useEffect(() => { + setChats((prev) => { + return { ...prev, [tabKey]: { ...prev[tabKey], selectedLines } }; + }); + }, [selectedLines]); + + const [inputValue, setInputValue] = useState({ + plain: '', + parsed: [], + }); + useEffect(() => { + setChats((prev) => { + return { ...prev, [tabKey]: { ...prev[tabKey], inputValue } }; + }); + }, [inputValue]); + + const [submittedQuery, setSubmittedQuery] = useState< + InputValueType & { options?: Options } + >( + initialQuery + ? { + parsed: [ + { + type: ParsedQueryTypeEnum.TEXT, + text: `#explain_${initialQuery.path}:${initialQuery.lines.join( + '-', + )}-${Date.now()}`, + }, + ], + plain: `#explain_${initialQuery.path}:${initialQuery.lines.join( + '-', + )}-${Date.now()}`, + options: initialQuery, + } + : { + parsed: [], + plain: '', + }, + ); + useEffect(() => { + setChats((prev) => { + return { ...prev, [tabKey]: { ...prev[tabKey], submittedQuery } }; + }); + }, [submittedQuery]); + + const [isLoading, setLoading] = useState(false); + useEffect(() => { + setChats((prev) => { + return { ...prev, [tabKey]: { ...prev[tabKey], isLoading } }; + }); + }, [isLoading]); + + const [isDeprecatedModalOpen, setDeprecatedModalOpen] = useState(false); + useEffect(() => { + setChats((prev) => { + return { ...prev, [tabKey]: { ...prev[tabKey], isDeprecatedModalOpen } }; + }); + }, [isDeprecatedModalOpen]); + + const [hideMessagesFrom, setHideMessagesFrom] = useState(null); + useEffect(() => { + setChats((prev) => { + return { ...prev, [tabKey]: { ...prev[tabKey], hideMessagesFrom } }; + }); + }, [hideMessagesFrom]); + + const [queryIdToEdit, setQueryIdToEdit] = useState(''); + useEffect(() => { + setChats((prev) => { + return { ...prev, [tabKey]: { ...prev[tabKey], queryIdToEdit } }; + }); + }, [queryIdToEdit]); + + const [inputImperativeValue, setInputImperativeValue] = useState | null>(null); + useEffect(() => { + setChats((prev) => { + return { ...prev, [tabKey]: { ...prev[tabKey], inputImperativeValue } }; + }); + }, [inputImperativeValue]); + + const [threadId, setThreadId] = useState(''); + useEffect(() => { + setChats((prev) => { + return { ...prev, [tabKey]: { ...prev[tabKey], threadId } }; + }); + }, [threadId]); + + const [conversationId, setConversationId] = useState(''); + useEffect(() => { + setChats((prev) => { + return { ...prev, [tabKey]: { ...prev[tabKey], conversationId } }; + }); + }, [conversationId]); + + const closeDeprecatedModal = useCallback(() => { + setDeprecatedModalOpen(false); + }, []); + + useEffect(() => { + setChats((prev) => { + return { + ...prev, + [tabKey]: { + ...prev[tabKey], + setConversation, + setInputValue, + setSelectedLines, + setSubmittedQuery, + setThreadId, + closeDeprecatedModal, + }, + }; + }); + }, []); + + const setInputValueImperatively = useCallback( + (value: ParsedQueryType[] | string) => { + setInputImperativeValue({ + type: 'paragraph', + content: + typeof value === 'string' + ? [ + { + type: 'text', + text: value, + }, + ] + : value + .filter((pq) => + ['path', 'lang', 'text', 'repo'].includes(pq.type), + ) + .map((pq) => + pq.type === 'text' + ? { type: 'text', text: pq.text } + : { + type: 'mention', + attrs: { + id: pq.text, + display: pq.text, + type: pq.type, + isFirst: false, + }, + }, + ), + }); + focusInput(); + }, + [], + ); + useEffect(() => { + setChats((prev) => { + return { + ...prev, + [tabKey]: { ...prev[tabKey], setInputValueImperatively }, + }; + }); + }, [setInputValueImperatively]); + + const makeSearch = useCallback( + async (query: string, options?: Options) => { + if (!query) { + return; + } + eventSource.current?.close(); + setInputValue({ plain: '', parsed: [] }); + setInputImperativeValue(null); + setLoading(true); + setQueryIdToEdit(''); + setHideMessagesFrom(null); + const url = `${apiUrl}/projects/${project?.id}/answer${ + options ? `/explain` : `` + }`; + const queryParams: Record = { + model: + preferredAnswerSpeed === 'normal' + ? 'gpt-4' + : 'gpt-3.5-turbo-finetuned', + }; + if (conversationId) { + queryParams.conversation_id = conversationId; + if (queryIdToEdit) { + queryParams.parent_query_id = queryIdToEdit; + } + } + if (options) { + queryParams.relative_path = options.path; + queryParams.repo_ref = options.repoRef; + if (options.branch) { + queryParams.branch = options.branch; + } + queryParams.line_start = options.lines[0].toString(); + queryParams.line_end = options.lines[1].toString(); + } else { + queryParams.q = query; + } + const fullUrl = url + '?' + new URLSearchParams(queryParams).toString(); + console.log(fullUrl); + eventSource.current = new EventSource(fullUrl); + setSelectedLines(null); + let firstResultCame: boolean; + eventSource.current.onerror = (err) => { + console.log('SSE error', err); + firstResultCame = false; + stopGenerating(); + setConversation((prev) => { + const newConversation = prev.slice(0, -1); + const lastMessage: ChatMessage = { + author: ChatMessageAuthor.Server, + isLoading: false, + error: t( + "We couldn't answer your question. You can try asking again in a few moments, or rephrasing your question.", + ), + loadingSteps: [], + queryId: '', + responseTimestamp: new Date().toISOString(), + }; + if (!options) { + // setInputValue(prev[prev.length - 2]?.text || submittedQuery); + setInputValueImperatively( + (prev[prev.length - 2] as ChatMessageUser)?.parsedQuery || + prev[prev.length - 2]?.text || + submittedQuery.parsed, + ); + } + setSubmittedQuery({ plain: '', parsed: [] }); + return [...newConversation, lastMessage]; + }); + }; + let conversation_id = ''; + setConversation((prev) => + prev[prev.length - 1].author === ChatMessageAuthor.Server && + (prev[prev.length - 1] as ChatMessageServer).isLoading + ? prev + : [ + ...prev, + { + author: ChatMessageAuthor.Server, + isLoading: true, + loadingSteps: [], + text: '', + conclusion: '', + queryId: '', + responseTimestamp: '', + }, + ], + ); + eventSource.current.onmessage = (ev) => { + console.log(ev.data); + if ( + ev.data === '{"Err":"incompatible client"}' || + ev.data === '{"Err":"failed to check compatibility"}' + ) { + eventSource.current?.close(); + if (ev.data === '{"Err":"incompatible client"}') { + setDeprecatedModalOpen(true); + } else { + setConversation((prev) => { + const newConversation = prev.slice(0, -1); + const lastMessage: ChatMessage = { + author: ChatMessageAuthor.Server, + isLoading: false, + error: t( + "We couldn't answer your question. You can try asking again in a few moments, or rephrasing your question.", + ), + loadingSteps: [], + queryId: '', + responseTimestamp: new Date().toISOString(), + }; + if (!options) { + // setInputValue(prev[prev.length - 1]?.text || submittedQuery); + setInputValueImperatively( + (prev[prev.length - 1] as ChatMessageUser)?.parsedQuery || + prev[prev.length - 2]?.text || + submittedQuery.parsed, + ); + } + setSubmittedQuery({ plain: '', parsed: [] }); + return [...newConversation, lastMessage]; + }); + } + setLoading(false); + return; + } + try { + const data = JSON.parse(ev.data); + if (data?.Ok?.ChatEvent) { + const newMessage = data.Ok.ChatEvent; + conversationsCache[conversation_id] = undefined; // clear cache on new answer + setConversation((prev) => { + const newConversation = prev?.slice(0, -1) || []; + const lastMessage = prev?.slice(-1)[0]; + const messageToAdd = { + author: ChatMessageAuthor.Server, + isLoading: true, + loadingSteps: mapLoadingSteps(newMessage.search_steps, t), + text: newMessage.answer, + conclusion: newMessage.conclusion, + queryId: newMessage.id, + responseTimestamp: newMessage.response_timestamp, + explainedFile: newMessage.focused_chunk?.repo_path, + }; + const lastMessages: ChatMessage[] = + lastMessage?.author === ChatMessageAuthor.Server + ? [messageToAdd] + : [...prev.slice(-1), messageToAdd]; + return [...newConversation, ...lastMessages]; + }); + // workaround: sometimes we get [^summary]: before it is removed from response + if (newMessage.answer?.length > 11 && !firstResultCame) { + if (newMessage.focused_chunk?.repo_path) { + openNewTab( + { + type: TabTypesEnum.FILE, + path: newMessage.focused_chunk.repo_path.path, + repoRef: newMessage.focused_chunk.repo_path.repo, + scrollToLine: + newMessage.focused_chunk.start_line > -1 + ? `${newMessage.focused_chunk.start_line}_${newMessage.focused_chunk.end_line}` + : undefined, + }, + side === 'left' ? 'right' : 'left', + ); + } + firstResultCame = true; + } + } else if (data?.Ok?.StreamEnd) { + const message = data.Ok.StreamEnd; + conversation_id = message.conversation_id; + setThreadId(message.thread_id); + setConversationId(message.conversation_id); + if (conversation.length < 2) { + updateTabProperty( + tabKey, + 'conversationId', + message.conversation_id, + side, + ); + } + eventSource.current?.close(); + eventSource.current = null; + setLoading(false); + setConversation((prev) => { + const newConversation = prev.slice(0, -1); + const lastMessage = { + ...prev.slice(-1)[0], + isLoading: false, + }; + return [...newConversation, lastMessage]; + }); + refreshCurrentProjectConversations(); + setTimeout(() => focusInput(), 100); + return; + } else if (data.Err) { + setConversation((prev) => { + const lastMessageIsServer = + prev[prev.length - 1].author === ChatMessageAuthor.Server; + const newConversation = prev.slice( + 0, + lastMessageIsServer ? -2 : -1, + ); + const lastMessage: ChatMessageServer = { + ...(lastMessageIsServer + ? (prev.slice(-1)[0] as ChatMessageServer) + : { + author: ChatMessageAuthor.Server, + loadingSteps: [], + queryId: '', + responseTimestamp: new Date().toISOString(), + }), + isLoading: false, + error: + data.Err === 'request failed 5 times' + ? t( + 'Failed to get a response from OpenAI. Try again in a few moments.', + ) + : t( + "We couldn't answer your question. You can try asking again in a few moments, or rephrasing your question.", + ), + }; + if (!options) { + setInputValueImperatively( + ( + prev[ + prev.length - (lastMessageIsServer ? 2 : 1) + ] as ChatMessageUser + )?.parsedQuery || + prev[prev.length - 2]?.text || + submittedQuery.parsed, + ); + } + setSubmittedQuery({ plain: '', parsed: [] }); + return [...newConversation, lastMessage]; + }); + } + } catch (err) { + console.log('failed to parse response', err); + } + }; + }, + [conversationId, t, queryIdToEdit, preferredAnswerSpeed, openNewTab, side], + ); + + useEffect(() => { + return () => { + eventSource.current?.close(); + }; + }, []); + + useEffect(() => { + if (!submittedQuery.plain) { + return; + } + let userQuery = submittedQuery.plain; + let userQueryParsed = submittedQuery.parsed; + const options = submittedQuery.options; + if (submittedQuery.plain.startsWith('#explain_')) { + const [prefix, ending] = submittedQuery.plain.split(':'); + const [lineStart, lineEnd] = ending.split('-'); + const filePath = prefix.slice(9); + userQuery = t( + `Explain the purpose of the file {{filePath}}, from lines {{lineStart}} - {{lineEnd}}`, + { + lineStart: Number(lineStart) + 1, + lineEnd: Number(lineEnd) + 1, + filePath, + }, + ); + userQueryParsed = [{ type: ParsedQueryTypeEnum.TEXT, text: userQuery }]; + } + setConversation((prev) => { + return (prev.length === 1 && submittedQuery.options) || + (prev.length === 2 && + submittedQuery.options?.lines && + submittedQuery.options === initialQuery) + ? prev + : [ + ...prev, + { + author: ChatMessageAuthor.User, + text: userQuery, + parsedQuery: userQueryParsed, + isLoading: false, + }, + ]; + }); + makeSearch(userQuery, options); + }, [submittedQuery]); + + useEffect(() => { + if (conversation.length && conversation.length < 3 && !tabTitle) { + updateTabProperty( + tabKey, + 'title', + conversation[0].text, + side, + ); + } + }, [conversation, tabKey, side, tabTitle]); + + const stopGenerating = useCallback(() => { + eventSource.current?.close(); + setLoading(false); + setConversation((prev) => { + const newConversation = prev.slice(0, -1); + const lastMessage = { + ...prev.slice(-1)[0], + isLoading: false, + }; + return [...newConversation, lastMessage]; + }); + setTimeout(focusInput, 100); + }, []); + useEffect(() => { + setChats((prev) => { + return { ...prev, [tabKey]: { ...prev[tabKey], stopGenerating } }; + }); + }, [stopGenerating]); + + const onMessageEdit = useCallback( + (parentQueryId: string, i: number) => { + setQueryIdToEdit(parentQueryId); + if (isLoading) { + stopGenerating(); + } + setHideMessagesFrom(i); + const mes = conversation[i] as ChatMessageUser; + setInputValueImperatively(mes.parsedQuery || mes.text!); + }, + [isLoading, conversation], + ); + useEffect(() => { + setChats((prev) => { + return { ...prev, [tabKey]: { ...prev[tabKey], onMessageEdit } }; + }); + }, [onMessageEdit]); + + const onMessageEditCancel = useCallback(() => { + setQueryIdToEdit(''); + setInputValue({ plain: '', parsed: [] }); + setInputImperativeValue(null); + setHideMessagesFrom(null); + }, []); + useEffect(() => { + setChats((prev) => { + return { ...prev, [tabKey]: { ...prev[tabKey], onMessageEditCancel } }; + }); + }, [onMessageEditCancel]); + + useEffect(() => { + // if it was open from history and not updated from sse message + if (convId && project?.id && !conversation.length) { + getConversation(project.id, convId).then((resp) => { + const conv: ChatMessage[] = []; + let hasOpenedTab = false; + resp.exchanges.forEach((m) => { + // @ts-ignore + const userQuery = m.search_steps.find((s) => s.type === 'QUERY'); + const parsedQuery = mapUserQuery(m); + conv.push({ + author: ChatMessageAuthor.User, + text: m.query.raw_query || userQuery?.content?.query || '', + parsedQuery, + isFromHistory: true, + }); + conv.push({ + author: ChatMessageAuthor.Server, + isLoading: false, + loadingSteps: mapLoadingSteps(m.search_steps, t), + text: m.answer, + conclusion: m.conclusion, + queryId: m.id, + responseTimestamp: m.response_timestamp, + explainedFile: m.focused_chunk?.repo_path.path, + }); + if (!hasOpenedTab && m.focused_chunk?.repo_path) { + openNewTab( + { + type: TabTypesEnum.FILE, + path: m.focused_chunk.repo_path.path, + repoRef: m.focused_chunk.repo_path.repo, + scrollToLine: + m.focused_chunk.start_line > -1 + ? `${m.focused_chunk.start_line}_${m.focused_chunk.end_line}` + : undefined, + }, + side === 'left' ? 'right' : 'left', + ); + hasOpenedTab = true; + } + }); + setConversation(conv); + setThreadId(resp.thread_id); + setConversationId(convId); + }); + } + }, [convId, project?.id]); + + return null; +}; + +export default memo(ChatPersistentState); diff --git a/client/src/Project/CurrentTabContent/ChatTab/Conversation.tsx b/client/src/Project/CurrentTabContent/ChatTab/Conversation.tsx new file mode 100644 index 0000000000..0bf5b86266 --- /dev/null +++ b/client/src/Project/CurrentTabContent/ChatTab/Conversation.tsx @@ -0,0 +1,88 @@ +import React, { + memo, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { ChatMessageServer } from '../../../types/general'; +import { ProjectContext } from '../../../context/projectContext'; +import { ChatContext, ChatsContext } from '../../../context/chatsContext'; +import ScrollToBottom from '../../../components/ScrollToBottom'; +import Input from './Input'; +import ScrollableContent from './ScrollableContent'; +import DeprecatedClientModal from './DeprecatedClientModal'; + +type Props = { + side: 'left' | 'right'; + tabKey: string; +}; + +const Conversation = ({ side, tabKey }: Props) => { + const { project } = useContext(ProjectContext.Current); + const { chats } = useContext(ChatsContext); + const scrollableRef = useRef(null); + const [isScrollable, setIsScrollable] = useState(false); + + const chatData: ChatContext | undefined = useMemo( + () => chats[tabKey], + [chats, tabKey], + ); + + useEffect(() => { + setTimeout(() => { + if (scrollableRef.current) { + setIsScrollable( + scrollableRef.current.scrollHeight > + scrollableRef.current.clientHeight, + ); + } + }, 100); + }, [chatData?.conversation, chatData?.hideMessagesFrom]); + + return !chatData ? null : ( +
    + + + + + +
    + ); +}; + +export default memo(Conversation); diff --git a/client/src/Project/CurrentTabContent/ChatTab/DeprecatedClientModal.tsx b/client/src/Project/CurrentTabContent/ChatTab/DeprecatedClientModal.tsx new file mode 100644 index 0000000000..1c47a51a7d --- /dev/null +++ b/client/src/Project/CurrentTabContent/ChatTab/DeprecatedClientModal.tsx @@ -0,0 +1,68 @@ +import { useContext } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { CloseSignIcon } from '../../../icons'; +import { DeviceContext } from '../../../context/deviceContext'; +import Button from '../../../components/Button'; +import Modal from '../../../components/Modal'; + +type Props = { + isOpen: boolean; + onClose: () => void; +}; + +const DeprecatedClientModal = ({ isOpen, onClose }: Props) => { + const { t } = useTranslation(); + const { openLink, relaunch } = useContext(DeviceContext); + return ( + +
    +
    +
    +

    + Update Required +

    +

    + + We've made some exciting enhancements to bloop! To continue + enjoying the full functionality, including the natural language + search feature, please update your app to the latest version. + +

    +

    + + To update your app, please visit our releases page on GitHub and + download the latest version manually. Thank you for using bloop. + +

    +
    +
    + + +
    +
    +
    + +
    +
    +
    + ); +}; + +export default DeprecatedClientModal; diff --git a/client/src/components/Chat/ChatFooter/Input/InputCore.tsx b/client/src/Project/CurrentTabContent/ChatTab/Input/InputCore.tsx similarity index 63% rename from client/src/components/Chat/ChatFooter/Input/InputCore.tsx rename to client/src/Project/CurrentTabContent/ChatTab/Input/InputCore.tsx index 2f807d5d62..2706719de3 100644 --- a/client/src/components/Chat/ChatFooter/Input/InputCore.tsx +++ b/client/src/Project/CurrentTabContent/ChatTab/Input/InputCore.tsx @@ -1,5 +1,5 @@ import { memo, useCallback, useEffect, useMemo, useState } from 'react'; -import { EditorState, Transaction } from 'prosemirror-state'; +import { EditorState, TextSelection, Transaction } from 'prosemirror-state'; import { Schema } from 'prosemirror-model'; import { keymap } from 'prosemirror-keymap'; import { baseKeymap } from 'prosemirror-commands'; @@ -11,11 +11,12 @@ import { useNodeViews, } from '@nytimes/react-prosemirror'; import { schema as basicSchema } from 'prosemirror-schema-basic'; -// @ts-ignore import * as icons from 'file-icons-js'; import { useTranslation } from 'react-i18next'; -import { getFileExtensionForLang, InputEditorContent } from '../../../../utils'; -import { ParsedQueryType } from '../../../../types/general'; +import { InputEditorContent, ParsedQueryType } from '../../../../types/general'; +import { getFileExtensionForLang } from '../../../../utils'; +import { blurInput } from '../../../../utils/domUtils'; +import { MentionOptionType } from '../../../../types/results'; import { getMentionsPlugin } from './mentionPlugin'; import { addMentionNodes, mapEditorContentToInputValue } from './utils'; import { placeholderPlugin } from './placeholderPlugin'; @@ -38,8 +39,9 @@ const reactNodeViews: Record = { }; type Props = { - getDataLang: (search: string) => Promise<{ id: string; display: string }[]>; - getDataPath: (search: string) => Promise<{ id: string; display: string }[]>; + getDataLang: (search: string) => Promise; + getDataPath: (search: string) => Promise; + getDataRepo: (search: string) => Promise; initialValue?: Record | null; onChange: (contents: InputEditorContent[]) => void; onSubmit?: (s: { parsed: ParsedQueryType[]; plain: string }) => void; @@ -49,6 +51,7 @@ type Props = { const InputCore = ({ getDataLang, getDataPath, + getDataRepo, initialValue, onChange, onSubmit, @@ -62,17 +65,18 @@ const InputCore = ({ getSuggestions: async ( type: string, text: string, - done: (s: Record[]) => void, + done: (s: MentionOptionType[]) => void, ) => { const data = await Promise.all([ + getDataRepo(text), getDataPath(text), getDataLang(text), ]); - done([...data[0], ...data[1]]); + done([...data[0], ...data[1], ...data[2]]); }, getSuggestionsHTML: (items) => { return ( - '
    ' + + '
    ' + items .map( (i) => @@ -80,7 +84,9 @@ const InputCore = ({ i.isFirst ? `
    ${t( - i.type === 'dir' + i.type === 'repo' + ? 'Repositories' + : i.type === 'dir' ? 'Directories' : i.type === 'lang' ? 'Languages' @@ -88,15 +94,21 @@ const InputCore = ({ )}
    ` : '' - }
    ${ - i.type === 'dir' + }
    ${ + i.type === 'repo' + ? ` ` + : i.type === 'dir' ? ` ` : `` - }${i.display}
    `, + }${i.display}${ + i.hint + ? `${i.hint}` + : '' + }
    `, ) .join('') + '

    Rj-?N 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 literal 11248 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 a;rQRG;ol@sLW=(Y0000orvpq@2?xbUs%F)! zS+yb*Lr#t?DkF^RD)e=L-9-55A4B+*-2uHAe@M46iHYm3z9E+}}M9yf+V>Cwy48 zBW~U@p~I&T{Q_!Y^0wiTXcY0{FxHP6l#R=;`C$b86!O`M6!I2ES3+n&RBaC9JC?6b zURKFbh&d})pS5o8y28zfx?5oh{fRQQ@(xddnRAcIwADz{@_v1X50`k*;7k7LDy*nN zPA~Ysy=+FTxnb60uU;__09@X9#LtVR@^{|eiaa0{nd`WmRTu?}PmSYTzuo*Kbv*CM z-)|>V#hXHe9e1DV$k0eMmAO%$AwMNogz>12I+TR*)r{cBxE;F&srdSbhlgjk>w2{} zgtsO92-Zg2NJ5*9d72N{2sa>p@pO|N7!!(kp%E|fyG`;nBw(;Ku;~Br0PTy6z1E?v z``ltSKFy0Ty<@~cy5Q-hKvg}R;z_}8(Bug&dK$KSZC#`$mG5Mkq=zq4)_hueLiENKWe zJ1&;Sm!&5i`Zq<(*0W=dfj?rpP6iz=SDEDE2){^uut^Q{^6z4dKZcDI^$Orq20ILW zvPlAlr~3v`z^fJzsDzU%NuR9|i{R-y>jk&Pg|bO8*$F~Tm>q1#B4#$*^-M@88*i8b zMz>o0E)q+wR%3gl>*N5<2Rft(nj}sO-^}R_+_QGFc_U0|%8PZx3@ulzJQ`hWLaFxV z0<3l3`12aNR9-}X&-Hh#+rJY3=zf<3EZch@I6mLM0f&SS=q$fmsm$>DnVw^*OD+-6 z{eC{J4X^YKuFR$DV)+B$j12Cs28ZsL7SvQfFE&A;Kr-X}L*^u7!-FPd88_g3Q0?lq zKNxY<_Md48HVF5xJYvGYY#$)CuKTsLbGT&l9u*xPg?`>y-`~X*)@S4!?iyFHRvL?5 z=yqiBFWJ%#B@c~W<+`{nReF9%d`WHkHYNC9lg`u7l=gt311`KL%q+z_2g1Jd_e4H2 z_91e#;5}%}EcZ7%FiuLkmg-U@76860qe*(WQeU;kS>`iRLR1uMVQci_Jxx9#z>cK%z{&##=uM0+5fPa3=Lv$gKZ!bAdp+KA5VXY_g3}xf~?QO&ljtdG# z=k&laCNSaZS78VZavHj@O8}+q3rjgOS=@B&MLBDq#P(OxCyp2TkTkO8NlD?(?1_t$ z#5Z|V9;v`8WYW-h+*4z2j({Ml}`t2tKzhY7Z{wos0)ekgS=@*;-^Ss=m)4hR7dJ4>Q@}V z#jhH^N9u@&j|8N|fVl1e%6S6$kiWUn@jl0l^Op1wDn>4L-;3UNff^C>F@LVXdw&|h zolD1P(kCn(gqP@g_h? za~A!1-XQ+rO)M=(tr}1Ih`t3g_BqqUihk#&5y)+ccttgZ&aLKI7m+y*V2oe*`B$}6 zZ}|)nvt*+euYvm)Z=9bc`rv80dlT;Xx3Pg1XTQD6(c%^;NCGXq!TPqucKW6oSYqBR zDqJcfE(9%Ja|Bwj$Owq8-~y77fJ9D81Q#Nq13u)$S*dfc z4_e`i!`6#MpeOtuv6>?k%?}F9!}9GXPR!crZ-FzTjk;ZE=ETnsW%{K~Ca>(}O`~=4 zaIaiB8fhV4g&z(?lj%X>XzPRK1*33cWc{%r2{-ZeN-)@FLx^u*Xn9yqSXf=|+Jnz) z4alG0XIU$H!znu}BK(O{D8k>k-FB$F+8oz6bv8nW-MlA8{vQ(>YMZ0f_OheLCusge zzi)ahzWRRt-QMD| zxg_9>Q)Yu0=pX;s%WHm|cHzhKF)n%=;NX55f=p|jw2c})hkb2>DBLNw=m%EorI~64 zR>mpuP)X%rivBF7!}_N2Fz=)~#w@1{R{_?)m?#~41m2OnTN2}8Q!^K`*Z%4B+f?tb zgXy`kAH*TF+TkxEFba8+izRJRi_|#Ky)j`Uf7kt$%UD)^X=$8v(*HT zjvLPD7MZIJ1lur)(e{&#vSH}1tGW?SfBNjtNIabcj|cF9cN8aaer%1PvN!zwp|Du7 z^mVaVVt)Rue|h;5d&Bih)ZU1U#RU6n7y{c%sJop9qux}v5~yQHpXHkdi|vpPD_H?&$Z<^OaYF&(Z7OZc`TG@>!JaUkHm`x^HN z>X8Qaeekh)-a#31Cv_}VY@80dJ%7=mY*Z?P@cxGp3{3@%*fp`By^J-9t&eLFR*RrR zyz1pg8o~WNjD4j-xwY}5T~mi{kC$zaYJnZNz(3Z7e;r*Q+b9(9jwfi|hQv2uD`m-- zR=bUD+3+&6+l?I6ZO<930@hQgx8sUBDVwxYbZ>%Rv@ub45VRg_1XKiku_IQ#TGAEy zpX-e4=fu?&+Dv=$*g>r$=`~@^I?np~NNvYPHa`NI#<~JgitwW}o<>l>BLu{PV>3on z!3WW!OCjQR4C^6Lds3x;6@=K+Na15{nAViogkDJ3`TyD+MSsv0p9mA^Zt)h6h8B2H zq2#Dd2|(Rd3NHOpJobh=PV-!ZwvnKG;+U%Q!T-x|K^vS_Q=YIr=QlK4%b9q6fKP#} zml*6yv#4=o&?HybEw+CF_B9xxBy{V?vkzJizv^3y)zGW*#xb47LEQZ97P69l_c&7k zBYKjx)4~M$K;{lmI|7!Q{KR#qcWEyie@44y(|Y--B$_f$U$7Oky~@MicrEaJlc_}h z+T<0|w82(wwRfwo?Wpnl7t$$&tyv=QnK3c;YWCAJ-#|23OiTXwK-QirWIK{g{Z1# zo|6+arZ9g2o2aQ0OU4PzjDfX01{W5BbbwW3pQ}ezY|_z&;PC_;z-xCL=A>Lbg% z998&j)-F%NEvZIzUdcB5G=^0{2~8)}m@1~Tt) zf~0V?`HzC=l`@dzd;ZqZef13YZwidW^?;wo`2^(n`^6IY^mBuOh}L5TnErz*+_#IW zykxf0U%VXzHZ~+3%xo!c&g+_-&ELC$Wh$lDXj4G#_VlvbR|UNn1OQu!V!|V%7?r8x zd4<`ARrg7s+Kr6BNv&RG>BgfR>fhr>An5V}X4RAI{Ly*eF%W@vDuMw(!&X%nRI-QE>vk>dj;III2 z!|y4^Rd`csue-2^YqY%-`ibMXhF<9vbI#RBhQ)V{^ax3;&}^!~dlPQ{Ly>ZOO-(c+w;PrmrrB^c3=!%uOMUheHpD z+7@KXJcj#>BLs!5@xIq$Y1GiX?T0Sc=$xZ{y$@%C1n|@%MQKPU{Vlbr+eQ?q#T|F3 zT?tnEbL=Vd&1f6`-n&i3@`TM9W?;51G6qOXd1q0>{?QF{wnah}g@p=+e(^V2QdJIF z?Z?LCq)b*Dp0nCO7h_Hw4{!(tY@fQSAhlUuwcFlY&whc${lN{I`fXLRWA@_Jmo7N>0BA`$J` z=d2LiSo*5XpIxQ&jw<|pkz@f{r7~Z(hct(hVU2QQmtVeK>(2EP|Bpy-5`vD`W@@1y|J_bGsI+%x;*=Vt83~=1fN?1+Xyj^UHRD z%!0Fcke{@B-cv-T9|pF67Lz;f0#?5}S^LaaOXM7`)CX>Jt1oclbRVN^<1l}PcYFjloVCJrQXbdHYF26qRI1R`9%m=%5v2v=+pSR z-_+8n5D{q^oUF>7r-NB7i^r!68}iBifU zpx^EKEf#Gfyr>jCo`m}E8BL3&VcoPu={W|HM*P|c#=Hl2znioR_!h?$gw<1R!quKi z!=;5rEF1RNDZ=Z51Txp3`ucjo%?B6%l{b~_sq*`}ZU@Rtd4D&N%Z@a|C44Lw+6oD^ zo-j5Zyx+S@yB6|#I|=91v{Iiyhrr3Q$c;AkTO!LP%{ZMz(ofk~HuJ7>9$W+t-M5WI zBJDf$qYSSL|5o3G`iY&}^bJmbz+Bx!f|1|RE2FW5Zsm{sDdaM0=S#Rtf^} ztOw{rt|{WhmS|*?gPYh!UdIT!4PSvN011XK_d$fqemM#{3R;ON*vVE|1;VCT8Y{G6 z-w#_M0>mOqVm@6ij?_}SGqib>$cmJ$H28n^d1uryX^9K)VdV(R^>XA(|G_f8(bZl^ zS^Kz8W7VA1fN)6#8Xr0Mr@w3r<@mAwkF*V}#jVs8>m8+uVzlRLh5%?8+|%af4sKsQ zuko0M_-8()yV6qCHJm4WWbgHYCH_e9<9HsX(csOND0BG^zs&BXl}P!Oa@a@f%IkO_!^4ed zW9HQv>ViuP@A+14Bm7-ULRm%U!cj_ZI;8nM!dMbQJ-ul6ZG);5><`0FtgztdZoFcL zVhC$MbU>zhI8)5eo#miNiHf=28#OlapSRoN?$W_D-3m}S$I#Wih9J_XoOWY1pApo` z_%$^8&Iyp_^Ek0qoF!ULEduCzrsji92K+X+n1Vp~@urXPj6=yL0PLsbD%ZYE<82mi6obyH)iMG6?###W;P3j>S5G@u^Sg* z3;q|s_IQ>MwDNGIJ293iwCiA5PV{3cbV54Y`h?b?`)yH}=jv+v3Kzgv#NU!WTpS+e z{gr@R4T;>MXaj34MrHZUgNOGZ1p}=kt+Gqxh;1t^5?Dg{)YrRja^n5O)AF9o;R7T~ z3)dQ+MK4;cH&MqmlqNR3HgCfr#}41uUWydnV_pz&(@GI=y;OjtX0@7je*78UMf*e; z_Qhp5BQdi*Z)z$8;G1qh%X}fWVBy9NI=KdY*6!!5z!_#Uj>49 zX09eIU9=qF1il>Fk`T{GIaUo}?Ex2}s?(e>S~lfi*3oD;tZp$`82v5|IUBfY8i5DU z@Bdq$oXVew7jMAC=^F`ah3wp!ePm&5sIk)Ts|Z{aIKRO|E{Blg8}!kCTt1&Fll(k` z-Hs(m`Ruan#9+pK-Of30nE_4Uw1A?;3gr(GU@7dQBQMt@yrJ+@q>_#mPe#{~u-#AciaEy_~qDCjPQ(JuqrK z*&&l!LmNe_MGVZ}bSJFA60zWF&tN?6}1Fmfow_CN9p!MCVh-pI?9u5je zb(li$PL$m*z;&@*xaNnQ(j#&yra0cm5_?HuV=#0jWk=VTwVaBYA8lU{#zeFG64V|!6my1&XTCq8g> z|8-bCpKjje;;8SMwSOf9^?SFy)I}|yEtJj`p623=N#o{lo3V3jBhnI|i!Ebm;;qe3 z&IA6L{%Cep^@;({uVKgT3&cE|wpcG<18oVs?rGzy%P5M8pHnZ?$AywdL2`z0FG72z z%yKi=&a(5)Vtz?EgW&dQR2IoHuTR<-pU%VjPv?5NyB*n4bC}SU{-|59;*#$&o!ZYt zqqeO@P;vPvoyntFsa5872GVQ~h3SaM?Kj;iddgsiIhPVoOrp9+Ik>iAw>zsU^|~OF6z1K!3A>%-F%Y4stYV9 zOrE4h4BeD3sn?-sYbnQ3q9Wj}KQVX2sD&o_wjjCb?SDT&g_O1=%Y+!Fmo9B#N zAuzpo)R2FyWa-$6oqkIb&2L05Hfq&COYrnc*O87itFEHq5YRSOlR3$)2!{M!``Jf= zMVG7_&|WdJ>;8Sr!T~e~9*o|5rF&HJ-2nq_{R;h{H(dGef~N$kn`?@fDY%Aw!{f!U z*-4J^n4;FPJFep)C8}0N&|SI2Mq$MJPvmN^hmUmxsDrrC=1@WG`ldDcwYgIu3?%?~&G0_L${aK)~{iI-8Pzk#55gSfcQLnFCykACsgBr+~Ft!r_|?J`WO zKSeEJWOqYW_L{oh9tMVzLp>g^Z)XMC8&F>YKmQ8|}jTnMLFHnbUMLu&{NmB z#>5YAhg;4qc>3>`=Fqw-BrcinN+@iDFqi> z=7b{fq2Da%r0W%)J3QGUhD5V>v#{#Bttq>9sNfyjDPfy!-i4J;w^S?Pdd~!<8}*-D z!TnkubRp~Guk3<-_e8$(E^`GUARhUVC-=&UpYF1O-8UENsqE$+In4!bzembg2*4h++4-{rKrBlIiKD8^xlQu%E>S0A?%B;Bwhbom4^i`LN~AB!x2H`>0QK)|I@raZgO&||JKhwGm9 zK=lg>=v0DXwRBdbmdJPafKUMKm5S#VlW>x7bD4n*4Onhf@aw#PYwbkX(fCj$?prBn zDd3gU1eDas%)_n}BkNKY8H^zfbdS5 zQG;%x-`mfxKfJFLwgivY#@%HQrFYbFG%vp~%QJtVi2dGs#qKDggmY~1fyzm~!Vmkr z)1&LJ=vW1o58?Wp%XF~n0Cl|#n~IV>SI5p-=!eB{l0#h`t<1)~ES|Im40i^2Ai5Bru|6t&6keuzqX zqbCPwMJ0+wrW`5mo0NA`$sMWZpUSW7lRmn-R>g)%72rNRl^WAcG}AaJ=roGs#r0fX zWf{{Iai?Q=ADuIzolKK7_In#9?wOH zxF;d0TVT0e;rK2*yEQ82%fOo-`&r<_LckDfj~Xl7XtF?$mMsEGE8|19WLsWXHHdv^ zo@fmBm1N^)yI$^T1fOTXBWu=m)w2NHhy88$4DM*Ojg*5OkLYvyNe+%kZhIRuQ}(<5 zvvbnI<-=d;^aA&94X^J{h)#zh0_VgoU|sdUZu-zF^{e^Z22Hi4gq|T|weM&hiB^W1 zgcN$HqT2az=@4gpk8--^ntmUS>r1;%+vnkS+d$fmq|dFUCXf9tt*AtTpQQN>ukbVGkE9gFYA1reax77C1CGd>c-+e|Q%Lj3Auut}Fbh??CWwK0t95>`CcY1=Sj>AvoxScTvfi^jGVwDj zaui*pN|dq04Kc5M*NUGD#d%Zipgm&&Pt@{Z|Gnz^5DpI!n~rkr{Tgj=Y9ev5uPE_A^tP$~Q9nXH?_}&ucdudmw z&KglPJ#xODqSMzYP?2<>ERR}%B|EV8p#|^r`+Lins-u6O01!ffo4}!nc;u!y{y?pw z4(T6Or!1P)mDFR}K=8)~AixGW3V)-$GbD{dpKv-B>Tqp_@>CGH7jy80#V4Qu*U}eQ z(Loos>Uxsg#Z~5g2lYgGi~s!%YQd4xs$j29wn-PKMhVOs2WB2YV=HDx!#IjS@e9Qg ziob&5tV&y`bQd^!$&j`-0OKx~=>=Ltwd*Rt1#^0h(6tIiP%Tv~sXEz@HRW<|aB)X* z_=3{Eigjq)h5==crKm@cQFA_$l@F)L=LolBEW>VGTqO7?0#*1Uf9_eB#ANKNYt#t5LVr528VUiQ-pA4hrna;Vk%@F!dQO{P{$jjSrIRi}%mU$u5unO)aJ z%5K%pO%>gMxGXBC?)1a5wnibrcXypnBF4Q?QG6&T2U7 zY#P8K8EBeR^c(!pwGsp5FV}}UA}U>zg+LQW)eZGBGUhL z`;nx)8)Hl=cltRw+jf4;I3rht{Lf zn0)5lXV12xc)Pg4`Kc$ij}s;K72W{w9)Ab;iBPNMoVUj* z)zyRjL-6P3pK-IxVHg+;jM#>WJOR1CvN*2rKVUr&Ly|&OYSCQwi3i8~Ew9O1X&k%w zyTWkSK<@idLaBq^-|^8>FLYq80g+c!Vzb2-G#lk1ffO2^8P1h~)HDnR#6-N5Yu?#^ zl15Uf@T%yWT7(X9qj6nv4hAnbEn(Kmhs0j}fw^AoJ(^4Mz7YH(t&Io8#VqFi6FLe0 zAABB%2A|Na-5fA!cXv|F4k`kvXwqj9tS1`7dhKbY-=PciW|BO#t?xQ_%(5^EK@s~o zJML~)il;eg@_9NRLk62oTQhqz*#E^S9mjM2y(KSmSgb(E5qb>>@<2S54A8n+X=@0a z3vB%qE!8ZY{t#EQU#e+M7!qprwtIJven)2Y!Ms%NH^$a@$G2nVmAF#fO`Io5Asaud z8T}W_^sL-ETkkE`#Ro_U_de&It~5#pG$(N0D*|H5P}-+(smP4fWW_~jHUL=$)1(mf z=%Nk>Gw^|sf3DJRt8T!Avs(*4RLDP52%Tl7-S#D?i53ifRnq{LnW1^)kdB9TSZ5;x^vi6z)ICqk1KHl2?|Q1*^@j2&Ky8ZQCh_tx6abC5_oB z!^?`jw82Pf0o%W84An{{XhB) zIH$PlmMGi8`CnOr+Ku~mm2jaDbsi|cpJ-@VdqrdN^lyH(<#pFB?27*$V87CBPsq`#tPG#o1s^QF(<4L029O8zHr<=F z2EzJMv=W(1^&T@RSDp}H2TW7iwVzd~soRl+-D%GQKxSeKw6nbMkStI{98Kel^8=zr-12i8phfLU=izAJZ;)=`_AQ{EtTI zW`8|`V(riJN5s)}5Tn2YqN&z>OG|N)I(2weqdx&jcKH&8W_sM*wCF z6i}PV)viF(3Ou@IU5n7ENdyYS<+!-4E7V7y(VqP66U%`)C4 zrlisSZFS@pcWADI?!5tTaQ-Y>G?J^SPiPUx!J5lF0ICbM~u^&M<7 z8G4yy5k1Hv_uWGa_W4(4xpeLY7QmOz!Bx@0R;2j?M(V|%SrA)AtHc30_{l1B!T1*% z!k$G^qVeC4I+|iAwQr@(Vtip^X1f`-50742yHxZr=ZAl&EO^sW53c}4_#<_^uWfK^ z-9ye!@#@yNEiLZ{YHe(1d zf?-~04g(?%Z3xU)ipN-ow1t*VSF_8!PJZ&)(>P>J@5zhX35jO+1)H*_zAyi|pszW) zqv!?4<-_Ku`PzHHwC|H7_yf1*5EB}Mqh?7QR87DQeZG)2PuUgk)Odf%le@+JY-}D_ zL~A!ss=L{+a*=^9QW6I7Pvg<}ua!|fNK;W)r+;n7hvpjXaQbUxj>R-sjvNH>y1wIi z5#aMceWk0tgObLFbs8@pY$S1me*$v!9o*~N@=_5=;ejHvtq5Je) zEcDL3sYQ)_jWoL$Zj3+awkyGp>5k2X6sUHwqPd0^$NzF+8F3?`AxH+8rYmItXkiBx zPYrP>&PS?TURjX&8_&BoAEAZQ_C)L0ddJdrR(*H-K2AKzQMT(gc8)rbHaZvw7;3^9 zWnAlwhY_>15R`rU3lLI-BmOAE+Y-?S`NC>Ln^iMYO!*q@z!UIhlkDsZcUT617g^>p zUzYCD74^Fv4A!g@^?aK}%PPlC+lw4>fgBbOs3+d5GyVg?S}%;!M`dzKy%>J@r$^@p z&5!s*vWQ%8)JKw5=p#nu4Eux4x}{dJrN$JS{V9T-H3gwctHc@M29GPR&%Mp1o5d+Tv{W&Q|(|_wV{d zS?r~o1<4sbu4YHb*HlE7GUlB_Dp}q(rt9uS7#2Jryt{EV2ArOkux?#!=$W%eitF7Q z5UvJpQbWHTMR~gm-#@jnhbHntCJNV^45sq>QWn0pFw^~71$sZtV6R$$eKqB5x9kQ* z&)@j4{Zu!(lE;Hw-dNRJBGE*nXtH)GG$`ixH1O{M>Fd(F|3=xiR~!;)Zz6sl~=t+88lc?9hd(k%k2J}yKH{J58^j<%2p z^xFbw&qH6EnoRF@5JSc0qfU3W1!DYww|Xfp)3H&d)NCJ7H6q*Vajy%zbqt3gO<6R} z6LVVGGM}}?DxNsZXAfK>K@m*Lv~VEP@->utO=zHts*`XhV_8b3;R;5pzKG{P0C(!GwPMMAcVuuEZAWQ z!EZ0fQPjOpmlR@G%!Ya5iBP zZdu9&iD2=_6VD#h-4o)ES{qS)n1gK@=LzhR)@eLcE#e7S>I-7i6@)j(NNkQ-D_#PO zd{nESktW5o8RQvi`M-|49RiZvmlYG97oN!;4;@_EkQrJhOX#G?zSdAO$n;v(o7_<$ z%|fWK9&gjtO39jX%yue7&1bU3Y~y6LCM7=iwDjuuEodS$Lg%V8T04d&u zlH(&7n0~h}WWEjA@dt1V@2#k3Ud}cBsEzo3R_RFT!#jRwBXiT0G|146q#XChQxhV@v}HDF9{#ZT=7+%M0Ufv&=vH@BC)rcwmQ zjb{KT`@hkbCR8tiPk;_5NcraTUmFTc`x4EnT1VRMP%C0Td2$U&Z=$}w7W~9`sW~dd zHV#e9$ViJ>;kL9u^Xq!P!poXBULgB;`-^j}8FRp&PB_(180Id5GgUzQUUx0!&rymg zL(HoNU)Ze~q|Oudw!pT_v53LfNaDrCKzbA_n7b~z8{4l`C?Yv0$c8x4TU5%RT1pCe z{l8yei-Clqlr;ojO!%1*&JT-wkB?|Hd@&A4Xq&>RCfFJtvPSF&r=tyc62wZ%f4L?j zNACPnFenklGBFUfC6NywjLgw`kVd6od%prKMrRPA-%xuSrEcW}x=iCw?B$eoBb-23 zc(u(o^=Z;?y?Xo@~m=WiZWM) zgB}$AXaI)T)M2aOFdC3~&zFahQ%dFWH|POC6{E&Zd&h(f5`l*Y5Wld+lpEJv-Nz>x%Y0rO-FkfYXC5S`p7Vnf+Y0P#O(&1G`=qU}2^0=z9;#nCf}^ zb(S99mlF%=%L6UXH@NLB({&cT{BCWRG>kd8{gm^=4azvJD&WNx>A#UX=S{-A+A(AJ ztE+BAzkpS8z{VYQ3|gU_?<`G`U&F`F@bNJsxWj(1)s+A_I?@b`((tbN%6CIUj4Xne z=~^3`-4##vuY^w?vZUnB?T5k`(lJvUbcNcNhoO}2?r53qJH@x%zMP82^i2Q8&oJmZ zpnGwnFYg+)MgQqwH`Ut^B}NZQia6{as=BkS6hK$amXU%;YFElPaES!LO&3gMtN_p0 zaRgMdI6BEC_wA)JNZ&Vys|qRim3YRKZ8`-F zeOhxl$^k5w)8aEC#v{B*#wP9u)&`r^0TKORsBK|ad`1sJ^k?YNnuZYiFCjo_?HDQG z89PRczF4;*LEp{R#_h%v4dI0QPcB``;(+K>D$c+yWo*RSo#2`vc`u1Xl9IHs?2odO z(K@=(0dYF;SzUb?EOFONc(TXJR%%#ncP#cMG}#}^>x>e{c-C*_GzOA-aj|KhywsrIGdl#$;pEjBXMvn&O-fnY z5ggZA{+EBPI1YR@PCmk|k*DqKP)7+DwvV9h&8+D{>ik?y|3)aAC#L>!U#%ERaIT48 zW&+!e*?=)~ZF_p#o1b5K0#T@2H5gH&V~T}GJ=y#|;bgr(FnCAk2EqgHSIitr(bsam zd*BLZ!I)c}G!kGje7!2zUPd%F@gC{rx?p9^k@z_$B=;2dHi7{qjJpqbOiPV{UJpQI z-$s=b(XN&>r;>YbjEVb(RmAP)far3rL>n-@ZW0W(ct*?9 zVF8vN61nKv0)1s=OeHrimQeRCAWTQcHgx|^hDh+M?)Cu8 z+G>n1GZbT97O9a6FwdNe1isyaBs;sVo@12KiLNXhS1%>lM{N4jgAzf!s;>=NOtoJA zR#7JTNZ|s`eN-n86ZP7TfROqh{JJGIunt84nO-Od2AR7aZf?(os4k2CJOiwMgQf4~ z?~B)und;|G^ow0YO)Xn+)qw?$S(NCdla80Y;vy)8&>2-3+n~fG_R-I!;L0eeeDx4v z35YP8H8FKq8%aubvR1szb$6^F>eqLo9L!>ZUu0s$4sdM@kh5lL$Ws9r6aL6V z#V9ZknMRQorxkE8R~W#{W3sbUVP)dGYE!UfaIzVb#G4AW(RxLXRO8>PR~G|Xq%}2q z+$YQ6s7^+nhzi=yPfTl7inaRAB@m32`k41F6YY?r(h3KwkZy~sqk{#O+rcc%OVlMD zc$3sN+%}Mws7Xzc7jtgX@bVb;L2}yT4E(R~goO;4=9y4zXQ^%{a|N@h zXl8D0SHxL}UIeQM)Hhre1~mp;zWY_CaF=m}22Lj@Qzth_b~Q_~ew8`H`1KT<4Nt>2 zJUmTH4BFL}sck90cC1=npFx7W)8Ot=C(O6J!R;Agb~!z+CJ&o_Uy$aO@!f2ZN|myr zvk?~NR?O*8@|55!+J|DmoB>H+Cd}=nLY1CIVCuujO?Z-#^Z}5WVaaLHGuDRQy?TZ& zSH+y))BB_S6VmwqZy!_=%Z@NQGx%KUpMULwxr0HX2g_d9Tf>i9<=HBv|nRHbeG=N&#o^(wrA zY?z`jZa)H@W1xaiQy^CXw?z=Tx8OejxYOfUr@7`c#Y~?DdKEXMPruqvPTTRKwJ-?1 z*BWL)or*Z*V@zrJb=DJg=*n8woc(td>h#R4TAAnk8k?%r)Rn>V?FfYR_kZ`nh)Uq)6*=r-V54P;l`c7{6>Yo1C?17V_`=Bo(eDeW>}_om1Arzxojwm4`s1-Ipu%YN0XHoW(g5z&`DdHHI!Dt zLmIi35@D#NWG89&H9SaFxdF7!JRN|Yw2>*823XyD%WtV|xdI;widHvX7mH6Z%3Sm_ zOmPxRUo*M|2TF-Ch76n9nw%56pACOt=lp3ERK=g;!V7@KW+~oZgXt+kP@lbwBR5Ond+8gy3g!$Ykov{Rw9C{k-xBv%G{7rL zhO3LUbCja|F>B5*(-P6FiM9lRn0{U1YZ1t*d2d?%)9!r)OA1l|7V#4Kru;C89MeCu9im z`d;6a&8X|II!oX7L%AX<{*FRy@TQQlFh~`J8;KiUK}RrCAn8Sqr;Iy=70hu5(gBmy zj-uCLH^4K1POW0AlM*_4K2%P%Cu>!LhsqD`>4Lt{NkJhvjQ|K?lwqQxlwWw}W^GiN zzRp~2EOK~B_^+59Bq{iP-ogHRZ2|JgI0Yb|q_!?b>V6ox`0nEtlI)4@Y13bK#McWo zgKi4aSGy&fD4wc=`dEB_htSpz)HxX(X*{D*w9kKT74gvLR*PbncEOTz_M^+@dNa3E z`l!Yrgj9qoAIoU0Dp&S8fBL-e+J08QI%l5UY^|Rf^fP-|MI-0>>0B*;rn~Q8mwF8Cr@&|7D-gpk z?TQ4-Yhe~>G$U`v3i$n3O{a-nTvC zFQpMSIC)Vwd=k)@f+(r$ts_sF0F_i%{^xqkl=kt16y6T0fdfPbR zJB}IgLgBvkTV^fXY}Ym}a}0FVkq-miyy}Rn5U=m{Xsem~IM5hogDtNE@oTx*r0LRw z+`0+)fLx^TVWlqlfA?>`GeCI%C5nL1;&UNFZrXF#EnucgfaoIdo;}-B{1QDKhm`7U zBWVJPr|@MLx6wu`3odRGHM*$(O}&xCjNfvlWhOxW9zaXA1pZq)Gn2A@2Fl)m`ce~# zu2(dFU|zmn81r_Uz?d^XenLmK3(OmYLJDFEizi%w>G(n8a3V!W3Sy*Dgl%AgipVo* z^EY0ctiyhN=ahs;Iag7c4QDKa5i6y*kS=6!BWa`-+yX92^Uik#wI{ke8<4ilJ)+JG61`!qjUq9|os;@+P0QbE8K((NliB z0E5F*H_7|ZN}gV^Lu^59`}PAP+x6X=Yb0!*&Pki}yo`DRohFWH73qUE5ZZDZm$FL7 zfwxvhrH)jZCy|Vj9^(Qo*C0E`-nOLvula1@Q5`bF^7EA~x9)okaDK%}i8My&W(_aB zlvxIBYoe>i2)lS#$4ybHsbWQhEOYPA>*`26aQ*#yoZ$<&b*HgRACtc%?R9@>XlDba z5w26-*r1ZPy%Q1IC$}p68FB96)Akc75dWFrc=oHTC?&-86_2~s^{k@xd9;Tc1^2>~ zdb?Ex7f%)`_AdY<{H+ji zjB8Y^S^JtCzP7F%*z6Ac2>mbdb1+RF6J|!UgLzv1j2EDi&l}#~Y@<`nU-vWJo6AUQ zZ8RMg-#G66A-{^hT490nCSlIs`u5*hWW(hqw|S>HS>U-CrJo z#Tuda(s4=`?E0P4uc9Yx^ zg2j4#%P-Rax-6wh=&o>sOZ)%DqVtit8b;V*Q%@0_4hdsxm{28=b{%?gD4iT6Ry?5b zpRol@4g3Kw5)UD|E@Essh{^-lWN=`J& zDyKznglPCBH{b}?#te^)a2g=#pXJa;E9`=vj@gQ(z0AwH*WP-v5@=!-JxL{Y6YIQ1 z3GoCkF`Ne)R1Nz@MAz&TrOfJ;lTO}-_Z~cnLPp(7ixCpv;#so}aq)|vX30FNv}Qk~ z>p9!Bfl@H6FM?=cPLMVzk%a|f$9*xy{@2t~0XV7(zsHV&^@Lf{l3iQVzI4RNdXnol zcIqnPs{AZvDl9BswYR{`LXK%RANb0Bj%lXUe*6}C98g=EbRLmLFc-tZ-3Mzw@CHxR zv72BUbu|{bs$9MEq>n<@AYlA@E5^RB>K^^#c~8U%_>#gJy#2mX8$I&Y=SoY!uLkh z9VYokMP@ONjr8eTT?%h%HI}%0b(TV778lK3#%t(U& zBWeAFK!G+g;~kx!RC@dFEK^0ImZy2h%W&tstHM!~jNa$kSN5un2U`vQxf3*>g%sxP zKE^esvb~b->Jw*ktovK$q5?7Z&3)zK+O`Q!X9h0At;3t@JmmqQkPtV|Wk7b~B36zC z{-N|hmG&9xz0Fs4GPTWinD)F?Rn@w=W;tCslBgWw9Jx0)c(O;i4r~GC4qvwiS`(bkwHd^ui;WV>j?Nm zAGQ4CKy)=kbS7MRC#(3JkrK(8sKZ%&Q71~+9-~b+4bR32-X~Pi#JtT-FeF?=;ltsw zrjEW$$g^C$7Z4<1n(n^~_rn{JI+)~B^7W6n31dmxyrv&dCzMj;6c2&^!RdPnG@g8Tf4C9Q~)|Zzb{DmhhIF`vg*5gDKS5`M`Z|U{AXEas!O92V11j=He zL&ITtH7&|Toarez#sgZan!@6$$i;EnbIr!U&o*~WEFv1R-A5Z~$MUA;4^B@mt`F+P zaJUipcmgO}eCT!ecpGtJM-_fN3nXE5WBv(SJipdr^qD(;lDz!cT6n-%YsW`_GaW0b zz>RK_OcNsVV2<@J#m{=l(4t4tY2*sK{rO+HX|%-e{h*v^rH=K>;c4fND;=a3yh8iGNh9k=4oT9&R4hG-66!6^jq zD4{AK-A`}z(`oQR-vBQwf#_i0M^E*Kav2^s1ModG;}3vudiT6Q8J+=9{~V|yeXuE( zv_LMf60k1_<^tzZq7X*8B}^`c;X9BEa+6arQ~{QSl2nyoB}8BqfYgCEK~=cyIcT>O zY8Td`0xrjez`>g}I?Gm(J4T6YViPLJAXL|1TUMR=WccT<$Z_tI~O~h z@|f?d&F8LknCseaO|q=Z6;HuBnFF=t?TWJ{ik&wqt~1AHG_=cKiMufhydb{*JgO%a z9i!T@ghWNPF=iE9gsmy!G8VVvp=fLlHX^riSR1bzj?*@h_lZA4#d>vz9_@5^d<6J- zbY6Qv0W$@D=C7oCeLr|lf#(6IBx3O7;~h|s!dC4)dpZ}y)b5Mt&aRn`j)p`qpS@+6 z$mM49$fhpCH>QO+XII^0X_n7*`sHkMV(c^VAabxCF8a20%li8I(`HrA5B=k<(0B76 z5rQ;15b!uoYmP^E0!Lo88NR;0G5oZ)db}e z?TY7O946WFQVMf~W9%BV9p7+l6M*gbi^pgp{FAeyqYonX2U!!=Q|LV$q<7aB2|Mzk zk9jZu-u(B-xi`75eD3K_oGdH~P+_*+^C&IQs*sojE;i-fRS%R=Qv zuGgYM4d9LJC`McGR*F~%eD+b_e&a%9X44|2b(9Wvw!mJCUj(e3VdqyfPAiHn4Of*4pr~{!I!WaMEqKEk zyl-JsG){?Xlw40Gnfmgv6Nrwtox>@|eVJQ_mzKMaeez?7YvLbfOyoxK;ZZiXIYg5R z>HO9s3-=;NNfg+ z)*|`=8@)#3=!2(O6;-J@kI|OAgELhg(9&G&<3DwM#~`yjQP|^PtjCk(>}D%;Cfsh& zDZaQh0p3mY=*fF>L&%@oTD?6jtft!;oNieg2lJv(wN|qP9{sxEAUzNU^j{6Ltk&zt z8;(6UyOEGHi>L0(AAIl$Yx9cw-crh2ONczdEumNd_gEkb`30fH`?_1*^LeQscZZu# zWn1`uQI_)*vvY#^m)6pL6Ir%D=;y`VJC}N%H$(4i&zsd6+m-+S1+h&4w&Rt7Py89+ zJ_?vY=8yfN^GAJsjm_jTJ-uqZ{qOgxAN_go=;3$Z@AI`J=EH981^Lif_^}Nm$s#i` ztWVxp3;YMKaa?uQ(!{NzFsa_Vd++@p8`T4xJz48fl=}yYcTe0nz7vX3e>L!`^H#K^ zqO=eGZ8BLrWrLBOc0%8eIWIpY5?RL-Pw_mznOW@FWT2jxg1sLR_jZtwQ!XK2Tn0v_^~$5^^l?2zESlq1zP;u8XF3?7;Nml@feR>qCtyO+fe$=O?MW!oz{s3#8*a z&2qbnhnrD9uX~oBT-_o=NW^D>$jk{vX3h4zpi0~Vl08Xs>cT*{nyWpQMK6Kpa32th zp5t7(afEG{bD%(?}XhO8)M1J0>S z3Y{^eZU;-k=B24$WH%#|zQw%TDbuGbD^a}`(nCNpqLocj44j9rJ@EHY75K#5#Y^e6 zA4AD_%E7cKi`O|AU5CMZV0AV5&9#lqaF*4Y)2)m~!)2l@sFTxG6Q%HGdS@3JX zeQqyv(XG@mZiVRI&+haF)S*r!^SZ0bef>UvYN?y~NEDG}JFSVy%9vIbUS8eMJR@5* z(3&oywdT1?t9oH!(K7w)J1g_##A|SApdsVq&MwbsJM`H6qBzsJ*b>C&MEA)W4crF@*gCKfc^d3iWoW)353I*(zlF zHz}>wv69~%4fFZ5(4VFj+_hPfx0>}V@27*u{{8NCGiK{-=G&rF-3N3lfzJVa>Xq$S zy0*F1`Bg;KJBFREZ4v#2BDTH~?m2w7jLcat{FZGoU6g2lI9YemJD@HJBw2 z0q}Cjwv3-X4w-%mc0Ujgy(pwgOe;Oe>XF&opc*oi7ck!f>yU7aKn{rS0d)ZME($yi zhk7#%eF$0qT~UO4QlUFJs0xTvQ0`G5n8TI^Yk}G<*B0d9S|8F1#f9AncXcHOD8iA3caA)OxsCFd&m(04b->BZ2e*9s)Ryt3B2x#3%( z<>53>cr{`hiw;pW82qt`@v!AExX^b_2>fB*XlZP(|+f=?s=TX@~ z-4Ol3#!j#DxY4!mgFkbv-tB_z6s_vTMZ`8E7M$;N`VTyWEL1dSEamM5)ymuK@+(Id z=H%-7wR)}dWM{qL`+nz_c3d#8bOJuZT)Xe?_3;zJvzL_0?}A%n5u`fs@6$v5xOj1F ztiG!9)bU+$p+799zWST{Uofh{nL+0c_-s62Tapu`8cK)&{HZBk;75GVJJQ?OxLKyd zUjsYs_innmF_C8FN75*Lk>}-ezUN@ftSM&I<5nJAjn6#dPTXt@(?~OQkG&9eBfiIL=P!Epp32e2YN@D>W{)J_aG^sUE7eqmSwnKaM=YJ8&FoY0YoqW zbBQH?s8P{hXf^0=V$&UE&b>>Ee5cmz|2sQ{pNx~j@?H5f0Q)HbWYy^?RBKo!qi)3+ zW(MF1nD4z5 zTkyM^b!$3sRD@8{KYsiq8FtfKvZ%P^-*u!~a4O?CI|AJ5naj?%f#|-81Nn++1272RmjGV^JTbBPzwhjidvTi;(O0XQ+^I6c+fA#+OZmFh^}JFKo!^CPTOV}z zBNDr}Z$7#HxOv_?Ke9EM#`15JQdvatW}1TZV6zD>^`p7a^(5-OZP@SrC%BiNn%g^( zb%y$9T#{UCw1SH}_l~W@{oLv#DxXB<#pn7((3~d1bRb0A@s|?Y1mIgWZb+5iK=m@# zOOL)jdpi1Da@%{aPd)gczOf{4w*tBI zK%;IK>l=C3aqY2mAa?A%|AK7*zaNQiSaZjBy)|i@Bam^jN~@ti@aLz^@lEvRo^w$+ z+i)s-YHaJ8O<0pz@}H)0fow;N*#LnRBD>-Hu9O%_Q}>TdT>zL8@O@Z8+4IB3cqfUAF^`pFMX4nhBn1cI5Qr3G4kebB z)&@ul3tK&9udb!fIV9b`*%M)JD5o|O`RK)StGLs8%Va0Q;V>ByY#v#3|Fn%vsJ`I^=6p8%i>R9Oax1zIkD|ljK zEj^UvVvfND<)$ITZDZ^GzI8L0DFM9hKFrH?xSmgDX>X?wY$TwhvUzQ=`r?bawbsKl z|rbW-Sd<44VC#V zr##&|?SnKEJhpFde7AjD02%p2p2~f2U1(PD7mn=b!<9tdwNdGzh+ApU$Mkk8fHwUs=<{}U2PRv`3zKc+9PEN3_8 zWnzs7(V2^Jr#9wXP>-H`eTKHAOW!Vm`T)P#VV$9qEvix@7=n3@!@7vLUKXfPO;sjrRW3ss=Zt27m1JzsSe^c_0C_H? z@!vZ+<7}lld1hr5<*TboG#U&ARfn{iSOm62g`g;x6#0P-N&rJ;4hjIZQ3|Pfo*F5h z!@mlYQ|f}JU(ZW92S5u3DfQqXHIP!Sl>b`;F6z(;~fLwWc?V_;B3QU$Vp4S82UIetNrp)fbGh!F;FE4rHFx>V%&eo=uyQdc(e^L z1IxlTi!%Yc;2goIeGb$IKS|47dCqSKi)Axcvb*-B<(lI<YfQ7|2`u!{l6NNXHvvbYg0v`CXtgAm*bhQ5wlw;2jyCYR(g#oZoSv zH3oMdi&MRalxbR@sO}vf_pqE9?=&0KyZ`5reFA-Kv=Ppq`N?{zAoeUm-q8oO_s6st z{>vo=@P51Ro^uZ0yxUGwAH~Y4jHj29x>diuEX1V`1?%3#m^<%NcfKf!N}1-5+;u=^ zz&~sA?KECmjrTqEnHQeA@lqO>UHd~x;w|r6aDa%RcMxTN4eYRYt*u3OWd%Li9&b5X zXFbh)u)9?6v0uGq$rSxvySDxhvEsX33p6UO!2aPuq3944qH{u~4@8~yuYrzrn$YPp zHgo=VQRJ0wH~#AMg!>3DlRLQt18_hd%a{yyPR#PBPko?|w&O1~wh6$uaD4mJQGWkN zFH``q!CN;z`YR>i++<$FD+%kwHAxf_dVsaRC<@cAR0*nyT zNvfv7ul-P(v$p{d-L_{>_-;}b2L}U*ZG!J;S?H$e30aF0{SJjF?ktWTdb}LWk%ic z96H|iOnwbsKY*`E{gpzDV92mb#B3180hP_tzyj=nG#gdrdzj!|6r(p;R8LB?VUI?l z$W10mKsAu8KP)}M)dAbVktVb=jK$k5%lMa>AY)9y<1Z58_TgxcIF5csE8e^19h$_w#FoP_JLUwiRlj{lHcy z&$2=;&P_Q&mue3p);wI54OXkSt}f|H)DgE7ad(khY%kTZ8py5gsS{Xq)3`$~ZqA)J z@eqnmuiAem2>>VM!4(9fycs%K9L}71)cLx-<}T3qy3oNr7YG1m9Py8?EwJnTk8R@5ZmohsuN1cK0nOfU>BWtw%!GkTb^Ek z*W5kP_AB+z|uUxbnJ?wG?{D3CP2nic1&#&L;)er?il_ z5R2Ty;G0@VS0T&Hr4aW8o?d~8=yenTRBFDzKfv~2P;6<%jx-wN7OMI8SDN0(GmHahAS zKp;Qx$XHh3@%~UHkb%tSU{#vN1VdY zDKLSgAs3_?5-=vT9}T&pjG(|mJZuTSNoARy^WA6DIAwLBDmsVh+IsR zl_qj>n5ikEi`krO$T2y~KdgMM+_)pfN2%q#+X=HSY@ zT+0fve|pllvrL@zYxe7gJ^9E79+(*eFZpITKcGtGrS@E4K^A$J%DLzJc(Pu%%VeV) zT2A;M!s`4zkojM(Htbu=T>YvlQ;DexCfm+;g32)?G3&NlmbP6g3a;+=J@$OyuxSg_ zm*KG7g$Z9CpQ!AtSOngKuRQ$meM19o&K+ZEvu1^V=b!wj*E%qTvrgAH`xX32pF{JU z^W`Fq2evoB+}&Zfts8d0jnCe(Q>R8NZ~lS}hwuXr{Lv;jpfFhV@o|>*@NAK$eVpat zZN~`S7^p)o>RsOHOimQpV44uqcS6H3vL_maI8Y>fWB2Ubw5>(&;=}t!JN5nTpIsU2 z_p%SK^oRd^?|iV3CYS}D4T*a3o?b7ao>%{E_{{8Nr2JZ7SN985o-#HPnce)3wGI9O z!cgH@dT*zjG|atKrpd3v?S03fvlV7}`5V<{`26^E>kVaD+z)STu6FYAW5+Zj+P)wQR4*+XIlk;+%@;xT_c+GCnDpffX^a(^jeeGx zP4g7a5}Ei8`nE7s&qGEz%^+=cZH0Z$Zh%>T9v%bvzN%cklKs2M3HIH#kDl%igccxc zgXBSa-4b2B!pFz!=zI3>_BJ8A4K!AOu?PdnL20f~y#`bR6Er_%WkoH0R&!{}uZ*Pyg~mB1;3q_16@j(P=fu2>G10>C$1Is^IyZEoIBC3q>} z*cgxzS`S>)aRHPHPR3jx_rd;{6q0lR5Ixtz zdmNYCV=BOhu?molc~N3JbnstetoZk)x*plgGliH#u#DQx@G#g5oBfQ;1JRfRMC`yI zrh@^$%|*j6F0ZM-ySbJ9hV7A?!6Ny!thB!?kBc5i%!jSfa*NVU`<=4~j_y#D^4(D; zT2a5;J&d{)J1f7kb8_lC8D_T^g?O51(Wq9PytAI&4{DWHZMZeU$g~p0oq>(1Yui>H zqZ+V@m8Ib@HrAI}N56UY6pj)Pc!s}Yet5bRTb9k!o4l`6)m{PSCl`=D-|4^|1#)|& zf<#Su=Un8PB0XaVY_`YAv17msANuoK0dSAgcrs)!7wMTW$G6+LSTA~)RilOp@hd^a z3pbapsMpo%!Shnk6vp^&yV@ADDVkZ^;$Ny+JOF9F03w=S-|9|G=*>~pziAQt)vxKl z*NN~~_V1mH@7z&U5B|ONw{*Me zzgN7Bz){+m;+JVphZ(4-hnn&vg<6iemU@4vRgZfe6$v*&m^J<*q-w;^bi!p_d zX#bf#w>Ut;Y5lQZ-MjM2bKH)<+}I`n-|BJVgt9mL>rC0>?VTQTc-%bpVG!<5AoTEs zpGTLkY5Vh+TlH^37+ij(QkuuOfGr>(v7+%)hg{!hJ65Ov-ka88vtw=3E(35>B%vA zHuTHD^~f^F+PID~jYz6BDdgsn}hu#U%hue&w1(K!Q-{5#)LpO-(Wsm}P;W z?@Q&3 z0Qn$JPzDD5gh?j2HE)zXX!4i-8fqo9zR&VF@^u*@$T? zFVJ+7DJvSN2r|ekjaDTFa(8=w$m8zX+WBM0PSLq$_0C>oZPh$-N9ZCyDo6(y(OZ-( z*)T;$4}vsyW{((>YC?2y}llQHI3qvpsuXK7*wxaFWv~??}I>azy)CR9H;J8 zSWYya#~!`t+T~_-rbVgSZU21pF}!B`@TMEC-7jAG z>r?&3YAP~e+Y^%rwL7&yRMxY68mz6NV!2yRICC>rHgH;fcT|Xr?Lc4N7|=%KwU&L~ zI$YD;7e4V)g?+Db@B8x%&$rs@S9#X|AW`ahAhdUtS^vH`%71UN9l}tigCL+Njr!`@ z%9V@n-q?Ec;#KsIKh_kp;ptyHdc|}HedT%onLSFj<1ZB31mLegn3k%;a&uiG zd!LXx@8^q?2j2VL0lN6}d?!k`xVqj)G#^J%eyA3rYAuAhkL00}A&Iv1l?nW@&ei!9ed)FEg%R9@5<^A(gqQ7F|k81pYka@Zg5S zMec;W-oSO^qjP%7Xt z+N{Yh;!L!vE7B*9 zGt7jT1QK`%a##t#e-6mt+}N1+Y*e5hfb8;jnlZwBNcMYTRD^isH#CYXOU_1Xz6k;PUi(hdom3`AYs+;R;q6!LEgHpRD zH5pSHc#q8jRqB03QY@PdBakAsM}KZ6H-mlh#9>~oQ+vk8z;{4RnVOs3jdo|02nTGX zu{ZqCR$xfL*G$JfkoJ#$na&w$ zlDxjrB=cnspTrz}UQs-aNq?o%?H*p;O1D0H^7UNWIq4vJk~EJNuP)*57!r*Hb417m(cZ>0vkCk;HttHDPt( zP*;T-+~3>kK8l(3Ona;XiVyz~fInxp^>5;Q803c$Cj z?7mZUTMhWjjco$(S0YZ3%4*c-JR4xEdOOH@cHnsX559G|fsP(U7au;in)N&V;V}1v z4vq}tVxH+n1{r%jNsvcvd{e)p;$%~7jE^ZR^lb|g&^k<*m-`7n$1}96S@YlO1}-c5 zyh3?)R;2jX8OBMTm06MIdtp+WC^ZW~u7m+6LRw&VL;nist*kN!UA&e zH#pZnq;vHoSlwsxoa_Z}c?U?sO{Kx~wrHj`y%P0`$)sQW$kc9o2c*xpr!Zls!PIR6 zXJ;g>y|3Q#hiS_DsKRd9(S3bgD(h@k=x#jBZv!=;nq}(KJW~JwlrbAsTpwM5%vo7X zPg$gFz)ZYmVYS}r<13v_^&9=6T!U<;sMOh*Q)U%+7Jg-cLU64^BgGGdB zROHfZ5af7byO(TGp1DnFH6s6)H16OK%voaJx~ciZwLe^>B3M4Y4$Jxio)Rurr#2CZDfl4 zJYViBr0NQ34NKvy%N&7KL+%?4#IzUK3C~F{&Cvwd2q9zSOG%-o(_D93Z9D1@)a_Bi zlYZYi?Sx2=D?M*h4aXkcA6`)N^uOvY}oA$Cq36SI~_D606mZtP*C{I0Deo9=T}W%fW~c$8-D;iSVLAFCk!`u zB)a)9ACD*FnkYyDm#LNHdh-5{BCApoZ?88)2iK39$_ibA{teevF_2sk(-rVpTKC_4 zjC#f;nua@QtgwS*lL6?DfbPgU-sHxU7yn3Ifsy8$v|2g!i}Sy~3!YILnvijih3>#& z_1Qd9ad%h@%QCnFNbd86f{$fSe>Y*(l{PvS?3r?Lnw%z&fFFCdez+Z{@m|j}S zX#d*ADgb~TGV7oyRGa$f7Hea1$Hb8XuDZ)FvCF4oke6~@l*R2?Tz+RSc6S1xW0>&lys$rxwexVC;>{>k zW8h&so=Xpcfh`8ZaxfgoPzf;(87k^^#HK|hPE)ChRBqUob#JXkA1HF-gRK1+04x?} z4_mBBnWKth)7I3a^+k|?J6W9kcXgls_++INqOfkVXsy%&cT1-CY9X&^PD-u(gSMml zSmOeK>=8)HEO@b@k&?@}P-&!^B0)V&by*}z*)AoO3Y~`pdIx|XWT*Hh-(`>H3A!YO zHj~z-<&b@`AV35A0zFVTK4@Y^Z;?q5ltuJSpc-C^jp-8n2 z#zob(=^h{|RZxL0H=5S%n_J?kJkNK+eR!?Cc}k_3cNbU{SFmLd9BNH~3sPek2wz!Z z&#|7*b39(D;urb@{(ex1K5KjYW-aJ?If#E!=Gm@F)vnvN-hwnVf{b14+|{~h({idP z^jN>oe*-}OwtWXD-ng|@9N%1x<64vd$H5@_en6Jh=wgFSHT(?=E8DpRJM(Ox$*dq@PsIE664d+YBQ03W{uUS(qXo4d-iMqiFaO<_10L;@x82VA#n9P za#t3`Px^Xer!U@m+hNT}@i9@V+kq^Zj*&aAU+OG3C#%KA#^6*CpbxEH zJL6bR^~;`DyAD>(yRxL52B+db*siq*`9amQKq;u#&N#vNVQ>JSU07)2Q>WfcOrH@0 z{@qJW=aEm{8*gX-fBCUZ0RBn@)-&Jd!L;*up>*X50J#asq}tU`?h{z=tIo9cH7+eS z{9<~*!sywejIV*-XRA^iqlE3Kzzi<3tOkzr8;CCNOAFJtO|JoLc%W%7fTGVCt2|f6 zsGjEPy{^YT4D*VK!!Chsu>m9?bS;uFs`df2PEt!<0K3xW0-JU7rli|s z<{-ODH0UerTAH=T2nG@WI=n(Q80^)G0aUYk-$E{A{sNI301v1x;4zkElE)jn2?~iv zoAj7E10X{1J3|IcM;~U&`Hc*~97uJ+V!=^{_rUq?RAx$7#IhW14~8-W8)ql*iPCf* zgrMei%_cxf5@16>3gA7%nuB}?!;u-bVa5bb1|B4U1dQx~RIpr-JzUVIK|L6&SnPWk zp|_^MDB_sgTU*)rpkkGHw3va+!wKxyLRqdu)}kQ~10P)gnsU+gz0y8>WUAuo`B+;o zG(0@9(aE27sd%?-$-A3%f7P?;>*4|WEG6iJSx&F=LSEsCJXUMs<8iMzP^&xbz{h!> zE8 zEv0FB6Qy)Dj>KMYS*$QbeITK?Q~=bczilk-ts>`@75W$N-V4b;>eH%KcL}ZL2%B9d zecy%$sk{=U$?ic}IPv*`+v%CRL1P|)U;|8&YE=_TPB~hP<6Z$*9e#zYaM$N7>e&@B zaR}GEysD~>^~!d)$Buo0neCPvXlO?{S1T*uPR|@h_@h660q!~7+QWd9X1d+p>8MK8 z?|1vbdp9@rBWZ>-5WEkBRXdHNbbKqz|L=gLud`Hk#;*8>;L0wZd*onb-s4>#85{+r z=7;*-@_%bi(mUcIODxYD&(ip&?nZHGe%E9+7$(fd<%K9N;(5LE;>Pv#?*U2pU?D_o zJ9y{n;>K3JWgF`GB;1(yu3WuXflttHJ2X7&dX?|VONt%g6=%+SvnG& zPKX@62Xxhiey=zirD98IIo%tS)3z_3s?hQYm~ebl$R7q}?{@I34>0NijG})Pr|8)z z5^}Sv90rqx?J%JPF3LpUv=D(=Rj-utZ5ydUSF$ukG)wemS)#ijvjnilrKb2du;ACq zOw{2#?#^-&z!WQLFu|1t-67-rhb_k6RjH!`kS|Cg*?vf62QraIsD*3g{q1p95^W0@)6WbG=T8 z^{kf2s2e*VF*mt77LZnF1y*L;J+tN?@^lmfc*4_h187vOO4Zt0l*B2~V-1ETS_pxf zfGqIzDL{fjUSihf=Q%RV?_g9$fNubunj$?j6>8J3-&E$q*;tUFmqlL*Sp{De8;KkM z%P|cZ;N7nXAt_u|kXb=`*760@1jzHPz_Jwvq6Y5)mIc@gjtArghKHDX0FW{Q7pw|f zBF%4Xx@J@Y!8wT{NdeP}ARrC!*ohP}0|K=SCT^`P$T}zklTlnA1ST^Ct3pcfbR3r+ zP?-H5I4EyPqx?5)Cfl~e|D+In4HTg}3DsM0-t)s$izpT@!lb&nA)|>&bqEMXMM$)m z7wH>YWA>imR&jT2%(Foi*&GbT^{v(Zvl_couI>FEjMuR!mXBOnPp`mhzNb-jZ&_Q3 zy`-~#>hQu;w012T^KE>_XY6kkNjl4X?;spc8TWEHb)_xWiUCk3iDO6ML}h04GmF;d z;-Yxp52Nt+KTH73J}qi_beVe52@}E75Ifq%S0WHovskpTE8uJux-g9&c6Ue#b&tuP5K# z?Wun@H$FKv9H?bMCW(M?o>I?UGH-GJM?2NBNdLj0n}20&nr58SU9}J|ft2%xm8IeC zdfonIu;|_^6#t~aYSNR#uO>DAgM(Cl*!5^>GqH1Pr8{sfYs;_E^Ld7+i+FVp&&6-p zUh|peSnGYj)ZU;t|4J|x{CX~ukmu!=B>D^5mN$2XYhP(}Y z&@mt+ZO9D4u19-}j?P?EgUo#$JY?Vx{16-;B%!!r*;Ljlv@qNJ5&|O&?*$1ufTkYx z*^d)Y6(WtQ0W!t;^4d~1vJ2pKP@^(R$xUqlGS&Hxd&B+(;(@@bFp|QkHVjk~un$a) zL5{LQjb7UZrJyzLIHMF*VqO-)>=b0S9WZHYD={4j6mjxgCtTywNd53iU<0Vp-UPO7 zb^z4I`yZ`Ci@~}u>IK*a#s@(0ARDm`V+jChGh~C%N($vPjV%P`iC<2e^5W^3c%CKX9Wo^RLb z-Eh+H7lkM~tLrQ6w*RRtm+whp{xoIkjzKSOQ3@tj$*+iBFyp_?voVAW9~CNR%1*BU~=&S*Oe?LJlA$W@bWvA zMau1x@f4YR!mI{pNUdF=(kYwA!t$9LSYB08_;e7}wsC3Jk4 zmRZqYc(@VM={*zjYd>?X#=))9a3%T?PrNFw!Zig$shq!MA9(*B8|(T3&)0UkeUz z8XEv@$2TorBfDB{$6rRQu0Eu!TYk{K`Oeo@Cf_xCHGUytfTQ=4uB8c6U0fO zKsrB3h-`v=zaMaP%wwplDSZxrY*plCBgxPdp(-#|G$q(h6K<-_ahkz@;LTK_&C+xa zfCw-TC&P%hK$_l7Y-Z&gcSNb1O3Dea19FNP1;(Z^n#-2s^`e178@%W}_c$cAfK>1@b-9802#oQ~?R9 zyX`u70)Wl0grqM7*}*Jg*2GCkt~ z4uq7OorVZfZeUyv034zsP$V4if8k8nRjusO_UK$uNR^akg#fUlL4o{$puo3)j7U%| zK(heL!H25}ga&mx3Ggij@9!WEE;cA0rnW%AdyglH$Uue%28L#Uli057odN*;Kz~qH zz)RmC;Kw@_soH_{aFJ$rT)8^jy5;8mt7XaWOG>_p;i5EabO$B0mPyo<`=xAC0K*nwjVDHU~!D_RxEZtD-rO*sGYYQ?=s%r=J>yUKjG${5RAEh1c-T!ZT} z6$IE;lE3&r{P_RUUGGbKJudxczw!?g^I1B3_91=f-oFQ=$T`eG8sVC@(gMsXxK#p& z!Dk1L597RVgzK6``y3SwIF}IKYhDR71>!!aBba9vx>#X&r$wY6dKer9?!)C+-CP%{ zYIntl-~WH8OM*0#K{WS!|8n+)TW;%z`2Z~S%`orqY^N-vJ`r@MRQ6Q>I4*D9xW4gsJkPtQ)Z~RSQ-=p}uk-xp?|#96|5ui~uM-9O|Mj<$ z|J6&y}0Q5^)K0O%>!lO0Z}A_U~J|TFwQNDfFvZc3rQhaJ&dji$IAwb zng7hG*Kgi`|7UFG-8Au)%}=hO&pZSloyBbe@J)7XY%OnrO6XS!lgTlrBp> z4oDY(XARjh1wi7Xf;do`O%|+D@Zu$)Cnyl2Q6U6mOiKwh9ahj)cs)o2`8A5^ZpcLI z1ds-@cH1OkmhVUaL&0-?fDk7HJo`_okoN$2a6C`W198~Yn$CcveF5_IX?RTlPCO3a zwUXy_1~Q#M13H?ea9kh*62$T>0l-(0n6%&=svtL$Jk{8-X$7*tI%L_@boVu-lV-CX z7#D2%`I=1KhO8STdKAI=fOL+A133b8FL2P-Mh^Kn0cC)WvU~w=m;nfzy@Rztqg4TT zRGLTM^TTlm6pAPXs?fiaklgJsS`_}79t8qPx()UvL4 zo<0~SApaLPGJGSj1|c0a{~4`qgL5WEJ}`xWFLehjwFTKf9&KlARwPub*0}Ks;JkY9 z^e2Hz+#L5Ynx1CY$=(7NwxkR$r!IS+qgbST4YHdj9I>wags@i(2e?+xPs>y~iJZXjDXT%eVbN(e3+t zY(jR0HlBm=&r2l)jEj?GvJIR#28(~FZQ3s~JO)aI$8&WH-0G7QP!P^dg$zCK2lno~ z(8P5}jVc}iyk}r-2~?lWhUqo13e?`^VP)&qH(yVb)fk(ocydtLn!?gjt{1qVjNwWR zj|ranQLm`ACwDv{b@Jvo&JG2kckk-e{y*7wXgnG8#02PBJyjN!`|a(Z*|{_n28b83$~d-k7fSN(sJW7`7wD;SUc>c3lA z+7y-H=DC(CqF*Vp(y}b>0O)(vriYu;wU8I4k{jOxQ@5WcqONtYhq}(wm9ff~5#lF{ zp$^K3{{Zu>&2*SVWg5R1B=CF6LhlE}ia--z1T6nPz=$-9pL(&@dYR>3EHkL&`%aw*SAX)*A+fh-U# zuLD5Ekir3=gZ4qo047M??*RxW3XUVgRC7|PBAG|2TdgA3!X^#Y7T6;b0D!*f=%*nC z6c8;V^^3xY@BsktdGw1HcgZhsPww*KAhmB@77g^U?lRG~`1(B`H@eHepQ3xL*U zqjmTeE8%SfG6fW6X~=_u^LHSL0zJjD;mZQ=UX3dB0X&fL=_#-TYPjxj^I5;do~tK|LcIXq#M`o@oUMk)DO34keJt2?9I}hx^5(78WV+sPfiy*#68O2e zA2(Q~dakjfdZ(23ZU8JREA_d!kWVy1^nLwK<|jieL9Ob}?U|lUhxzU#kr#4f#Y6)a z>6br8 zCr>Pt1hez_#>Pz`q&baJTA+=Px@F_6Qt{_)>NjGE!QfGjF*jUWR3(Rts`wa?k10-T zF5II{P&^#M>^W_P-EIc1Q<#sEGff)pBBQOHlVmvE8V^W`>YEu*kqo!1cF_T*w+|GU zEg;%E`@Q18_2sSKwmp4wJ}llcHBqUpu5PV2L+7^#F@Gnk`A@4XyJL&lU%7bkUB&U^ zh`6@?Zb-B5RC)GmaHS8*oURZ1`P-6c^RPtTKPV;t3CQ68DJV9(CTjF63X~PNlinqb z!$h>(SsO0G=arRaQz-a6^w%#FV1ADy&7QR#^h3RF+Hrk)0*Ltz0kR+_$yr`168F+| zdwfR*en@cU)Sc`19Dmfkbg92@&rWAh3}@(5PlbQ>CQIA#UmV*Cz+cHYr5DD3uD6Pk z!<>YvZ57XQ!H#6@hsrr&?$lxL4{|e6rM{GX`C@Z>Haq!cv^Y95TLA8HE=P_ip z_&dQY1us=@7EyV`_vx&{KESbF2ZXp%rs6%8V^*%wr-yxU2xheX0E9kdn?7L9nB_5^ zXJsFf&FMVKF$G{#z#16X^F(PZxe@hE_N=hy+e{S+Hg0zkNw+N3G-R$b#&YQQRnV+K zj-^T!N9+;Mq-T;LuY!L(3CAY#creR+dz#r+)v}#_y)L{gLE`|3mjI2YWGN;9#2v?V z0NA;0Tk6v=f$e6Nrd7y}{VEvz8WMbkZ2(UVK7Su6TrHF?sy-5Nq@#Kbhj2`Qpl~+8 zW3mjAK`?!N@b$^4CyzGk@)3+H8um-D>rJnJJyHUU|KI6y1Y>V>djNaz;nk=O@Qq$* zkT!bYp-`hFz{}@}Axt;+4=@#jB_Oe7nSwq7`M!gJZ-5OWz+PY@k^y-s0J;YGAJxnr z1R$zlKsUrwg*+etx->5{T2-ct94-*Z|I8da5DNy+k8_0_xHdO-4Th65*949!Pr(cl zdZ|eClmtn)*|r;5#)~{tX62@8w44C0|1U0W6#IbWP2}AB`^>T4#9TZ-*+gTMT2+l{ zrPt^Ccp*zm=~}%RJmos*P>~MrT)MnDR&CY68tJZ})c#}=p)JPSK zEdx{fw-e*mZBe4$ucb~Mi;quC_<6rm?unw}VyzZBpkvp8G)l?c803-MwbT;$t6(g$<(l1>c5y zk1J3Nv~ViH^ERkugT5$QE475y`)}m}FA8Zz(U4le%3F?;x6;-4Dh`6F!Jv$(>rcS3 zH=|xD4<8MmDiZoPfj`H#tKV_{*~=f9KRABxV5qlR0d8)t^v+-U?Za2hEHHb|U%xX; z*?%AR^yzTIDbkdJHKyl_bnTYSrTkZS9h_)n!|v1ddN7$J@$cI1?7QLoeo#p13oR>% z7gs@vDxDe#M3Q7hwz)5l;>U>HS|-f@Kr+~*m7w<1mGSxv@c4sy-ut9XC1`?WPjWH< zAAr7i-~bnDQ+XuPx?@YlD{ zJar0-8P9*NW<~o;JqA)G`o$=r?{sW=W}{Ckw(m7Gv)>msLHdm2wY;Am21sF98XW{Y zNWo2ysTW>WJpYw!t2-kKaXTc47lvC|p7f(P>bUqpk?z6T8|3X zz=y;`NS7R2^N9)a!qyPSrYI>0&|44~xqw`tEO2IrK@iqkWzx-iqAV7`%X|a){R<%L zR}poN5=yTF@xV%e=&fW&aTs&zO_iC{`A}-x9og#IC1vP)2&`ru!gD6VK=XP?YNp>S zwP?dE)r(?e!DQ%}qB`3w&jTP%Qm&$*8Yv3Hp#f|SU=EVIplA)?CQ3AFH3`~1=cDd` zfW!dmFG%}Df(Q<3fFd{6KpAg7&jfM<3t2uHN%R0#s2q_27s!RN3Dn3gpm34Ok}A{t zUtwyj12_-tg0!uf*)Gs<6hI}%ht~j`!5mkfr#;AuFQ;HnfK&}OghZ)nM+{2usHcEg zQy@%tK#D4LP-(zIptdcPw(hlUxffwM4Is!s1vk433dhl5tD@GEK>-v?CBiZ$!-Z z;Jwm%J7~p`2*N6RVlXH^(;9P+z$yK3+)o(XMEl`>5ox4aSz7e4;}gh^ztrn?!C10M z7!VF-6AWcgVhdljJ$v71S4T#cvmC4~NUKdlAINaWfw*=VlSeS0XtydHgbG?a`+`6A z;l&>qt?xf`av`P)KMl9q43MUtX&#ZJe!B2kd)p4sUl9y%wy~D}hgC@W^%I~)aK9!lilhW`!hM3{dpaBG^@XEzpN)q3!dN@(b~+nZ!^+&AJj*|lma3BG z(g(uX>#e6fH?&!q+gnl;cW$j*e-VU`uK_W>ooAanf@rx7G&(=sk@i&87w(lYw>=`SVk*WbrTh>-kJ~3%qW|BW%$wC5i+-fh>`fUWhLjFq=b$&!*?X(uMS}x8AQo z7BfqUY-%YFOTmXAH&l&~5Au3qITBQna-S(S^g+C>G#yg}uYrz$f~p84+mI`pt$2}Z zisY46cIa};6#M}YFqj}mlnW$qknv3sASGt?fd-6oAT>8#gA~5eci${5+}Px(KLAqT zW7KLew7QN*>+>>=8g7Sd}nBf)ME>zR_{85M;DFV6`%; z%p0i*LpTQ0$OzOH1R3%SI1ziw!knS$O-N*aAWBHgfm6{c5Rb2dF7pn=QGmI;2vW$M zu1Agmp=iW8a^ZM)S(YTWBizYx*QwOK#>QIruJxtP8|=VA8+{;1o9Ic-(a|Jz=3v;; z_Jn(>-x0Grrrl?H19crNiQD0N{0$%#Buk2>@zy=pg|W)81J_U$Dzhs5KF{^*s>rrL zAi!{6JOvctsbnBGWT{$3ju|K-7l8mezO9#GWU3&h%x5{y!NS=q2%p|e8|xM-+q)*R zuwS2?e7RMx8q4m}QgkO+O#qq}h$*TWx=k}Sa!}Rq@mYoj@ zya4=YF3YX+Z+X`tAIQ-6T9#hwZgrdMz5M@3lg@jYU3qfY%kSVi{R}#N2RFwpGX6bT zq3_5_{tSE;+Hj0A&-mS&%PV569sK_ass0ws0sp$QHQ2Fx=dMkP=}!TL+yk_=LMazz zFS>45?J2m2z`9Ux>MUPgVRYiO8?+X{nL7+p)UUM0oAYs;?K34M49j?HW&K(s==Xc2 z?_B!gqpL^XbGnsX)i>qlw(eX~m56VA?%%#MLB~Hyj~zR)Z2|mOh;0J!YL4S4$=>)l z`&_w5O$$);4Tf0)_=R6xQuD{IxL^MltiN`0*BT3~FEHla1p@U~d8Rh%75kO~<|_b= zGhy4`9ah}0^1S$Vw;t|60)H8-JXeh`7lR<~ z!$g{6J>*!WkVO{&`j7?NH`>-J)4J9-rEA4p($kZ@wR9rRgvycxIgWCGP&C1I*Z}Dg zr*YO)rq&Jo>#Hwp!Yjb^>gpMgom-%kGnpn50Es&uOvaD|n^y(T8>DhufPB4SBzEJs zgG7$f+;q;CxP*J?bFXsM67LJ)}93rUft|qnZG-Mqg=DB#B2I2Z7wcl{AHh z6qFJ=+ELIH3SwE3GA)RyA5d723n7~rWM*x!ii298iXrmCmvbaP=VPKw)3L3%oKxQSO$T>Wc=cYabyy7AF(J_pZS%j#1-O^fK z)4L%PUILHe*|9O}BjD`=o(OHWGj=_d0&z-*DMmFKpuJe+=Ze=ZTdY!!P*vNG&RR(gu>4i|n5r4sP%d!4? zHY+((mghBXFIXE!)>mOXNs{6y&GG(Hp(#*{>SkFC$l9rfdqS3)+Kdw*K(}H+W_Pua z)vD3Q;HckT_4w4lU2mBx`ny|U2R;XnPzzzOfX-W%h~RcOWE>QrY4xB&XQyVoYLVx_ zDRiBe+PE#?q-p99y(=rl{zT9*k&7m>L;&gqSn_IbCz8+BS(y-=5|yhNO8BjCllJPC z^vZ&U<1=hP9ezLpOPQOkor8P+Xpx99E3o%;HwJ(I$iddVo)-#@%R94}Kl5Gp99=h` z`Zas`SelmK3-@&IhCLTJ1(!$iq`NaO%3o;jnEbxp+RCp0xqe$CZ2VpnTR#I7{1#!> zpc2J9E1hLvg8M9@#)_icIow#yUE6=&vaJdx^6l9$|MbM{PBIvXd$7%b8d8=FJD=P+ zUz43=O6{0xuD;_#-#rUA??I5|Qs$-<`v?1{PTiNAhDXO|?y*muIw8<@eA8o_0KA&x zjvT{wQmM&Dp8Jvx&I?fE~31R(V2FU>9&r8p0k?OyHS&q8K56?o;i z&F~yZ%D+_`w{g7{{-(;xh%)&-j^kefqVP!P7NjhpUjgKLYc_^>P@Q z95Ddz`%2bHz`Hlq__~QL>BF>FVT|pzD&8uV*aj=`2)O-sQ%iLqTiq}jt!Zup*%%Ok z9An$5Y2uiU>cT9S8`%MnhJ!yp^1#jZxx|K<-xnDCMGO#kO>0M*-Sik0b<3eG3C3#% zZmkf+GZMUU-J`9Tf;wS&73U_Hq$K>xg&g(>cnZK`Bw&Fikh&m~QP#(tufUvr&a9zt z)J*3-vt=$&C}QFM0EMO)PjhLi3N0Wd0C7N-0I*FrKhHq}3M7F6LO7-?OKv*&0bPLa zB>-(2Nt6M=7ohc<4BprW)01o@!3Umc!%NJDy-1Z}l)2|oHp;bc5CV;=HF#`dIuFVb z0g!;w7!(2tiUoudbXlM>N^MhY~6E6 z5mc;36w7`t)UhwFlG$AhiS}d%RPOTpjJk8ESzFV%kwpMIxRh*{SDoNMdSa^W*XM1jV$tZ;i7I+sgwr^Lh4 zJhl%FI`JG3!l$YYT3=cz?>Fl{2i@+a+io9UHwqhdJQa6))2?^$OC}L`+k=;CQaC@L zBpU~%Yf~$tinMH{X|mASSbOZ?O$W-(`ex#Uj#s4FBe@JeoMh@&@Zk{9Y83bL9wT8j zD{~NBwCt`fXE4y`1ga$9zP)v@vHryD-q~IrN4HrX`E6pkFMvIW1HVe5P86XU-CVtn zCsw+A3M9VN2LA{Ks(<;ncMs=}eZh^|bHPIGUlwLL%!&VU`*S<~jMyIFS9!F{l-FAI z=H+@M<|Yrj_kZ+)gTA4_`phRml`hWtN8hvZN9=&d7UsSJkdnEGOct8`zmSFBZ98U% zIJ|@5blIt^>p{!^4TY^Ii#+`RaqXKyKL3?6mgfO=rvUHTB3=g^ch)c~eh-x3vy$`k zqEJ&D@S=!I21Dy7s-QyNBT{k1G`oXrst=ij>GfmQzaa@} zn#RN_3UCil3Yj+$17Sn92mr^-wzU$$v<=$B$unU z3${D}qG{SwgDl;(Or;*eacbOSoJT`sRtVT>sz*!fl%zGt_z82&_Pd}WaD}R&DF#%d ze*PvMghK(~D$&+PiWZl0us9IP!EZ0%`DO(HA~*I0Fx&E)*EFDQ{CHv+cL;+T0P?@F zML;W0`sGLwFr*+ct1FBQVfqk&M2_sBqRbjgxCq8g2}ARGrtQBr8xcU3k4O6vRNB5O zIR@$hb_*P%+4j|t0syufn;mHe=S@aMg5Y(V0?jr9#>ja>hQ#Zjqu0VSCLKn@zqksDmbe+t^Gi-pog7A@ zN<6=W*SZ(7|E+4=yVIXWnQ3P{=z9+U zv%bsh^<*)f0Do-5=jCDl4)s=stYea>q7-&Ai$4rEJgb&Wcjst_eFG2J8~JZ|?`=BTtI_MUBG^R^mx7 zaQEq=Ai%|SVL838)RsHP9@q@(^?7$UIvxM=FW+VDeb>YGi4)uHf4|wWO#oiqq2Iai zFN=mxQ}>!bz876AR5~6UKcT-dB>>XrrFXuwABCZR;Jq(azV2@)OtZMD&p-|Z8MzAb zf_#o4{WfZ=L=@H>Oy&o7Oo~n7C%^2~gMS48lL0pTIDqt`%m*jI_4R=5zX>w;Td)ic zK>qtYOlp_xzICVMqytX#VL_`6!fgObE*Jy#lKNG=0eMaiUTsBXasu?~EpVHS zU~ce#RCNy7HKvx2^8&nUV!H5{KSS0mD1s~!9+aWs7LWtTNF`{roNFp6#U|@k#v4yF z49WIxQ+W)DIyW!OY&=?Wmv$xNDW~n(h9MeT#@7VEwg^b|VbufwUUeu|S3r?C1USx3 z*Fn>x->3oHWb@b*{DIStIRW6X0C=P!B{ox|Lx`5hU05!U&sAd#X1mBhbxfm?fqI z2KN-tse{vJKHsO+D2iX4oT;~g@AZN9Oh;SMR}S7`pEV@$?LWAF6Ogg*84hJL7X{d2KH<4M zoJH$ZDXjn8oM;`5WBL4WnC=^E?#SUi-wtZq0fkBeHj_12kC7cpS`<730^CRk!!6hI zS8Ye!7Wcb_6I9N@I0nSCmO#LOXl@!@xIbmET|FK@?RZfp~PS9zQ`aY8lNs@yTAoZr4g zcE-BomoKKR3>_^qDnj8)(JLtjq<&YZ-hjMhI5v;VlzMHUt8d57xn2z-&*y>$ ze!4#HUZW0nz@u#RS7N79ll3rwT&=*hG==2c;Y-07K$oMggFM|(X|xDPk^<^$)0-DG zYmmK5_+sEao8@sJAyh2DImmWF)yKB!rUyQDX9xsfxnHrE-ncn)P!CDJs zeOBZ**5ZS>3fKii0su9Q3RG!%Aor3{fBaE-Jm{~EV>jUqo&#`y3Ucf*&%q=F;Pou6 zeCI}sT;M08?Rv8u2$VKOn*=i#q<5cTY-$IPp)+J8ol|_{C&+%0V%CV{7@#k*8~%Qo z=7@^i)Ih*%o34s%)CrK#Q4zstV;&Qx27rNcVXTNOga1&o9MD(`^rig->=Cgr?`s+? z5f}^C$n@Oo@(2H>B{Js0o`2D@wCH!x-FL zFXVB#1GtzE=Qbsk9-5T_kg3<;QtwX-(f}1mI6k&r$7(5%vA`|?vPc*>R4oQpmutzI z*^R?&44HO#0fcu9ZsoUu0&=#|un(k(!d}=a;d!mf+=lo(^{od*zpQA0Ag5n_Cg6 zUv1ZFtR@9{6MV*QHExorAdNah9W+3-$%-|w$R_(68v<)57Si1xb(1P)&ZC^G0J+YZ z&OxK-xavnTcXxSx^1;fd*e?-lEpe);#8-K<^w=b1M@T(0`)7)*StC8yKAs~5lr z?n36oAS-I`7;~=a`1$7{!MLWV6+$w(3$oxl5oF_cVC*_^8(X~*EHLqDD;_G56_}u_M#iucCCU#IM*up=NSC@^We`n&8m6> zo~Bct>HOycg)qvzWmD5KWjB zpbP-$^O%nSUm50r(WrL70I{(kOwK)OHmFR;KMTka9KYE+z=#EAnO|v^1X`mF1&w!a z$Usmt)`>=@Wg!6~5)I7bK!%|hR~s&>HC@!*f;WWp&5W&SOKchr8MVa^De6Y>J5XF4 z*8;B|k9IUPq(zvT3a~H??}98H3(a&Mb3HL(r2v@w1ULcMp|KF>Y@;_7MB zd~mS5f%==9NF-Zu%_e}zRTQkz)+jT4|S}_3c&QH3#~Zc&1N(~MYgmh0 z;iM9m;;m$;Rvn)l6+B;1m|lka|5i{q*5K1{VA$#GZ?~*3vmj`$FD38wJW?yu;@QR+ z+=ZCNgKlv@yzj4$Ps^t)QRs6o4BiI!%7=j)@0M77DIT%_D8|iEcXK$iXYMmv+yBEM&$NPh8(^W^oTbNmfy*hLt29RD-~I(XECD!)m1? z1o<@Z-kXZN|GZUc+-W*oYFzw3VAb7Qx3c(ZHZfbJjAMK?h+^&R2`Q1Z z`dnwOcQp9?9lukcQ}^kk4?baCyuO!x)_I=C5!~enkpB1z7Z0uO~vw zWw*k_QgbOi5Jl>N$+@FWt-5utkm4UANBtZL>^=_YEsz(!7YQ}R2>FcW;!)6ycceq{ z9fOVHrerI5k~;cR^(NhDG%TI;B#QeHfFAhxzSZolBzH$$5!NOG&#KhHQt;Y5i>NW9 zA^RHbf|sWe03f8Sp=E#znMHOM%s5yikhQ`WGBp4otygShTGg5?xQq)dK*u$Hx~6(0 zlLO+7K#@@Z*eHfWHbV^=cpd{VOUSq_RBL#q4YA3zvFFgX*)EqCdeU_2HI<0)Hq~;p z3c(ur@upFaS%>dIW^PsxfM<_G8(GGGhqp9lJsqtE0Fq#eAVx|(C=E7v*A6X_$g&Y} z0Z$sB${N)(2*pN5sFCtb7Ay^rmuB^Y+0hS3OL1c>U-LH&897YuNPt`qLxX&A`&`}BrdW7a-u53X~$&RkuDjp|fyE5mgl89~+hTBCud^P;p~Ji9eojW9MI zmvC`XP**=BGm6gD`OmvaJ^?sr~!Ax1^qQ}UDD$@y&C?pLWCr*SL z_1X1)A1oDJR78o_V7%91Ou^j(jRP(x5LW8>Is?aJAy0XDVBS$+eLMvOp#@}Nq1Cc? zFsv%RqxK~Qz71H}rciXk1Hzo;Dga;ydjZuUquvL8_#7tE0;eYc);QqOs=)nF&ojY< zDJ?WS4}_ttAO&-DTPb-Trg$0dpaO1v-?Bh~Pm8znJpJm9y-l+x!t25DngB(_+r77% zrm38R_m~>?vUuT0>l9|5y%{_0G}S-NbG%Csb(IUpECx9=>~8sv?R^P8Lf;O4+-J*N z-koQa24>{eVN}c*T8DTFB(;>5Z+tK0h%EJaTnIudwc#MEdA1KSlsgMlzE7#TY%~16 ztdPspYb=3vvmXfGRgsbRfNfS}JlFzK&}Wh_g$><|R#%E;VGY@C`zD#_p)+5sMXLvc z-HqKbNT14p|I%W|UOY8lpzZjk#5MtV4aCWxo5|BHZZ#WD?1dnZb9?vdrq|ktEdK9b zOeeolpX}5roM$On^78u5qvuweM<2Yg`c>KQ+Fpo7JfHjep88Z2-6=VUh4Kd=MQXN z>l_$%`=1Y5_G7JSD>v2jc`qC7n%A86sm#*dQ8)8~x<{=_6@&cmiW~sKWVU9V6&zhY z?1HDAnzq2E1rLBZWb?K+%Dw^3N8o6Hi)T49g?9!b%Do7^T?NZ5m^8&drNe7V2gWpf4!G9)!ej#3J8twm{+7QcD7lsK3d10 zj#>)exX`9spy@7H9RspcNVK^GvN{6m0GrwhIcg|mY7BroAyd={=K;rLkKR8F;ddpR z1CW@sG+w(kvLDg}WL`iSvm&A28|^oY?23oeeg^k}s{^OpA67%LzMiAOn%RmTtd61! z*RVA@AW%NKwja*LeL>tH0D{7Duw)L!Nq%r~F@o2pX5R?7?F&Qw;52C3+w8PJiatQW z^MUIGgi9*8eva?jTeXVS%5%xwYJl>h=yOgeNa2Tak4as9HqFL~QKF)(#QTAhRa0)i&vBgPwq!`5f}F+X5?^vTVwVxQNvevSa! zmZZX#uSFkqEjG(JcY(D&1J=)B$%j)sQ!lo5jPDrsyI+J0dWdl1YNF;{ zA!Lxn#j;zcIoNZna9)kAfgTbXYPQ+GJ56)A={nu@=e8=>vNAq-5~_Kt^1#(jtF8~(;8EIyy9eI86`Bm*RQaO6V4VK+ zbc*b9X%O>?L2}W%|Nc>d;Yg*v{9BJbnx^H`X0k0y`8WX58->zlTiIruseN7<-qRh5 z`u*Mv?Vp)>hA%Ju4|yVf7P5IeoCy2Wfn11rM)#IHS7z}ur1$k*g5&R8yRylcg`W%R zZsb>dT?|T?oF%GFS54brl=e#$G(7-sC%pNE+!CAa;t(Ue~#1NZ{TxiF1zypi0m z4Iq|7q_YSzdB|*?05a$TneKwvf=tz9#LCFz8GwpeGH0s%LE`r;$pOKuRz*1?KxvOJO$J z>Tl%h7iJkGoS%NF+TSQ8qh)^}-^Y%n3+PGYyMm3fJISY`G21=*|5>amIGS zSe3k>3>Q{1Lhb07;<$HKl)ss_XANs3>FUu<>6LU%HNxv_jfF> z@Y~^q+MsT&w%Qhk>rt^89TP>dIyn`rD@F{PvE3-=x^C0eFqXDtP^kiND7SEXN^F zmjZtpW3>9Z#v`U@|eS$^*0Md3NB1AWH+W==IcM&lQDe!)yT3s8?{7XLlXCH+kx*cV>70#50dai{npJ zDr~Pdo2_AXg}?-XBmw0$b0Nzt{Hj^nOeeCqoRTmb1a+7~Y^>7$C_Bg1aUTc=5P~>I zPT(7lA12~d!@RQru53X40A6eetik{Q01af{)b&SMFl14HqyR)^1|VcsOwv&WJ%=R2 zfbK{|Fk9Tg?@T{^Bj1Bx3v#-JK%p2Fxmlh&D(tIPOm6|m)=PM6NJHW5O&@&)06=}u zG;;;Bw_%}`tMFd%xWGjy&deHmJOX|5KeHiEG3pm^qxcTea(E0!NphojPC~W~*&(9v zo&X7>Y`TO@-hnp&NfyQ+8|@HiWOp0RakSYFAdzDOTshuolOEHimwI$WGVo|qLo{kN zEFe3#!%-b0C;|rD2})u*I$yKmAlgij2R6sVbOkd1;Yj�|)gsK@JBM#fIYv>tJs{ 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