use gpui_kit::{
AnyElement, App, Entity, IntoElement, ParentElement as _, SharedString, Styled as _,
component::{
ActiveTheme as _, Disableable as _, Sizable as _,
button::{Button, ButtonVariants as _},
h_flex,
setting::{SettingGroup, SettingItem, SettingPage},
v_flex,
},
div, px, relative,
};
#[cfg(feature = "grobid")]
use crate::control;
use crate::{
SettingRow, dirty_header, dropdown_control,
rag::{Changes, RagField, RagSettings, ServiceSource},
switch_control, text_control,
};
use super::{ContainerStatus, DockerStatus, ExternalServices, ServiceKind};
pub fn external_services_page(
settings: Entity<RagSettings>,
service: Entity<ExternalServices>,
changes: Changes,
cx: &App,
) -> SettingPage {
let dirty = {
let changes = changes.clone();
move |cx: &App| {
let current = changes(cx);
ServiceKind::ALL
.iter()
.any(|kind| group_dirty(¤t, *kind))
}
};
let is_dirty = dirty(cx);
let title: SharedString = if is_dirty {
"External services ●".into()
} else {
"External services".into()
};
let description: SharedString = if cfg!(feature = "grobid") {
"GROBID turns PDF headers into bibliography records. Ollama supplies local chat and embedding models. llama.cpp serves a GGUF model through an OpenAI-compatible API. Use a server that is already running or let the app pull and run the official Docker image."
} else {
"Ollama supplies local chat and embedding models. llama.cpp serves a GGUF model through an OpenAI-compatible API. Use a server that is already running or let the app pull and run the official Docker image."
}
.into();
let mut page = SettingPage::new(title)
.description(description)
.default_open(true);
if is_dirty {
page = page.header_style(&dirty_header(cx));
}
if uses_docker_source(&settings, cx) {
page = page.group(docker_group(settings.clone(), service.clone()));
}
#[cfg(feature = "grobid")]
{
page = page.group(grobid_group(
settings.clone(),
service.clone(),
changes.clone(),
cx,
));
}
page = page
.group(ollama_group(
settings.clone(),
service.clone(),
changes.clone(),
cx,
))
.group(llamacpp_group(settings, service, changes, cx));
page
}
fn group_dirty(changes: &crate::rag::RagChanges, kind: ServiceKind) -> bool {
let fields: &[RagField] = match kind {
ServiceKind::Grobid => &[
RagField::GrobidSource,
RagField::GrobidUrl,
RagField::GrobidImage,
RagField::GrobidAutoStart,
RagField::GrobidContainer,
],
ServiceKind::Ollama => &[
RagField::OllamaSource,
RagField::OllamaImage,
RagField::OllamaAutoStart,
RagField::OllamaContainer,
],
ServiceKind::LlamaCpp => &[
RagField::LlamaCppSource,
RagField::LlamaCppImage,
RagField::LlamaCppModel,
RagField::LlamaCppArgs,
RagField::LlamaCppAutoStart,
RagField::LlamaCppContainer,
],
};
fields.iter().any(|field| changes.contains(*field))
}
fn uses_docker_source(settings: &Entity<RagSettings>, cx: &App) -> bool {
let settings = settings.read(cx);
ServiceKind::ALL.iter().any(|kind| {
matches!(
settings.service_source(*kind),
ServiceSource::Managed | ServiceSource::ExistingContainer
)
})
}
pub(crate) fn docker_group(
settings: Entity<RagSettings>,
service: Entity<ExternalServices>,
) -> SettingGroup {
let status_service = service.clone();
let status = SettingItem::render(move |_, _, cx| {
let status = status_service.read(cx).docker();
let color = if matches!(status, DockerStatus::Unavailable { .. }) {
cx.theme().danger
} else {
cx.theme().muted_foreground
};
div()
.text_sm()
.text_color(color)
.child(format!("Docker: {}", status.summary()))
.into_any_element()
});
let refresh_service = service;
let actions = SettingItem::render(move |_, _, cx| {
let enabled = uses_docker_source(&settings, cx);
let refresh = refresh_service.clone();
Button::new("services-docker-refresh")
.label("Refresh status")
.small()
.outline()
.disabled(!enabled)
.on_click(move |_, _, cx| {
refresh.update(cx, |service, cx| service.refresh_all(cx));
})
.into_any_element()
});
SettingGroup::new()
.title("Docker")
.description(
"Docker Desktop or the Docker daemon must be running, and this user must be allowed to access the Docker socket.",
)
.item(actions)
.item(status)
.footer(|_, cx| {
div()
.text_xs()
.text_color(cx.theme().muted_foreground)
.child("Linux without socket access: add the user to the docker group and sign in again. Other systems: start Docker Desktop for this user and press Refresh.")
})
}
#[cfg(feature = "grobid")]
pub(crate) fn grobid_group(
settings: Entity<RagSettings>,
service: Entity<ExternalServices>,
changes: Changes,
cx: &App,
) -> SettingGroup {
SettingGroup::new()
.title("GROBID")
.description("Parses PDF headers and merges the result into the bibliography.")
.items(service_items(
ServiceKind::Grobid,
&settings,
&service,
&changes,
cx,
))
.footer(|_, cx| {
div()
.text_xs()
.text_color(cx.theme().muted_foreground)
.child("The app stops the container when it quits. Removing the container leaves the image on disk.")
})
}
pub(crate) fn ollama_group(
settings: Entity<RagSettings>,
service: Entity<ExternalServices>,
changes: Changes,
cx: &App,
) -> SettingGroup {
SettingGroup::new()
.title("Ollama")
.description(
"Serves local chat and embedding models at localhost:11434. The managed container keeps its models in the ragrig-ollama-models volume.",
)
.items(service_items(
ServiceKind::Ollama,
&settings,
&service,
&changes,
cx,
))
.footer(|_, cx| {
div()
.text_xs()
.text_color(cx.theme().muted_foreground)
.child("Models are pulled on demand with Pull models; they survive container removal because the volume stays. If a server already answers on port 11434, the app uses it instead of starting a container. The app stops the container when it quits.")
})
}
pub(crate) fn llamacpp_group(
settings: Entity<RagSettings>,
service: Entity<ExternalServices>,
changes: Changes,
cx: &App,
) -> SettingGroup {
SettingGroup::new()
.title("llama.cpp")
.description(
"Serves one GGUF model through an OpenAI-compatible API. The chat URL lives in RAG → Chat.",
)
.items(service_items(
ServiceKind::LlamaCpp,
&settings,
&service,
&changes,
cx,
))
.footer(|_, cx| {
div()
.text_xs()
.text_color(cx.theme().muted_foreground)
.child("A Hugging Face repository is downloaded into the ragrig-llamacpp-models volume. An absolute .gguf path is mounted read-only from the host. If a server already answers on port 8080, the app uses it instead of starting a container. The app stops the container when it quits.")
})
}
pub(crate) fn service_items(
kind: ServiceKind,
settings: &Entity<RagSettings>,
service: &Entity<ExternalServices>,
changes: &Changes,
cx: &App,
) -> Vec<SettingItem> {
let source = source_value(settings, kind, cx);
let mut items = vec![source_row(settings, service, changes, kind).into_item()];
match kind {
ServiceKind::Grobid => match source {
ServiceSource::Existing => {
#[cfg(feature = "grobid")]
items.push(grobid_url_row(settings, service, changes).into_item());
items.push(status_item(settings, service, kind));
}
ServiceSource::ExistingContainer => {
items.push(container_name_row(settings, changes, kind));
items.push(auto_start_row(settings, changes, kind));
#[cfg(feature = "grobid")]
items.push(grobid_url_row(settings, service, changes).into_item());
items.push(actions_item(settings, service, kind));
items.push(status_item(settings, service, kind));
}
ServiceSource::Managed => {
items.push(auto_start_row(settings, changes, kind));
items.push(image_row(settings, changes, kind).into_item());
items.push(actions_item(settings, service, kind));
items.push(status_item(settings, service, kind));
}
},
ServiceKind::Ollama => match source {
ServiceSource::Existing => {
items.push(probe_actions_item(service, kind));
items.push(status_item(settings, service, kind));
}
ServiceSource::ExistingContainer => {
items.push(container_name_row(settings, changes, kind));
items.push(auto_start_row(settings, changes, kind));
items.push(actions_item(settings, service, kind));
items.push(status_item(settings, service, kind));
}
ServiceSource::Managed => {
items.push(auto_start_row(settings, changes, kind));
items.push(image_row(settings, changes, kind).into_item());
items.push(actions_item(settings, service, kind));
items.push(status_item(settings, service, kind));
}
},
ServiceKind::LlamaCpp => match source {
ServiceSource::Existing => {
items.push(probe_actions_item(service, kind));
items.push(status_item(settings, service, kind));
}
ServiceSource::ExistingContainer => {
items.push(container_name_row(settings, changes, kind));
items.push(auto_start_row(settings, changes, kind));
items.push(actions_item(settings, service, kind));
items.push(status_item(settings, service, kind));
}
ServiceSource::Managed => {
items.push(auto_start_row(settings, changes, kind));
items.push(image_row(settings, changes, kind).into_item());
items.push(llamacpp_model_row(settings, changes).into_item());
items.push(llamacpp_args_row(settings, changes).into_item());
items.push(actions_item(settings, service, kind));
items.push(status_item(settings, service, kind));
}
},
}
items
}
fn source_value(settings: &Entity<RagSettings>, kind: ServiceKind, cx: &App) -> ServiceSource {
settings.read(cx).service_source(kind)
}
fn source_row(
settings: &Entity<RagSettings>,
service: &Entity<ExternalServices>,
changes: &Changes,
kind: ServiceKind,
) -> SettingRow {
let options: Vec<(SharedString, SharedString)> = ServiceSource::options()
.iter()
.map(|(source, label)| {
(
SharedString::from(source.label()),
SharedString::from(*label),
)
})
.collect();
let settings_for_read = settings.clone();
let settings_for_write = settings.clone();
let service_for_write = service.clone();
let dropdown = dropdown_control(
source_key(kind),
move |_| options.clone(),
move |cx| {
settings_for_read
.read(cx)
.service_source(kind)
.label()
.to_owned()
},
move |value, cx| {
let source = ServiceSource::from_label(&value).unwrap_or(ServiceSource::Existing);
settings_for_write.update(cx, |settings, cx| {
if settings.service_source(kind) != source {
settings.set_service_source(kind, source);
cx.notify();
}
});
service_for_write.update(cx, |service, cx| {
if source == ServiceSource::Existing {
service.probe_existing(kind, cx);
} else {
service.refresh(kind, cx);
}
});
},
);
let dirty = {
let changes = changes.clone();
move |cx: &App| changes(cx).contains(source_field(kind))
};
SettingRow::new("Source", dropdown)
.description(match kind {
ServiceKind::Grobid => {
"A running server can be discovered or entered by URL. An existing container is started and stopped by name."
}
ServiceKind::Ollama => {
"Use a daemon at localhost:11434, start an existing container by name, or let the app run the official container."
}
ServiceKind::LlamaCpp => {
"Use an existing llama-server, start an existing container by name, or let the app run the official container."
}
})
.dirty(dirty)
}
fn auto_start_row(
settings: &Entity<RagSettings>,
changes: &Changes,
kind: ServiceKind,
) -> SettingItem {
let settings_for_read = settings.clone();
let writer = settings.clone();
let auto_start = switch_control(
auto_start_key(kind),
move |cx| settings_for_read.read(cx).service_auto_start(kind),
move |value, cx| {
writer.update(cx, |settings, cx| {
settings.set_service_auto_start(kind, value);
cx.notify();
});
},
);
let changes = changes.clone();
SettingRow::new("Auto start", auto_start)
.description(format!(
"Start the managed {} container when the app opens.",
kind.label()
))
.dirty(move |cx| changes(cx).contains(auto_start_field(kind)))
.into_item()
}
fn image_row(settings: &Entity<RagSettings>, changes: &Changes, kind: ServiceKind) -> SettingRow {
let settings_for_read = settings.clone();
let writer = settings.clone();
let image = text_control(
image_key(kind),
move |cx| settings_for_read.read(cx).service_image(kind).to_owned(),
move |value, cx| {
writer.update(cx, |settings, cx| {
settings.set_service_image(kind, value);
cx.notify();
});
},
);
let changes = changes.clone();
SettingRow::new("Docker image", image)
.description(match kind {
ServiceKind::Grobid => {
"The full image adds the Deep Learning models (about 8 GB); a -crf tag is smaller and multi-architecture."
}
ServiceKind::Ollama => {
"The official ollama/ollama image. The managed container runs on CPU; use an existing server for GPU acceleration. Model data lives in a volume."
}
ServiceKind::LlamaCpp => {
"The official ghcr.io/ggml-org/llama.cpp:server image. The managed container runs on CPU; use an existing server for GPU acceleration."
}
})
.dirty(move |cx| changes(cx).contains(image_field(kind)))
}
fn container_name_row(
settings: &Entity<RagSettings>,
changes: &Changes,
kind: ServiceKind,
) -> SettingItem {
let settings_for_read = settings.clone();
let writer = settings.clone();
let name = text_control(
container_key(kind),
move |cx| {
settings_for_read
.read(cx)
.service_container(kind)
.to_owned()
},
move |value, cx| {
writer.update(cx, |settings, cx| {
settings.set_service_container(kind, value);
cx.notify();
});
},
);
let changes = changes.clone();
SettingRow::new("Container name", name)
.description(
"The Docker container Scico starts and stops. It is never recreated or removed.",
)
.dirty(move |cx| changes(cx).contains(container_field(kind)))
.into_item()
}
fn llamacpp_model_row(settings: &Entity<RagSettings>, changes: &Changes) -> SettingRow {
let settings_for_read = settings.clone();
let writer = settings.clone();
let model = text_control(
"ragrig-llamacpp-model",
move |cx| settings_for_read.read(cx).llamacpp_model.clone(),
move |value, cx| {
writer.update(cx, |settings, cx| {
settings.llamacpp_model = value;
cx.notify();
});
},
);
let changes = changes.clone();
SettingRow::new("Model", model)
.description(
"Hugging Face repository (owner/repo[:quant]) or an absolute path to a .gguf file on this machine.",
)
.dirty(move |cx| changes(cx).contains(RagField::LlamaCppModel))
}
fn llamacpp_args_row(settings: &Entity<RagSettings>, changes: &Changes) -> SettingRow {
let settings_for_read = settings.clone();
let writer = settings.clone();
let args = text_control(
"ragrig-llamacpp-args",
move |cx| settings_for_read.read(cx).llamacpp_args.clone(),
move |value, cx| {
writer.update(cx, |settings, cx| {
settings.llamacpp_args = value;
cx.notify();
});
},
);
let changes = changes.clone();
SettingRow::new("Extra arguments", args)
.description("Space separated llama-server flags, for example `--n-gpu-layers all`. Do not change --host or --port.")
.dirty(move |cx| changes(cx).contains(RagField::LlamaCppArgs))
}
#[cfg(feature = "grobid")]
fn grobid_url_row(
settings: &Entity<RagSettings>,
service: &Entity<ExternalServices>,
changes: &Changes,
) -> SettingRow {
let settings_for_read = settings.clone();
let settings_for_write = settings.clone();
let service_for_clear = service.clone();
let settings_for_test = settings.clone();
let service_for_test = service.clone();
let service_for_discover = service.clone();
let body = control(move |options, window, cx| {
let settings = settings_for_read.clone();
let writer = settings_for_write.clone();
let clearer = service_for_clear.clone();
let input = text_control(
"grobid-url",
move |cx| settings.read(cx).grobid_url.clone(),
move |value, cx| {
writer.update(cx, |settings, cx| {
settings.grobid_url = value;
cx.notify();
});
clearer.update(cx, |service, cx| {
service.clear_probe(ServiceKind::Grobid, cx);
});
},
)(options, window, cx);
let test_settings = settings_for_test.clone();
let test_service = service_for_test.clone();
let discover_service = service_for_discover.clone();
h_flex()
.w_full()
.items_center()
.gap_2()
.child(div().flex_1().child(input))
.child(
Button::new("grobid-test-url")
.label("Test")
.small()
.outline()
.on_click(move |_, _, cx| {
let url = test_settings.read(cx).grobid_url.clone();
test_service.update(cx, |service, cx| {
service.test_url(ServiceKind::Grobid, url, cx);
});
}),
)
.child(
Button::new("grobid-discover")
.label("Discover on localhost")
.small()
.outline()
.on_click(move |_, _, cx| {
discover_service.update(cx, |service, cx| {
service.discover(ServiceKind::Grobid, cx);
});
}),
)
.into_any_element()
});
let changes = changes.clone();
SettingRow::new("GROBID server", body)
.description("A scheme is added when omitted. Discovery checks ports 8070 and 8080.")
.dirty(move |cx| changes(cx).contains(RagField::GrobidUrl))
.vertical()
}
fn probe_actions_item(service: &Entity<ExternalServices>, kind: ServiceKind) -> SettingItem {
let service = service.clone();
SettingItem::render(move |_, _, _cx| {
let probe_service = service.clone();
let discover_service = service.clone();
h_flex()
.w_full()
.items_center()
.gap_2()
.child(
Button::new(SharedString::from(format!("{}-test", kind.container())))
.label("Test")
.small()
.outline()
.on_click(move |_, _, cx| {
probe_service.update(cx, |service, cx| service.probe_existing(kind, cx));
}),
)
.child(
Button::new(SharedString::from(format!("{}-discover", kind.container())))
.label("Discover on localhost")
.small()
.outline()
.on_click(move |_, _, cx| {
discover_service.update(cx, |service, cx| service.discover(kind, cx));
}),
)
.into_any_element()
})
}
fn actions_item(
settings: &Entity<RagSettings>,
service: &Entity<ExternalServices>,
kind: ServiceKind,
) -> SettingItem {
let service_for_actions = service.clone();
let settings_for_actions = settings.clone();
SettingItem::render(move |_, _, cx| {
let service = service_for_actions.read(cx);
let source = settings_for_actions.read(cx).service_source(kind);
let busy = service.is_busy(kind);
let running = matches!(
service.container(kind),
ContainerStatus::Running | ContainerStatus::Provisioning { .. }
);
let missing = matches!(service.container(kind), ContainerStatus::Missing);
let stopped = matches!(service.container(kind), ContainerStatus::Stopped);
let start_service = service_for_actions.clone();
let stop_service = service_for_actions.clone();
let remove_service = service_for_actions.clone();
let refresh_service = service_for_actions.clone();
let cancel_service = service_for_actions.clone();
let models_service = service_for_actions.clone();
let models = service_for_actions.read(cx).pullable_models(cx);
let mut row = h_flex().w_full().items_center().gap_2();
if running {
row = row.child(
Button::new(SharedString::from(format!("{}-stop", kind.container())))
.label("Stop")
.small()
.outline()
.disabled(busy)
.on_click(move |_, _, cx| {
stop_service.update(cx, |service, cx| service.stop(kind, cx));
}),
);
if kind == ServiceKind::Ollama {
row = row.child(
Button::new("ollama-pull-models")
.label("Pull models")
.small()
.outline()
.disabled(busy || models.is_empty())
.on_click(move |_, _, cx| {
models_service.update(cx, |service, cx| service.pull_models(cx));
}),
);
}
} else {
row = row.child(
Button::new(SharedString::from(format!("{}-start", kind.container())))
.label(if source == ServiceSource::Managed && !stopped {
"Pull and start"
} else {
"Start"
})
.small()
.primary()
.disabled(busy)
.on_click(move |_, _, cx| {
start_service.update(cx, |service, cx| service.start(kind, cx));
}),
);
}
if service.is_cancelling(kind) {
row = row.child(
Button::new(SharedString::from(format!(
"{}-cancelling",
kind.container()
)))
.label("Cancelling…")
.small()
.outline()
.disabled(true),
);
} else if service.is_cancellable(kind) {
row = row.child(
Button::new(SharedString::from(format!("{}-cancel", kind.container())))
.label("Cancel")
.small()
.outline()
.on_click(move |_, _, cx| {
cancel_service.update(cx, |service, cx| service.cancel(kind, cx));
}),
);
}
if source == ServiceSource::Managed {
row = row.child(
Button::new(SharedString::from(format!("{}-remove", kind.container())))
.label("Remove container")
.small()
.outline()
.disabled(busy || missing)
.on_click(move |_, _, cx| {
remove_service.update(cx, |service, cx| service.remove(kind, cx));
}),
);
}
row = row.child(
Button::new(SharedString::from(format!("{}-refresh", kind.container())))
.label("Refresh")
.small()
.ghost()
.on_click(move |_, _, cx| {
refresh_service.update(cx, |service, cx| service.refresh(kind, cx));
}),
);
row.into_any_element()
})
}
fn status_item(
settings: &Entity<RagSettings>,
service: &Entity<ExternalServices>,
kind: ServiceKind,
) -> SettingItem {
let service_for_status = service.clone();
let settings_for_status = settings.clone();
SettingItem::render(move |_, _, cx| {
let service = service_for_status.read(cx);
let source = settings_for_status.read(cx).service_source(kind);
let docker_color = if matches!(service.docker(), DockerStatus::Unavailable { .. }) {
cx.theme().danger
} else {
cx.theme().muted_foreground
};
let container_color = if matches!(service.container(kind), ContainerStatus::Failed { .. }) {
cx.theme().danger
} else {
cx.theme().muted_foreground
};
let mut column = v_flex().gap_1();
if source == ServiceSource::Existing {
column = column.child(
div()
.text_sm()
.text_color(cx.theme().muted_foreground)
.child(service.probe(kind).summary()),
);
} else {
column = column
.child(
div()
.text_sm()
.text_color(docker_color)
.child(format!("Docker: {}", service.docker().summary())),
)
.child(
div()
.text_sm()
.text_color(container_color)
.child(format!("Container: {}", service.container(kind).summary())),
)
.child(
div()
.text_sm()
.text_color(cx.theme().muted_foreground)
.child(service.probe(kind).summary()),
);
}
match service.container(kind) {
ContainerStatus::Pulling {
received, total, ..
}
| ContainerStatus::Provisioning {
received, total, ..
} => {
column = column.child(progress(*received, *total, cx));
}
_ => {}
}
for probe in service.discovered(kind) {
column = column.child(
div()
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(match &probe.version {
Some(version) => {
format!("Found {} {version} at {}", kind.label(), probe.url)
}
None => format!("Found {} at {}", kind.label(), probe.url),
}),
);
}
if kind == ServiceKind::Ollama && source != ServiceSource::Existing {
let models = service.pullable_models(cx);
if !models.is_empty() {
column = column.child(
div()
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(format!("Models to pull: {}", models.join(", "))),
);
}
}
if let Some(notice) = service.notice(kind) {
column = column.child(
div()
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(notice.to_owned()),
);
}
column.into_any_element()
})
}
fn source_key(kind: ServiceKind) -> &'static str {
match kind {
ServiceKind::Grobid => "grobid-source",
ServiceKind::Ollama => "ollama-source",
ServiceKind::LlamaCpp => "llamacpp-source",
}
}
fn auto_start_key(kind: ServiceKind) -> &'static str {
match kind {
ServiceKind::Grobid => "grobid-auto-start",
ServiceKind::Ollama => "ollama-auto-start",
ServiceKind::LlamaCpp => "llamacpp-auto-start",
}
}
fn image_key(kind: ServiceKind) -> &'static str {
match kind {
ServiceKind::Grobid => "grobid-image",
ServiceKind::Ollama => "ollama-image",
ServiceKind::LlamaCpp => "llamacpp-image",
}
}
fn container_key(kind: ServiceKind) -> &'static str {
match kind {
ServiceKind::Grobid => "grobid-container",
ServiceKind::Ollama => "ollama-container",
ServiceKind::LlamaCpp => "llamacpp-container",
}
}
fn source_field(kind: ServiceKind) -> RagField {
match kind {
ServiceKind::Grobid => RagField::GrobidSource,
ServiceKind::Ollama => RagField::OllamaSource,
ServiceKind::LlamaCpp => RagField::LlamaCppSource,
}
}
fn image_field(kind: ServiceKind) -> RagField {
match kind {
ServiceKind::Grobid => RagField::GrobidImage,
ServiceKind::Ollama => RagField::OllamaImage,
ServiceKind::LlamaCpp => RagField::LlamaCppImage,
}
}
fn container_field(kind: ServiceKind) -> RagField {
match kind {
ServiceKind::Grobid => RagField::GrobidContainer,
ServiceKind::Ollama => RagField::OllamaContainer,
ServiceKind::LlamaCpp => RagField::LlamaCppContainer,
}
}
fn auto_start_field(kind: ServiceKind) -> RagField {
match kind {
ServiceKind::Grobid => RagField::GrobidAutoStart,
ServiceKind::Ollama => RagField::OllamaAutoStart,
ServiceKind::LlamaCpp => RagField::LlamaCppAutoStart,
}
}
fn progress(received: u64, total: u64, cx: &App) -> AnyElement {
let ratio = if total > 0 {
(received as f32 / total as f32).clamp(0.0, 1.0)
} else {
0.0
};
div()
.h(px(5.0))
.w_full()
.rounded_full()
.bg(cx.theme().muted)
.child(
div()
.h_full()
.w(relative(ratio))
.rounded_full()
.bg(cx.theme().primary),
)
.into_any_element()
}