fix: chat "this note" context searches all notes + Ollama model selector missing search
- When chat scope is "this note" (noteContext present), skip RAG/semantic search entirely. Previously the AI received all user notes as context even when scoped to a single note, causing irrelevant responses. - Replace 3 native <select> elements for Ollama models with searchable Combobox component (tags, embeddings, chat providers). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -446,27 +446,21 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="AI_MODEL_TAGS_OLLAMA">{t('admin.ai.model')}</Label>
|
<Label>{t('admin.ai.model')}</Label>
|
||||||
<select
|
<input type="hidden" name="AI_MODEL_TAGS_OLLAMA" value={selectedTagsModel} />
|
||||||
id="AI_MODEL_TAGS_OLLAMA"
|
<Combobox
|
||||||
name="AI_MODEL_TAGS_OLLAMA"
|
options={ollamaTagsModels.length > 0
|
||||||
|
? ollamaTagsModels.map((m) => ({ value: m, label: m }))
|
||||||
|
: selectedTagsModel
|
||||||
|
? [{ value: selectedTagsModel, label: `${selectedTagsModel} (${t('admin.ai.saved')})` }]
|
||||||
|
: []
|
||||||
|
}
|
||||||
value={selectedTagsModel}
|
value={selectedTagsModel}
|
||||||
onChange={(e) => setSelectedTagsModel(e.target.value)}
|
onChange={setSelectedTagsModel}
|
||||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
placeholder={selectedTagsModel || t('admin.ai.clickToLoadModels')}
|
||||||
>
|
searchPlaceholder={t('admin.ai.searchModel')}
|
||||||
{ollamaTagsModels.length > 0 ? (
|
emptyMessage={t('admin.ai.noModels')}
|
||||||
<>
|
/>
|
||||||
{!ollamaTagsModels.includes(selectedTagsModel) && selectedTagsModel && (
|
|
||||||
<option value={selectedTagsModel}>{selectedTagsModel} ({t('admin.ai.configured')})</option>
|
|
||||||
)}
|
|
||||||
{ollamaTagsModels.map((model) => (
|
|
||||||
<option key={model} value={model}>{model}</option>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<option value={selectedTagsModel || 'granite4:latest'}>{selectedTagsModel || 'granite4:latest'} {t('admin.ai.saved')}</option>
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{isLoadingTagsModels ? t('admin.ai.fetchingModels') : t('admin.ai.selectOllamaModel')}
|
{isLoadingTagsModels ? t('admin.ai.fetchingModels') : t('admin.ai.selectOllamaModel')}
|
||||||
</p>
|
</p>
|
||||||
@@ -620,27 +614,21 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="AI_MODEL_EMBEDDING_OLLAMA">{t('admin.ai.model')}</Label>
|
<Label>{t('admin.ai.model')}</Label>
|
||||||
<select
|
<input type="hidden" name="AI_MODEL_EMBEDDING_OLLAMA" value={selectedEmbeddingModel} />
|
||||||
id="AI_MODEL_EMBEDDING_OLLAMA"
|
<Combobox
|
||||||
name="AI_MODEL_EMBEDDING_OLLAMA"
|
options={ollamaEmbeddingsModels.length > 0
|
||||||
|
? ollamaEmbeddingsModels.map((m) => ({ value: m, label: m }))
|
||||||
|
: selectedEmbeddingModel
|
||||||
|
? [{ value: selectedEmbeddingModel, label: `${selectedEmbeddingModel} (${t('admin.ai.saved')})` }]
|
||||||
|
: []
|
||||||
|
}
|
||||||
value={selectedEmbeddingModel}
|
value={selectedEmbeddingModel}
|
||||||
onChange={(e) => setSelectedEmbeddingModel(e.target.value)}
|
onChange={setSelectedEmbeddingModel}
|
||||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
placeholder={selectedEmbeddingModel || t('admin.ai.clickToLoadModels')}
|
||||||
>
|
searchPlaceholder={t('admin.ai.searchModel')}
|
||||||
{ollamaEmbeddingsModels.length > 0 ? (
|
emptyMessage={t('admin.ai.noModels')}
|
||||||
<>
|
/>
|
||||||
{!ollamaEmbeddingsModels.includes(selectedEmbeddingModel) && selectedEmbeddingModel && (
|
|
||||||
<option value={selectedEmbeddingModel}>{selectedEmbeddingModel} ({t('admin.ai.configured')})</option>
|
|
||||||
)}
|
|
||||||
{ollamaEmbeddingsModels.map((model) => (
|
|
||||||
<option key={model} value={model}>{model}</option>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<option value={selectedEmbeddingModel || 'embeddinggemma:latest'}>{selectedEmbeddingModel || 'embeddinggemma:latest'} {t('admin.ai.saved')}</option>
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{isLoadingEmbeddingsModels ? t('admin.ai.fetchingModels') : t('admin.ai.selectEmbeddingModel')}
|
{isLoadingEmbeddingsModels ? t('admin.ai.fetchingModels') : t('admin.ai.selectEmbeddingModel')}
|
||||||
</p>
|
</p>
|
||||||
@@ -790,27 +778,21 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="AI_MODEL_CHAT_OLLAMA">{t('admin.ai.model')}</Label>
|
<Label>{t('admin.ai.model')}</Label>
|
||||||
<select
|
<input type="hidden" name="AI_MODEL_CHAT_OLLAMA" value={selectedChatModel} />
|
||||||
id="AI_MODEL_CHAT_OLLAMA"
|
<Combobox
|
||||||
name="AI_MODEL_CHAT_OLLAMA"
|
options={ollamaChatModels.length > 0
|
||||||
|
? ollamaChatModels.map((m) => ({ value: m, label: m }))
|
||||||
|
: selectedChatModel
|
||||||
|
? [{ value: selectedChatModel, label: `${selectedChatModel} (${t('admin.ai.saved')})` }]
|
||||||
|
: []
|
||||||
|
}
|
||||||
value={selectedChatModel}
|
value={selectedChatModel}
|
||||||
onChange={(e) => setSelectedChatModel(e.target.value)}
|
onChange={setSelectedChatModel}
|
||||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
placeholder={selectedChatModel || t('admin.ai.clickToLoadModels')}
|
||||||
>
|
searchPlaceholder={t('admin.ai.searchModel')}
|
||||||
{ollamaChatModels.length > 0 ? (
|
emptyMessage={t('admin.ai.noModels')}
|
||||||
<>
|
/>
|
||||||
{!ollamaChatModels.includes(selectedChatModel) && selectedChatModel && (
|
|
||||||
<option value={selectedChatModel}>{selectedChatModel} ({t('admin.ai.configured')})</option>
|
|
||||||
)}
|
|
||||||
{ollamaChatModels.map((model) => (
|
|
||||||
<option key={model} value={model}>{model}</option>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<option value={selectedChatModel || 'granite4:latest'}>{selectedChatModel || 'granite4:latest'} {t('admin.ai.saved')}</option>
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{isLoadingChatModels ? t('admin.ai.fetchingModels') : t('admin.ai.selectOllamaModel')}
|
{isLoadingChatModels ? t('admin.ai.fetchingModels') : t('admin.ai.selectOllamaModel')}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -99,40 +99,46 @@ export async function POST(req: Request) {
|
|||||||
// This ensures the AI always has access to the notebook content,
|
// This ensures the AI always has access to the notebook content,
|
||||||
// even for vague queries like "what's in this notebook?"
|
// even for vague queries like "what's in this notebook?"
|
||||||
let notebookContext = ''
|
let notebookContext = ''
|
||||||
if (notebookId) {
|
let searchNotes = ''
|
||||||
const notebookNotes = await prisma.note.findMany({
|
|
||||||
where: {
|
// When scope is "this note" (noteContext present), skip RAG retrieval entirely
|
||||||
notebookId,
|
// The note content is already injected as copilotContext below
|
||||||
userId,
|
if (!noteContext) {
|
||||||
trashedAt: null,
|
if (notebookId) {
|
||||||
},
|
const notebookNotes = await prisma.note.findMany({
|
||||||
orderBy: { updatedAt: 'desc' },
|
where: {
|
||||||
take: 20,
|
notebookId,
|
||||||
select: { id: true, title: true, content: true, updatedAt: true },
|
userId,
|
||||||
})
|
trashedAt: null,
|
||||||
if (notebookNotes.length > 0) {
|
},
|
||||||
notebookContext = notebookNotes
|
orderBy: { updatedAt: 'desc' },
|
||||||
.map(n => `NOTE [${n.title || untitledText}] (updated ${n.updatedAt.toLocaleDateString()}):\n${(n.content || '').substring(0, 1500)}`)
|
take: 20,
|
||||||
.join('\n\n---\n\n')
|
select: { id: true, title: true, content: true, updatedAt: true },
|
||||||
|
})
|
||||||
|
if (notebookNotes.length > 0) {
|
||||||
|
notebookContext = notebookNotes
|
||||||
|
.map(n => `NOTE [${n.title || untitledText}] (updated ${n.updatedAt.toLocaleDateString()}):\n${(n.content || '').substring(0, 1500)}`)
|
||||||
|
.join('\n\n---\n\n')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Also run semantic search for the specific query
|
// Also run semantic search for the specific query
|
||||||
let searchResults: any[] = []
|
let searchResults: any[] = []
|
||||||
try {
|
try {
|
||||||
searchResults = await semanticSearchService.search(currentMessage, {
|
searchResults = await semanticSearchService.search(currentMessage, {
|
||||||
notebookId,
|
notebookId,
|
||||||
limit: notebookId ? 10 : 5,
|
limit: notebookId ? 10 : 5,
|
||||||
threshold: notebookId ? 0.3 : 0.5,
|
threshold: notebookId ? 0.3 : 0.5,
|
||||||
defaultTitle: untitledText,
|
defaultTitle: untitledText,
|
||||||
})
|
})
|
||||||
} catch {
|
} catch {
|
||||||
// Search failure should not block chat
|
// Search failure should not block chat
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchNotes = searchResults
|
searchNotes = searchResults
|
||||||
.map((r) => `NOTE [${r.title || untitledText}]: ${r.content}`)
|
.map((r) => `NOTE [${r.title || untitledText}]: ${r.content}`)
|
||||||
.join('\n\n---\n\n')
|
.join('\n\n---\n\n')
|
||||||
|
}
|
||||||
|
|
||||||
// Combine: full notebook context + semantic search results (deduplicated)
|
// Combine: full notebook context + semantic search results (deduplicated)
|
||||||
const contextNotes = [notebookContext, searchNotes].filter(Boolean).join('\n\n---\n\n')
|
const contextNotes = [notebookContext, searchNotes].filter(Boolean).join('\n\n---\n\n')
|
||||||
@@ -354,7 +360,7 @@ ${noteContext.content || '(empty)'}
|
|||||||
${imageContextParts.length > 0 ? `\nImages: ${imageContextParts.length} image(s) attached. When the user asks about images, describe what you see in them.` : ''}
|
${imageContextParts.length > 0 ? `\nImages: ${imageContextParts.length} image(s) attached. When the user asks about images, describe what you see in them.` : ''}
|
||||||
|
|
||||||
The user wants you to write in a **${noteContext.tone || 'professional'}** tone.
|
The user wants you to write in a **${noteContext.tone || 'professional'}** tone.
|
||||||
Keep your suggestions tailored to this note and tone. You can suggest rewrites, answer questions about the note, or draft new sections.`
|
IMPORTANT: Focus ONLY on this note. Do NOT reference other notes or external information unless the user explicitly asks. Your job is to help with this specific note — suggest rewrites, answer questions about it, or draft new sections.`
|
||||||
}
|
}
|
||||||
|
|
||||||
const systemPrompt = `${prompts.system}
|
const systemPrompt = `${prompts.system}
|
||||||
|
|||||||
Reference in New Issue
Block a user