1mod zeta2_context_view;
2
3use std::{str::FromStr, sync::Arc, time::Duration};
4
5use client::{Client, UserStore};
6use cloud_llm_client::predict_edits_v3::PromptFormat;
7use collections::HashMap;
8use editor::{Editor, EditorEvent, EditorMode, MultiBuffer};
9use feature_flags::FeatureFlagAppExt as _;
10use futures::{FutureExt, StreamExt as _, channel::oneshot, future::Shared};
11use gpui::{
12 Empty, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, actions,
13 prelude::*,
14};
15use language::Buffer;
16use project::{Project, telemetry_snapshot::TelemetrySnapshot};
17use ui::{ButtonLike, ContextMenu, ContextMenuEntry, DropdownMenu, KeyBinding, prelude::*};
18use ui_input::InputField;
19use util::ResultExt;
20use workspace::{Item, SplitDirection, Workspace};
21use zeta::{
22 AgenticContextOptions, ContextMode, DEFAULT_SYNTAX_CONTEXT_OPTIONS, EditPredictionInputs, Zeta,
23 Zeta2FeatureFlag, ZetaDebugInfo, ZetaEditPredictionDebugInfo, ZetaOptions,
24};
25
26use edit_prediction_context::{EditPredictionContextOptions, EditPredictionExcerptOptions};
27use zeta2_context_view::Zeta2ContextView;
28
29actions!(
30 dev,
31 [
32 /// Opens the edit prediction context view.
33 OpenZeta2ContextView,
34 /// Opens the edit prediction inspector.
35 OpenZeta2Inspector,
36 /// Rate prediction as positive.
37 Zeta2RatePredictionPositive,
38 /// Rate prediction as negative.
39 Zeta2RatePredictionNegative,
40 ]
41);
42
43pub fn init(cx: &mut App) {
44 cx.observe_new(move |workspace: &mut Workspace, _, _cx| {
45 workspace.register_action(move |workspace, _: &OpenZeta2Inspector, window, cx| {
46 let project = workspace.project();
47 workspace.split_item(
48 SplitDirection::Right,
49 Box::new(cx.new(|cx| {
50 Zeta2Inspector::new(
51 &project,
52 workspace.client(),
53 workspace.user_store(),
54 window,
55 cx,
56 )
57 })),
58 window,
59 cx,
60 );
61 });
62 })
63 .detach();
64
65 cx.observe_new(move |workspace: &mut Workspace, _, _cx| {
66 workspace.register_action(move |workspace, _: &OpenZeta2ContextView, window, cx| {
67 let project = workspace.project();
68 workspace.split_item(
69 SplitDirection::Right,
70 Box::new(cx.new(|cx| {
71 Zeta2ContextView::new(
72 project.clone(),
73 workspace.client(),
74 workspace.user_store(),
75 window,
76 cx,
77 )
78 })),
79 window,
80 cx,
81 );
82 });
83 })
84 .detach();
85}
86
87// TODO show included diagnostics, and events
88
89pub struct Zeta2Inspector {
90 focus_handle: FocusHandle,
91 project: Entity<Project>,
92 last_prediction: Option<LastPrediction>,
93 max_excerpt_bytes_input: Entity<InputField>,
94 min_excerpt_bytes_input: Entity<InputField>,
95 cursor_context_ratio_input: Entity<InputField>,
96 max_prompt_bytes_input: Entity<InputField>,
97 context_mode: ContextModeState,
98 zeta: Entity<Zeta>,
99 _active_editor_subscription: Option<Subscription>,
100 _update_state_task: Task<()>,
101 _receive_task: Task<()>,
102}
103
104pub enum ContextModeState {
105 Llm,
106 Syntax {
107 max_retrieved_declarations: Entity<InputField>,
108 },
109}
110
111struct LastPrediction {
112 prompt_editor: Entity<Editor>,
113 retrieval_time: Duration,
114 request_time: Option<Duration>,
115 buffer: WeakEntity<Buffer>,
116 position: language::Anchor,
117 state: LastPredictionState,
118 inputs: EditPredictionInputs,
119 project_snapshot: Shared<Task<Arc<TelemetrySnapshot>>>,
120 _task: Option<Task<()>>,
121}
122
123#[derive(Clone, Copy, PartialEq)]
124enum Feedback {
125 Positive,
126 Negative,
127}
128
129enum LastPredictionState {
130 Requested,
131 Success {
132 model_response_editor: Entity<Editor>,
133 feedback_editor: Entity<Editor>,
134 feedback: Option<Feedback>,
135 request_id: String,
136 },
137 Failed {
138 message: String,
139 },
140}
141
142impl Zeta2Inspector {
143 pub fn new(
144 project: &Entity<Project>,
145 client: &Arc<Client>,
146 user_store: &Entity<UserStore>,
147 window: &mut Window,
148 cx: &mut Context<Self>,
149 ) -> Self {
150 let zeta = Zeta::global(client, user_store, cx);
151 let mut request_rx = zeta.update(cx, |zeta, _cx| zeta.debug_info());
152
153 let receive_task = cx.spawn_in(window, async move |this, cx| {
154 while let Some(prediction) = request_rx.next().await {
155 this.update_in(cx, |this, window, cx| {
156 this.update_last_prediction(prediction, window, cx)
157 })
158 .ok();
159 }
160 });
161
162 let mut this = Self {
163 focus_handle: cx.focus_handle(),
164 project: project.clone(),
165 last_prediction: None,
166 max_excerpt_bytes_input: Self::number_input("Max Excerpt Bytes", window, cx),
167 min_excerpt_bytes_input: Self::number_input("Min Excerpt Bytes", window, cx),
168 cursor_context_ratio_input: Self::number_input("Cursor Context Ratio", window, cx),
169 max_prompt_bytes_input: Self::number_input("Max Prompt Bytes", window, cx),
170 context_mode: ContextModeState::Llm,
171 zeta: zeta.clone(),
172 _active_editor_subscription: None,
173 _update_state_task: Task::ready(()),
174 _receive_task: receive_task,
175 };
176 this.set_options_state(&zeta.read(cx).options().clone(), window, cx);
177 this
178 }
179
180 fn set_options_state(
181 &mut self,
182 options: &ZetaOptions,
183 window: &mut Window,
184 cx: &mut Context<Self>,
185 ) {
186 let excerpt_options = options.context.excerpt();
187 self.max_excerpt_bytes_input.update(cx, |input, cx| {
188 input.set_text(excerpt_options.max_bytes.to_string(), window, cx);
189 });
190 self.min_excerpt_bytes_input.update(cx, |input, cx| {
191 input.set_text(excerpt_options.min_bytes.to_string(), window, cx);
192 });
193 self.cursor_context_ratio_input.update(cx, |input, cx| {
194 input.set_text(
195 format!(
196 "{:.2}",
197 excerpt_options.target_before_cursor_over_total_bytes
198 ),
199 window,
200 cx,
201 );
202 });
203 self.max_prompt_bytes_input.update(cx, |input, cx| {
204 input.set_text(options.max_prompt_bytes.to_string(), window, cx);
205 });
206
207 match &options.context {
208 ContextMode::Agentic(_) => {
209 self.context_mode = ContextModeState::Llm;
210 }
211 ContextMode::Syntax(_) => {
212 self.context_mode = ContextModeState::Syntax {
213 max_retrieved_declarations: Self::number_input(
214 "Max Retrieved Definitions",
215 window,
216 cx,
217 ),
218 };
219 }
220 }
221 cx.notify();
222 }
223
224 fn set_zeta_options(&mut self, options: ZetaOptions, cx: &mut Context<Self>) {
225 self.zeta.update(cx, |this, _cx| this.set_options(options));
226
227 if let Some(prediction) = self.last_prediction.as_mut() {
228 if let Some(buffer) = prediction.buffer.upgrade() {
229 let position = prediction.position;
230 let project = self.project.clone();
231 self.zeta.update(cx, |zeta, cx| {
232 zeta.refresh_prediction_from_buffer(project, buffer, position, cx)
233 });
234 prediction.state = LastPredictionState::Requested;
235 } else {
236 self.last_prediction.take();
237 }
238 }
239
240 cx.notify();
241 }
242
243 fn number_input(
244 label: &'static str,
245 window: &mut Window,
246 cx: &mut Context<Self>,
247 ) -> Entity<InputField> {
248 let input = cx.new(|cx| {
249 InputField::new(window, cx, "")
250 .label(label)
251 .label_min_width(px(64.))
252 });
253
254 cx.subscribe_in(
255 &input.read(cx).editor().clone(),
256 window,
257 |this, _, event, _window, cx| {
258 let EditorEvent::BufferEdited = event else {
259 return;
260 };
261
262 fn number_input_value<T: FromStr + Default>(
263 input: &Entity<InputField>,
264 cx: &App,
265 ) -> T {
266 input
267 .read(cx)
268 .editor()
269 .read(cx)
270 .text(cx)
271 .parse::<T>()
272 .unwrap_or_default()
273 }
274
275 let zeta_options = this.zeta.read(cx).options().clone();
276
277 let excerpt_options = EditPredictionExcerptOptions {
278 max_bytes: number_input_value(&this.max_excerpt_bytes_input, cx),
279 min_bytes: number_input_value(&this.min_excerpt_bytes_input, cx),
280 target_before_cursor_over_total_bytes: number_input_value(
281 &this.cursor_context_ratio_input,
282 cx,
283 ),
284 };
285
286 let context = match zeta_options.context {
287 ContextMode::Agentic(_context_options) => {
288 ContextMode::Agentic(AgenticContextOptions {
289 excerpt: excerpt_options,
290 })
291 }
292 ContextMode::Syntax(context_options) => {
293 let max_retrieved_declarations = match &this.context_mode {
294 ContextModeState::Llm => {
295 zeta::DEFAULT_SYNTAX_CONTEXT_OPTIONS.max_retrieved_declarations
296 }
297 ContextModeState::Syntax {
298 max_retrieved_declarations,
299 } => number_input_value(max_retrieved_declarations, cx),
300 };
301
302 ContextMode::Syntax(EditPredictionContextOptions {
303 excerpt: excerpt_options,
304 max_retrieved_declarations,
305 ..context_options
306 })
307 }
308 };
309
310 this.set_zeta_options(
311 ZetaOptions {
312 context,
313 max_prompt_bytes: number_input_value(&this.max_prompt_bytes_input, cx),
314 max_diagnostic_bytes: zeta_options.max_diagnostic_bytes,
315 prompt_format: zeta_options.prompt_format,
316 file_indexing_parallelism: zeta_options.file_indexing_parallelism,
317 buffer_change_grouping_interval: zeta_options
318 .buffer_change_grouping_interval,
319 },
320 cx,
321 );
322 },
323 )
324 .detach();
325 input
326 }
327
328 fn update_last_prediction(
329 &mut self,
330 prediction: zeta::ZetaDebugInfo,
331 window: &mut Window,
332 cx: &mut Context<Self>,
333 ) {
334 self._update_state_task = cx.spawn_in(window, {
335 let language_registry = self.project.read(cx).languages().clone();
336 async move |this, cx| {
337 let mut languages = HashMap::default();
338 let ZetaDebugInfo::EditPredictionRequested(prediction) = prediction else {
339 return;
340 };
341 for ext in prediction
342 .inputs
343 .included_files
344 .iter()
345 .filter_map(|file| file.path.extension())
346 {
347 if !languages.contains_key(ext) {
348 // Most snippets are gonna be the same language,
349 // so we think it's fine to do this sequentially for now
350 languages.insert(
351 ext.to_owned(),
352 language_registry
353 .language_for_name_or_extension(&ext.to_string_lossy())
354 .await
355 .ok(),
356 );
357 }
358 }
359
360 let markdown_language = language_registry
361 .language_for_name("Markdown")
362 .await
363 .log_err();
364
365 let json_language = language_registry.language_for_name("Json").await.log_err();
366
367 this.update_in(cx, |this, window, cx| {
368 let ZetaEditPredictionDebugInfo {
369 response_rx,
370 position,
371 buffer,
372 retrieval_time,
373 local_prompt,
374 ..
375 } = prediction;
376
377 let task = cx.spawn_in(window, {
378 let markdown_language = markdown_language.clone();
379 let json_language = json_language.clone();
380 async move |this, cx| {
381 let response = response_rx.await;
382
383 this.update_in(cx, |this, window, cx| {
384 if let Some(prediction) = this.last_prediction.as_mut() {
385 prediction.state = match response {
386 Ok((Ok(response), request_time)) => {
387 prediction.request_time = Some(request_time);
388
389 let feedback_editor = cx.new(|cx| {
390 let buffer = cx.new(|cx| {
391 let mut buffer = Buffer::local("", cx);
392 buffer.set_language(
393 markdown_language.clone(),
394 cx,
395 );
396 buffer
397 });
398 let buffer =
399 cx.new(|cx| MultiBuffer::singleton(buffer, cx));
400 let mut editor = Editor::new(
401 EditorMode::AutoHeight {
402 min_lines: 3,
403 max_lines: None,
404 },
405 buffer,
406 None,
407 window,
408 cx,
409 );
410 editor.set_placeholder_text(
411 "Write feedback here",
412 window,
413 cx,
414 );
415 editor.set_show_line_numbers(false, cx);
416 editor.set_show_gutter(false, cx);
417 editor.set_show_scrollbars(false, cx);
418 editor
419 });
420
421 cx.subscribe_in(
422 &feedback_editor,
423 window,
424 |this, editor, ev, window, cx| match ev {
425 EditorEvent::BufferEdited => {
426 if let Some(last_prediction) =
427 this.last_prediction.as_mut()
428 && let LastPredictionState::Success {
429 feedback: feedback_state,
430 ..
431 } = &mut last_prediction.state
432 {
433 if feedback_state.take().is_some() {
434 editor.update(cx, |editor, cx| {
435 editor.set_placeholder_text(
436 "Write feedback here",
437 window,
438 cx,
439 );
440 });
441 cx.notify();
442 }
443 }
444 }
445 _ => {}
446 },
447 )
448 .detach();
449
450 LastPredictionState::Success {
451 model_response_editor: cx.new(|cx| {
452 let buffer = cx.new(|cx| {
453 let mut buffer = Buffer::local(
454 serde_json::to_string_pretty(&response)
455 .unwrap_or_default(),
456 cx,
457 );
458 buffer.set_language(json_language, cx);
459 buffer
460 });
461 let buffer = cx.new(|cx| {
462 MultiBuffer::singleton(buffer, cx)
463 });
464 let mut editor = Editor::new(
465 EditorMode::full(),
466 buffer,
467 None,
468 window,
469 cx,
470 );
471 editor.set_read_only(true);
472 editor.set_show_line_numbers(false, cx);
473 editor.set_show_gutter(false, cx);
474 editor.set_show_scrollbars(false, cx);
475 editor
476 }),
477 feedback_editor,
478 feedback: None,
479 request_id: response.id.clone(),
480 }
481 }
482 Ok((Err(err), request_time)) => {
483 prediction.request_time = Some(request_time);
484 LastPredictionState::Failed { message: err }
485 }
486 Err(oneshot::Canceled) => LastPredictionState::Failed {
487 message: "Canceled".to_string(),
488 },
489 };
490 }
491 })
492 .ok();
493 }
494 });
495
496 let project_snapshot_task = TelemetrySnapshot::new(&this.project, cx);
497
498 this.last_prediction = Some(LastPrediction {
499 prompt_editor: cx.new(|cx| {
500 let buffer = cx.new(|cx| {
501 let mut buffer =
502 Buffer::local(local_prompt.unwrap_or_else(|err| err), cx);
503 buffer.set_language(markdown_language.clone(), cx);
504 buffer
505 });
506 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
507 let mut editor =
508 Editor::new(EditorMode::full(), buffer, None, window, cx);
509 editor.set_read_only(true);
510 editor.set_show_line_numbers(false, cx);
511 editor.set_show_gutter(false, cx);
512 editor.set_show_scrollbars(false, cx);
513 editor
514 }),
515 retrieval_time,
516 request_time: None,
517 buffer,
518 position,
519 state: LastPredictionState::Requested,
520 project_snapshot: cx
521 .foreground_executor()
522 .spawn(async move { Arc::new(project_snapshot_task.await) })
523 .shared(),
524 inputs: prediction.inputs,
525 _task: Some(task),
526 });
527 cx.notify();
528 })
529 .ok();
530 }
531 });
532 }
533
534 fn handle_rate_positive(
535 &mut self,
536 _action: &Zeta2RatePredictionPositive,
537 window: &mut Window,
538 cx: &mut Context<Self>,
539 ) {
540 self.handle_rate(Feedback::Positive, window, cx);
541 }
542
543 fn handle_rate_negative(
544 &mut self,
545 _action: &Zeta2RatePredictionNegative,
546 window: &mut Window,
547 cx: &mut Context<Self>,
548 ) {
549 self.handle_rate(Feedback::Negative, window, cx);
550 }
551
552 fn handle_rate(&mut self, kind: Feedback, window: &mut Window, cx: &mut Context<Self>) {
553 let Some(last_prediction) = self.last_prediction.as_mut() else {
554 return;
555 };
556
557 let project_snapshot_task = last_prediction.project_snapshot.clone();
558
559 cx.spawn_in(window, async move |this, cx| {
560 let project_snapshot = project_snapshot_task.await;
561 this.update_in(cx, |this, window, cx| {
562 let Some(last_prediction) = this.last_prediction.as_mut() else {
563 return;
564 };
565
566 let LastPredictionState::Success {
567 feedback: feedback_state,
568 feedback_editor,
569 model_response_editor,
570 request_id,
571 ..
572 } = &mut last_prediction.state
573 else {
574 return;
575 };
576
577 *feedback_state = Some(kind);
578 let text = feedback_editor.update(cx, |feedback_editor, cx| {
579 feedback_editor.set_placeholder_text(
580 "Submitted. Edit or submit again to change.",
581 window,
582 cx,
583 );
584 feedback_editor.text(cx)
585 });
586 cx.notify();
587
588 cx.defer_in(window, {
589 let model_response_editor = model_response_editor.downgrade();
590 move |_, window, cx| {
591 if let Some(model_response_editor) = model_response_editor.upgrade() {
592 model_response_editor.focus_handle(cx).focus(window);
593 }
594 }
595 });
596
597 let kind = match kind {
598 Feedback::Positive => "positive",
599 Feedback::Negative => "negative",
600 };
601
602 telemetry::event!(
603 "Zeta2 Prediction Rated",
604 id = request_id,
605 kind = kind,
606 text = text,
607 request = last_prediction.inputs,
608 project_snapshot = project_snapshot,
609 );
610 })
611 .log_err();
612 })
613 .detach();
614 }
615
616 fn render_options(&self, window: &mut Window, cx: &mut Context<Self>) -> Div {
617 v_flex()
618 .gap_2()
619 .child(
620 h_flex()
621 .child(Headline::new("Options").size(HeadlineSize::Small))
622 .justify_between()
623 .child(
624 ui::Button::new("reset-options", "Reset")
625 .disabled(self.zeta.read(cx).options() == &zeta::DEFAULT_OPTIONS)
626 .style(ButtonStyle::Outlined)
627 .size(ButtonSize::Large)
628 .on_click(cx.listener(|this, _, window, cx| {
629 this.set_options_state(&zeta::DEFAULT_OPTIONS, window, cx);
630 })),
631 ),
632 )
633 .child(
634 v_flex()
635 .gap_2()
636 .child(
637 h_flex()
638 .gap_2()
639 .items_end()
640 .child(self.max_excerpt_bytes_input.clone())
641 .child(self.min_excerpt_bytes_input.clone())
642 .child(self.cursor_context_ratio_input.clone())
643 .child(self.render_context_mode_dropdown(window, cx)),
644 )
645 .child(
646 h_flex()
647 .gap_2()
648 .items_end()
649 .children(match &self.context_mode {
650 ContextModeState::Llm => None,
651 ContextModeState::Syntax {
652 max_retrieved_declarations,
653 } => Some(max_retrieved_declarations.clone()),
654 })
655 .child(self.max_prompt_bytes_input.clone())
656 .child(self.render_prompt_format_dropdown(window, cx)),
657 ),
658 )
659 }
660
661 fn render_context_mode_dropdown(&self, window: &mut Window, cx: &mut Context<Self>) -> Div {
662 let this = cx.weak_entity();
663
664 v_flex()
665 .gap_1p5()
666 .child(
667 Label::new("Context Mode")
668 .size(LabelSize::Small)
669 .color(Color::Muted),
670 )
671 .child(
672 DropdownMenu::new(
673 "ep-ctx-mode",
674 match &self.context_mode {
675 ContextModeState::Llm => "LLM-based",
676 ContextModeState::Syntax { .. } => "Syntax",
677 },
678 ContextMenu::build(window, cx, move |menu, _window, _cx| {
679 menu.item(
680 ContextMenuEntry::new("LLM-based")
681 .toggleable(
682 IconPosition::End,
683 matches!(self.context_mode, ContextModeState::Llm),
684 )
685 .handler({
686 let this = this.clone();
687 move |window, cx| {
688 this.update(cx, |this, cx| {
689 let current_options =
690 this.zeta.read(cx).options().clone();
691 match current_options.context.clone() {
692 ContextMode::Agentic(_) => {}
693 ContextMode::Syntax(context_options) => {
694 let options = ZetaOptions {
695 context: ContextMode::Agentic(
696 AgenticContextOptions {
697 excerpt: context_options.excerpt,
698 },
699 ),
700 ..current_options
701 };
702 this.set_options_state(&options, window, cx);
703 this.set_zeta_options(options, cx);
704 }
705 }
706 })
707 .ok();
708 }
709 }),
710 )
711 .item(
712 ContextMenuEntry::new("Syntax")
713 .toggleable(
714 IconPosition::End,
715 matches!(self.context_mode, ContextModeState::Syntax { .. }),
716 )
717 .handler({
718 move |window, cx| {
719 this.update(cx, |this, cx| {
720 let current_options =
721 this.zeta.read(cx).options().clone();
722 match current_options.context.clone() {
723 ContextMode::Agentic(context_options) => {
724 let options = ZetaOptions {
725 context: ContextMode::Syntax(
726 EditPredictionContextOptions {
727 excerpt: context_options.excerpt,
728 ..DEFAULT_SYNTAX_CONTEXT_OPTIONS
729 },
730 ),
731 ..current_options
732 };
733 this.set_options_state(&options, window, cx);
734 this.set_zeta_options(options, cx);
735 }
736 ContextMode::Syntax(_) => {}
737 }
738 })
739 .ok();
740 }
741 }),
742 )
743 }),
744 )
745 .style(ui::DropdownStyle::Outlined),
746 )
747 }
748
749 fn render_prompt_format_dropdown(&self, window: &mut Window, cx: &mut Context<Self>) -> Div {
750 let active_format = self.zeta.read(cx).options().prompt_format;
751 let this = cx.weak_entity();
752
753 v_flex()
754 .gap_1p5()
755 .child(
756 Label::new("Prompt Format")
757 .size(LabelSize::Small)
758 .color(Color::Muted),
759 )
760 .child(
761 DropdownMenu::new(
762 "ep-prompt-format",
763 active_format.to_string(),
764 ContextMenu::build(window, cx, move |mut menu, _window, _cx| {
765 for prompt_format in PromptFormat::iter() {
766 menu = menu.item(
767 ContextMenuEntry::new(prompt_format.to_string())
768 .toggleable(IconPosition::End, active_format == prompt_format)
769 .handler({
770 let this = this.clone();
771 move |_window, cx| {
772 this.update(cx, |this, cx| {
773 let current_options =
774 this.zeta.read(cx).options().clone();
775 let options = ZetaOptions {
776 prompt_format,
777 ..current_options
778 };
779 this.set_zeta_options(options, cx);
780 })
781 .ok();
782 }
783 }),
784 )
785 }
786 menu
787 }),
788 )
789 .style(ui::DropdownStyle::Outlined),
790 )
791 }
792
793 fn render_stats(&self) -> Option<Div> {
794 let Some(prediction) = self.last_prediction.as_ref() else {
795 return None;
796 };
797
798 Some(
799 v_flex()
800 .p_4()
801 .gap_2()
802 .min_w(px(160.))
803 .child(Headline::new("Stats").size(HeadlineSize::Small))
804 .child(Self::render_duration(
805 "Context retrieval",
806 Some(prediction.retrieval_time),
807 ))
808 .child(Self::render_duration("Request", prediction.request_time)),
809 )
810 }
811
812 fn render_duration(name: &'static str, time: Option<Duration>) -> Div {
813 h_flex()
814 .gap_1()
815 .child(Label::new(name).color(Color::Muted).size(LabelSize::Small))
816 .child(match time {
817 Some(time) => Label::new(if time.as_micros() >= 1000 {
818 format!("{} ms", time.as_millis())
819 } else {
820 format!("{} µs", time.as_micros())
821 })
822 .size(LabelSize::Small),
823 None => Label::new("...").size(LabelSize::Small),
824 })
825 }
826
827 fn render_content(&self, _: &mut Window, cx: &mut Context<Self>) -> AnyElement {
828 if !cx.has_flag::<Zeta2FeatureFlag>() {
829 return Self::render_message("`zeta2` feature flag is not enabled");
830 }
831
832 match self.last_prediction.as_ref() {
833 None => Self::render_message("No prediction"),
834 Some(prediction) => self.render_last_prediction(prediction, cx).into_any(),
835 }
836 }
837
838 fn render_message(message: impl Into<SharedString>) -> AnyElement {
839 v_flex()
840 .size_full()
841 .justify_center()
842 .items_center()
843 .child(Label::new(message).size(LabelSize::Large))
844 .into_any()
845 }
846
847 fn render_last_prediction(&self, prediction: &LastPrediction, cx: &mut Context<Self>) -> Div {
848 h_flex()
849 .items_start()
850 .w_full()
851 .flex_1()
852 .border_t_1()
853 .border_color(cx.theme().colors().border)
854 .bg(cx.theme().colors().editor_background)
855 .child(
856 v_flex()
857 .flex_1()
858 .gap_2()
859 .p_4()
860 .h_full()
861 .child(
862 h_flex()
863 .justify_between()
864 .child(ui::Headline::new("Prompt").size(ui::HeadlineSize::XSmall))
865 .child(match prediction.state {
866 LastPredictionState::Requested
867 | LastPredictionState::Failed { .. } => ui::Chip::new("Local")
868 .bg_color(cx.theme().status().warning_background)
869 .label_color(Color::Success),
870 LastPredictionState::Success { .. } => ui::Chip::new("Cloud")
871 .bg_color(cx.theme().status().success_background)
872 .label_color(Color::Success),
873 }),
874 )
875 .child(prediction.prompt_editor.clone()),
876 )
877 .child(ui::vertical_divider())
878 .child(
879 v_flex()
880 .flex_1()
881 .gap_2()
882 .h_full()
883 .child(
884 v_flex()
885 .flex_1()
886 .gap_2()
887 .p_4()
888 .child(
889 ui::Headline::new("Model Response").size(ui::HeadlineSize::XSmall),
890 )
891 .child(match &prediction.state {
892 LastPredictionState::Success {
893 model_response_editor,
894 ..
895 } => model_response_editor.clone().into_any_element(),
896 LastPredictionState::Requested => v_flex()
897 .gap_2()
898 .child(Label::new("Loading...").buffer_font(cx))
899 .into_any_element(),
900 LastPredictionState::Failed { message } => v_flex()
901 .gap_2()
902 .max_w_96()
903 .child(Label::new(message.clone()).buffer_font(cx))
904 .into_any_element(),
905 }),
906 )
907 .child(ui::divider())
908 .child(
909 if let LastPredictionState::Success {
910 feedback_editor,
911 feedback: feedback_state,
912 ..
913 } = &prediction.state
914 {
915 v_flex()
916 .key_context("Zeta2Feedback")
917 .on_action(cx.listener(Self::handle_rate_positive))
918 .on_action(cx.listener(Self::handle_rate_negative))
919 .gap_2()
920 .p_2()
921 .child(feedback_editor.clone())
922 .child(
923 h_flex()
924 .justify_end()
925 .w_full()
926 .child(
927 ButtonLike::new("rate-positive")
928 .when(
929 *feedback_state == Some(Feedback::Positive),
930 |this| this.style(ButtonStyle::Filled),
931 )
932 .child(
933 KeyBinding::for_action(
934 &Zeta2RatePredictionPositive,
935 cx,
936 )
937 .size(TextSize::Small.rems(cx)),
938 )
939 .child(ui::Icon::new(ui::IconName::ThumbsUp))
940 .on_click(cx.listener(|this, _, window, cx| {
941 this.handle_rate_positive(
942 &Zeta2RatePredictionPositive,
943 window,
944 cx,
945 );
946 })),
947 )
948 .child(
949 ButtonLike::new("rate-negative")
950 .when(
951 *feedback_state == Some(Feedback::Negative),
952 |this| this.style(ButtonStyle::Filled),
953 )
954 .child(
955 KeyBinding::for_action(
956 &Zeta2RatePredictionNegative,
957 cx,
958 )
959 .size(TextSize::Small.rems(cx)),
960 )
961 .child(ui::Icon::new(ui::IconName::ThumbsDown))
962 .on_click(cx.listener(|this, _, window, cx| {
963 this.handle_rate_negative(
964 &Zeta2RatePredictionNegative,
965 window,
966 cx,
967 );
968 })),
969 ),
970 )
971 .into_any()
972 } else {
973 Empty.into_any_element()
974 },
975 ),
976 )
977 }
978}
979
980impl Focusable for Zeta2Inspector {
981 fn focus_handle(&self, _cx: &App) -> FocusHandle {
982 self.focus_handle.clone()
983 }
984}
985
986impl Item for Zeta2Inspector {
987 type Event = ();
988
989 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
990 "Zeta2 Inspector".into()
991 }
992}
993
994impl EventEmitter<()> for Zeta2Inspector {}
995
996impl Render for Zeta2Inspector {
997 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
998 v_flex()
999 .size_full()
1000 .bg(cx.theme().colors().editor_background)
1001 .child(
1002 h_flex()
1003 .w_full()
1004 .child(
1005 v_flex()
1006 .flex_1()
1007 .p_4()
1008 .h_full()
1009 .justify_between()
1010 .child(self.render_options(window, cx))
1011 .gap_4(),
1012 )
1013 .child(ui::vertical_divider())
1014 .children(self.render_stats()),
1015 )
1016 .child(self.render_content(window, cx))
1017 }
1018}