Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withLink
import androidx.compose.ui.unit.dp
import io.askimo.ui.common.components.primaryButton
import io.askimo.ui.common.components.secondaryButton
import io.askimo.ui.common.i18n.stringResource
import io.askimo.ui.common.theme.AppComponents
import io.askimo.ui.common.theme.AppTextStyles
Expand All @@ -54,6 +55,8 @@ fun errorDialog(
linkText: String? = null,
linkUrl: String? = null,
details: String? = null,
actionLabel: String? = null,
action: (() -> Unit)? = null,
) {
val linkColor = MaterialTheme.colorScheme.onSurface
var showDetails by remember { mutableStateOf(false) }
Expand Down Expand Up @@ -200,8 +203,16 @@ fun errorDialog(
confirmButton = {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
horizontalArrangement = Arrangement.spacedBy(Spacing.small, Alignment.End),
) {
if (actionLabel != null && action != null) {
secondaryButton(onClick = {
onDismiss()
action()
}) {
Text(actionLabel)
}
}
primaryButton(onClick = onDismiss) {
Text(stringResource("action.ok"))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import io.askimo.core.event.error.IndexingErrorEvent
import io.askimo.core.event.error.IndexingErrorType
import io.askimo.core.event.error.ModelNotAvailableEvent
import io.askimo.core.event.error.SendMessageErrorEvent
import io.askimo.core.event.internal.NavigateToProviderSettingsEvent
import io.askimo.core.exception.AuthenticationException
import io.askimo.core.exception.ExceptionMapper
import io.askimo.core.exception.ModelNotFoundChatException
import io.askimo.core.exception.ProviderNotConfiguredException
import io.askimo.core.i18n.LocalizationManager

/**
Expand All @@ -24,13 +28,22 @@ data class ErrorDialogState(
val linkText: String? = null,
val linkUrl: String? = null,
val details: String? = null,
/** Label for an optional in-dialog action button (e.g. "Configure Provider"). */
val actionLabel: String? = null,
/** Callback invoked when the user clicks the action button. The dialog is dismissed before this runs. */
val action: (() -> Unit)? = null,
)

/**
* Listens to [EventBus.errorEvents] and surfaces errors through [ErrorDialogState].
*
* Call this composable once at the top-level [app] scope and pass the returned
* state (along with the dismiss callback) down to wherever the error dialog is rendered.
* Call this composable once at the top-level app scope.
*
* For errors the user can fix by reconfiguring their provider
* ([AuthenticationException], [ProviderNotConfiguredException], [ModelNotFoundChatException]),
* the resulting [ErrorDialogState] includes an action button that posts
* [NavigateToProviderSettingsEvent] on the internal event bus.
* The top-level composable should listen for that event and open the provider wizard.
*
* @param onStateChange called whenever a new [ErrorDialogState] should be applied.
*/
Expand Down Expand Up @@ -136,11 +149,27 @@ fun globalErrorHandler(onStateChange: (ErrorDialogState) -> Unit) {
mapped.getMessageKey(),
*mapped.getMessageArgs().values.toTypedArray(),
)
val isProviderConfigError = mapped is AuthenticationException ||
mapped is ProviderNotConfiguredException ||
mapped is ModelNotFoundChatException

onStateChange(
ErrorDialogState(
show = true,
title = LocalizationManager.getString("error.send_message.title"),
message = localizedMsg,
actionLabel = if (isProviderConfigError) {
LocalizationManager.getString("error.action.configure_provider")
} else {
null
},
// Posts a navigation event so the UI layer can open the wizard
// without this component holding any reference to the UI.
action = if (isProviderConfigError) {
{ EventBus.post(NavigateToProviderSettingsEvent()) }
} else {
null
},
),
)
}
Expand Down
3 changes: 3 additions & 0 deletions desktop-shared/src/main/resources/i18n/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -951,6 +951,8 @@ provider.instance.name.duplicate=A provider named "{0}" already exists
provider.setup.required.title=Setup AI Provider
provider.setup.required.message=Before you can start chatting, you need to set up an AI provider. Please select a provider from the list and configure it.
provider.setup.required.button=Set Up Provider
provider.setup.required.skip=Set up later
provider.setup.empty.state.button=Add Provider
provider.configure.prompt=Configure provider:
provider.setup.guide=How to set up {0}
provider.apikey.stored=API key stored securely
Expand Down Expand Up @@ -1116,6 +1118,7 @@ error.app.message=An unexpected error occurred: {0}

# Send Message Errors
error.send_message.title=Failed to Send Message
error.action.configure_provider=Configure Provider

# Indexing Errors
error.indexing.model_not_found.title=Embedding Model Not Found
Expand Down
3 changes: 3 additions & 0 deletions desktop-shared/src/main/resources/i18n/messages_de.properties
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,8 @@ provider.instance.name.duplicate=Ein Anbieter mit dem Namen "{0}" existiert bere
provider.setup.required.title=AI-Anbieter einrichten
provider.setup.required.message=Bevor Sie mit dem Chatten beginnen können, müssen Sie einen AI-Anbieter einrichten. Bitte wählen Sie einen Anbieter aus der Liste aus und konfigurieren Sie ihn.
provider.setup.required.button=Anbieter einrichten
provider.setup.required.skip=Später einrichten
provider.setup.empty.state.button=Anbieter hinzufügen
provider.configure.prompt=Anbieter konfigurieren:
provider.setup.guide=So richten Sie {0} ein
provider.apikey.stored=API-Schlüssel sicher gespeichert
Expand Down Expand Up @@ -1115,6 +1117,7 @@ error.app.message=Ein unerwarteter Fehler ist aufgetreten: {0}

# Send Message Errors
error.send_message.title=Nachricht konnte nicht gesendet werden
error.action.configure_provider=Anbieter konfigurieren

# Indexing Errors
error.indexing.model_not_found.title=Einbettungsmodell nicht gefunden
Expand Down
3 changes: 3 additions & 0 deletions desktop-shared/src/main/resources/i18n/messages_es.properties
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,8 @@ provider.instance.name.duplicate=Ya existe un proveedor llamado "{0}"
provider.setup.required.title=Configurar proveedor de AI
provider.setup.required.message=Antes de comenzar a chatear, debe configurar un proveedor de AI. Seleccione un proveedor de la lista y configúrelo.
provider.setup.required.button=Configurar proveedor
provider.setup.required.skip=Configurar más tarde
provider.setup.empty.state.button=Agregar proveedor
provider.configure.prompt=Configurar proveedor:
provider.setup.guide=Cómo configurar {0}
provider.apikey.stored=Clave API guardada de forma segura
Expand Down Expand Up @@ -1115,6 +1117,7 @@ error.app.message=Ocurrió un error inesperado: {0}

# Send Message Errors
error.send_message.title=No se pudo enviar el mensaje
error.action.configure_provider=Configurar proveedor

# Indexing Errors
error.indexing.model_not_found.title=Modelo de incrustación no encontrado
Expand Down
3 changes: 3 additions & 0 deletions desktop-shared/src/main/resources/i18n/messages_fr.properties
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,8 @@ provider.instance.name.duplicate=Un fournisseur nommé "{0}" existe déjà
provider.setup.required.title=Configurer le fournisseur d'AI
provider.setup.required.message=Avant de pouvoir commencer à discuter, vous devez configurer un fournisseur d'AI. Veuillez sélectionner un fournisseur dans la liste et le configurer.
provider.setup.required.button=Configurer le fournisseur
provider.setup.required.skip=Configurer plus tard
provider.setup.empty.state.button=Ajouter un fournisseur
provider.configure.prompt=Configurer le fournisseur :
provider.setup.guide=Comment configurer {0}
provider.apikey.stored=Clé API stockée de manière sécurisée
Expand Down Expand Up @@ -1115,6 +1117,7 @@ error.app.message=Une erreur inattendue s’est produite : {0}

# Send Message Errors
error.send_message.title=Échec de l’envoi du message
error.action.configure_provider=Configurer le fournisseur

# Indexing Errors
error.indexing.model_not_found.title=Modèle d’embedding introuvable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,8 @@ provider.instance.name.duplicate="{0}" という名前のプロバイダーは
provider.setup.required.title=AIプロバイダーのセットアップ
provider.setup.required.message=チャットを開始する前に、AIプロバイダーをセットアップする必要があります。リストからプロバイダーを選択し、設定を行ってください。
provider.setup.required.button=プロバイダーをセットアップ
provider.setup.required.skip=後で設定する
provider.setup.empty.state.button=プロバイダーを追加
provider.configure.prompt=プロバイダーの設定:
provider.setup.guide={0} のセットアップ方法
provider.apikey.stored=APIキーは安全に保存されています
Expand Down Expand Up @@ -1115,6 +1117,7 @@ error.app.message=予期しないエラーが発生しました: {0}

# Send Message Errors
error.send_message.title=メッセージの送信に失敗しました
error.action.configure_provider=プロバイダーを設定

# Indexing Errors
error.indexing.model_not_found.title=埋め込みモデルが見つかりません
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,8 @@ provider.instance.name.duplicate="{0}" 이름의 프로바이더가 이미 존
provider.setup.required.title=AI 제공업체 설정
provider.setup.required.message=채팅을 시작하려면 먼저 AI 제공업체를 설정해야 합니다. 목록에서 제공업체를 선택하고 구성해 주세요.
provider.setup.required.button=제공업체 설정
provider.setup.required.skip=나중에 설정
provider.setup.empty.state.button=공급자 추가
provider.configure.prompt=제공업체 구성:
provider.setup.guide={0} 설정 방법
provider.apikey.stored=API 키가 안전하게 저장됨
Expand Down Expand Up @@ -1115,6 +1117,7 @@ error.app.message=예기치 않은 오류가 발생했습니다: {0}

# Send Message Errors
error.send_message.title=메시지 전송 실패
error.action.configure_provider=공급자 구성

# Indexing Errors
error.indexing.model_not_found.title=임베딩 모델을 찾을 수 없음
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,8 @@ provider.instance.name.duplicate=Já existe um provedor chamado "{0}"
provider.setup.required.title=Configurar provedor de AI
provider.setup.required.message=Antes de começar a conversar, você precisa configurar um provedor de AI. Selecione um provedor da lista e configure-o.
provider.setup.required.button=Configurar provedor
provider.setup.required.skip=Configurar mais tarde
provider.setup.empty.state.button=Adicionar provedor
provider.configure.prompt=Configurar provedor:
provider.setup.guide=Como configurar o {0}
provider.apikey.stored=Chave API armazenada de forma segura
Expand Down Expand Up @@ -1115,6 +1117,7 @@ error.app.message=Ocorreu um erro inesperado: {0}

# Send Message Errors
error.send_message.title=Falha ao enviar mensagem
error.action.configure_provider=Configurar provedor

# Indexing Errors
error.indexing.model_not_found.title=Modelo de embedding não encontrado
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,8 @@ provider.instance.name.duplicate=Nhà cung cấp có tên "{0}" đã tồn tại
provider.setup.required.title=Thiết lập nhà cung cấp AI
provider.setup.required.message=Trước khi có thể bắt đầu trò chuyện, bạn cần thiết lập một nhà cung cấp AI. Vui lòng chọn một nhà cung cấp từ danh sách và cấu hình nó.
provider.setup.required.button=Thiết lập nhà cung cấp
provider.setup.required.skip=Thiết lập sau
provider.setup.empty.state.button=Thêm nhà cung cấp
provider.configure.prompt=Cấu hình nhà cung cấp:
provider.setup.guide=Cách thiết lập {0}
provider.apikey.stored=Khóa API được lưu trữ an toàn
Expand Down Expand Up @@ -1115,6 +1117,7 @@ error.app.message=Đã xảy ra lỗi không mong muốn: {0}

# Send Message Errors
error.send_message.title=Gửi tin nhắn thất bại
error.action.configure_provider=Cấu hình nhà cung cấp

# Indexing Errors
error.indexing.model_not_found.title=Không tìm thấy mô hình embedding
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,8 @@ provider.instance.name.duplicate=名为"{0}"的提供商已存在
provider.setup.required.title=设置 AI 服务商
provider.setup.required.message=在开始聊天之前,您需要设置一个 AI 服务商。请从列表中选择一个服务商并进行配置。
provider.setup.required.button=设置服务商
provider.setup.required.skip=稍后设置
provider.setup.empty.state.button=添加提供商
provider.configure.prompt=配置服务商:
provider.setup.guide=如何设置 {0}
provider.apikey.stored=API 密钥已安全存储
Expand Down Expand Up @@ -1115,6 +1117,7 @@ error.app.message=发生了一个意外错误:{0}

# Send Message Errors
error.send_message.title=发送消息失败
error.action.configure_provider=配置提供程序

# Indexing Errors
error.indexing.model_not_found.title=未找到嵌入模型
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,8 @@ provider.instance.name.duplicate=名為「{0}」的提供商已存在
provider.setup.required.title=設定 AI 服務商
provider.setup.required.message=在開始聊天之前,您需要設定 AI 服務商。請從清單中選擇一個服務商並進行設定。
provider.setup.required.button=設定服務商
provider.setup.required.skip=稍後設定
provider.setup.empty.state.button=新增提供商
provider.configure.prompt=設定服務商:
provider.setup.guide=如何設定 {0}
provider.apikey.stored=API 金鑰已安全儲存
Expand Down Expand Up @@ -1115,6 +1117,7 @@ error.app.message=發生未預期的錯誤:{0}

# Send Message Errors
error.send_message.title=傳送訊息失敗
error.action.configure_provider=設定提供者

# Indexing Errors
error.indexing.model_not_found.title=找不到嵌入模型
Expand Down
26 changes: 22 additions & 4 deletions desktop/src/main/kotlin/io/askimo/desktop/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import io.askimo.core.db.DatabaseManager
import io.askimo.core.event.Event
import io.askimo.core.event.EventBus
import io.askimo.core.event.internal.LanguageDirectiveChangedEvent
import io.askimo.core.event.internal.NavigateToProviderSettingsEvent
import io.askimo.core.event.internal.RunCodeEvent
import io.askimo.core.event.system.InvalidateCacheEvent
import io.askimo.core.i18n.LocalizationManager
Expand Down Expand Up @@ -411,9 +412,6 @@ fun app(frameWindowScope: FrameWindowScope? = null, windowState: WindowState? =
}
}

// Listen for errors – handled by the dedicated GlobalErrorHandler
globalErrorHandler { state -> errorDialogState = state }

val scope = rememberCoroutineScope()

// Backup and restore helper functions
Expand Down Expand Up @@ -508,6 +506,17 @@ fun app(frameWindowScope: FrameWindowScope? = null, windowState: WindowState? =
val settingsViewModel = remember { koin.get<AIProviderViewModel> { parametersOf(scope) } }
val updateViewModel = remember { koin.get<UpdateViewModel> { parametersOf(scope) } }

// Listen for errors via the GlobalErrorHandler.
globalErrorHandler { state -> errorDialogState = state }

// React to NavigateToProviderSettingsEvent posted by GlobalErrorHandler (and any other
// component) to open the provider wizard without the emitter holding a UI reference.
LaunchedEffect(Unit) {
EventBus.internalEvents
.filterIsInstance<NavigateToProviderSettingsEvent>()
.collect { settingsViewModel.onChangeProvider() }
}

val deleteSessionCommand = remember {
koin.get<DeleteSessionFromProjectCommand> { parametersOf(scope) }
}
Expand Down Expand Up @@ -1435,7 +1444,7 @@ fun app(frameWindowScope: FrameWindowScope? = null, windowState: WindowState? =
// Provider setup required dialog
if (showProviderSetupDialog) {
alertDialog(
onDismissRequest = { },
onDismissRequest = { showProviderSetupDialog = false },
title = {
Text(
text = stringResource("provider.setup.required.title"),
Expand All @@ -1458,6 +1467,13 @@ fun app(frameWindowScope: FrameWindowScope? = null, windowState: WindowState? =
Text(stringResource("provider.setup.required.button"))
}
},
dismissButton = {
secondaryButton(
onClick = { showProviderSetupDialog = false },
) {
Text(stringResource("provider.setup.required.skip"))
}
},
)
}

Expand Down Expand Up @@ -1857,6 +1873,8 @@ fun app(frameWindowScope: FrameWindowScope? = null, windowState: WindowState? =
linkText = errorDialogState.linkText,
linkUrl = errorDialogState.linkUrl,
details = errorDialogState.details,
actionLabel = errorDialogState.actionLabel,
action = errorDialogState.action,
onDismiss = {
errorDialogState = ErrorDialogState()
},
Expand Down
Loading