feat: add model load error handling to improve UX (#5802)
* feat: model load error handling * chore: clean up * test: add tests * fix: provider name
This commit is contained in:
parent
bcb60378c0
commit
8d84c3b884
@ -82,7 +82,7 @@ fn validate_proxy_config(config: &ProxyConfig) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SSL verification settings are all optional booleans, no validation needed
|
// SSL verification settings are all optional booleans, no validation needed
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,7 +155,7 @@ fn _get_client_for_item(
|
|||||||
// Note: reqwest doesn't have fine-grained SSL verification controls
|
// Note: reqwest doesn't have fine-grained SSL verification controls
|
||||||
// for verify_proxy_ssl, verify_proxy_host_ssl, verify_peer_ssl, verify_host_ssl
|
// for verify_proxy_ssl, verify_proxy_host_ssl, verify_peer_ssl, verify_host_ssl
|
||||||
// These settings are handled by the underlying TLS implementation
|
// These settings are handled by the underlying TLS implementation
|
||||||
|
|
||||||
// Check if this URL should bypass proxy
|
// Check if this URL should bypass proxy
|
||||||
let no_proxy = proxy_config.no_proxy.as_deref().unwrap_or(&[]);
|
let no_proxy = proxy_config.no_proxy.as_deref().unwrap_or(&[]);
|
||||||
if !should_bypass_proxy(&item.url, no_proxy) {
|
if !should_bypass_proxy(&item.url, no_proxy) {
|
||||||
@ -617,17 +617,17 @@ mod tests {
|
|||||||
config.verify_proxy_host_ssl = Some(false);
|
config.verify_proxy_host_ssl = Some(false);
|
||||||
config.verify_peer_ssl = Some(true);
|
config.verify_peer_ssl = Some(true);
|
||||||
config.verify_host_ssl = Some(true);
|
config.verify_host_ssl = Some(true);
|
||||||
|
|
||||||
// Should validate successfully
|
// Should validate successfully
|
||||||
assert!(validate_proxy_config(&config).is_ok());
|
assert!(validate_proxy_config(&config).is_ok());
|
||||||
|
|
||||||
// Test with all SSL settings as false
|
// Test with all SSL settings as false
|
||||||
config.ignore_ssl = Some(false);
|
config.ignore_ssl = Some(false);
|
||||||
config.verify_proxy_ssl = Some(false);
|
config.verify_proxy_ssl = Some(false);
|
||||||
config.verify_proxy_host_ssl = Some(false);
|
config.verify_proxy_host_ssl = Some(false);
|
||||||
config.verify_peer_ssl = Some(false);
|
config.verify_peer_ssl = Some(false);
|
||||||
config.verify_host_ssl = Some(false);
|
config.verify_host_ssl = Some(false);
|
||||||
|
|
||||||
// Should still validate successfully
|
// Should still validate successfully
|
||||||
assert!(validate_proxy_config(&config).is_ok());
|
assert!(validate_proxy_config(&config).is_ok());
|
||||||
}
|
}
|
||||||
@ -641,7 +641,7 @@ mod tests {
|
|||||||
config.verify_proxy_host_ssl = Some(true);
|
config.verify_proxy_host_ssl = Some(true);
|
||||||
config.verify_peer_ssl = Some(false);
|
config.verify_peer_ssl = Some(false);
|
||||||
config.verify_host_ssl = Some(true);
|
config.verify_host_ssl = Some(true);
|
||||||
|
|
||||||
assert!(validate_proxy_config(&config).is_ok());
|
assert!(validate_proxy_config(&config).is_ok());
|
||||||
assert!(create_proxy_from_config(&config).is_ok());
|
assert!(create_proxy_from_config(&config).is_ok());
|
||||||
}
|
}
|
||||||
@ -650,61 +650,30 @@ mod tests {
|
|||||||
fn test_proxy_config_ssl_defaults() {
|
fn test_proxy_config_ssl_defaults() {
|
||||||
// Test with no SSL settings (should use None defaults)
|
// Test with no SSL settings (should use None defaults)
|
||||||
let config = create_test_proxy_config("https://proxy.example.com:8080");
|
let config = create_test_proxy_config("https://proxy.example.com:8080");
|
||||||
|
|
||||||
assert_eq!(config.ignore_ssl, None);
|
assert_eq!(config.ignore_ssl, None);
|
||||||
assert_eq!(config.verify_proxy_ssl, None);
|
assert_eq!(config.verify_proxy_ssl, None);
|
||||||
assert_eq!(config.verify_proxy_host_ssl, None);
|
assert_eq!(config.verify_proxy_host_ssl, None);
|
||||||
assert_eq!(config.verify_peer_ssl, None);
|
assert_eq!(config.verify_peer_ssl, None);
|
||||||
assert_eq!(config.verify_host_ssl, None);
|
assert_eq!(config.verify_host_ssl, None);
|
||||||
|
|
||||||
assert!(validate_proxy_config(&config).is_ok());
|
assert!(validate_proxy_config(&config).is_ok());
|
||||||
assert!(create_proxy_from_config(&config).is_ok());
|
assert!(create_proxy_from_config(&config).is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_proxy_config_serialization() {
|
|
||||||
// Test that proxy config with SSL settings can be serialized/deserialized
|
|
||||||
let mut config = create_test_proxy_config("https://proxy.example.com:8080");
|
|
||||||
config.username = Some("user".to_string());
|
|
||||||
config.password = Some("pass".to_string());
|
|
||||||
config.ignore_ssl = Some(true);
|
|
||||||
config.verify_proxy_ssl = Some(false);
|
|
||||||
config.verify_proxy_host_ssl = Some(false);
|
|
||||||
config.verify_peer_ssl = Some(true);
|
|
||||||
config.verify_host_ssl = Some(true);
|
|
||||||
config.no_proxy = Some(vec!["localhost".to_string(), "*.example.com".to_string()]);
|
|
||||||
|
|
||||||
// Serialize to JSON
|
|
||||||
let json = serde_json::to_string(&config).unwrap();
|
|
||||||
|
|
||||||
// Deserialize from JSON
|
|
||||||
let deserialized: ProxyConfig = serde_json::from_str(&json).unwrap();
|
|
||||||
|
|
||||||
// Verify all fields are preserved
|
|
||||||
assert_eq!(deserialized.url, config.url);
|
|
||||||
assert_eq!(deserialized.username, config.username);
|
|
||||||
assert_eq!(deserialized.password, config.password);
|
|
||||||
assert_eq!(deserialized.ignore_ssl, config.ignore_ssl);
|
|
||||||
assert_eq!(deserialized.verify_proxy_ssl, config.verify_proxy_ssl);
|
|
||||||
assert_eq!(deserialized.verify_proxy_host_ssl, config.verify_proxy_host_ssl);
|
|
||||||
assert_eq!(deserialized.verify_peer_ssl, config.verify_peer_ssl);
|
|
||||||
assert_eq!(deserialized.verify_host_ssl, config.verify_host_ssl);
|
|
||||||
assert_eq!(deserialized.no_proxy, config.no_proxy);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_download_item_with_ssl_proxy() {
|
fn test_download_item_with_ssl_proxy() {
|
||||||
// Test that DownloadItem can be created with SSL proxy configuration
|
// Test that DownloadItem can be created with SSL proxy configuration
|
||||||
let mut proxy_config = create_test_proxy_config("https://proxy.example.com:8080");
|
let mut proxy_config = create_test_proxy_config("https://proxy.example.com:8080");
|
||||||
proxy_config.ignore_ssl = Some(true);
|
proxy_config.ignore_ssl = Some(true);
|
||||||
proxy_config.verify_proxy_ssl = Some(false);
|
proxy_config.verify_proxy_ssl = Some(false);
|
||||||
|
|
||||||
let download_item = DownloadItem {
|
let download_item = DownloadItem {
|
||||||
url: "https://example.com/file.zip".to_string(),
|
url: "https://example.com/file.zip".to_string(),
|
||||||
save_path: "downloads/file.zip".to_string(),
|
save_path: "downloads/file.zip".to_string(),
|
||||||
proxy: Some(proxy_config),
|
proxy: Some(proxy_config),
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(download_item.proxy.is_some());
|
assert!(download_item.proxy.is_some());
|
||||||
let proxy = download_item.proxy.unwrap();
|
let proxy = download_item.proxy.unwrap();
|
||||||
assert_eq!(proxy.ignore_ssl, Some(true));
|
assert_eq!(proxy.ignore_ssl, Some(true));
|
||||||
@ -716,16 +685,16 @@ mod tests {
|
|||||||
// Test client creation with SSL settings
|
// Test client creation with SSL settings
|
||||||
let mut proxy_config = create_test_proxy_config("https://proxy.example.com:8080");
|
let mut proxy_config = create_test_proxy_config("https://proxy.example.com:8080");
|
||||||
proxy_config.ignore_ssl = Some(true);
|
proxy_config.ignore_ssl = Some(true);
|
||||||
|
|
||||||
let download_item = DownloadItem {
|
let download_item = DownloadItem {
|
||||||
url: "https://example.com/file.zip".to_string(),
|
url: "https://example.com/file.zip".to_string(),
|
||||||
save_path: "downloads/file.zip".to_string(),
|
save_path: "downloads/file.zip".to_string(),
|
||||||
proxy: Some(proxy_config),
|
proxy: Some(proxy_config),
|
||||||
};
|
};
|
||||||
|
|
||||||
let header_map = HeaderMap::new();
|
let header_map = HeaderMap::new();
|
||||||
let result = _get_client_for_item(&download_item, &header_map);
|
let result = _get_client_for_item(&download_item, &header_map);
|
||||||
|
|
||||||
// Should create client successfully even with SSL settings
|
// Should create client successfully even with SSL settings
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
@ -736,7 +705,7 @@ mod tests {
|
|||||||
let mut config = create_test_proxy_config("http://proxy.example.com:8080");
|
let mut config = create_test_proxy_config("http://proxy.example.com:8080");
|
||||||
config.ignore_ssl = Some(true);
|
config.ignore_ssl = Some(true);
|
||||||
config.verify_proxy_ssl = Some(false);
|
config.verify_proxy_ssl = Some(false);
|
||||||
|
|
||||||
assert!(validate_proxy_config(&config).is_ok());
|
assert!(validate_proxy_config(&config).is_ok());
|
||||||
assert!(create_proxy_from_config(&config).is_ok());
|
assert!(create_proxy_from_config(&config).is_ok());
|
||||||
}
|
}
|
||||||
@ -748,8 +717,98 @@ mod tests {
|
|||||||
config.ignore_ssl = Some(false);
|
config.ignore_ssl = Some(false);
|
||||||
config.verify_peer_ssl = Some(true);
|
config.verify_peer_ssl = Some(true);
|
||||||
config.verify_host_ssl = Some(true);
|
config.verify_host_ssl = Some(true);
|
||||||
|
|
||||||
assert!(validate_proxy_config(&config).is_ok());
|
assert!(validate_proxy_config(&config).is_ok());
|
||||||
assert!(create_proxy_from_config(&config).is_ok());
|
assert!(create_proxy_from_config(&config).is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_download_item_creation() {
|
||||||
|
let item = DownloadItem {
|
||||||
|
url: "https://example.com/file.tar.gz".to_string(),
|
||||||
|
save_path: "models/test.tar.gz".to_string(),
|
||||||
|
proxy: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(item.url, "https://example.com/file.tar.gz");
|
||||||
|
assert_eq!(item.save_path, "models/test.tar.gz");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_download_event_creation() {
|
||||||
|
let event = DownloadEvent {
|
||||||
|
transferred: 1024,
|
||||||
|
total: 2048,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(event.transferred, 1024);
|
||||||
|
assert_eq!(event.total, 2048);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_err_to_string() {
|
||||||
|
let error = "Test error";
|
||||||
|
let result = err_to_string(error);
|
||||||
|
assert_eq!(result, "Error: Test error");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_convert_headers_valid() {
|
||||||
|
let mut headers = HashMap::new();
|
||||||
|
headers.insert("Content-Type".to_string(), "application/json".to_string());
|
||||||
|
headers.insert("Authorization".to_string(), "Bearer token123".to_string());
|
||||||
|
|
||||||
|
let result = _convert_headers(&headers);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
let header_map = result.unwrap();
|
||||||
|
assert_eq!(header_map.len(), 2);
|
||||||
|
assert_eq!(header_map.get("Content-Type").unwrap(), "application/json");
|
||||||
|
assert_eq!(header_map.get("Authorization").unwrap(), "Bearer token123");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_convert_headers_invalid_header_name() {
|
||||||
|
let mut headers = HashMap::new();
|
||||||
|
headers.insert("Invalid\nHeader".to_string(), "value".to_string());
|
||||||
|
|
||||||
|
let result = _convert_headers(&headers);
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_convert_headers_invalid_header_value() {
|
||||||
|
let mut headers = HashMap::new();
|
||||||
|
headers.insert("Content-Type".to_string(), "invalid\nvalue".to_string());
|
||||||
|
|
||||||
|
let result = _convert_headers(&headers);
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_download_manager_state_default() {
|
||||||
|
let state = DownloadManagerState::default();
|
||||||
|
assert!(state.cancel_tokens.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_download_event_serialization() {
|
||||||
|
let event = DownloadEvent {
|
||||||
|
transferred: 512,
|
||||||
|
total: 1024,
|
||||||
|
};
|
||||||
|
|
||||||
|
let json = serde_json::to_string(&event).unwrap();
|
||||||
|
assert!(json.contains("\"transferred\":512"));
|
||||||
|
assert!(json.contains("\"total\":1024"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_download_item_deserialization() {
|
||||||
|
let json = r#"{"url":"https://example.com/file.zip","save_path":"downloads/file.zip"}"#;
|
||||||
|
let item: DownloadItem = serde_json::from_str(json).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(item.url, "https://example.com/file.zip");
|
||||||
|
assert_eq!(item.save_path, "downloads/file.zip");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
96
web-app/src/containers/ErrorMessage.tsx
Normal file
96
web-app/src/containers/ErrorMessage.tsx
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import { useRef, useState } from 'react'
|
||||||
|
|
||||||
|
import { ThreadMessage } from '@janhq/core'
|
||||||
|
|
||||||
|
import { CheckIcon, ClipboardIcon, SearchCodeIcon } from 'lucide-react'
|
||||||
|
|
||||||
|
const ErrorMessage = ({ message }: { message?: ThreadMessage }) => {
|
||||||
|
// const setModalTroubleShooting = useSetAtom(modalTroubleShootingAtom)
|
||||||
|
const errorDivRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [copied, setCopied] = useState(false)
|
||||||
|
|
||||||
|
const handleCopy = () => {
|
||||||
|
if (errorDivRef.current) {
|
||||||
|
const errorText = errorDivRef.current.innerText
|
||||||
|
if (errorText) {
|
||||||
|
navigator.clipboard.writeText(errorText)
|
||||||
|
setCopied(true)
|
||||||
|
setTimeout(() => setCopied(false), 2000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getErrorTitle = () => {
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
data-testid="passthrough-error-message"
|
||||||
|
className="first-letter:uppercase"
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
{message?.content[0]?.text?.value && (
|
||||||
|
<span>{message?.content[0]?.text?.value}</span>
|
||||||
|
)}
|
||||||
|
{!message?.content[0]?.text?.value && (
|
||||||
|
<span>Something went wrong. Please try again.</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto my-6 max-w-[700px] px-4">
|
||||||
|
<div
|
||||||
|
className="mx-auto max-w-[400px] rounded-lg border border-[hsla(var(--app-border))]"
|
||||||
|
key={message?.id}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between border-b border-inherit px-4 py-2">
|
||||||
|
<h6 className="flex items-center gap-x-1 font-semibold text-[hsla(var(--destructive-bg))]">
|
||||||
|
<span className="h-2 w-2 rounded-full bg-[hsla(var(--destructive-bg))]" />
|
||||||
|
<span>Error</span>
|
||||||
|
</h6>
|
||||||
|
<div className="flex items-center gap-x-4 text-xs">
|
||||||
|
<div className="font-semibold">
|
||||||
|
<span
|
||||||
|
className="flex cursor-pointer items-center gap-x-1 text-[hsla(var(--app-link))]"
|
||||||
|
// onClick={() => setModalTroubleShooting(true)}
|
||||||
|
>
|
||||||
|
<SearchCodeIcon size={14} className="text-inherit" />
|
||||||
|
Troubleshooting
|
||||||
|
</span>
|
||||||
|
{/* <ModalTroubleShooting /> */}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="flex cursor-pointer items-center gap-x-1 font-semibold text-[hsla(var(--text-secondary))]"
|
||||||
|
onClick={handleCopy}
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<>
|
||||||
|
<CheckIcon
|
||||||
|
size={14}
|
||||||
|
className="text-[hsla(var(--success-bg))]"
|
||||||
|
/>
|
||||||
|
Copied
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ClipboardIcon size={14} className="text-inherit" />
|
||||||
|
Copy
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="max-h-[80px] w-full overflow-x-auto p-4 py-2">
|
||||||
|
<div
|
||||||
|
className="font-serif text-xs leading-relaxed text-[hsla(var(--text-secondary))]"
|
||||||
|
ref={errorDivRef}
|
||||||
|
>
|
||||||
|
{getErrorTitle()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default ErrorMessage
|
||||||
80
web-app/src/containers/dialogs/LoadModelErrorDialog.tsx
Normal file
80
web-app/src/containers/dialogs/LoadModelErrorDialog.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { AlertTriangle } from 'lucide-react'
|
||||||
|
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||||
|
import { useModelLoad } from '@/hooks/useModelLoad'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
|
export default function LoadModelErrorDialog() {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { modelLoadError, setModelLoadError } = useModelLoad()
|
||||||
|
|
||||||
|
const handleCopy = () => {
|
||||||
|
navigator.clipboard.writeText(modelLoadError ?? '')
|
||||||
|
toast.success('Copy successful', {
|
||||||
|
id: 'copy-model',
|
||||||
|
description: 'Model load error information copied to clipboard',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDialogOpen = (open: boolean) => {
|
||||||
|
setModelLoadError(open ? modelLoadError : undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={!!modelLoadError} onOpenChange={handleDialogOpen}>
|
||||||
|
<DialogContent showCloseButton={false}>
|
||||||
|
<DialogHeader>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="shrink-0">
|
||||||
|
<AlertTriangle className="size-4" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<DialogTitle>{t('common:error')}</DialogTitle>
|
||||||
|
<DialogDescription className="mt-1 text-main-view-fg/70">
|
||||||
|
Failed to load model
|
||||||
|
</DialogDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="bg-main-view-fg/8 p-2 border border-main-view-fg/5 rounded-lg">
|
||||||
|
<p
|
||||||
|
className="text-sm text-main-view-fg/70 leading-relaxed max-h-[200px] overflow-y-auto"
|
||||||
|
ref={(el) => {
|
||||||
|
if (el) {
|
||||||
|
el.scrollTop = el.scrollHeight
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{modelLoadError}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="flex flex-col gap-2 sm:flex-row sm:justify-right">
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
onClick={() => handleCopy()}
|
||||||
|
className="flex-1 text-right sm:flex-none"
|
||||||
|
>
|
||||||
|
{t('common:copy')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
onClick={() => handleDialogOpen(false)}
|
||||||
|
className="flex-1 text-right sm:flex-none"
|
||||||
|
>
|
||||||
|
{t('common:cancel')}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -19,7 +19,6 @@ import {
|
|||||||
import { CompletionMessagesBuilder } from '@/lib/messages'
|
import { CompletionMessagesBuilder } from '@/lib/messages'
|
||||||
import { ChatCompletionMessageToolCall } from 'openai/resources'
|
import { ChatCompletionMessageToolCall } from 'openai/resources'
|
||||||
import { useAssistant } from './useAssistant'
|
import { useAssistant } from './useAssistant'
|
||||||
import { toast } from 'sonner'
|
|
||||||
import { getTools } from '@/services/mcp'
|
import { getTools } from '@/services/mcp'
|
||||||
import { MCPTool } from '@/types/completion'
|
import { MCPTool } from '@/types/completion'
|
||||||
import { listen } from '@tauri-apps/api/event'
|
import { listen } from '@tauri-apps/api/event'
|
||||||
@ -31,6 +30,7 @@ import { useToolAvailable } from '@/hooks/useToolAvailable'
|
|||||||
import { OUT_OF_CONTEXT_SIZE } from '@/utils/error'
|
import { OUT_OF_CONTEXT_SIZE } from '@/utils/error'
|
||||||
import { updateSettings } from '@/services/providers'
|
import { updateSettings } from '@/services/providers'
|
||||||
import { useContextSizeApproval } from './useModelContextApproval'
|
import { useContextSizeApproval } from './useModelContextApproval'
|
||||||
|
import { useModelLoad } from './useModelLoad'
|
||||||
|
|
||||||
export const useChat = () => {
|
export const useChat = () => {
|
||||||
const { prompt, setPrompt } = usePrompt()
|
const { prompt, setPrompt } = usePrompt()
|
||||||
@ -61,6 +61,7 @@ export const useChat = () => {
|
|||||||
updateThreadTimestamp,
|
updateThreadTimestamp,
|
||||||
} = useThreads()
|
} = useThreads()
|
||||||
const { getMessages, addMessage } = useMessages()
|
const { getMessages, addMessage } = useMessages()
|
||||||
|
const { setModelLoadError } = useModelLoad()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const provider = useMemo(() => {
|
const provider = useMemo(() => {
|
||||||
@ -232,9 +233,7 @@ export const useChat = () => {
|
|||||||
try {
|
try {
|
||||||
if (selectedModel?.id) {
|
if (selectedModel?.id) {
|
||||||
updateLoadingModel(true)
|
updateLoadingModel(true)
|
||||||
await startModel(activeProvider, selectedModel.id).catch(
|
await startModel(activeProvider, selectedModel.id)
|
||||||
console.error
|
|
||||||
)
|
|
||||||
updateLoadingModel(false)
|
updateLoadingModel(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -368,7 +367,7 @@ export const useChat = () => {
|
|||||||
activeThread.model?.id &&
|
activeThread.model?.id &&
|
||||||
provider?.provider === 'llamacpp'
|
provider?.provider === 'llamacpp'
|
||||||
) {
|
) {
|
||||||
await stopModel(activeThread.model.id, 'cortex')
|
await stopModel(activeThread.model.id, 'llamacpp')
|
||||||
throw new Error('No response received from the model')
|
throw new Error('No response received from the model')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -404,9 +403,7 @@ export const useChat = () => {
|
|||||||
error && typeof error === 'object' && 'message' in error
|
error && typeof error === 'object' && 'message' in error
|
||||||
? error.message
|
? error.message
|
||||||
: error
|
: error
|
||||||
|
setModelLoadError(`${errorMessage}`)
|
||||||
toast.error(`Error sending message: ${errorMessage}`)
|
|
||||||
console.error('Error sending message:', error)
|
|
||||||
} finally {
|
} finally {
|
||||||
updateLoadingModel(false)
|
updateLoadingModel(false)
|
||||||
updateStreamingContent(undefined)
|
updateStreamingContent(undefined)
|
||||||
@ -436,6 +433,7 @@ export const useChat = () => {
|
|||||||
showIncreaseContextSizeModal,
|
showIncreaseContextSizeModal,
|
||||||
increaseModelContextSize,
|
increaseModelContextSize,
|
||||||
toggleOnContextShifting,
|
toggleOnContextShifting,
|
||||||
|
setModelLoadError,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
11
web-app/src/hooks/useModelLoad.ts
Normal file
11
web-app/src/hooks/useModelLoad.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { create } from 'zustand'
|
||||||
|
|
||||||
|
type ModelLoadState = {
|
||||||
|
modelLoadError?: string
|
||||||
|
setModelLoadError: (error: string | undefined) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useModelLoad = create<ModelLoadState>()((set) => ({
|
||||||
|
modelLoadError: undefined,
|
||||||
|
setModelLoadError: (error) => set({ modelLoadError: error }),
|
||||||
|
}))
|
||||||
@ -20,6 +20,7 @@ import { cn } from '@/lib/utils'
|
|||||||
import ToolApproval from '@/containers/dialogs/ToolApproval'
|
import ToolApproval from '@/containers/dialogs/ToolApproval'
|
||||||
import { TranslationProvider } from '@/i18n/TranslationContext'
|
import { TranslationProvider } from '@/i18n/TranslationContext'
|
||||||
import OutOfContextPromiseModal from '@/containers/dialogs/OutOfContextDialog'
|
import OutOfContextPromiseModal from '@/containers/dialogs/OutOfContextDialog'
|
||||||
|
import LoadModelErrorDialog from '@/containers/dialogs/LoadModelErrorDialog'
|
||||||
|
|
||||||
export const Route = createRootRoute({
|
export const Route = createRootRoute({
|
||||||
component: RootLayout,
|
component: RootLayout,
|
||||||
@ -97,6 +98,7 @@ function RootLayout() {
|
|||||||
{/* <TanStackRouterDevtools position="bottom-right" /> */}
|
{/* <TanStackRouterDevtools position="bottom-right" /> */}
|
||||||
<CortexFailureDialog />
|
<CortexFailureDialog />
|
||||||
<ToolApproval />
|
<ToolApproval />
|
||||||
|
<LoadModelErrorDialog />
|
||||||
<OutOfContextPromiseModal />
|
<OutOfContextPromiseModal />
|
||||||
</TranslationProvider>
|
</TranslationProvider>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user