Merge branch 'main' into feat/refactor-folder-structure
This commit is contained in:
commit
16d49c5f2d
11 changed files with 800 additions and 52 deletions
|
|
@ -56,8 +56,13 @@ export const useDoclingHealthQuery = (
|
||||||
queryKey: ["docling-health"],
|
queryKey: ["docling-health"],
|
||||||
queryFn: checkDoclingHealth,
|
queryFn: checkDoclingHealth,
|
||||||
retry: 1,
|
retry: 1,
|
||||||
refetchInterval: 30000, // Check every 30 seconds
|
refetchInterval: (query) => {
|
||||||
staleTime: 25000, // Consider data stale after 25 seconds
|
// If healthy, check every 30 seconds; otherwise check every 3 seconds
|
||||||
|
return query.state.data?.status === "healthy" ? 30000 : 3000;
|
||||||
|
},
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
|
refetchOnMount: true,
|
||||||
|
staleTime: 30000, // Consider data stale after 25 seconds
|
||||||
...options,
|
...options,
|
||||||
},
|
},
|
||||||
queryClient,
|
queryClient,
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,13 @@ export const useProviderHealthQuery = (
|
||||||
queryKey: ["provider", "health"],
|
queryKey: ["provider", "health"],
|
||||||
queryFn: checkProviderHealth,
|
queryFn: checkProviderHealth,
|
||||||
retry: false, // Don't retry health checks automatically
|
retry: false, // Don't retry health checks automatically
|
||||||
|
refetchInterval: (query) => {
|
||||||
|
// If healthy, check every 30 seconds; otherwise check every 3 seconds
|
||||||
|
return query.state.data?.status === "healthy" ? 30000 : 3000;
|
||||||
|
},
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
|
refetchOnMount: true,
|
||||||
|
staleTime: 30000, // Consider data stale after 25 seconds
|
||||||
enabled: !!settings?.edited && options?.enabled !== false, // Only run after onboarding is complete
|
enabled: !!settings?.edited && options?.enabled !== false, // Only run after onboarding is complete
|
||||||
...options,
|
...options,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -365,4 +365,22 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.thinking-dots::after {
|
||||||
|
content: ".";
|
||||||
|
animation: thinking-dots 1.4s steps(3, end) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes thinking-dots {
|
||||||
|
0% {
|
||||||
|
content: ".";
|
||||||
|
}
|
||||||
|
33.33% {
|
||||||
|
content: "..";
|
||||||
|
}
|
||||||
|
66.66%,
|
||||||
|
100% {
|
||||||
|
content: "...";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
97
frontend/src/app/chat/components/assistant-message.tsx
Normal file
97
frontend/src/app/chat/components/assistant-message.tsx
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
import { GitBranch } from "lucide-react";
|
||||||
|
import { motion } from "motion/react";
|
||||||
|
import DogIcon from "@/components/logo/dog-icon";
|
||||||
|
import { MarkdownRenderer } from "@/components/markdown-renderer";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import type { FunctionCall } from "../types";
|
||||||
|
import { FunctionCalls } from "./function-calls";
|
||||||
|
import { Message } from "./message";
|
||||||
|
|
||||||
|
interface AssistantMessageProps {
|
||||||
|
content: string;
|
||||||
|
functionCalls?: FunctionCall[];
|
||||||
|
messageIndex?: number;
|
||||||
|
expandedFunctionCalls: Set<string>;
|
||||||
|
onToggle: (functionCallId: string) => void;
|
||||||
|
isStreaming?: boolean;
|
||||||
|
showForkButton?: boolean;
|
||||||
|
onFork?: (e: React.MouseEvent) => void;
|
||||||
|
isCompleted?: boolean;
|
||||||
|
isInactive?: boolean;
|
||||||
|
animate?: boolean;
|
||||||
|
delay?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AssistantMessage({
|
||||||
|
content,
|
||||||
|
functionCalls = [],
|
||||||
|
messageIndex,
|
||||||
|
expandedFunctionCalls,
|
||||||
|
onToggle,
|
||||||
|
isStreaming = false,
|
||||||
|
showForkButton = false,
|
||||||
|
onFork,
|
||||||
|
isCompleted = false,
|
||||||
|
isInactive = false,
|
||||||
|
animate = true,
|
||||||
|
delay = 0.2,
|
||||||
|
}: AssistantMessageProps) {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={animate ? { opacity: 0, y: -20 } : { opacity: 1, y: 0 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={
|
||||||
|
animate
|
||||||
|
? { duration: 0.4, delay: delay, ease: "easeOut" }
|
||||||
|
: { duration: 0 }
|
||||||
|
}
|
||||||
|
className={isCompleted ? "opacity-50" : ""}
|
||||||
|
>
|
||||||
|
<Message
|
||||||
|
icon={
|
||||||
|
<div className="w-8 h-8 flex items-center justify-center flex-shrink-0 select-none">
|
||||||
|
<DogIcon
|
||||||
|
className="h-6 w-6 transition-colors duration-300"
|
||||||
|
disabled={isCompleted || isInactive}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
actions={
|
||||||
|
showForkButton && onFork ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onFork}
|
||||||
|
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-accent rounded text-muted-foreground hover:text-foreground"
|
||||||
|
title="Fork conversation from here"
|
||||||
|
>
|
||||||
|
<GitBranch className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FunctionCalls
|
||||||
|
functionCalls={functionCalls}
|
||||||
|
messageIndex={messageIndex}
|
||||||
|
expandedFunctionCalls={expandedFunctionCalls}
|
||||||
|
onToggle={onToggle}
|
||||||
|
/>
|
||||||
|
<div className="relative">
|
||||||
|
<MarkdownRenderer
|
||||||
|
className={cn(
|
||||||
|
"text-sm py-1.5 transition-colors duration-300",
|
||||||
|
isCompleted ? "text-placeholder-foreground" : "text-foreground",
|
||||||
|
)}
|
||||||
|
chatMessage={
|
||||||
|
isStreaming
|
||||||
|
? (content.trim()
|
||||||
|
? content +
|
||||||
|
' <span class="inline-block w-1 h-4 bg-primary ml-1 animate-pulse"></span>'
|
||||||
|
: '<span class="text-muted-foreground italic">Thinking<span class="thinking-dots"></span></span>')
|
||||||
|
: content
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Message>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
210
frontend/src/app/onboarding/components/ibm-onboarding.tsx
Normal file
210
frontend/src/app/onboarding/components/ibm-onboarding.tsx
Normal file
|
|
@ -0,0 +1,210 @@
|
||||||
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { LabelInput } from "@/components/label-input";
|
||||||
|
import { LabelWrapper } from "@/components/label-wrapper";
|
||||||
|
import IBMLogo from "@/components/logo/ibm-logo";
|
||||||
|
import { useDebouncedValue } from "@/lib/debounce";
|
||||||
|
import type { OnboardingVariables } from "../../api/mutations/useOnboardingMutation";
|
||||||
|
import { useGetIBMModelsQuery } from "../../api/queries/useGetModelsQuery";
|
||||||
|
import { useModelSelection } from "../hooks/useModelSelection";
|
||||||
|
import { useUpdateSettings } from "../hooks/useUpdateSettings";
|
||||||
|
import { AdvancedOnboarding } from "./advanced";
|
||||||
|
import { ModelSelector } from "./model-selector";
|
||||||
|
|
||||||
|
export function IBMOnboarding({
|
||||||
|
isEmbedding = false,
|
||||||
|
setSettings,
|
||||||
|
sampleDataset,
|
||||||
|
setSampleDataset,
|
||||||
|
setIsLoadingModels,
|
||||||
|
alreadyConfigured = false,
|
||||||
|
}: {
|
||||||
|
isEmbedding?: boolean;
|
||||||
|
setSettings: Dispatch<SetStateAction<OnboardingVariables>>;
|
||||||
|
sampleDataset: boolean;
|
||||||
|
setSampleDataset: (dataset: boolean) => void;
|
||||||
|
setIsLoadingModels?: (isLoading: boolean) => void;
|
||||||
|
alreadyConfigured?: boolean;
|
||||||
|
}) {
|
||||||
|
const [endpoint, setEndpoint] = useState(alreadyConfigured ? "" : "https://us-south.ml.cloud.ibm.com");
|
||||||
|
const [apiKey, setApiKey] = useState("");
|
||||||
|
const [projectId, setProjectId] = useState("");
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{
|
||||||
|
value: "https://us-south.ml.cloud.ibm.com",
|
||||||
|
label: "https://us-south.ml.cloud.ibm.com",
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "https://eu-de.ml.cloud.ibm.com",
|
||||||
|
label: "https://eu-de.ml.cloud.ibm.com",
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "https://eu-gb.ml.cloud.ibm.com",
|
||||||
|
label: "https://eu-gb.ml.cloud.ibm.com",
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "https://au-syd.ml.cloud.ibm.com",
|
||||||
|
label: "https://au-syd.ml.cloud.ibm.com",
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "https://jp-tok.ml.cloud.ibm.com",
|
||||||
|
label: "https://jp-tok.ml.cloud.ibm.com",
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "https://ca-tor.ml.cloud.ibm.com",
|
||||||
|
label: "https://ca-tor.ml.cloud.ibm.com",
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const debouncedEndpoint = useDebouncedValue(endpoint, 500);
|
||||||
|
const debouncedApiKey = useDebouncedValue(apiKey, 500);
|
||||||
|
const debouncedProjectId = useDebouncedValue(projectId, 500);
|
||||||
|
|
||||||
|
// Fetch models from API when all credentials are provided
|
||||||
|
const {
|
||||||
|
data: modelsData,
|
||||||
|
isLoading: isLoadingModels,
|
||||||
|
error: modelsError,
|
||||||
|
} = useGetIBMModelsQuery(
|
||||||
|
{
|
||||||
|
endpoint: debouncedEndpoint ? debouncedEndpoint : undefined,
|
||||||
|
apiKey: debouncedApiKey ? debouncedApiKey : undefined,
|
||||||
|
projectId: debouncedProjectId ? debouncedProjectId : undefined,
|
||||||
|
},
|
||||||
|
{ enabled: !!debouncedEndpoint || !!debouncedApiKey || !!debouncedProjectId || alreadyConfigured },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use custom hook for model selection logic
|
||||||
|
const {
|
||||||
|
languageModel,
|
||||||
|
embeddingModel,
|
||||||
|
setLanguageModel,
|
||||||
|
setEmbeddingModel,
|
||||||
|
languageModels,
|
||||||
|
embeddingModels,
|
||||||
|
} = useModelSelection(modelsData, isEmbedding);
|
||||||
|
const handleSampleDatasetChange = (dataset: boolean) => {
|
||||||
|
setSampleDataset(dataset);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsLoadingModels?.(isLoadingModels);
|
||||||
|
}, [isLoadingModels, setIsLoadingModels]);
|
||||||
|
|
||||||
|
// Update settings when values change
|
||||||
|
useUpdateSettings(
|
||||||
|
"watsonx",
|
||||||
|
{
|
||||||
|
endpoint,
|
||||||
|
apiKey,
|
||||||
|
projectId,
|
||||||
|
languageModel,
|
||||||
|
embeddingModel,
|
||||||
|
},
|
||||||
|
setSettings,
|
||||||
|
isEmbedding,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<LabelWrapper
|
||||||
|
label="watsonx.ai API Endpoint"
|
||||||
|
helperText="Base URL of the API"
|
||||||
|
id="api-endpoint"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<ModelSelector
|
||||||
|
options={alreadyConfigured ? [] : options}
|
||||||
|
value={endpoint}
|
||||||
|
custom
|
||||||
|
onValueChange={alreadyConfigured ? () => {} : setEndpoint}
|
||||||
|
searchPlaceholder="Search endpoint..."
|
||||||
|
noOptionsPlaceholder={
|
||||||
|
alreadyConfigured
|
||||||
|
? "https://•••••••••••••••••••••••••••••••••••••••••"
|
||||||
|
: "No endpoints available"
|
||||||
|
}
|
||||||
|
placeholder="Select endpoint..."
|
||||||
|
/>
|
||||||
|
{alreadyConfigured && (
|
||||||
|
<p className="text-mmd text-muted-foreground">
|
||||||
|
Reusing endpoint from model provider selection.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</LabelWrapper>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<LabelInput
|
||||||
|
label="watsonx Project ID"
|
||||||
|
helperText="Project ID for the model"
|
||||||
|
id="project-id"
|
||||||
|
required
|
||||||
|
placeholder={
|
||||||
|
alreadyConfigured ? "••••••••••••••••••••••••" : "your-project-id"
|
||||||
|
}
|
||||||
|
value={projectId}
|
||||||
|
onChange={(e) => setProjectId(e.target.value)}
|
||||||
|
disabled={alreadyConfigured}
|
||||||
|
/>
|
||||||
|
{alreadyConfigured && (
|
||||||
|
<p className="text-mmd text-muted-foreground">
|
||||||
|
Reusing project ID from model provider selection.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<LabelInput
|
||||||
|
label="watsonx API key"
|
||||||
|
helperText="API key to access watsonx.ai"
|
||||||
|
id="api-key"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
placeholder={
|
||||||
|
alreadyConfigured
|
||||||
|
? "•••••••••••••••••••••••••••••••••••••••••"
|
||||||
|
: "your-api-key"
|
||||||
|
}
|
||||||
|
value={apiKey}
|
||||||
|
onChange={(e) => setApiKey(e.target.value)}
|
||||||
|
disabled={alreadyConfigured}
|
||||||
|
/>
|
||||||
|
{alreadyConfigured && (
|
||||||
|
<p className="text-mmd text-muted-foreground">
|
||||||
|
Reusing API key from model provider selection.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{isLoadingModels && (
|
||||||
|
<p className="text-mmd text-muted-foreground">
|
||||||
|
Validating configuration...
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{modelsError && (
|
||||||
|
<p className="text-mmd text-accent-amber-foreground">
|
||||||
|
Connection failed. Check your configuration.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<AdvancedOnboarding
|
||||||
|
icon={<IBMLogo className="w-4 h-4" />}
|
||||||
|
languageModels={languageModels}
|
||||||
|
embeddingModels={embeddingModels}
|
||||||
|
languageModel={languageModel}
|
||||||
|
embeddingModel={embeddingModel}
|
||||||
|
sampleDataset={sampleDataset}
|
||||||
|
setLanguageModel={setLanguageModel}
|
||||||
|
setEmbeddingModel={setEmbeddingModel}
|
||||||
|
setSampleDataset={handleSampleDatasetChange}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
174
frontend/src/app/onboarding/components/ollama-onboarding.tsx
Normal file
174
frontend/src/app/onboarding/components/ollama-onboarding.tsx
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { LabelInput } from "@/components/label-input";
|
||||||
|
import { LabelWrapper } from "@/components/label-wrapper";
|
||||||
|
import OllamaLogo from "@/components/logo/ollama-logo";
|
||||||
|
import { useDebouncedValue } from "@/lib/debounce";
|
||||||
|
import type { OnboardingVariables } from "../../api/mutations/useOnboardingMutation";
|
||||||
|
import { useGetOllamaModelsQuery } from "../../api/queries/useGetModelsQuery";
|
||||||
|
import { useModelSelection } from "../hooks/useModelSelection";
|
||||||
|
import { useUpdateSettings } from "../hooks/useUpdateSettings";
|
||||||
|
import { ModelSelector } from "./model-selector";
|
||||||
|
|
||||||
|
export function OllamaOnboarding({
|
||||||
|
setSettings,
|
||||||
|
sampleDataset,
|
||||||
|
setSampleDataset,
|
||||||
|
setIsLoadingModels,
|
||||||
|
isEmbedding = false,
|
||||||
|
alreadyConfigured = false,
|
||||||
|
}: {
|
||||||
|
setSettings: Dispatch<SetStateAction<OnboardingVariables>>;
|
||||||
|
sampleDataset: boolean;
|
||||||
|
setSampleDataset: (dataset: boolean) => void;
|
||||||
|
setIsLoadingModels?: (isLoading: boolean) => void;
|
||||||
|
isEmbedding?: boolean;
|
||||||
|
alreadyConfigured?: boolean;
|
||||||
|
}) {
|
||||||
|
const [endpoint, setEndpoint] = useState(alreadyConfigured ? undefined : `http://localhost:11434`);
|
||||||
|
const [showConnecting, setShowConnecting] = useState(false);
|
||||||
|
const debouncedEndpoint = useDebouncedValue(endpoint, 500);
|
||||||
|
|
||||||
|
// Fetch models from API when endpoint is provided (debounced)
|
||||||
|
const {
|
||||||
|
data: modelsData,
|
||||||
|
isLoading: isLoadingModels,
|
||||||
|
error: modelsError,
|
||||||
|
} = useGetOllamaModelsQuery(
|
||||||
|
debouncedEndpoint ? { endpoint: debouncedEndpoint } : undefined,
|
||||||
|
{ enabled: !!debouncedEndpoint || alreadyConfigured },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use custom hook for model selection logic
|
||||||
|
const {
|
||||||
|
languageModel,
|
||||||
|
embeddingModel,
|
||||||
|
setLanguageModel,
|
||||||
|
setEmbeddingModel,
|
||||||
|
languageModels,
|
||||||
|
embeddingModels,
|
||||||
|
} = useModelSelection(modelsData, isEmbedding);
|
||||||
|
|
||||||
|
// Handle delayed display of connecting state
|
||||||
|
useEffect(() => {
|
||||||
|
let timeoutId: NodeJS.Timeout;
|
||||||
|
|
||||||
|
if (debouncedEndpoint && isLoadingModels) {
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
setIsLoadingModels?.(true);
|
||||||
|
setShowConnecting(true);
|
||||||
|
}, 500);
|
||||||
|
} else {
|
||||||
|
setShowConnecting(false);
|
||||||
|
setIsLoadingModels?.(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [debouncedEndpoint, isLoadingModels, setIsLoadingModels]);
|
||||||
|
|
||||||
|
// Update settings when values change
|
||||||
|
useUpdateSettings(
|
||||||
|
"ollama",
|
||||||
|
{
|
||||||
|
endpoint,
|
||||||
|
languageModel,
|
||||||
|
embeddingModel,
|
||||||
|
},
|
||||||
|
setSettings,
|
||||||
|
isEmbedding,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check validation state based on models query
|
||||||
|
const hasConnectionError = debouncedEndpoint && modelsError;
|
||||||
|
const hasNoModels =
|
||||||
|
modelsData &&
|
||||||
|
!modelsData.language_models?.length &&
|
||||||
|
!modelsData.embedding_models?.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<LabelInput
|
||||||
|
label="Ollama Base URL"
|
||||||
|
helperText="Base URL of your Ollama server"
|
||||||
|
id="api-endpoint"
|
||||||
|
required
|
||||||
|
placeholder={
|
||||||
|
alreadyConfigured
|
||||||
|
? "http://••••••••••••••••••••"
|
||||||
|
: "http://localhost:11434"
|
||||||
|
}
|
||||||
|
value={endpoint}
|
||||||
|
onChange={(e) => setEndpoint(e.target.value)}
|
||||||
|
disabled={alreadyConfigured}
|
||||||
|
/>
|
||||||
|
{alreadyConfigured && (
|
||||||
|
<p className="text-mmd text-muted-foreground">
|
||||||
|
Reusing endpoint from model provider selection.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{showConnecting && (
|
||||||
|
<p className="text-mmd text-muted-foreground">
|
||||||
|
Connecting to Ollama server...
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{hasConnectionError && (
|
||||||
|
<p className="text-mmd text-accent-amber-foreground">
|
||||||
|
Can't reach Ollama at {debouncedEndpoint}. Update the base URL or
|
||||||
|
start the server.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{hasNoModels && (
|
||||||
|
<p className="text-mmd text-accent-amber-foreground">
|
||||||
|
No models found. Install embedding and agent models on your Ollama
|
||||||
|
server.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{isEmbedding && setEmbeddingModel && (
|
||||||
|
<LabelWrapper
|
||||||
|
label="Embedding model"
|
||||||
|
helperText="Model used for knowledge ingest and retrieval"
|
||||||
|
id="embedding-model"
|
||||||
|
required={true}
|
||||||
|
>
|
||||||
|
<ModelSelector
|
||||||
|
options={embeddingModels}
|
||||||
|
icon={<OllamaLogo className="w-4 h-4" />}
|
||||||
|
noOptionsPlaceholder={
|
||||||
|
isLoadingModels
|
||||||
|
? "Loading models..."
|
||||||
|
: "No embedding models detected. Install an embedding model to continue."
|
||||||
|
}
|
||||||
|
value={embeddingModel}
|
||||||
|
onValueChange={setEmbeddingModel}
|
||||||
|
/>
|
||||||
|
</LabelWrapper>
|
||||||
|
)}
|
||||||
|
{!isEmbedding && setLanguageModel && (
|
||||||
|
<LabelWrapper
|
||||||
|
label="Language model"
|
||||||
|
helperText="Model used for chat"
|
||||||
|
id="embedding-model"
|
||||||
|
required={true}
|
||||||
|
>
|
||||||
|
<ModelSelector
|
||||||
|
options={languageModels}
|
||||||
|
icon={<OllamaLogo className="w-4 h-4" />}
|
||||||
|
noOptionsPlaceholder={
|
||||||
|
isLoadingModels
|
||||||
|
? "Loading models..."
|
||||||
|
: "No language models detected. Install a language model to continue."
|
||||||
|
}
|
||||||
|
value={languageModel}
|
||||||
|
onValueChange={setLanguageModel}
|
||||||
|
/>
|
||||||
|
</LabelWrapper>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
168
frontend/src/app/onboarding/components/openai-onboarding.tsx
Normal file
168
frontend/src/app/onboarding/components/openai-onboarding.tsx
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { LabelInput } from "@/components/label-input";
|
||||||
|
import { LabelWrapper } from "@/components/label-wrapper";
|
||||||
|
import OpenAILogo from "@/components/logo/openai-logo";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { useDebouncedValue } from "@/lib/debounce";
|
||||||
|
import type { OnboardingVariables } from "../../api/mutations/useOnboardingMutation";
|
||||||
|
import { useGetOpenAIModelsQuery } from "../../api/queries/useGetModelsQuery";
|
||||||
|
import { useModelSelection } from "../hooks/useModelSelection";
|
||||||
|
import { useUpdateSettings } from "../hooks/useUpdateSettings";
|
||||||
|
import { AdvancedOnboarding } from "./advanced";
|
||||||
|
|
||||||
|
export function OpenAIOnboarding({
|
||||||
|
setSettings,
|
||||||
|
sampleDataset,
|
||||||
|
setSampleDataset,
|
||||||
|
setIsLoadingModels,
|
||||||
|
isEmbedding = false,
|
||||||
|
hasEnvApiKey = false,
|
||||||
|
alreadyConfigured = false,
|
||||||
|
}: {
|
||||||
|
setSettings: Dispatch<SetStateAction<OnboardingVariables>>;
|
||||||
|
sampleDataset: boolean;
|
||||||
|
setSampleDataset: (dataset: boolean) => void;
|
||||||
|
setIsLoadingModels?: (isLoading: boolean) => void;
|
||||||
|
isEmbedding?: boolean;
|
||||||
|
hasEnvApiKey?: boolean;
|
||||||
|
alreadyConfigured?: boolean;
|
||||||
|
}) {
|
||||||
|
const [apiKey, setApiKey] = useState("");
|
||||||
|
const [getFromEnv, setGetFromEnv] = useState(hasEnvApiKey && !alreadyConfigured);
|
||||||
|
const debouncedApiKey = useDebouncedValue(apiKey, 500);
|
||||||
|
|
||||||
|
// Fetch models from API when API key is provided
|
||||||
|
const {
|
||||||
|
data: modelsData,
|
||||||
|
isLoading: isLoadingModels,
|
||||||
|
error: modelsError,
|
||||||
|
} = useGetOpenAIModelsQuery(
|
||||||
|
getFromEnv
|
||||||
|
? { apiKey: "" }
|
||||||
|
: debouncedApiKey
|
||||||
|
? { apiKey: debouncedApiKey }
|
||||||
|
: undefined,
|
||||||
|
{ enabled: debouncedApiKey !== "" || getFromEnv || alreadyConfigured },
|
||||||
|
);
|
||||||
|
// Use custom hook for model selection logic
|
||||||
|
const {
|
||||||
|
languageModel,
|
||||||
|
embeddingModel,
|
||||||
|
setLanguageModel,
|
||||||
|
setEmbeddingModel,
|
||||||
|
languageModels,
|
||||||
|
embeddingModels,
|
||||||
|
} = useModelSelection(modelsData, isEmbedding);
|
||||||
|
const handleSampleDatasetChange = (dataset: boolean) => {
|
||||||
|
setSampleDataset(dataset);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGetFromEnvChange = (fromEnv: boolean) => {
|
||||||
|
setGetFromEnv(fromEnv);
|
||||||
|
if (fromEnv) {
|
||||||
|
setApiKey("");
|
||||||
|
}
|
||||||
|
setEmbeddingModel?.("");
|
||||||
|
setLanguageModel?.("");
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsLoadingModels?.(isLoadingModels);
|
||||||
|
}, [isLoadingModels, setIsLoadingModels]);
|
||||||
|
|
||||||
|
// Update settings when values change
|
||||||
|
useUpdateSettings(
|
||||||
|
"openai",
|
||||||
|
{
|
||||||
|
apiKey,
|
||||||
|
languageModel,
|
||||||
|
embeddingModel,
|
||||||
|
},
|
||||||
|
setSettings,
|
||||||
|
isEmbedding,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="space-y-5">
|
||||||
|
{!alreadyConfigured && (
|
||||||
|
<LabelWrapper
|
||||||
|
label="Use environment OpenAI API key"
|
||||||
|
id="get-api-key"
|
||||||
|
description="Reuse the key from your environment config. Turn off to enter a different key."
|
||||||
|
flex
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div>
|
||||||
|
<Switch
|
||||||
|
checked={getFromEnv}
|
||||||
|
onCheckedChange={handleGetFromEnvChange}
|
||||||
|
disabled={!hasEnvApiKey}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
{!hasEnvApiKey && (
|
||||||
|
<TooltipContent>
|
||||||
|
OpenAI API key not detected in the environment.
|
||||||
|
</TooltipContent>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
</LabelWrapper>
|
||||||
|
)}
|
||||||
|
{(!getFromEnv || alreadyConfigured) && (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<LabelInput
|
||||||
|
label="OpenAI API key"
|
||||||
|
helperText="The API key for your OpenAI account."
|
||||||
|
className={modelsError ? "!border-destructive" : ""}
|
||||||
|
id="api-key"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
placeholder={
|
||||||
|
alreadyConfigured
|
||||||
|
? "sk-•••••••••••••••••••••••••••••••••••••••••"
|
||||||
|
: "sk-..."
|
||||||
|
}
|
||||||
|
value={apiKey}
|
||||||
|
onChange={(e) => setApiKey(e.target.value)}
|
||||||
|
disabled={alreadyConfigured}
|
||||||
|
/>
|
||||||
|
{alreadyConfigured && (
|
||||||
|
<p className="text-mmd text-muted-foreground">
|
||||||
|
Reusing key from model provider selection.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{isLoadingModels && (
|
||||||
|
<p className="text-mmd text-muted-foreground">
|
||||||
|
Validating API key...
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{modelsError && (
|
||||||
|
<p className="text-mmd text-destructive">
|
||||||
|
Invalid OpenAI API key. Verify or replace the key.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<AdvancedOnboarding
|
||||||
|
icon={<OpenAILogo className="w-4 h-4" />}
|
||||||
|
languageModels={languageModels}
|
||||||
|
embeddingModels={embeddingModels}
|
||||||
|
languageModel={languageModel}
|
||||||
|
embeddingModel={embeddingModel}
|
||||||
|
sampleDataset={sampleDataset}
|
||||||
|
setLanguageModel={setLanguageModel}
|
||||||
|
setSampleDataset={handleSampleDatasetChange}
|
||||||
|
setEmbeddingModel={setEmbeddingModel}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -112,7 +112,7 @@ async def _test_openai_completion_with_tools(api_key: str, llm_model: str) -> No
|
||||||
}
|
}
|
||||||
|
|
||||||
# Simple tool calling test
|
# Simple tool calling test
|
||||||
payload = {
|
base_payload = {
|
||||||
"model": llm_model,
|
"model": llm_model,
|
||||||
"messages": [
|
"messages": [
|
||||||
{"role": "user", "content": "What tools do you have available?"}
|
{"role": "user", "content": "What tools do you have available?"}
|
||||||
|
|
@ -136,10 +136,11 @@ async def _test_openai_completion_with_tools(api_key: str, llm_model: str) -> No
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"max_tokens": 50,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
|
# Try with max_tokens first
|
||||||
|
payload = {**base_payload, "max_tokens": 50}
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
"https://api.openai.com/v1/chat/completions",
|
"https://api.openai.com/v1/chat/completions",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
|
|
@ -147,6 +148,17 @@ async def _test_openai_completion_with_tools(api_key: str, llm_model: str) -> No
|
||||||
timeout=30.0,
|
timeout=30.0,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# If max_tokens doesn't work, try with max_completion_tokens
|
||||||
|
if response.status_code != 200:
|
||||||
|
logger.info("max_tokens parameter failed, trying max_completion_tokens instead")
|
||||||
|
payload = {**base_payload, "max_completion_tokens": 50}
|
||||||
|
response = await client.post(
|
||||||
|
"https://api.openai.com/v1/chat/completions",
|
||||||
|
headers=headers,
|
||||||
|
json=payload,
|
||||||
|
timeout=30.0,
|
||||||
|
)
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
logger.error(f"OpenAI completion test failed: {response.status_code} - {response.text}")
|
logger.error(f"OpenAI completion test failed: {response.status_code} - {response.text}")
|
||||||
raise Exception(f"OpenAI API error: {response.status_code}")
|
raise Exception(f"OpenAI API error: {response.status_code}")
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import httpx
|
import httpx
|
||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
from api.provider_validation import test_embedding
|
|
||||||
from utils.container_utils import transform_localhost_url
|
from utils.container_utils import transform_localhost_url
|
||||||
from utils.logging_config import get_logger
|
from utils.logging_config import get_logger
|
||||||
|
|
||||||
|
|
@ -229,20 +228,14 @@ class ModelsService:
|
||||||
f"Model: {model_name}, Capabilities: {capabilities}"
|
f"Model: {model_name}, Capabilities: {capabilities}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if model has required capabilities
|
# Check if model has embedding capability
|
||||||
|
has_embedding = "embedding" in capabilities
|
||||||
|
# Check if model has required capabilities for language models
|
||||||
has_completion = DESIRED_CAPABILITY in capabilities
|
has_completion = DESIRED_CAPABILITY in capabilities
|
||||||
has_tools = TOOL_CALLING_CAPABILITY in capabilities
|
has_tools = TOOL_CALLING_CAPABILITY in capabilities
|
||||||
|
|
||||||
# Check if it's an embedding model
|
if has_embedding:
|
||||||
try:
|
# Embedding models have embedding capability
|
||||||
await test_embedding("ollama", endpoint=endpoint, embedding_model=model_name)
|
|
||||||
is_embedding = True
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Failed to test embedding for model {model_name}: {str(e)}")
|
|
||||||
is_embedding = False
|
|
||||||
|
|
||||||
if is_embedding:
|
|
||||||
# Embedding models only need completion capability
|
|
||||||
embedding_models.append(
|
embedding_models.append(
|
||||||
{
|
{
|
||||||
"value": model_name,
|
"value": model_name,
|
||||||
|
|
@ -250,7 +243,7 @@ class ModelsService:
|
||||||
"default": "nomic-embed-text" in model_name.lower(),
|
"default": "nomic-embed-text" in model_name.lower(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
elif not is_embedding and has_completion and has_tools:
|
if has_completion and has_tools:
|
||||||
# Language models need both completion and tool calling
|
# Language models need both completion and tool calling
|
||||||
language_models.append(
|
language_models.append(
|
||||||
{
|
{
|
||||||
|
|
@ -333,34 +326,6 @@ class ModelsService:
|
||||||
if project_id:
|
if project_id:
|
||||||
headers["Project-ID"] = project_id
|
headers["Project-ID"] = project_id
|
||||||
|
|
||||||
# Validate credentials with a minimal completion request
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
validation_url = f"{watson_endpoint}/ml/v1/text/generation"
|
|
||||||
validation_params = {"version": "2024-09-16"}
|
|
||||||
validation_payload = {
|
|
||||||
"input": "test",
|
|
||||||
"model_id": "ibm/granite-3-2b-instruct",
|
|
||||||
"project_id": project_id,
|
|
||||||
"parameters": {
|
|
||||||
"max_new_tokens": 1,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
validation_response = await client.post(
|
|
||||||
validation_url,
|
|
||||||
headers=headers,
|
|
||||||
params=validation_params,
|
|
||||||
json=validation_payload,
|
|
||||||
timeout=10.0,
|
|
||||||
)
|
|
||||||
|
|
||||||
if validation_response.status_code != 200:
|
|
||||||
raise Exception(
|
|
||||||
f"Invalid credentials or endpoint: {validation_response.status_code} - {validation_response.text}"
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info("IBM Watson credentials validated successfully")
|
|
||||||
|
|
||||||
# Fetch foundation models using the correct endpoint
|
# Fetch foundation models using the correct endpoint
|
||||||
models_url = f"{watson_endpoint}/ml/v1/foundation_model_specs"
|
models_url = f"{watson_endpoint}/ml/v1/foundation_model_specs"
|
||||||
|
|
||||||
|
|
@ -424,6 +389,39 @@ class ModelsService:
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Validate credentials with the first available LLM model
|
||||||
|
if language_models:
|
||||||
|
first_llm_model = language_models[0]["value"]
|
||||||
|
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
validation_url = f"{watson_endpoint}/ml/v1/text/generation"
|
||||||
|
validation_params = {"version": "2024-09-16"}
|
||||||
|
validation_payload = {
|
||||||
|
"input": "test",
|
||||||
|
"model_id": first_llm_model,
|
||||||
|
"project_id": project_id,
|
||||||
|
"parameters": {
|
||||||
|
"max_new_tokens": 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
validation_response = await client.post(
|
||||||
|
validation_url,
|
||||||
|
headers=headers,
|
||||||
|
params=validation_params,
|
||||||
|
json=validation_payload,
|
||||||
|
timeout=10.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
if validation_response.status_code != 200:
|
||||||
|
raise Exception(
|
||||||
|
f"Invalid credentials or endpoint: {validation_response.status_code} - {validation_response.text}"
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"IBM Watson credentials validated successfully using model: {first_llm_model}")
|
||||||
|
else:
|
||||||
|
logger.warning("No language models available to validate credentials")
|
||||||
|
|
||||||
if not language_models and not embedding_models:
|
if not language_models and not embedding_models:
|
||||||
raise Exception("No IBM models retrieved from API")
|
raise Exception("No IBM models retrieved from API")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ class DoclingManager:
|
||||||
# Bind to all interfaces by default (can be overridden with DOCLING_BIND_HOST env var)
|
# Bind to all interfaces by default (can be overridden with DOCLING_BIND_HOST env var)
|
||||||
self._host = os.getenv('DOCLING_BIND_HOST', '0.0.0.0')
|
self._host = os.getenv('DOCLING_BIND_HOST', '0.0.0.0')
|
||||||
self._running = False
|
self._running = False
|
||||||
|
self._starting = False
|
||||||
self._external_process = False
|
self._external_process = False
|
||||||
|
|
||||||
# PID file to track docling-serve across sessions (in current working directory)
|
# PID file to track docling-serve across sessions (in current working directory)
|
||||||
|
|
@ -126,6 +127,7 @@ class DoclingManager:
|
||||||
if self._process is not None and self._process.poll() is None:
|
if self._process is not None and self._process.poll() is None:
|
||||||
self._running = True
|
self._running = True
|
||||||
self._external_process = False
|
self._external_process = False
|
||||||
|
self._starting = False # Clear starting flag if service is running
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Check if we have a PID from file
|
# Check if we have a PID from file
|
||||||
|
|
@ -133,6 +135,7 @@ class DoclingManager:
|
||||||
if pid is not None and self._is_process_running(pid):
|
if pid is not None and self._is_process_running(pid):
|
||||||
self._running = True
|
self._running = True
|
||||||
self._external_process = True
|
self._external_process = True
|
||||||
|
self._starting = False # Clear starting flag if service is running
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# No running process found
|
# No running process found
|
||||||
|
|
@ -142,6 +145,19 @@ class DoclingManager:
|
||||||
|
|
||||||
def get_status(self) -> Dict[str, Any]:
|
def get_status(self) -> Dict[str, Any]:
|
||||||
"""Get current status of docling serve."""
|
"""Get current status of docling serve."""
|
||||||
|
# Check for starting state first
|
||||||
|
if self._starting:
|
||||||
|
display_host = "localhost" if self._host == "0.0.0.0" else self._host
|
||||||
|
return {
|
||||||
|
"status": "starting",
|
||||||
|
"port": self._port,
|
||||||
|
"host": self._host,
|
||||||
|
"endpoint": None,
|
||||||
|
"docs_url": None,
|
||||||
|
"ui_url": None,
|
||||||
|
"pid": None
|
||||||
|
}
|
||||||
|
|
||||||
if self.is_running():
|
if self.is_running():
|
||||||
# Try to get PID from process handle first, then from PID file
|
# Try to get PID from process handle first, then from PID file
|
||||||
pid = None
|
pid = None
|
||||||
|
|
@ -196,6 +212,9 @@ class DoclingManager:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._add_log_entry(f"Error checking port availability: {e}")
|
self._add_log_entry(f"Error checking port availability: {e}")
|
||||||
|
|
||||||
|
# Set starting flag to show "Starting" status in UI
|
||||||
|
self._starting = True
|
||||||
|
|
||||||
# Clear log buffer when starting
|
# Clear log buffer when starting
|
||||||
self._log_buffer = []
|
self._log_buffer = []
|
||||||
self._add_log_entry("Starting docling serve as external process...")
|
self._add_log_entry("Starting docling serve as external process...")
|
||||||
|
|
@ -261,6 +280,8 @@ class DoclingManager:
|
||||||
|
|
||||||
if result == 0:
|
if result == 0:
|
||||||
self._add_log_entry(f"Docling-serve is now listening on {self._host}:{self._port}")
|
self._add_log_entry(f"Docling-serve is now listening on {self._host}:{self._port}")
|
||||||
|
# Service is now running, clear starting flag
|
||||||
|
self._starting = False
|
||||||
break
|
break
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
@ -294,16 +315,24 @@ class DoclingManager:
|
||||||
self._add_log_entry(f"Error reading final output: {e}")
|
self._add_log_entry(f"Error reading final output: {e}")
|
||||||
|
|
||||||
self._running = False
|
self._running = False
|
||||||
|
self._starting = False
|
||||||
return False, f"Docling serve process exited immediately (code: {return_code})"
|
return False, f"Docling serve process exited immediately (code: {return_code})"
|
||||||
|
|
||||||
|
# If we get here and the process is still running but not listening yet,
|
||||||
|
# clear the starting flag anyway (it's running, just not ready)
|
||||||
|
if self._process.poll() is None:
|
||||||
|
self._starting = False
|
||||||
|
|
||||||
display_host = "localhost" if self._host == "0.0.0.0" else self._host
|
display_host = "localhost" if self._host == "0.0.0.0" else self._host
|
||||||
return True, f"Docling serve starting on http://{display_host}:{port}"
|
return True, f"Docling serve starting on http://{display_host}:{port}"
|
||||||
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
|
self._starting = False
|
||||||
return False, "docling-serve not available. Please install: uv add docling-serve"
|
return False, "docling-serve not available. Please install: uv add docling-serve"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._running = False
|
self._running = False
|
||||||
self._process = None
|
self._process = None
|
||||||
|
self._starting = False
|
||||||
return False, f"Error starting docling serve: {str(e)}"
|
return False, f"Error starting docling serve: {str(e)}"
|
||||||
|
|
||||||
def _start_output_capture(self):
|
def _start_output_capture(self):
|
||||||
|
|
|
||||||
|
|
@ -206,10 +206,21 @@ class MonitorScreen(Screen):
|
||||||
|
|
||||||
# Add docling serve to its own table
|
# Add docling serve to its own table
|
||||||
docling_status = self.docling_manager.get_status()
|
docling_status = self.docling_manager.get_status()
|
||||||
docling_running = docling_status["status"] == "running"
|
docling_status_value = docling_status["status"]
|
||||||
docling_status_text = "running" if docling_running else "stopped"
|
docling_running = docling_status_value == "running"
|
||||||
docling_style = "bold green" if docling_running else "bold red"
|
docling_starting = docling_status_value == "starting"
|
||||||
docling_port = f"{docling_status['host']}:{docling_status['port']}" if docling_running else "N/A"
|
|
||||||
|
if docling_running:
|
||||||
|
docling_status_text = "running"
|
||||||
|
docling_style = "bold green"
|
||||||
|
elif docling_starting:
|
||||||
|
docling_status_text = "starting"
|
||||||
|
docling_style = "bold yellow"
|
||||||
|
else:
|
||||||
|
docling_status_text = "stopped"
|
||||||
|
docling_style = "bold red"
|
||||||
|
|
||||||
|
docling_port = f"{docling_status['host']}:{docling_status['port']}" if (docling_running or docling_starting) else "N/A"
|
||||||
docling_pid = str(docling_status.get("pid")) if docling_status.get("pid") else "N/A"
|
docling_pid = str(docling_status.get("pid")) if docling_status.get("pid") else "N/A"
|
||||||
|
|
||||||
if self.docling_table:
|
if self.docling_table:
|
||||||
|
|
@ -375,15 +386,25 @@ class MonitorScreen(Screen):
|
||||||
"""Start docling serve."""
|
"""Start docling serve."""
|
||||||
self.operation_in_progress = True
|
self.operation_in_progress = True
|
||||||
try:
|
try:
|
||||||
success, message = await self.docling_manager.start()
|
# Start the service (this sets _starting = True internally at the start)
|
||||||
|
# Create task and let it begin executing (which sets the flag)
|
||||||
|
start_task = asyncio.create_task(self.docling_manager.start())
|
||||||
|
# Give it a tiny moment to set the _starting flag
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
# Refresh immediately to show "Starting" status
|
||||||
|
await self._refresh_services()
|
||||||
|
# Now wait for start to complete
|
||||||
|
success, message = await start_task
|
||||||
if success:
|
if success:
|
||||||
self.notify(message, severity="information")
|
self.notify(message, severity="information")
|
||||||
else:
|
else:
|
||||||
self.notify(f"Failed to start docling serve: {message}", severity="error")
|
self.notify(f"Failed to start docling serve: {message}", severity="error")
|
||||||
# Refresh the services table to show updated status
|
# Refresh again to show final status (running or stopped)
|
||||||
await self._refresh_services()
|
await self._refresh_services()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.notify(f"Error starting docling serve: {str(e)}", severity="error")
|
self.notify(f"Error starting docling serve: {str(e)}", severity="error")
|
||||||
|
# Refresh on error to clear starting status
|
||||||
|
await self._refresh_services()
|
||||||
finally:
|
finally:
|
||||||
self.operation_in_progress = False
|
self.operation_in_progress = False
|
||||||
|
|
||||||
|
|
@ -646,7 +667,11 @@ class MonitorScreen(Screen):
|
||||||
suffix = f"-{random.randint(10000, 99999)}"
|
suffix = f"-{random.randint(10000, 99999)}"
|
||||||
|
|
||||||
# Add docling serve controls
|
# Add docling serve controls
|
||||||
docling_running = self.docling_manager.is_running()
|
docling_status = self.docling_manager.get_status()
|
||||||
|
docling_status_value = docling_status["status"]
|
||||||
|
docling_running = docling_status_value == "running"
|
||||||
|
docling_starting = docling_status_value == "starting"
|
||||||
|
|
||||||
if docling_running:
|
if docling_running:
|
||||||
docling_controls.mount(
|
docling_controls.mount(
|
||||||
Button("Stop", variant="error", id=f"docling-stop-btn{suffix}")
|
Button("Stop", variant="error", id=f"docling-stop-btn{suffix}")
|
||||||
|
|
@ -654,6 +679,11 @@ class MonitorScreen(Screen):
|
||||||
docling_controls.mount(
|
docling_controls.mount(
|
||||||
Button("Restart", variant="primary", id=f"docling-restart-btn{suffix}")
|
Button("Restart", variant="primary", id=f"docling-restart-btn{suffix}")
|
||||||
)
|
)
|
||||||
|
elif docling_starting:
|
||||||
|
# Show disabled button or no button when starting
|
||||||
|
start_btn = Button("Starting...", variant="warning", id=f"docling-start-btn{suffix}")
|
||||||
|
start_btn.disabled = True
|
||||||
|
docling_controls.mount(start_btn)
|
||||||
else:
|
else:
|
||||||
docling_controls.mount(
|
docling_controls.mount(
|
||||||
Button("Start", variant="success", id=f"docling-start-btn{suffix}")
|
Button("Start", variant="success", id=f"docling-start-btn{suffix}")
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue