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_immediate(
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_immediate(
464 json_language,
465 cx,
466 );
467 buffer
468 });
469 let buffer = cx.new(|cx| {
470 MultiBuffer::singleton(buffer, cx)
471 });
472 let mut editor = Editor::new(
473 EditorMode::full(),
474 buffer,
475 None,
476 window,
477 cx,
478 );
479 editor.set_read_only(true);
480 editor.set_show_line_numbers(false, cx);
481 editor.set_show_gutter(false, cx);
482 editor.set_show_scrollbars(false, cx);
483 editor
484 }),
485 feedback_editor,
486 feedback: None,
487 request_id: response.id.clone(),
488 }
489 }
490 Ok((Err(err), request_time)) => {
491 prediction.request_time = Some(request_time);
492 LastPredictionState::Failed { message: err }
493 }
494 Err(oneshot::Canceled) => LastPredictionState::Failed {
495 message: "Canceled".to_string(),
496 },
497 };
498 }
499 })
500 .ok();
501 }
502 });
503
504 let project_snapshot_task = TelemetrySnapshot::new(&this.project, cx);
505
506 this.last_prediction = Some(LastPrediction {
507 prompt_editor: cx.new(|cx| {
508 let buffer = cx.new(|cx| {
509 let mut buffer =
510 Buffer::local(local_prompt.unwrap_or_else(|err| err), cx);
511 buffer.set_language_immediate(markdown_language.clone(), cx);
512 buffer
513 });
514 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
515 let mut editor =
516 Editor::new(EditorMode::full(), buffer, None, window, cx);
517 editor.set_read_only(true);
518 editor.set_show_line_numbers(false, cx);
519 editor.set_show_gutter(false, cx);
520 editor.set_show_scrollbars(false, cx);
521 editor
522 }),
523 retrieval_time,
524 request_time: None,
525 buffer,
526 position,
527 state: LastPredictionState::Requested,
528 project_snapshot: cx
529 .foreground_executor()
530 .spawn(async move { Arc::new(project_snapshot_task.await) })
531 .shared(),
532 inputs: prediction.inputs,
533 _task: Some(task),
534 });
535 cx.notify();
536 })
537 .ok();
538 }
539 });
540 }
541
542 fn handle_rate_positive(
543 &mut self,
544 _action: &Zeta2RatePredictionPositive,
545 window: &mut Window,
546 cx: &mut Context<Self>,
547 ) {
548 self.handle_rate(Feedback::Positive, window, cx);
549 }
550
551 fn handle_rate_negative(
552 &mut self,
553 _action: &Zeta2RatePredictionNegative,
554 window: &mut Window,
555 cx: &mut Context<Self>,
556 ) {
557 self.handle_rate(Feedback::Negative, window, cx);
558 }
559
560 fn handle_rate(&mut self, kind: Feedback, window: &mut Window, cx: &mut Context<Self>) {
561 let Some(last_prediction) = self.last_prediction.as_mut() else {
562 return;
563 };
564
565 let project_snapshot_task = last_prediction.project_snapshot.clone();
566
567 cx.spawn_in(window, async move |this, cx| {
568 let project_snapshot = project_snapshot_task.await;
569 this.update_in(cx, |this, window, cx| {
570 let Some(last_prediction) = this.last_prediction.as_mut() else {
571 return;
572 };
573
574 let LastPredictionState::Success {
575 feedback: feedback_state,
576 feedback_editor,
577 model_response_editor,
578 request_id,
579 ..
580 } = &mut last_prediction.state
581 else {
582 return;
583 };
584
585 *feedback_state = Some(kind);
586 let text = feedback_editor.update(cx, |feedback_editor, cx| {
587 feedback_editor.set_placeholder_text(
588 "Submitted. Edit or submit again to change.",
589 window,
590 cx,
591 );
592 feedback_editor.text(cx)
593 });
594 cx.notify();
595
596 cx.defer_in(window, {
597 let model_response_editor = model_response_editor.downgrade();
598 move |_, window, cx| {
599 if let Some(model_response_editor) = model_response_editor.upgrade() {
600 model_response_editor.focus_handle(cx).focus(window);
601 }
602 }
603 });
604
605 let kind = match kind {
606 Feedback::Positive => "positive",
607 Feedback::Negative => "negative",
608 };
609
610 telemetry::event!(
611 "Zeta2 Prediction Rated",
612 id = request_id,
613 kind = kind,
614 text = text,
615 request = last_prediction.inputs,
616 project_snapshot = project_snapshot,
617 );
618 })
619 .log_err();
620 })
621 .detach();
622 }
623
624 fn render_options(&self, window: &mut Window, cx: &mut Context<Self>) -> Div {
625 v_flex()
626 .gap_2()
627 .child(
628 h_flex()
629 .child(Headline::new("Options").size(HeadlineSize::Small))
630 .justify_between()
631 .child(
632 ui::Button::new("reset-options", "Reset")
633 .disabled(self.zeta.read(cx).options() == &zeta::DEFAULT_OPTIONS)
634 .style(ButtonStyle::Outlined)
635 .size(ButtonSize::Large)
636 .on_click(cx.listener(|this, _, window, cx| {
637 this.set_options_state(&zeta::DEFAULT_OPTIONS, window, cx);
638 })),
639 ),
640 )
641 .child(
642 v_flex()
643 .gap_2()
644 .child(
645 h_flex()
646 .gap_2()
647 .items_end()
648 .child(self.max_excerpt_bytes_input.clone())
649 .child(self.min_excerpt_bytes_input.clone())
650 .child(self.cursor_context_ratio_input.clone())
651 .child(self.render_context_mode_dropdown(window, cx)),
652 )
653 .child(
654 h_flex()
655 .gap_2()
656 .items_end()
657 .children(match &self.context_mode {
658 ContextModeState::Llm => None,
659 ContextModeState::Syntax {
660 max_retrieved_declarations,
661 } => Some(max_retrieved_declarations.clone()),
662 })
663 .child(self.max_prompt_bytes_input.clone())
664 .child(self.render_prompt_format_dropdown(window, cx)),
665 ),
666 )
667 }
668
669 fn render_context_mode_dropdown(&self, window: &mut Window, cx: &mut Context<Self>) -> Div {
670 let this = cx.weak_entity();
671
672 v_flex()
673 .gap_1p5()
674 .child(
675 Label::new("Context Mode")
676 .size(LabelSize::Small)
677 .color(Color::Muted),
678 )
679 .child(
680 DropdownMenu::new(
681 "ep-ctx-mode",
682 match &self.context_mode {
683 ContextModeState::Llm => "LLM-based",
684 ContextModeState::Syntax { .. } => "Syntax",
685 },
686 ContextMenu::build(window, cx, move |menu, _window, _cx| {
687 menu.item(
688 ContextMenuEntry::new("LLM-based")
689 .toggleable(
690 IconPosition::End,
691 matches!(self.context_mode, ContextModeState::Llm),
692 )
693 .handler({
694 let this = this.clone();
695 move |window, cx| {
696 this.update(cx, |this, cx| {
697 let current_options =
698 this.zeta.read(cx).options().clone();
699 match current_options.context.clone() {
700 ContextMode::Agentic(_) => {}
701 ContextMode::Syntax(context_options) => {
702 let options = ZetaOptions {
703 context: ContextMode::Agentic(
704 AgenticContextOptions {
705 excerpt: context_options.excerpt,
706 },
707 ),
708 ..current_options
709 };
710 this.set_options_state(&options, window, cx);
711 this.set_zeta_options(options, cx);
712 }
713 }
714 })
715 .ok();
716 }
717 }),
718 )
719 .item(
720 ContextMenuEntry::new("Syntax")
721 .toggleable(
722 IconPosition::End,
723 matches!(self.context_mode, ContextModeState::Syntax { .. }),
724 )
725 .handler({
726 move |window, cx| {
727 this.update(cx, |this, cx| {
728 let current_options =
729 this.zeta.read(cx).options().clone();
730 match current_options.context.clone() {
731 ContextMode::Agentic(context_options) => {
732 let options = ZetaOptions {
733 context: ContextMode::Syntax(
734 EditPredictionContextOptions {
735 excerpt: context_options.excerpt,
736 ..DEFAULT_SYNTAX_CONTEXT_OPTIONS
737 },
738 ),
739 ..current_options
740 };
741 this.set_options_state(&options, window, cx);
742 this.set_zeta_options(options, cx);
743 }
744 ContextMode::Syntax(_) => {}
745 }
746 })
747 .ok();
748 }
749 }),
750 )
751 }),
752 )
753 .style(ui::DropdownStyle::Outlined),
754 )
755 }
756
757 fn render_prompt_format_dropdown(&self, window: &mut Window, cx: &mut Context<Self>) -> Div {
758 let active_format = self.zeta.read(cx).options().prompt_format;
759 let this = cx.weak_entity();
760
761 v_flex()
762 .gap_1p5()
763 .child(
764 Label::new("Prompt Format")
765 .size(LabelSize::Small)
766 .color(Color::Muted),
767 )
768 .child(
769 DropdownMenu::new(
770 "ep-prompt-format",
771 active_format.to_string(),
772 ContextMenu::build(window, cx, move |mut menu, _window, _cx| {
773 for prompt_format in PromptFormat::iter() {
774 menu = menu.item(
775 ContextMenuEntry::new(prompt_format.to_string())
776 .toggleable(IconPosition::End, active_format == prompt_format)
777 .handler({
778 let this = this.clone();
779 move |_window, cx| {
780 this.update(cx, |this, cx| {
781 let current_options =
782 this.zeta.read(cx).options().clone();
783 let options = ZetaOptions {
784 prompt_format,
785 ..current_options
786 };
787 this.set_zeta_options(options, cx);
788 })
789 .ok();
790 }
791 }),
792 )
793 }
794 menu
795 }),
796 )
797 .style(ui::DropdownStyle::Outlined),
798 )
799 }
800
801 fn render_stats(&self) -> Option<Div> {
802 let Some(prediction) = self.last_prediction.as_ref() else {
803 return None;
804 };
805
806 Some(
807 v_flex()
808 .p_4()
809 .gap_2()
810 .min_w(px(160.))
811 .child(Headline::new("Stats").size(HeadlineSize::Small))
812 .child(Self::render_duration(
813 "Context retrieval",
814 Some(prediction.retrieval_time),
815 ))
816 .child(Self::render_duration("Request", prediction.request_time)),
817 )
818 }
819
820 fn render_duration(name: &'static str, time: Option<Duration>) -> Div {
821 h_flex()
822 .gap_1()
823 .child(Label::new(name).color(Color::Muted).size(LabelSize::Small))
824 .child(match time {
825 Some(time) => Label::new(if time.as_micros() >= 1000 {
826 format!("{} ms", time.as_millis())
827 } else {
828 format!("{} µs", time.as_micros())
829 })
830 .size(LabelSize::Small),
831 None => Label::new("...").size(LabelSize::Small),
832 })
833 }
834
835 fn render_content(&self, _: &mut Window, cx: &mut Context<Self>) -> AnyElement {
836 if !cx.has_flag::<Zeta2FeatureFlag>() {
837 return Self::render_message("`zeta2` feature flag is not enabled");
838 }
839
840 match self.last_prediction.as_ref() {
841 None => Self::render_message("No prediction"),
842 Some(prediction) => self.render_last_prediction(prediction, cx).into_any(),
843 }
844 }
845
846 fn render_message(message: impl Into<SharedString>) -> AnyElement {
847 v_flex()
848 .size_full()
849 .justify_center()
850 .items_center()
851 .child(Label::new(message).size(LabelSize::Large))
852 .into_any()
853 }
854
855 fn render_last_prediction(&self, prediction: &LastPrediction, cx: &mut Context<Self>) -> Div {
856 h_flex()
857 .items_start()
858 .w_full()
859 .flex_1()
860 .border_t_1()
861 .border_color(cx.theme().colors().border)
862 .bg(cx.theme().colors().editor_background)
863 .child(
864 v_flex()
865 .flex_1()
866 .gap_2()
867 .p_4()
868 .h_full()
869 .child(
870 h_flex()
871 .justify_between()
872 .child(ui::Headline::new("Prompt").size(ui::HeadlineSize::XSmall))
873 .child(match prediction.state {
874 LastPredictionState::Requested
875 | LastPredictionState::Failed { .. } => ui::Chip::new("Local")
876 .bg_color(cx.theme().status().warning_background)
877 .label_color(Color::Success),
878 LastPredictionState::Success { .. } => ui::Chip::new("Cloud")
879 .bg_color(cx.theme().status().success_background)
880 .label_color(Color::Success),
881 }),
882 )
883 .child(prediction.prompt_editor.clone()),
884 )
885 .child(ui::vertical_divider())
886 .child(
887 v_flex()
888 .flex_1()
889 .gap_2()
890 .h_full()
891 .child(
892 v_flex()
893 .flex_1()
894 .gap_2()
895 .p_4()
896 .child(
897 ui::Headline::new("Model Response").size(ui::HeadlineSize::XSmall),
898 )
899 .child(match &prediction.state {
900 LastPredictionState::Success {
901 model_response_editor,
902 ..
903 } => model_response_editor.clone().into_any_element(),
904 LastPredictionState::Requested => v_flex()
905 .gap_2()
906 .child(Label::new("Loading...").buffer_font(cx))
907 .into_any_element(),
908 LastPredictionState::Failed { message } => v_flex()
909 .gap_2()
910 .max_w_96()
911 .child(Label::new(message.clone()).buffer_font(cx))
912 .into_any_element(),
913 }),
914 )
915 .child(ui::divider())
916 .child(
917 if let LastPredictionState::Success {
918 feedback_editor,
919 feedback: feedback_state,
920 ..
921 } = &prediction.state
922 {
923 v_flex()
924 .key_context("Zeta2Feedback")
925 .on_action(cx.listener(Self::handle_rate_positive))
926 .on_action(cx.listener(Self::handle_rate_negative))
927 .gap_2()
928 .p_2()
929 .child(feedback_editor.clone())
930 .child(
931 h_flex()
932 .justify_end()
933 .w_full()
934 .child(
935 ButtonLike::new("rate-positive")
936 .when(
937 *feedback_state == Some(Feedback::Positive),
938 |this| this.style(ButtonStyle::Filled),
939 )
940 .child(
941 KeyBinding::for_action(
942 &Zeta2RatePredictionPositive,
943 cx,
944 )
945 .size(TextSize::Small.rems(cx)),
946 )
947 .child(ui::Icon::new(ui::IconName::ThumbsUp))
948 .on_click(cx.listener(|this, _, window, cx| {
949 this.handle_rate_positive(
950 &Zeta2RatePredictionPositive,
951 window,
952 cx,
953 );
954 })),
955 )
956 .child(
957 ButtonLike::new("rate-negative")
958 .when(
959 *feedback_state == Some(Feedback::Negative),
960 |this| this.style(ButtonStyle::Filled),
961 )
962 .child(
963 KeyBinding::for_action(
964 &Zeta2RatePredictionNegative,
965 cx,
966 )
967 .size(TextSize::Small.rems(cx)),
968 )
969 .child(ui::Icon::new(ui::IconName::ThumbsDown))
970 .on_click(cx.listener(|this, _, window, cx| {
971 this.handle_rate_negative(
972 &Zeta2RatePredictionNegative,
973 window,
974 cx,
975 );
976 })),
977 ),
978 )
979 .into_any()
980 } else {
981 Empty.into_any_element()
982 },
983 ),
984 )
985 }
986}
987
988impl Focusable for Zeta2Inspector {
989 fn focus_handle(&self, _cx: &App) -> FocusHandle {
990 self.focus_handle.clone()
991 }
992}
993
994impl Item for Zeta2Inspector {
995 type Event = ();
996
997 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
998 "Zeta2 Inspector".into()
999 }
1000}
1001
1002impl EventEmitter<()> for Zeta2Inspector {}
1003
1004impl Render for Zeta2Inspector {
1005 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1006 v_flex()
1007 .size_full()
1008 .bg(cx.theme().colors().editor_background)
1009 .child(
1010 h_flex()
1011 .w_full()
1012 .child(
1013 v_flex()
1014 .flex_1()
1015 .p_4()
1016 .h_full()
1017 .justify_between()
1018 .child(self.render_options(window, cx))
1019 .gap_4(),
1020 )
1021 .child(ui::vertical_divider())
1022 .children(self.render_stats()),
1023 )
1024 .child(self.render_content(window, cx))
1025 }
1026}