1use std::{ops::Range, path::Path, rc::Rc, sync::Arc, time::Duration};
2
3use file_icons::FileIcons;
4use futures::FutureExt as _;
5use gpui::{
6 Animation, AnimationExt as _, AnyView, ClickEvent, Entity, Image, MouseButton, Task,
7 pulsating_between,
8};
9use language_model::LanguageModelImage;
10use project::Project;
11use prompt_store::PromptStore;
12use rope::Point;
13use ui::{IconButtonShape, Tooltip, prelude::*, tooltip_container};
14
15use crate::context::{
16 AgentContext, AgentContextHandle, ContextId, ContextKind, DirectoryContext,
17 DirectoryContextHandle, FetchedUrlContext, FileContext, FileContextHandle, ImageContext,
18 ImageStatus, RulesContext, RulesContextHandle, SelectionContext, SelectionContextHandle,
19 SymbolContext, SymbolContextHandle, ThreadContext, ThreadContextHandle,
20};
21
22#[derive(IntoElement)]
23pub enum ContextPill {
24 Added {
25 context: AddedContext,
26 dupe_name: bool,
27 focused: bool,
28 on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
29 on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
30 },
31 Suggested {
32 name: SharedString,
33 icon_path: Option<SharedString>,
34 kind: ContextKind,
35 focused: bool,
36 on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
37 },
38}
39
40impl ContextPill {
41 pub fn added(
42 context: AddedContext,
43 dupe_name: bool,
44 focused: bool,
45 on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
46 ) -> Self {
47 Self::Added {
48 context,
49 dupe_name,
50 on_remove,
51 focused,
52 on_click: None,
53 }
54 }
55
56 pub fn suggested(
57 name: SharedString,
58 icon_path: Option<SharedString>,
59 kind: ContextKind,
60 focused: bool,
61 ) -> Self {
62 Self::Suggested {
63 name,
64 icon_path,
65 kind,
66 focused,
67 on_click: None,
68 }
69 }
70
71 pub fn on_click(mut self, listener: Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>) -> Self {
72 match &mut self {
73 ContextPill::Added { on_click, .. } => {
74 *on_click = Some(listener);
75 }
76 ContextPill::Suggested { on_click, .. } => {
77 *on_click = Some(listener);
78 }
79 }
80 self
81 }
82
83 pub fn id(&self) -> ElementId {
84 match self {
85 Self::Added { context, .. } => context.handle.element_id("context-pill".into()),
86 Self::Suggested { .. } => "suggested-context-pill".into(),
87 }
88 }
89
90 pub fn icon(&self) -> Icon {
91 match self {
92 Self::Suggested {
93 icon_path: Some(icon_path),
94 ..
95 }
96 | Self::Added {
97 context:
98 AddedContext {
99 icon_path: Some(icon_path),
100 ..
101 },
102 ..
103 } => Icon::from_path(icon_path),
104 Self::Suggested { kind, .. }
105 | Self::Added {
106 context: AddedContext { kind, .. },
107 ..
108 } => Icon::new(kind.icon()),
109 }
110 }
111}
112
113impl RenderOnce for ContextPill {
114 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
115 let color = cx.theme().colors();
116
117 let base_pill = h_flex()
118 .id(self.id())
119 .pl_1()
120 .pb(px(1.))
121 .border_1()
122 .rounded_sm()
123 .gap_1()
124 .child(self.icon().size(IconSize::XSmall).color(Color::Muted));
125
126 match &self {
127 ContextPill::Added {
128 context,
129 dupe_name,
130 on_remove,
131 focused,
132 on_click,
133 } => {
134 let status_is_error = matches!(context.status, ContextStatus::Error { .. });
135
136 base_pill
137 .pr(if on_remove.is_some() { px(2.) } else { px(4.) })
138 .map(|pill| {
139 if status_is_error {
140 pill.bg(cx.theme().status().error_background)
141 .border_color(cx.theme().status().error_border)
142 } else if *focused {
143 pill.bg(color.element_background)
144 .border_color(color.border_focused)
145 } else {
146 pill.bg(color.element_background)
147 .border_color(color.border.opacity(0.5))
148 }
149 })
150 .child(
151 h_flex()
152 .id("context-data")
153 .gap_1()
154 .child(
155 div().max_w_64().child(
156 Label::new(context.name.clone())
157 .size(LabelSize::Small)
158 .truncate(),
159 ),
160 )
161 .when_some(context.parent.as_ref(), |element, parent_name| {
162 if *dupe_name {
163 element.child(
164 Label::new(parent_name.clone())
165 .size(LabelSize::XSmall)
166 .color(Color::Muted),
167 )
168 } else {
169 element
170 }
171 })
172 .when_some(context.tooltip.as_ref(), |element, tooltip| {
173 element.tooltip(Tooltip::text(tooltip.clone()))
174 })
175 .map(|element| match &context.status {
176 ContextStatus::Ready => element
177 .when_some(
178 context.render_hover.as_ref(),
179 |element, render_hover| {
180 let render_hover = render_hover.clone();
181 element.hoverable_tooltip(move |window, cx| {
182 render_hover(window, cx)
183 })
184 },
185 )
186 .into_any(),
187 ContextStatus::Loading { message } => element
188 .tooltip(ui::Tooltip::text(message.clone()))
189 .with_animation(
190 "pulsating-ctx-pill",
191 Animation::new(Duration::from_secs(2))
192 .repeat()
193 .with_easing(pulsating_between(0.4, 0.8)),
194 |label, delta| label.opacity(delta),
195 )
196 .into_any_element(),
197 ContextStatus::Error { message } => element
198 .tooltip(ui::Tooltip::text(message.clone()))
199 .into_any_element(),
200 }),
201 )
202 .when_some(on_remove.as_ref(), |element, on_remove| {
203 element.child(
204 IconButton::new(
205 context.handle.element_id("remove".into()),
206 IconName::Close,
207 )
208 .shape(IconButtonShape::Square)
209 .icon_size(IconSize::XSmall)
210 .tooltip(Tooltip::text("Remove Context"))
211 .on_click({
212 let on_remove = on_remove.clone();
213 move |event, window, cx| on_remove(event, window, cx)
214 }),
215 )
216 })
217 .when_some(on_click.as_ref(), |element, on_click| {
218 let on_click = on_click.clone();
219 element.cursor_pointer().on_click(move |event, window, cx| {
220 on_click(event, window, cx);
221 cx.stop_propagation();
222 })
223 })
224 .into_any_element()
225 }
226 ContextPill::Suggested {
227 name,
228 icon_path: _,
229 kind: _,
230 focused,
231 on_click,
232 } => base_pill
233 .cursor_pointer()
234 .pr_1()
235 .border_dashed()
236 .map(|pill| {
237 if *focused {
238 pill.border_color(color.border_focused)
239 .bg(color.element_background.opacity(0.5))
240 } else {
241 pill.border_color(color.border)
242 }
243 })
244 .hover(|style| style.bg(color.element_hover.opacity(0.5)))
245 .child(
246 div().max_w_64().child(
247 Label::new(name.clone())
248 .size(LabelSize::Small)
249 .color(Color::Muted)
250 .truncate(),
251 ),
252 )
253 .tooltip(|window, cx| {
254 Tooltip::with_meta("Suggested Context", None, "Click to add it", window, cx)
255 })
256 .when_some(on_click.as_ref(), |element, on_click| {
257 let on_click = on_click.clone();
258 element.on_click(move |event, window, cx| {
259 on_click(event, window, cx);
260 cx.stop_propagation();
261 })
262 })
263 .into_any(),
264 }
265 }
266}
267
268pub enum ContextStatus {
269 Ready,
270 Loading { message: SharedString },
271 Error { message: SharedString },
272}
273
274#[derive(RegisterComponent)]
275pub struct AddedContext {
276 pub handle: AgentContextHandle,
277 pub kind: ContextKind,
278 pub name: SharedString,
279 pub parent: Option<SharedString>,
280 pub tooltip: Option<SharedString>,
281 pub icon_path: Option<SharedString>,
282 pub status: ContextStatus,
283 pub render_hover: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
284}
285
286impl AddedContext {
287 /// Creates an `AddedContext` by retrieving relevant details of `AgentContext`. This returns a
288 /// `None` if `DirectoryContext` or `RulesContext` no longer exist.
289 ///
290 /// TODO: `None` cases are unremovable from `ContextStore` and so are a very minor memory leak.
291 pub fn new_pending(
292 handle: AgentContextHandle,
293 prompt_store: Option<&Entity<PromptStore>>,
294 project: &Project,
295 cx: &App,
296 ) -> Option<AddedContext> {
297 match handle {
298 AgentContextHandle::File(handle) => Self::pending_file(handle, cx),
299 AgentContextHandle::Directory(handle) => Self::pending_directory(handle, project, cx),
300 AgentContextHandle::Symbol(handle) => Self::pending_symbol(handle, cx),
301 AgentContextHandle::Selection(handle) => Self::pending_selection(handle, cx),
302 AgentContextHandle::FetchedUrl(handle) => Some(Self::fetched_url(handle)),
303 AgentContextHandle::Thread(handle) => Some(Self::pending_thread(handle, cx)),
304 AgentContextHandle::Rules(handle) => Self::pending_rules(handle, prompt_store, cx),
305 AgentContextHandle::Image(handle) => Some(Self::image(handle)),
306 }
307 }
308
309 pub fn new_attached(context: &AgentContext, cx: &App) -> AddedContext {
310 match context {
311 AgentContext::File(context) => Self::attached_file(context, cx),
312 AgentContext::Directory(context) => Self::attached_directory(context),
313 AgentContext::Symbol(context) => Self::attached_symbol(context, cx),
314 AgentContext::Selection(context) => Self::attached_selection(context, cx),
315 AgentContext::FetchedUrl(context) => Self::fetched_url(context.clone()),
316 AgentContext::Thread(context) => Self::attached_thread(context),
317 AgentContext::Rules(context) => Self::attached_rules(context),
318 AgentContext::Image(context) => Self::image(context.clone()),
319 }
320 }
321
322 fn pending_file(handle: FileContextHandle, cx: &App) -> Option<AddedContext> {
323 let full_path = handle.buffer.read(cx).file()?.full_path(cx);
324 Some(Self::file(handle, &full_path, cx))
325 }
326
327 fn attached_file(context: &FileContext, cx: &App) -> AddedContext {
328 Self::file(context.handle.clone(), &context.full_path, cx)
329 }
330
331 fn file(handle: FileContextHandle, full_path: &Path, cx: &App) -> AddedContext {
332 let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
333 let name = full_path
334 .file_name()
335 .map(|n| n.to_string_lossy().into_owned().into())
336 .unwrap_or_else(|| full_path_string.clone());
337 let parent = full_path
338 .parent()
339 .and_then(|p| p.file_name())
340 .map(|n| n.to_string_lossy().into_owned().into());
341 AddedContext {
342 kind: ContextKind::File,
343 name,
344 parent,
345 tooltip: Some(full_path_string),
346 icon_path: FileIcons::get_icon(&full_path, cx),
347 status: ContextStatus::Ready,
348 render_hover: None,
349 handle: AgentContextHandle::File(handle),
350 }
351 }
352
353 fn pending_directory(
354 handle: DirectoryContextHandle,
355 project: &Project,
356 cx: &App,
357 ) -> Option<AddedContext> {
358 let worktree = project.worktree_for_entry(handle.entry_id, cx)?.read(cx);
359 let entry = worktree.entry_for_id(handle.entry_id)?;
360 let full_path = worktree.full_path(&entry.path);
361 Some(Self::directory(handle, &full_path))
362 }
363
364 fn attached_directory(context: &DirectoryContext) -> AddedContext {
365 Self::directory(context.handle.clone(), &context.full_path)
366 }
367
368 fn directory(handle: DirectoryContextHandle, full_path: &Path) -> AddedContext {
369 let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
370 let name = full_path
371 .file_name()
372 .map(|n| n.to_string_lossy().into_owned().into())
373 .unwrap_or_else(|| full_path_string.clone());
374 let parent = full_path
375 .parent()
376 .and_then(|p| p.file_name())
377 .map(|n| n.to_string_lossy().into_owned().into());
378 AddedContext {
379 kind: ContextKind::Directory,
380 name,
381 parent,
382 tooltip: Some(full_path_string),
383 icon_path: None,
384 status: ContextStatus::Ready,
385 render_hover: None,
386 handle: AgentContextHandle::Directory(handle),
387 }
388 }
389
390 fn pending_symbol(handle: SymbolContextHandle, cx: &App) -> Option<AddedContext> {
391 let excerpt =
392 ContextFileExcerpt::new(&handle.full_path(cx)?, handle.enclosing_line_range(cx), cx);
393 Some(AddedContext {
394 kind: ContextKind::Symbol,
395 name: handle.symbol.clone(),
396 parent: Some(excerpt.file_name_and_range.clone()),
397 tooltip: None,
398 icon_path: None,
399 status: ContextStatus::Ready,
400 render_hover: {
401 let handle = handle.clone();
402 Some(Rc::new(move |_, cx| {
403 excerpt.hover_view(handle.text(cx), cx).into()
404 }))
405 },
406 handle: AgentContextHandle::Symbol(handle),
407 })
408 }
409
410 fn attached_symbol(context: &SymbolContext, cx: &App) -> AddedContext {
411 let excerpt = ContextFileExcerpt::new(&context.full_path, context.line_range.clone(), cx);
412 AddedContext {
413 kind: ContextKind::Symbol,
414 name: context.handle.symbol.clone(),
415 parent: Some(excerpt.file_name_and_range.clone()),
416 tooltip: None,
417 icon_path: None,
418 status: ContextStatus::Ready,
419 render_hover: {
420 let text = context.text.clone();
421 Some(Rc::new(move |_, cx| {
422 excerpt.hover_view(text.clone(), cx).into()
423 }))
424 },
425 handle: AgentContextHandle::Symbol(context.handle.clone()),
426 }
427 }
428
429 fn pending_selection(handle: SelectionContextHandle, cx: &App) -> Option<AddedContext> {
430 let excerpt = ContextFileExcerpt::new(&handle.full_path(cx)?, handle.line_range(cx), cx);
431 Some(AddedContext {
432 kind: ContextKind::Selection,
433 name: excerpt.file_name_and_range.clone(),
434 parent: excerpt.parent_name.clone(),
435 tooltip: None,
436 icon_path: excerpt.icon_path.clone(),
437 status: ContextStatus::Ready,
438 render_hover: {
439 let handle = handle.clone();
440 Some(Rc::new(move |_, cx| {
441 excerpt.hover_view(handle.text(cx), cx).into()
442 }))
443 },
444 handle: AgentContextHandle::Selection(handle),
445 })
446 }
447
448 fn attached_selection(context: &SelectionContext, cx: &App) -> AddedContext {
449 let excerpt = ContextFileExcerpt::new(&context.full_path, context.line_range.clone(), cx);
450 AddedContext {
451 kind: ContextKind::Selection,
452 name: excerpt.file_name_and_range.clone(),
453 parent: excerpt.parent_name.clone(),
454 tooltip: None,
455 icon_path: excerpt.icon_path.clone(),
456 status: ContextStatus::Ready,
457 render_hover: {
458 let text = context.text.clone();
459 Some(Rc::new(move |_, cx| {
460 excerpt.hover_view(text.clone(), cx).into()
461 }))
462 },
463 handle: AgentContextHandle::Selection(context.handle.clone()),
464 }
465 }
466
467 fn fetched_url(context: FetchedUrlContext) -> AddedContext {
468 AddedContext {
469 kind: ContextKind::FetchedUrl,
470 name: context.url.clone(),
471 parent: None,
472 tooltip: None,
473 icon_path: None,
474 status: ContextStatus::Ready,
475 render_hover: None,
476 handle: AgentContextHandle::FetchedUrl(context),
477 }
478 }
479
480 fn pending_thread(handle: ThreadContextHandle, cx: &App) -> AddedContext {
481 AddedContext {
482 kind: ContextKind::Thread,
483 name: handle.title(cx),
484 parent: None,
485 tooltip: None,
486 icon_path: None,
487 status: if handle.thread.read(cx).is_generating_detailed_summary() {
488 ContextStatus::Loading {
489 message: "Summarizing…".into(),
490 }
491 } else {
492 ContextStatus::Ready
493 },
494 render_hover: {
495 let thread = handle.thread.clone();
496 Some(Rc::new(move |_, cx| {
497 let text = thread.read(cx).latest_detailed_summary_or_text();
498 ContextPillHover::new_text(text.clone(), cx).into()
499 }))
500 },
501 handle: AgentContextHandle::Thread(handle),
502 }
503 }
504
505 fn attached_thread(context: &ThreadContext) -> AddedContext {
506 AddedContext {
507 kind: ContextKind::Thread,
508 name: context.title.clone(),
509 parent: None,
510 tooltip: None,
511 icon_path: None,
512 status: ContextStatus::Ready,
513 render_hover: {
514 let text = context.text.clone();
515 Some(Rc::new(move |_, cx| {
516 ContextPillHover::new_text(text.clone(), cx).into()
517 }))
518 },
519 handle: AgentContextHandle::Thread(context.handle.clone()),
520 }
521 }
522
523 fn pending_rules(
524 handle: RulesContextHandle,
525 prompt_store: Option<&Entity<PromptStore>>,
526 cx: &App,
527 ) -> Option<AddedContext> {
528 let title = prompt_store
529 .as_ref()?
530 .read(cx)
531 .metadata(handle.prompt_id.into())?
532 .title
533 .unwrap_or_else(|| "Unnamed Rule".into());
534 Some(AddedContext {
535 kind: ContextKind::Rules,
536 name: title.clone(),
537 parent: None,
538 tooltip: None,
539 icon_path: None,
540 status: ContextStatus::Ready,
541 render_hover: None,
542 handle: AgentContextHandle::Rules(handle),
543 })
544 }
545
546 fn attached_rules(context: &RulesContext) -> AddedContext {
547 let title = context
548 .title
549 .clone()
550 .unwrap_or_else(|| "Unnamed Rule".into());
551 AddedContext {
552 kind: ContextKind::Rules,
553 name: title,
554 parent: None,
555 tooltip: None,
556 icon_path: None,
557 status: ContextStatus::Ready,
558 render_hover: {
559 let text = context.text.clone();
560 Some(Rc::new(move |_, cx| {
561 ContextPillHover::new_text(text.clone(), cx).into()
562 }))
563 },
564 handle: AgentContextHandle::Rules(context.handle.clone()),
565 }
566 }
567
568 fn image(context: ImageContext) -> AddedContext {
569 AddedContext {
570 kind: ContextKind::Image,
571 name: "Image".into(),
572 parent: None,
573 tooltip: None,
574 icon_path: None,
575 status: match context.status() {
576 ImageStatus::Loading => ContextStatus::Loading {
577 message: "Loading…".into(),
578 },
579 ImageStatus::Error => ContextStatus::Error {
580 message: "Failed to load image".into(),
581 },
582 ImageStatus::Ready => ContextStatus::Ready,
583 },
584 render_hover: Some(Rc::new({
585 let image = context.original_image.clone();
586 move |_, cx| {
587 let image = image.clone();
588 ContextPillHover::new(cx, move |_, _| {
589 gpui::img(image.clone())
590 .max_w_96()
591 .max_h_96()
592 .into_any_element()
593 })
594 .into()
595 }
596 })),
597 handle: AgentContextHandle::Image(context),
598 }
599 }
600}
601
602#[derive(Debug, Clone)]
603struct ContextFileExcerpt {
604 pub file_name_and_range: SharedString,
605 pub full_path_and_range: SharedString,
606 pub parent_name: Option<SharedString>,
607 pub icon_path: Option<SharedString>,
608}
609
610impl ContextFileExcerpt {
611 pub fn new(full_path: &Path, line_range: Range<Point>, cx: &App) -> Self {
612 let full_path_string = full_path.to_string_lossy().into_owned();
613 let file_name = full_path
614 .file_name()
615 .map(|n| n.to_string_lossy().into_owned())
616 .unwrap_or_else(|| full_path_string.clone());
617
618 let line_range_text = format!(" ({}-{})", line_range.start.row + 1, line_range.end.row + 1);
619 let mut full_path_and_range = full_path_string;
620 full_path_and_range.push_str(&line_range_text);
621 let mut file_name_and_range = file_name;
622 file_name_and_range.push_str(&line_range_text);
623
624 let parent_name = full_path
625 .parent()
626 .and_then(|p| p.file_name())
627 .map(|n| n.to_string_lossy().into_owned().into());
628
629 let icon_path = FileIcons::get_icon(&full_path, cx);
630
631 ContextFileExcerpt {
632 file_name_and_range: file_name_and_range.into(),
633 full_path_and_range: full_path_and_range.into(),
634 parent_name,
635 icon_path,
636 }
637 }
638
639 fn hover_view(&self, text: SharedString, cx: &mut App) -> Entity<ContextPillHover> {
640 let icon_path = self.icon_path.clone();
641 let full_path_and_range = self.full_path_and_range.clone();
642 ContextPillHover::new(cx, move |_, cx| {
643 v_flex()
644 .child(
645 h_flex()
646 .gap_0p5()
647 .w_full()
648 .max_w_full()
649 .border_b_1()
650 .border_color(cx.theme().colors().border.opacity(0.6))
651 .children(
652 icon_path
653 .clone()
654 .map(Icon::from_path)
655 .map(|icon| icon.color(Color::Muted).size(IconSize::XSmall)),
656 )
657 .child(
658 // TODO: make this truncate on the left.
659 Label::new(full_path_and_range.clone())
660 .size(LabelSize::Small)
661 .ml_1(),
662 ),
663 )
664 .child(
665 div()
666 .id("context-pill-hover-contents")
667 .overflow_scroll()
668 .max_w_128()
669 .max_h_96()
670 .child(Label::new(text.clone()).buffer_font(cx)),
671 )
672 .into_any_element()
673 })
674 }
675}
676
677struct ContextPillHover {
678 render_hover: Box<dyn Fn(&mut Window, &mut App) -> AnyElement>,
679}
680
681impl ContextPillHover {
682 fn new(
683 cx: &mut App,
684 render_hover: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
685 ) -> Entity<Self> {
686 cx.new(|_| Self {
687 render_hover: Box::new(render_hover),
688 })
689 }
690
691 fn new_text(content: SharedString, cx: &mut App) -> Entity<Self> {
692 Self::new(cx, move |_, _| {
693 div()
694 .id("context-pill-hover-contents")
695 .overflow_scroll()
696 .max_w_128()
697 .max_h_96()
698 .child(content.clone())
699 .into_any_element()
700 })
701 }
702}
703
704impl Render for ContextPillHover {
705 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
706 tooltip_container(window, cx, move |this, window, cx| {
707 this.occlude()
708 .on_mouse_move(|_, _, cx| cx.stop_propagation())
709 .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
710 .child((self.render_hover)(window, cx))
711 })
712 }
713}
714
715impl Component for AddedContext {
716 fn scope() -> ComponentScope {
717 ComponentScope::Agent
718 }
719
720 fn sort_name() -> &'static str {
721 "AddedContext"
722 }
723
724 fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
725 let mut next_context_id = ContextId::zero();
726 let image_ready = (
727 "Ready",
728 AddedContext::image(ImageContext {
729 context_id: next_context_id.post_inc(),
730 project_path: None,
731 original_image: Arc::new(Image::empty()),
732 image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
733 }),
734 );
735
736 let image_loading = (
737 "Loading",
738 AddedContext::image(ImageContext {
739 context_id: next_context_id.post_inc(),
740 project_path: None,
741 original_image: Arc::new(Image::empty()),
742 image_task: cx
743 .background_spawn(async move {
744 smol::Timer::after(Duration::from_secs(60 * 5)).await;
745 Some(LanguageModelImage::empty())
746 })
747 .shared(),
748 }),
749 );
750
751 let image_error = (
752 "Error",
753 AddedContext::image(ImageContext {
754 context_id: next_context_id.post_inc(),
755 project_path: None,
756 original_image: Arc::new(Image::empty()),
757 image_task: Task::ready(None).shared(),
758 }),
759 );
760
761 Some(
762 v_flex()
763 .gap_6()
764 .children(
765 vec![image_ready, image_loading, image_error]
766 .into_iter()
767 .map(|(text, context)| {
768 single_example(
769 text,
770 ContextPill::added(context, false, false, None).into_any_element(),
771 )
772 }),
773 )
774 .into_any(),
775 )
776 }
777}