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