baveshraam's picture
FIX: SurrealDB 2.0 migration syntax and Frontend/CORS link
f871fed
'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/features/ai-models.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>
)
}