Updated all references from non-existent docs/features/ai-models.md to the correct path docs/5-CONFIGURATION/ai-providers.md Fixes #412
266 lines
No EOL
8.9 KiB
TypeScript
266 lines
No EOL
8.9 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useState } from 'react'
|
|
import { useForm } from 'react-hook-form'
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
|
import { Label } from '@/components/ui/label'
|
|
import { Alert, AlertDescription } from '@/components/ui/alert'
|
|
import { Button } from '@/components/ui/button'
|
|
import { ModelDefaults, Model } from '@/lib/types/models'
|
|
import { useUpdateModelDefaults } from '@/lib/hooks/use-models'
|
|
import { AlertCircle, X } from 'lucide-react'
|
|
import { EmbeddingModelChangeDialog } from './EmbeddingModelChangeDialog'
|
|
|
|
interface DefaultModelsSectionProps {
|
|
models: Model[]
|
|
defaults: ModelDefaults
|
|
}
|
|
|
|
interface DefaultConfig {
|
|
key: keyof ModelDefaults
|
|
label: string
|
|
description: string
|
|
modelType: 'language' | 'embedding' | 'text_to_speech' | 'speech_to_text'
|
|
required?: boolean
|
|
}
|
|
|
|
const defaultConfigs: DefaultConfig[] = [
|
|
{
|
|
key: 'default_chat_model',
|
|
label: 'Chat Model',
|
|
description: 'Used for chat conversations',
|
|
modelType: 'language',
|
|
required: true
|
|
},
|
|
{
|
|
key: 'default_transformation_model',
|
|
label: 'Transformation Model',
|
|
description: 'Used for summaries, insights, and transformations',
|
|
modelType: 'language',
|
|
required: true
|
|
},
|
|
{
|
|
key: 'default_tools_model',
|
|
label: 'Tools Model',
|
|
description: 'Used for function calling - OpenAI or Anthropic recommended',
|
|
modelType: 'language'
|
|
},
|
|
{
|
|
key: 'large_context_model',
|
|
label: 'Large Context Model',
|
|
description: 'Used for processing large documents - Gemini recommended',
|
|
modelType: 'language'
|
|
},
|
|
{
|
|
key: 'default_embedding_model',
|
|
label: 'Embedding Model',
|
|
description: 'Used for semantic search and vector embeddings',
|
|
modelType: 'embedding',
|
|
required: true
|
|
},
|
|
{
|
|
key: 'default_text_to_speech_model',
|
|
label: 'Text-to-Speech Model',
|
|
description: 'Used for podcast generation',
|
|
modelType: 'text_to_speech'
|
|
},
|
|
{
|
|
key: 'default_speech_to_text_model',
|
|
label: 'Speech-to-Text Model',
|
|
description: 'Used for audio transcription',
|
|
modelType: 'speech_to_text'
|
|
}
|
|
]
|
|
|
|
export function DefaultModelsSection({ models, defaults }: DefaultModelsSectionProps) {
|
|
const updateDefaults = useUpdateModelDefaults()
|
|
const { setValue, watch } = useForm<ModelDefaults>({
|
|
defaultValues: defaults
|
|
})
|
|
|
|
// State for embedding model change dialog
|
|
const [showEmbeddingDialog, setShowEmbeddingDialog] = useState(false)
|
|
const [pendingEmbeddingChange, setPendingEmbeddingChange] = useState<{
|
|
key: keyof ModelDefaults
|
|
value: string
|
|
oldModelId?: string
|
|
newModelId?: string
|
|
} | null>(null)
|
|
|
|
// Update form when defaults change
|
|
useEffect(() => {
|
|
if (defaults) {
|
|
Object.entries(defaults).forEach(([key, value]) => {
|
|
setValue(key as keyof ModelDefaults, value)
|
|
})
|
|
}
|
|
}, [defaults, setValue])
|
|
|
|
const handleChange = (key: keyof ModelDefaults, value: string) => {
|
|
// Special handling for embedding model changes
|
|
if (key === 'default_embedding_model') {
|
|
const currentEmbeddingModel = defaults[key]
|
|
|
|
// Only show dialog if there's an existing embedding model and it's changing
|
|
if (currentEmbeddingModel && currentEmbeddingModel !== value) {
|
|
setPendingEmbeddingChange({
|
|
key,
|
|
value,
|
|
oldModelId: currentEmbeddingModel,
|
|
newModelId: value
|
|
})
|
|
setShowEmbeddingDialog(true)
|
|
return
|
|
}
|
|
}
|
|
|
|
// For all other changes or new embedding model assignment
|
|
const newDefaults = { [key]: value || null }
|
|
updateDefaults.mutate(newDefaults)
|
|
}
|
|
|
|
const handleConfirmEmbeddingChange = () => {
|
|
if (pendingEmbeddingChange) {
|
|
const newDefaults = {
|
|
[pendingEmbeddingChange.key]: pendingEmbeddingChange.value || null
|
|
}
|
|
updateDefaults.mutate(newDefaults)
|
|
setPendingEmbeddingChange(null)
|
|
}
|
|
}
|
|
|
|
const handleCancelEmbeddingChange = () => {
|
|
setPendingEmbeddingChange(null)
|
|
setShowEmbeddingDialog(false)
|
|
}
|
|
|
|
const getModelsForType = (type: 'language' | 'embedding' | 'text_to_speech' | 'speech_to_text') => {
|
|
return models.filter(model => model.type === type)
|
|
}
|
|
|
|
const missingRequired = defaultConfigs
|
|
.filter(config => {
|
|
if (!config.required) return false
|
|
const value = defaults[config.key]
|
|
if (!value) return true
|
|
// Check if the model still exists
|
|
const modelsOfType = models.filter(m => m.type === config.modelType)
|
|
return !modelsOfType.some(m => m.id === value)
|
|
})
|
|
.map(config => config.label)
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Default Model Assignments</CardTitle>
|
|
<CardDescription>
|
|
Configure which models to use for different purposes across Open Notebook
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
{missingRequired.length > 0 && (
|
|
<Alert>
|
|
<AlertCircle className="h-4 w-4" />
|
|
<AlertDescription>
|
|
Missing required models: {missingRequired.join(', ')}.
|
|
Open Notebook may not function properly without these.
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
<div className="grid gap-6 md:grid-cols-2">
|
|
{defaultConfigs.map((config) => {
|
|
const availableModels = getModelsForType(config.modelType)
|
|
const currentValue = watch(config.key) || undefined
|
|
|
|
// Check if the current value exists in available models
|
|
const isValidModel = currentValue && availableModels.some(m => m.id === currentValue)
|
|
|
|
return (
|
|
<div key={config.key} className="space-y-2">
|
|
<Label>
|
|
{config.label}
|
|
{config.required && <span className="text-destructive ml-1">*</span>}
|
|
</Label>
|
|
<div className="flex gap-2">
|
|
<Select
|
|
value={currentValue || ""}
|
|
onValueChange={(value) => handleChange(config.key, value)}
|
|
>
|
|
<SelectTrigger className={
|
|
config.required && !isValidModel && availableModels.length > 0
|
|
? 'border-destructive'
|
|
: ''
|
|
}>
|
|
<SelectValue placeholder={
|
|
config.required && !isValidModel && availableModels.length > 0
|
|
? "⚠️ Required - Select a model"
|
|
: "Select a model"
|
|
} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{availableModels.sort((a, b) => a.name.localeCompare(b.name)).map((model) => (
|
|
<SelectItem key={model.id} value={model.id}>
|
|
<div className="flex items-center justify-between w-full">
|
|
<span>{model.name}</span>
|
|
<span className="text-xs text-muted-foreground ml-2">
|
|
{model.provider}
|
|
</span>
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
{!config.required && currentValue && (
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => handleChange(config.key, "")}
|
|
className="h-10 w-10"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">{config.description}</p>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
<div className="pt-4 border-t">
|
|
<a
|
|
href="https://github.com/lfnovo/open-notebook/blob/main/docs/5-CONFIGURATION/ai-providers.md"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-sm text-primary hover:underline"
|
|
>
|
|
Which model should I choose? →
|
|
</a>
|
|
</div>
|
|
</CardContent>
|
|
|
|
{/* Embedding Model Change Dialog */}
|
|
<EmbeddingModelChangeDialog
|
|
open={showEmbeddingDialog}
|
|
onOpenChange={(open) => {
|
|
if (!open) {
|
|
handleCancelEmbeddingChange()
|
|
}
|
|
}}
|
|
onConfirm={handleConfirmEmbeddingChange}
|
|
oldModelName={
|
|
pendingEmbeddingChange?.oldModelId
|
|
? models.find(m => m.id === pendingEmbeddingChange.oldModelId)?.name
|
|
: undefined
|
|
}
|
|
newModelName={
|
|
pendingEmbeddingChange?.newModelId
|
|
? models.find(m => m.id === pendingEmbeddingChange.newModelId)?.name
|
|
: undefined
|
|
}
|
|
/>
|
|
</Card>
|
|
)
|
|
} |