zeta2 inspector: Feedback box (#40732)

Adds a way to submit feedback about a zeta2 prediction from the
inspector. The telemetry event includes:
- project snapshot (git + unsaved buffer state)
- the full request and response
- user feedback kind and text 

Release Notes:

- N/A
This commit is contained in:
Agus Zubiaga
2025-10-21 07:30:21 -03:00
committed by GitHub
parent 977887b65f
commit b487d2cfe0
11 changed files with 540 additions and 219 deletions

1
Cargo.lock generated
View File

@@ -21533,6 +21533,7 @@ dependencies = [
"serde",
"serde_json",
"settings",
"telemetry",
"text",
"ui",
"ui_input",

View File

@@ -1290,5 +1290,13 @@
"home": "settings_editor::FocusFirstNavEntry",
"end": "settings_editor::FocusLastNavEntry"
}
},
{
"context": "Zeta2Feedback > Editor",
"bindings": {
"enter": "editor::Newline",
"ctrl-enter up": "dev::Zeta2RatePredictionPositive",
"ctrl-enter down": "dev::Zeta2RatePredictionNegative"
}
}
]

View File

@@ -1396,5 +1396,13 @@
"home": "settings_editor::FocusFirstNavEntry",
"end": "settings_editor::FocusLastNavEntry"
}
},
{
"context": "Zeta2Feedback > Editor",
"bindings": {
"enter": "editor::Newline",
"cmd-enter up": "dev::Zeta2RatePredictionPositive",
"cmd-enter down": "dev::Zeta2RatePredictionNegative"
}
}
]

View File

@@ -1319,5 +1319,13 @@
"home": "settings_editor::FocusFirstNavEntry",
"end": "settings_editor::FocusLastNavEntry"
}
},
{
"context": "Zeta2Feedback > Editor",
"bindings": {
"enter": "editor::Newline",
"ctrl-enter up": "dev::Zeta2RatePredictionPositive",
"ctrl-enter down": "dev::Zeta2RatePredictionNegative"
}
}
]

View File

@@ -48,24 +48,10 @@ use util::rel_path::RelPath;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ProjectSnapshot {
pub worktree_snapshots: Vec<WorktreeSnapshot>,
pub worktree_snapshots: Vec<project::telemetry_snapshot::TelemetryWorktreeSnapshot>,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct WorktreeSnapshot {
pub worktree_path: String,
pub git_state: Option<GitState>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct GitState {
pub remote_url: Option<String>,
pub head_sha: Option<String>,
pub current_branch: Option<String>,
pub diff: Option<String>,
}
const RULES_FILE_NAMES: [&str; 9] = [
".rules",
".cursorrules",

View File

@@ -1,9 +1,8 @@
use crate::{
ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DbLanguageModel, DbThread,
DeletePathTool, DiagnosticsTool, EditFileTool, FetchTool, FindPathTool, GitState, GrepTool,
DeletePathTool, DiagnosticsTool, EditFileTool, FetchTool, FindPathTool, GrepTool,
ListDirectoryTool, MovePathTool, NowTool, OpenTool, ProjectSnapshot, ReadFileTool,
SystemPromptTemplate, Template, Templates, TerminalTool, ThinkingTool, WebSearchTool,
WorktreeSnapshot,
};
use acp_thread::{MentionUri, UserMessageId};
use action_log::ActionLog;
@@ -26,7 +25,6 @@ use futures::{
future::Shared,
stream::FuturesUnordered,
};
use git::repository::DiffType;
use gpui::{
App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity,
};
@@ -37,10 +35,7 @@ use language_model::{
LanguageModelToolResultContent, LanguageModelToolSchemaFormat, LanguageModelToolUse,
LanguageModelToolUseId, Role, SelectedModel, StopReason, TokenUsage, ZED_CLOUD_PROVIDER_ID,
};
use project::{
Project,
git_store::{GitStore, RepositoryState},
};
use project::Project;
use prompt_store::ProjectContext;
use schemars::{JsonSchema, Schema};
use serde::{Deserialize, Serialize};
@@ -880,101 +875,17 @@ impl Thread {
project: Entity<Project>,
cx: &mut Context<Self>,
) -> Task<Arc<ProjectSnapshot>> {
let git_store = project.read(cx).git_store().clone();
let worktree_snapshots: Vec<_> = project
.read(cx)
.visible_worktrees(cx)
.map(|worktree| Self::worktree_snapshot(worktree, git_store.clone(), cx))
.collect();
let task = project::telemetry_snapshot::TelemetrySnapshot::new(&project, cx);
cx.spawn(async move |_, _| {
let worktree_snapshots = futures::future::join_all(worktree_snapshots).await;
let snapshot = task.await;
Arc::new(ProjectSnapshot {
worktree_snapshots,
worktree_snapshots: snapshot.worktree_snapshots,
timestamp: Utc::now(),
})
})
}
fn worktree_snapshot(
worktree: Entity<project::Worktree>,
git_store: Entity<GitStore>,
cx: &App,
) -> Task<WorktreeSnapshot> {
cx.spawn(async move |cx| {
// Get worktree path and snapshot
let worktree_info = cx.update(|app_cx| {
let worktree = worktree.read(app_cx);
let path = worktree.abs_path().to_string_lossy().into_owned();
let snapshot = worktree.snapshot();
(path, snapshot)
});
let Ok((worktree_path, _snapshot)) = worktree_info else {
return WorktreeSnapshot {
worktree_path: String::new(),
git_state: None,
};
};
let git_state = git_store
.update(cx, |git_store, cx| {
git_store
.repositories()
.values()
.find(|repo| {
repo.read(cx)
.abs_path_to_repo_path(&worktree.read(cx).abs_path())
.is_some()
})
.cloned()
})
.ok()
.flatten()
.map(|repo| {
repo.update(cx, |repo, _| {
let current_branch =
repo.branch.as_ref().map(|branch| branch.name().to_owned());
repo.send_job(None, |state, _| async move {
let RepositoryState::Local { backend, .. } = state else {
return GitState {
remote_url: None,
head_sha: None,
current_branch,
diff: None,
};
};
let remote_url = backend.remote_url("origin");
let head_sha = backend.head_sha().await;
let diff = backend.diff(DiffType::HeadToWorktree).await.ok();
GitState {
remote_url,
head_sha,
current_branch,
diff,
}
})
})
});
let git_state = match git_state {
Some(git_state) => match git_state.ok() {
Some(git_state) => git_state.await.ok(),
None => None,
},
None => None,
};
WorktreeSnapshot {
worktree_path,
git_state,
}
})
}
pub fn project_context(&self) -> &Entity<ProjectContext> {
&self.project_context
}

View File

@@ -16,6 +16,7 @@ pub mod project_settings;
pub mod search;
mod task_inventory;
pub mod task_store;
pub mod telemetry_snapshot;
pub mod terminals;
pub mod toolchain_store;
pub mod worktree_store;

View File

@@ -0,0 +1,125 @@
use git::repository::DiffType;
use gpui::{App, Entity, Task};
use serde::{Deserialize, Serialize};
use worktree::Worktree;
use crate::{
Project,
git_store::{GitStore, RepositoryState},
};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TelemetrySnapshot {
pub worktree_snapshots: Vec<TelemetryWorktreeSnapshot>,
}
impl TelemetrySnapshot {
pub fn new(project: &Entity<Project>, cx: &mut App) -> Task<TelemetrySnapshot> {
let git_store = project.read(cx).git_store().clone();
let worktree_snapshots: Vec<_> = project
.read(cx)
.visible_worktrees(cx)
.map(|worktree| TelemetryWorktreeSnapshot::new(worktree, git_store.clone(), cx))
.collect();
cx.spawn(async move |_| {
let worktree_snapshots = futures::future::join_all(worktree_snapshots).await;
Self { worktree_snapshots }
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TelemetryWorktreeSnapshot {
pub worktree_path: String,
pub git_state: Option<GitState>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct GitState {
pub remote_url: Option<String>,
pub head_sha: Option<String>,
pub current_branch: Option<String>,
pub diff: Option<String>,
}
impl TelemetryWorktreeSnapshot {
fn new(
worktree: Entity<Worktree>,
git_store: Entity<GitStore>,
cx: &App,
) -> Task<TelemetryWorktreeSnapshot> {
cx.spawn(async move |cx| {
// Get worktree path and snapshot
let worktree_info = cx.update(|app_cx| {
let worktree = worktree.read(app_cx);
let path = worktree.abs_path().to_string_lossy().into_owned();
let snapshot = worktree.snapshot();
(path, snapshot)
});
let Ok((worktree_path, _snapshot)) = worktree_info else {
return TelemetryWorktreeSnapshot {
worktree_path: String::new(),
git_state: None,
};
};
let git_state = git_store
.update(cx, |git_store, cx| {
git_store
.repositories()
.values()
.find(|repo| {
repo.read(cx)
.abs_path_to_repo_path(&worktree.read(cx).abs_path())
.is_some()
})
.cloned()
})
.ok()
.flatten()
.map(|repo| {
repo.update(cx, |repo, _| {
let current_branch =
repo.branch.as_ref().map(|branch| branch.name().to_owned());
repo.send_job(None, |state, _| async move {
let RepositoryState::Local { backend, .. } = state else {
return GitState {
remote_url: None,
head_sha: None,
current_branch,
diff: None,
};
};
let remote_url = backend.remote_url("origin");
let head_sha = backend.head_sha().await;
let diff = backend.diff(DiffType::HeadToWorktree).await.ok();
GitState {
remote_url,
head_sha,
current_branch,
diff,
}
})
})
});
let git_state = match git_state {
Some(git_state) => match git_state.ok() {
Some(git_state) => git_state.await.ok(),
None => None,
},
None => None,
};
TelemetryWorktreeSnapshot {
worktree_path,
git_state,
}
})
}
}

View File

@@ -11,7 +11,7 @@ use edit_prediction_context::{
DeclarationId, DeclarationStyle, EditPredictionContext, EditPredictionContextOptions,
EditPredictionExcerptOptions, EditPredictionScoreOptions, SyntaxIndex, SyntaxIndexState,
};
use feature_flags::FeatureFlag;
use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
use futures::AsyncReadExt as _;
use futures::channel::{mpsc, oneshot};
use gpui::http_client::{AsyncBody, Method};
@@ -32,7 +32,6 @@ use std::sync::Arc;
use std::time::{Duration, Instant};
use thiserror::Error;
use util::rel_path::RelPathBuf;
use util::some_or_debug_panic;
use workspace::notifications::{ErrorMessagePrompt, NotificationId, show_app_notification};
mod prediction;
@@ -103,12 +102,12 @@ pub struct ZetaOptions {
}
pub struct PredictionDebugInfo {
pub context: EditPredictionContext,
pub request: predict_edits_v3::PredictEditsRequest,
pub retrieval_time: TimeDelta,
pub buffer: WeakEntity<Buffer>,
pub position: language::Anchor,
pub local_prompt: Result<String, String>,
pub response_rx: oneshot::Receiver<Result<RequestDebugInfo, String>>,
pub response_rx: oneshot::Receiver<Result<predict_edits_v3::PredictEditsResponse, String>>,
}
pub type RequestDebugInfo = predict_edits_v3::DebugInfo;
@@ -571,6 +570,9 @@ impl Zeta {
if path.pop() { Some(path) } else { None }
});
// TODO data collection
let can_collect_data = cx.is_staff();
let request_task = cx.background_spawn({
let snapshot = snapshot.clone();
let buffer = buffer.clone();
@@ -606,25 +608,22 @@ impl Zeta {
options.max_diagnostic_bytes,
);
let debug_context = debug_tx.map(|tx| (tx, context.clone()));
let request = make_cloud_request(
excerpt_path,
context,
events,
// TODO data collection
false,
can_collect_data,
diagnostic_groups,
diagnostic_groups_truncated,
None,
debug_context.is_some(),
debug_tx.is_some(),
&worktree_snapshots,
index_state.as_deref(),
Some(options.max_prompt_bytes),
options.prompt_format,
);
let debug_response_tx = if let Some((debug_tx, context)) = debug_context {
let debug_response_tx = if let Some(debug_tx) = &debug_tx {
let (response_tx, response_rx) = oneshot::channel();
let local_prompt = PlannedPrompt::populate(&request)
@@ -633,7 +632,7 @@ impl Zeta {
debug_tx
.unbounded_send(PredictionDebugInfo {
context,
request: request.clone(),
retrieval_time,
buffer: buffer.downgrade(),
local_prompt,
@@ -660,12 +659,12 @@ impl Zeta {
if let Some(debug_response_tx) = debug_response_tx {
debug_response_tx
.send(response.as_ref().map_err(|err| err.to_string()).and_then(
|response| match some_or_debug_panic(response.0.debug_info.clone()) {
Some(debug_info) => Ok(debug_info),
None => Err("Missing debug info".to_string()),
},
))
.send(
response
.as_ref()
.map_err(|err| err.to_string())
.map(|response| response.0.clone()),
)
.ok();
}

View File

@@ -27,6 +27,7 @@ multi_buffer.workspace = true
ordered-float.workspace = true
project.workspace = true
serde.workspace = true
telemetry.workspace = true
text.workspace = true
ui.workspace = true
ui_input.workspace = true

View File

@@ -1,37 +1,38 @@
use std::{
cmp::Reverse, collections::hash_map::Entry, path::PathBuf, str::FromStr, sync::Arc,
time::Duration,
};
use std::{cmp::Reverse, path::PathBuf, str::FromStr, sync::Arc, time::Duration};
use chrono::TimeDelta;
use client::{Client, UserStore};
use cloud_llm_client::predict_edits_v3::{DeclarationScoreComponents, PromptFormat};
use cloud_llm_client::predict_edits_v3::{
self, DeclarationScoreComponents, PredictEditsRequest, PredictEditsResponse, PromptFormat,
};
use collections::HashMap;
use editor::{Editor, EditorEvent, EditorMode, ExcerptRange, MultiBuffer};
use feature_flags::FeatureFlagAppExt as _;
use futures::{StreamExt as _, channel::oneshot};
use futures::{FutureExt, StreamExt as _, channel::oneshot, future::Shared};
use gpui::{
CursorStyle, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity,
actions, prelude::*,
CursorStyle, Empty, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task,
WeakEntity, actions, prelude::*,
};
use language::{Buffer, DiskState};
use ordered_float::OrderedFloat;
use project::{Project, WorktreeId};
use ui::{ContextMenu, ContextMenuEntry, DropdownMenu, prelude::*};
use project::{Project, WorktreeId, telemetry_snapshot::TelemetrySnapshot};
use ui::{ButtonLike, ContextMenu, ContextMenuEntry, DropdownMenu, KeyBinding, prelude::*};
use ui_input::SingleLineInput;
use util::{ResultExt, paths::PathStyle, rel_path::RelPath};
use workspace::{Item, SplitDirection, Workspace};
use zeta2::{PredictionDebugInfo, Zeta, Zeta2FeatureFlag, ZetaOptions};
use edit_prediction_context::{
DeclarationStyle, EditPredictionContextOptions, EditPredictionExcerptOptions,
};
use edit_prediction_context::{EditPredictionContextOptions, EditPredictionExcerptOptions};
actions!(
dev,
[
/// Opens the language server protocol logs viewer.
OpenZeta2Inspector
OpenZeta2Inspector,
/// Rate prediction as positive.
Zeta2RatePredictionPositive,
/// Rate prediction as negative.
Zeta2RatePredictionNegative,
]
);
@@ -89,16 +90,24 @@ struct LastPrediction {
buffer: WeakEntity<Buffer>,
position: language::Anchor,
state: LastPredictionState,
request: PredictEditsRequest,
project_snapshot: Shared<Task<Arc<TelemetrySnapshot>>>,
_task: Option<Task<()>>,
}
#[derive(Clone, Copy, PartialEq)]
enum Feedback {
Positive,
Negative,
}
enum LastPredictionState {
Requested,
Success {
inference_time: TimeDelta,
parsing_time: TimeDelta,
prompt_planning_time: TimeDelta,
model_response_editor: Entity<Editor>,
feedback_editor: Entity<Editor>,
feedback: Option<Feedback>,
response: predict_edits_v3::PredictEditsResponse,
},
Failed {
message: String,
@@ -129,7 +138,7 @@ impl Zeta2Inspector {
focus_handle: cx.focus_handle(),
project: project.clone(),
last_prediction: None,
active_view: ActiveView::Context,
active_view: ActiveView::Inference,
max_excerpt_bytes_input: Self::number_input("Max Excerpt Bytes", window, cx),
min_excerpt_bytes_input: Self::number_input("Min Excerpt Bytes", window, cx),
cursor_context_ratio_input: Self::number_input("Cursor Context Ratio", window, cx),
@@ -300,17 +309,23 @@ impl Zeta2Inspector {
let language_registry = self.project.read(cx).languages().clone();
async move |this, cx| {
let mut languages = HashMap::default();
for lang_id in prediction
.context
.declarations
for ext in prediction
.request
.referenced_declarations
.iter()
.map(|snippet| snippet.declaration.identifier().language_id)
.chain(prediction.context.excerpt_text.language_id)
.filter_map(|snippet| snippet.path.extension())
.chain(prediction.request.excerpt_path.extension())
{
if let Entry::Vacant(entry) = languages.entry(lang_id) {
if !languages.contains_key(ext) {
// Most snippets are gonna be the same language,
// so we think it's fine to do this sequentially for now
entry.insert(language_registry.language_for_id(lang_id).await.ok());
languages.insert(
ext.to_owned(),
language_registry
.language_for_name_or_extension(&ext.to_string_lossy())
.await
.ok(),
);
}
}
@@ -333,13 +348,12 @@ impl Zeta2Inspector {
let excerpt_buffer = cx.new(|cx| {
let mut buffer =
Buffer::local(prediction.context.excerpt_text.body, cx);
Buffer::local(prediction.request.excerpt.clone(), cx);
if let Some(language) = prediction
.context
.excerpt_text
.language_id
.as_ref()
.and_then(|id| languages.get(id))
.request
.excerpt_path
.extension()
.and_then(|ext| languages.get(ext))
{
buffer.set_language(language.clone(), cx);
}
@@ -353,25 +367,18 @@ impl Zeta2Inspector {
cx,
);
let mut declarations = prediction.context.declarations.clone();
let mut declarations =
prediction.request.referenced_declarations.clone();
declarations.sort_unstable_by_key(|declaration| {
Reverse(OrderedFloat(
declaration.score(DeclarationStyle::Declaration),
))
Reverse(OrderedFloat(declaration.declaration_score))
});
for snippet in &declarations {
let path = this
.project
.read(cx)
.path_for_entry(snippet.declaration.project_entry_id(), cx);
let snippet_file = Arc::new(ExcerptMetadataFile {
title: RelPath::unix(&format!(
"{} (Score: {})",
path.map(|p| p.path.display(path_style).to_string())
.unwrap_or_else(|| "".to_string()),
snippet.score(DeclarationStyle::Declaration)
snippet.path.display(),
snippet.declaration_score
))
.unwrap()
.into(),
@@ -380,11 +387,10 @@ impl Zeta2Inspector {
});
let excerpt_buffer = cx.new(|cx| {
let mut buffer =
Buffer::local(snippet.declaration.item_text().0, cx);
let mut buffer = Buffer::local(snippet.text.clone(), cx);
buffer.file_updated(snippet_file, cx);
if let Some(language) =
languages.get(&snippet.declaration.identifier().language_id)
if let Some(ext) = snippet.path.extension()
&& let Some(language) = languages.get(ext)
{
buffer.set_language(language.clone(), cx);
}
@@ -399,7 +405,7 @@ impl Zeta2Inspector {
let excerpt_id = excerpt_ids.first().unwrap();
excerpt_score_components
.insert(*excerpt_id, snippet.components.clone());
.insert(*excerpt_id, snippet.score_components.clone());
}
multibuffer
@@ -431,25 +437,91 @@ impl Zeta2Inspector {
if let Some(prediction) = this.last_prediction.as_mut() {
prediction.state = match response {
Ok(Ok(response)) => {
prediction.prompt_editor.update(
cx,
|prompt_editor, cx| {
prompt_editor.set_text(
response.prompt,
window,
if let Some(debug_info) = &response.debug_info {
prediction.prompt_editor.update(
cx,
|prompt_editor, cx| {
prompt_editor.set_text(
debug_info.prompt.as_str(),
window,
cx,
);
},
);
}
let feedback_editor = cx.new(|cx| {
let buffer = cx.new(|cx| {
let mut buffer = Buffer::local("", cx);
buffer.set_language(
markdown_language.clone(),
cx,
);
buffer
});
let buffer =
cx.new(|cx| MultiBuffer::singleton(buffer, cx));
let mut editor = Editor::new(
EditorMode::AutoHeight {
min_lines: 3,
max_lines: None,
},
buffer,
None,
window,
cx,
);
editor.set_placeholder_text(
"Write feedback here",
window,
cx,
);
editor.set_show_line_numbers(false, cx);
editor.set_show_gutter(false, cx);
editor.set_show_scrollbars(false, cx);
editor
});
cx.subscribe_in(
&feedback_editor,
window,
|this, editor, ev, window, cx| match ev {
EditorEvent::BufferEdited => {
if let Some(last_prediction) =
this.last_prediction.as_mut()
&& let LastPredictionState::Success {
feedback: feedback_state,
..
} = &mut last_prediction.state
{
if feedback_state.take().is_some() {
editor.update(cx, |editor, cx| {
editor.set_placeholder_text(
"Write feedback here",
window,
cx,
);
});
cx.notify();
}
}
}
_ => {}
},
);
)
.detach();
LastPredictionState::Success {
prompt_planning_time: response.prompt_planning_time,
inference_time: response.inference_time,
parsing_time: response.parsing_time,
model_response_editor: cx.new(|cx| {
let buffer = cx.new(|cx| {
let mut buffer = Buffer::local(
response.model_response,
response
.debug_info
.as_ref()
.map(|p| p.model_response.as_str())
.unwrap_or(
"(Debug info not available)",
),
cx,
);
buffer.set_language(markdown_language, cx);
@@ -471,6 +543,9 @@ impl Zeta2Inspector {
editor.set_show_scrollbars(false, cx);
editor
}),
feedback_editor,
feedback: None,
response,
}
}
Ok(Err(err)) => {
@@ -486,6 +561,8 @@ impl Zeta2Inspector {
}
});
let project_snapshot_task = TelemetrySnapshot::new(&this.project, cx);
this.last_prediction = Some(LastPrediction {
context_editor,
prompt_editor: cx.new(|cx| {
@@ -508,6 +585,11 @@ impl Zeta2Inspector {
buffer,
position,
state: LastPredictionState::Requested,
project_snapshot: cx
.foreground_executor()
.spawn(async move { Arc::new(project_snapshot_task.await) })
.shared(),
request: prediction.request,
_task: Some(task),
});
cx.notify();
@@ -517,6 +599,103 @@ impl Zeta2Inspector {
});
}
fn handle_rate_positive(
&mut self,
_action: &Zeta2RatePredictionPositive,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.handle_rate(Feedback::Positive, window, cx);
}
fn handle_rate_negative(
&mut self,
_action: &Zeta2RatePredictionNegative,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.handle_rate(Feedback::Negative, window, cx);
}
fn handle_rate(&mut self, kind: Feedback, window: &mut Window, cx: &mut Context<Self>) {
let Some(last_prediction) = self.last_prediction.as_mut() else {
return;
};
if !last_prediction.request.can_collect_data {
return;
}
let project_snapshot_task = last_prediction.project_snapshot.clone();
cx.spawn_in(window, async move |this, cx| {
let project_snapshot = project_snapshot_task.await;
this.update_in(cx, |this, window, cx| {
let Some(last_prediction) = this.last_prediction.as_mut() else {
return;
};
let LastPredictionState::Success {
feedback: feedback_state,
feedback_editor,
model_response_editor,
response,
..
} = &mut last_prediction.state
else {
return;
};
*feedback_state = Some(kind);
let text = feedback_editor.update(cx, |feedback_editor, cx| {
feedback_editor.set_placeholder_text(
"Submitted. Edit or submit again to change.",
window,
cx,
);
feedback_editor.text(cx)
});
cx.notify();
cx.defer_in(window, {
let model_response_editor = model_response_editor.downgrade();
move |_, window, cx| {
if let Some(model_response_editor) = model_response_editor.upgrade() {
model_response_editor.focus_handle(cx).focus(window);
}
}
});
let kind = match kind {
Feedback::Positive => "positive",
Feedback::Negative => "negative",
};
telemetry::event!(
"Zeta2 Prediction Rated",
id = response.request_id,
kind = kind,
text = text,
request = last_prediction.request,
response = response,
project_snapshot = project_snapshot,
);
})
.log_err();
})
.detach();
}
fn focus_feedback(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if let Some(last_prediction) = self.last_prediction.as_mut() {
if let LastPredictionState::Success {
feedback_editor, ..
} = &mut last_prediction.state
{
feedback_editor.focus_handle(cx).focus(window);
}
};
}
fn render_options(&self, window: &mut Window, cx: &mut Context<Self>) -> Div {
v_flex()
.gap_2()
@@ -618,8 +797,9 @@ impl Zeta2Inspector {
),
ui::ToggleButtonSimple::new(
"Inference",
cx.listener(|this, _, _, cx| {
cx.listener(|this, _, window, cx| {
this.active_view = ActiveView::Inference;
this.focus_feedback(window, cx);
cx.notify();
}),
),
@@ -640,21 +820,24 @@ impl Zeta2Inspector {
return None;
};
let (prompt_planning_time, inference_time, parsing_time) = match &prediction.state {
LastPredictionState::Success {
inference_time,
parsing_time,
prompt_planning_time,
let (prompt_planning_time, inference_time, parsing_time) =
if let LastPredictionState::Success {
response:
PredictEditsResponse {
debug_info: Some(debug_info),
..
},
..
} => (
Some(*prompt_planning_time),
Some(*inference_time),
Some(*parsing_time),
),
LastPredictionState::Requested | LastPredictionState::Failed { .. } => {
} = &prediction.state
{
(
Some(debug_info.prompt_planning_time),
Some(debug_info.inference_time),
Some(debug_info.parsing_time),
)
} else {
(None, None, None)
}
};
};
Some(
v_flex()
@@ -690,14 +873,16 @@ impl Zeta2Inspector {
})
}
fn render_content(&self, cx: &mut Context<Self>) -> AnyElement {
fn render_content(&self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
if !cx.has_flag::<Zeta2FeatureFlag>() {
return Self::render_message("`zeta2` feature flag is not enabled");
}
match self.last_prediction.as_ref() {
None => Self::render_message("No prediction"),
Some(prediction) => self.render_last_prediction(prediction, cx).into_any(),
Some(prediction) => self
.render_last_prediction(prediction, window, cx)
.into_any(),
}
}
@@ -710,7 +895,12 @@ impl Zeta2Inspector {
.into_any()
}
fn render_last_prediction(&self, prediction: &LastPrediction, cx: &mut Context<Self>) -> Div {
fn render_last_prediction(
&self,
prediction: &LastPrediction,
window: &mut Window,
cx: &mut Context<Self>,
) -> Div {
match &self.active_view {
ActiveView::Context => div().size_full().child(prediction.context_editor.clone()),
ActiveView::Inference => h_flex()
@@ -748,24 +938,107 @@ impl Zeta2Inspector {
.flex_1()
.gap_2()
.h_full()
.p_4()
.child(ui::Headline::new("Model Response").size(ui::HeadlineSize::XSmall))
.child(match &prediction.state {
LastPredictionState::Success {
model_response_editor,
..
} => model_response_editor.clone().into_any_element(),
LastPredictionState::Requested => v_flex()
.p_4()
.child(
v_flex()
.flex_1()
.gap_2()
.child(Label::new("Loading...").buffer_font(cx))
.into_any(),
LastPredictionState::Failed { message } => v_flex()
.p_4()
.gap_2()
.child(Label::new(message.clone()).buffer_font(cx))
.into_any(),
}),
.child(
ui::Headline::new("Model Response")
.size(ui::HeadlineSize::XSmall),
)
.child(match &prediction.state {
LastPredictionState::Success {
model_response_editor,
..
} => model_response_editor.clone().into_any_element(),
LastPredictionState::Requested => v_flex()
.gap_2()
.child(Label::new("Loading...").buffer_font(cx))
.into_any_element(),
LastPredictionState::Failed { message } => v_flex()
.gap_2()
.max_w_96()
.child(Label::new(message.clone()).buffer_font(cx))
.into_any_element(),
}),
)
.child(ui::divider())
.child(
if prediction.request.can_collect_data
&& let LastPredictionState::Success {
feedback_editor,
feedback: feedback_state,
..
} = &prediction.state
{
v_flex()
.key_context("Zeta2Feedback")
.on_action(cx.listener(Self::handle_rate_positive))
.on_action(cx.listener(Self::handle_rate_negative))
.gap_2()
.p_2()
.child(feedback_editor.clone())
.child(
h_flex()
.justify_end()
.w_full()
.child(
ButtonLike::new("rate-positive")
.when(
*feedback_state == Some(Feedback::Positive),
|this| this.style(ButtonStyle::Filled),
)
.children(
KeyBinding::for_action(
&Zeta2RatePredictionPositive,
window,
cx,
)
.map(|k| k.size(TextSize::Small.rems(cx))),
)
.child(ui::Icon::new(ui::IconName::ThumbsUp))
.on_click(cx.listener(
|this, _, window, cx| {
this.handle_rate_positive(
&Zeta2RatePredictionPositive,
window,
cx,
);
},
)),
)
.child(
ButtonLike::new("rate-negative")
.when(
*feedback_state == Some(Feedback::Negative),
|this| this.style(ButtonStyle::Filled),
)
.children(
KeyBinding::for_action(
&Zeta2RatePredictionNegative,
window,
cx,
)
.map(|k| k.size(TextSize::Small.rems(cx))),
)
.child(ui::Icon::new(ui::IconName::ThumbsDown))
.on_click(cx.listener(
|this, _, window, cx| {
this.handle_rate_negative(
&Zeta2RatePredictionNegative,
window,
cx,
);
},
)),
),
)
.into_any()
} else {
Empty.into_any_element()
},
),
),
}
}
@@ -808,7 +1081,7 @@ impl Render for Zeta2Inspector {
.child(ui::vertical_divider())
.children(self.render_stats()),
)
.child(self.render_content(cx))
.child(self.render_content(window, cx))
}
}