feat(AI): nomad.md for custom instructions
This commit is contained in:
parent
6a4f02dd46
commit
6cd10df6c1
|
|
@ -0,0 +1,22 @@
|
|||
import { NomadMdService } from '#services/nomad_md_service'
|
||||
import { updateNomadMdSchema } from '#validators/nomad_md'
|
||||
import { inject } from '@adonisjs/core'
|
||||
import type { HttpContext } from '@adonisjs/core/http'
|
||||
|
||||
@inject()
|
||||
export default class NomadMdController {
|
||||
constructor(private nomadMdService: NomadMdService) {}
|
||||
|
||||
async show({ response }: HttpContext) {
|
||||
const content = await this.nomadMdService.read()
|
||||
return response.status(200).json({ content })
|
||||
}
|
||||
|
||||
async update({ request, response }: HttpContext) {
|
||||
const { content } = await request.validateUsing(updateNomadMdSchema)
|
||||
// Empty request strings arrive as undefined (AdonisJS null-coerces them);
|
||||
// treat that as an explicit "clear the file".
|
||||
await this.nomadMdService.write(content ?? '')
|
||||
return response.status(200).json({ success: true, message: 'NOMAD.md saved successfully' })
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { ChatService } from '#services/chat_service'
|
||||
import { DockerService } from '#services/docker_service'
|
||||
import { NomadMdService } from '#services/nomad_md_service'
|
||||
import { OllamaService } from '#services/ollama_service'
|
||||
import { RagService } from '#services/rag_service'
|
||||
import Service from '#models/service'
|
||||
|
|
@ -20,7 +21,8 @@ export default class OllamaController {
|
|||
private chatService: ChatService,
|
||||
private dockerService: DockerService,
|
||||
private ollamaService: OllamaService,
|
||||
private ragService: RagService
|
||||
private ragService: RagService,
|
||||
private nomadMdService: NomadMdService
|
||||
) { }
|
||||
|
||||
async availableModels({ request }: HttpContext) {
|
||||
|
|
@ -71,6 +73,16 @@ export default class OllamaController {
|
|||
reqData.messages.unshift(systemPrompt)
|
||||
}
|
||||
|
||||
// Inject the user-managed NOMAD.md as its own leading system message so the
|
||||
// user's persistent instructions take precedence, while the default
|
||||
// formatting prompt and any RAG context below remain intact. A missing or
|
||||
// blank file yields null and changes nothing.
|
||||
const nomadPrompt = await this.nomadMdService.getSystemPrompt()
|
||||
if (nomadPrompt) {
|
||||
logger.debug('[OllamaController] Injecting NOMAD.md system prompt')
|
||||
reqData.messages.unshift({ role: 'system' as const, content: nomadPrompt })
|
||||
}
|
||||
|
||||
// Query rewriting for better RAG retrieval with manageable context
|
||||
// Will return user's latest message if no rewriting is needed
|
||||
const rewrittenQuery = await this.rewriteQueryWithContext(reqData.messages, reqData.model)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
import { rename, writeFile } from 'node:fs/promises'
|
||||
import { dirname } from 'node:path'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import app from '@adonisjs/core/services/app'
|
||||
import { ensureDirectoryExists, getFile } from '../utils/fs.js'
|
||||
|
||||
/**
|
||||
* Manages the user-editable `NOMAD.md` file that is injected as a system prompt
|
||||
* during chat completions. The file lives in the admin storage directory
|
||||
* (`/app/storage/NOMAD.md` in the container, `/opt/project-nomad/storage/NOMAD.md`
|
||||
* on the host), so an end user can also edit it directly on disk. The file is
|
||||
* created lazily on the first save — a missing or empty file simply means no
|
||||
* custom prompt is injected.
|
||||
*/
|
||||
export class NomadMdService {
|
||||
static STORAGE_PATH = 'storage/NOMAD.md'
|
||||
|
||||
private get filePath(): string {
|
||||
return app.makePath(NomadMdService.STORAGE_PATH)
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the raw file contents. Returns an empty string when the file does not
|
||||
* exist yet (the editor renders a template client-side in that case).
|
||||
*/
|
||||
async read(): Promise<string> {
|
||||
const content = await getFile(this.filePath, 'string')
|
||||
return content ?? ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the file. Written atomically (tmp file + rename) so a concurrent
|
||||
* on-disk edit never observes a half-written file.
|
||||
*/
|
||||
async write(content: string): Promise<void> {
|
||||
const filePath = this.filePath
|
||||
await ensureDirectoryExists(dirname(filePath))
|
||||
const tmpPath = `${filePath}.tmp.${randomUUID()}`
|
||||
await writeFile(tmpPath, content, 'utf-8')
|
||||
await rename(tmpPath, filePath)
|
||||
}
|
||||
|
||||
/**
|
||||
* The trimmed contents suitable for injection as a system message, or `null`
|
||||
* when the file is missing or blank (so chat behaviour is unchanged).
|
||||
*/
|
||||
async getSystemPrompt(): Promise<string | null> {
|
||||
const content = (await this.read()).trim()
|
||||
return content.length > 0 ? content : null
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import vine from '@vinejs/vine'
|
||||
|
||||
// Allow an empty/absent value so the user can clear their NOMAD.md — AdonisJS
|
||||
// converts empty request strings to null, so `optional()` (coerced to '' in the
|
||||
// controller) is what lets a "clear" through. The cap keeps a single system
|
||||
// prompt from growing unbounded.
|
||||
export const updateNomadMdSchema = vine.compile(
|
||||
vine.object({
|
||||
content: vine.string().maxLength(100_000).optional(),
|
||||
})
|
||||
)
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import { useEffect, useRef } from 'react'
|
||||
import { Compartment, EditorState } from '@codemirror/state'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import { markdown } from '@codemirror/lang-markdown'
|
||||
import { oneDark } from '@codemirror/theme-one-dark'
|
||||
import { basicSetup } from 'codemirror'
|
||||
import { useTheme } from '~/hooks/useTheme'
|
||||
|
||||
interface MarkdownEditorProps {
|
||||
/** Initial document contents. Read once on mount; later edits are reported via onChange. */
|
||||
initialValue: string
|
||||
onChange: (value: string) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* A thin React wrapper that mounts a CodeMirror 6 editor tuned for Markdown.
|
||||
* The editor is uncontrolled after mount (its state lives in CodeMirror); the
|
||||
* current value is surfaced to the parent through onChange. The one-dark theme
|
||||
* is applied via a Compartment so light/dark toggles reconfigure in place
|
||||
* without rebuilding the document.
|
||||
*/
|
||||
export default function MarkdownEditor({ initialValue, onChange, className }: MarkdownEditorProps) {
|
||||
const hostRef = useRef<HTMLDivElement | null>(null)
|
||||
const viewRef = useRef<EditorView | null>(null)
|
||||
const themeCompartment = useRef(new Compartment())
|
||||
const onChangeRef = useRef(onChange)
|
||||
onChangeRef.current = onChange
|
||||
const { theme } = useTheme()
|
||||
|
||||
useEffect(() => {
|
||||
if (!hostRef.current) return
|
||||
|
||||
const state = EditorState.create({
|
||||
doc: initialValue,
|
||||
extensions: [
|
||||
basicSetup,
|
||||
markdown(),
|
||||
EditorView.lineWrapping,
|
||||
// Fill the host element and scroll internally rather than growing the page.
|
||||
EditorView.theme({
|
||||
'&': { height: '100%' },
|
||||
'.cm-scroller': { overflow: 'auto' },
|
||||
}),
|
||||
themeCompartment.current.of(theme === 'dark' ? oneDark : []),
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged) {
|
||||
onChangeRef.current(update.state.doc.toString())
|
||||
}
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
const view = new EditorView({ state, parent: hostRef.current })
|
||||
viewRef.current = view
|
||||
|
||||
return () => {
|
||||
view.destroy()
|
||||
viewRef.current = null
|
||||
}
|
||||
// Mount once; initialValue/theme changes are handled below or by remounting.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
viewRef.current?.dispatch({
|
||||
effects: themeCompartment.current.reconfigure(theme === 'dark' ? oneDark : []),
|
||||
})
|
||||
}, [theme])
|
||||
|
||||
return <div ref={hostRef} className={className} />
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import { ChatSession } from '../../../types/chat'
|
|||
import { IconMessage } from '@tabler/icons-react'
|
||||
import { useState } from 'react'
|
||||
import KnowledgeBaseModal from './KnowledgeBaseModal'
|
||||
import NomadMdModal from './NomadMdModal'
|
||||
|
||||
interface ChatSidebarProps {
|
||||
sessions: ChatSession[]
|
||||
|
|
@ -27,6 +28,7 @@ export default function ChatSidebar({
|
|||
const [isKnowledgeBaseModalOpen, setIsKnowledgeBaseModalOpen] = useState(
|
||||
() => new URLSearchParams(window.location.search).get('knowledge_base') === 'true'
|
||||
)
|
||||
const [isNomadMdModalOpen, setIsNomadMdModalOpen] = useState(false)
|
||||
|
||||
function handleCloseKnowledgeBase() {
|
||||
setIsKnowledgeBaseModalOpen(false)
|
||||
|
|
@ -127,6 +129,17 @@ export default function ChatSidebar({
|
|||
>
|
||||
Knowledge Base
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
onClick={() => {
|
||||
setIsNomadMdModalOpen(true)
|
||||
}}
|
||||
icon="IconFileDescription"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
fullWidth
|
||||
>
|
||||
NOMAD.md
|
||||
</StyledButton>
|
||||
{sessions.length > 0 && (
|
||||
<StyledButton
|
||||
onClick={onClearHistory}
|
||||
|
|
@ -142,6 +155,12 @@ export default function ChatSidebar({
|
|||
{isKnowledgeBaseModalOpen && (
|
||||
<KnowledgeBaseModal aiAssistantName={aiAssistantName} onClose={handleCloseKnowledgeBase} />
|
||||
)}
|
||||
{isNomadMdModalOpen && (
|
||||
<NomadMdModal
|
||||
aiAssistantName={aiAssistantName}
|
||||
onClose={() => setIsNomadMdModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,121 @@
|
|||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { IconX } from '@tabler/icons-react'
|
||||
import StyledButton from '~/components/StyledButton'
|
||||
import MarkdownEditor from '~/components/MarkdownEditor'
|
||||
import { useNotifications } from '~/context/NotificationContext'
|
||||
import api from '~/lib/api'
|
||||
|
||||
interface NomadMdModalProps {
|
||||
aiAssistantName?: string
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
// Seeded into the editor when no NOMAD.md exists yet. Nothing is written to disk
|
||||
// until the user saves, so this is purely a starting point they can replace.
|
||||
const NOMAD_MD_TEMPLATE = `# NOMAD.md
|
||||
|
||||
<!--
|
||||
This file holds custom instructions for your AI assistant. Everything here is
|
||||
sent to the assistant as a system prompt on every chat — use it to set persona,
|
||||
tone, priorities, and standing rules.
|
||||
|
||||
It is also stored on disk at storage/NOMAD.md, so you can edit it directly.
|
||||
Replace this template with your own instructions, then click Save.
|
||||
-->
|
||||
|
||||
## About me
|
||||
|
||||
- (e.g. I'm setting up an off-grid homestead in a cold climate.)
|
||||
|
||||
## How the assistant should respond
|
||||
|
||||
- Be concise and practical.
|
||||
- Prioritize safety and proven methods.
|
||||
`
|
||||
|
||||
export default function NomadMdModal({ aiAssistantName, onClose }: NomadMdModalProps) {
|
||||
const queryClient = useQueryClient()
|
||||
const { addNotification } = useNotifications()
|
||||
const [content, setContent] = useState<string | null>(null)
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['nomad-md'],
|
||||
queryFn: () => api.getNomadMd(),
|
||||
})
|
||||
|
||||
// Seed the editor once the file loads: existing content, or the template when empty.
|
||||
useEffect(() => {
|
||||
if (data && content === null) {
|
||||
setContent(data.content.trim().length > 0 ? data.content : NOMAD_MD_TEMPLATE)
|
||||
}
|
||||
}, [data, content])
|
||||
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: (value: string) => api.saveNomadMd(value),
|
||||
onSuccess: (result) => {
|
||||
if (!result?.success) {
|
||||
addNotification({ type: 'error', message: 'Failed to save NOMAD.md.' })
|
||||
return
|
||||
}
|
||||
addNotification({ type: 'success', message: 'NOMAD.md saved. It applies to new messages.' })
|
||||
queryClient.invalidateQueries({ queryKey: ['nomad-md'] })
|
||||
onClose()
|
||||
},
|
||||
onError: (error: any) => {
|
||||
addNotification({ type: 'error', message: error?.message || 'Failed to save NOMAD.md.' })
|
||||
},
|
||||
})
|
||||
|
||||
const assistantName = aiAssistantName?.trim() || 'your AI assistant'
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/30 backdrop-blur-sm transition-opacity">
|
||||
<div className="bg-surface-primary rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden flex flex-col">
|
||||
<div className="flex items-center justify-between p-6 border-b border-border-subtle shrink-0">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold text-text-primary">NOMAD.md</h2>
|
||||
<p className="text-sm text-text-muted mt-1">
|
||||
Custom instructions passed to {assistantName} as a system prompt on every chat.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 hover:bg-surface-secondary rounded-lg transition-colors"
|
||||
>
|
||||
<IconX className="h-6 w-6 text-text-muted" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="overflow-y-auto flex-1 p-6">
|
||||
{isLoading || content === null ? (
|
||||
<div className="py-16 text-center text-text-muted">Loading…</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-border-subtle overflow-hidden h-[55vh]">
|
||||
<MarkdownEditor initialValue={content} onChange={setContent} className="h-full text-sm" />
|
||||
</div>
|
||||
)}
|
||||
<p className="text-xs text-text-muted mt-3">
|
||||
Tip: this file is also stored on disk at{' '}
|
||||
<code className="font-mono">storage/NOMAD.md</code> and can be edited directly.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-3 p-6 border-t border-border-subtle shrink-0">
|
||||
<StyledButton variant="outline" onClick={onClose} disabled={saveMutation.isPending}>
|
||||
Cancel
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
variant="primary"
|
||||
icon="IconCircleCheck"
|
||||
onClick={() => content !== null && saveMutation.mutate(content)}
|
||||
loading={saveMutation.isPending}
|
||||
disabled={content === null}
|
||||
>
|
||||
Save
|
||||
</StyledButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1021,6 +1021,23 @@ class API {
|
|||
})()
|
||||
}
|
||||
|
||||
async getNomadMd() {
|
||||
return catchInternal(async () => {
|
||||
const response = await this.client.get<{ content: string }>('/ai/nomad-md')
|
||||
return response.data
|
||||
})()
|
||||
}
|
||||
|
||||
async saveNomadMd(content: string) {
|
||||
return catchInternal(async () => {
|
||||
const response = await this.client.put<{ success: boolean; message: string }>(
|
||||
'/ai/nomad-md',
|
||||
{ content }
|
||||
)
|
||||
return response.data
|
||||
})()
|
||||
}
|
||||
|
||||
async preflightCheck(service_name: string) {
|
||||
return catchInternal(async () => {
|
||||
const response = await this.client.get<{
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@
|
|||
"@adonisjs/transmit-client": "1.1.0",
|
||||
"@adonisjs/vite": "4.0.0",
|
||||
"@chonkiejs/core": "0.0.7",
|
||||
"@codemirror/lang-markdown": "6.5.0",
|
||||
"@codemirror/state": "6.7.1",
|
||||
"@codemirror/theme-one-dark": "6.1.3",
|
||||
"@codemirror/view": "6.43.6",
|
||||
"@headlessui/react": "2.2.9",
|
||||
"@inertiajs/react": "2.3.13",
|
||||
"@markdoc/markdoc": "0.5.4",
|
||||
|
|
@ -43,6 +47,7 @@
|
|||
"better-sqlite3": "12.6.2",
|
||||
"bullmq": "5.77.6",
|
||||
"cheerio": "1.2.0",
|
||||
"codemirror": "6.0.2",
|
||||
"compression": "1.8.1",
|
||||
"dockerode": "4.0.9",
|
||||
"edge.js": "6.4.0",
|
||||
|
|
@ -1270,6 +1275,159 @@
|
|||
"@chonkiejs/chunk": "^0.9.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/autocomplete": {
|
||||
"version": "6.20.3",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz",
|
||||
"integrity": "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/commands": {
|
||||
"version": "6.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.4.tgz",
|
||||
"integrity": "sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.7.0",
|
||||
"@codemirror/view": "^6.27.0",
|
||||
"@lezer/common": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-css": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
|
||||
"integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@lezer/common": "^1.0.2",
|
||||
"@lezer/css": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-html": {
|
||||
"version": "6.4.11",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz",
|
||||
"integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/lang-css": "^6.0.0",
|
||||
"@codemirror/lang-javascript": "^6.0.0",
|
||||
"@codemirror/language": "^6.4.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@lezer/css": "^1.1.0",
|
||||
"@lezer/html": "^1.3.12"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-javascript": {
|
||||
"version": "6.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz",
|
||||
"integrity": "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.6.0",
|
||||
"@codemirror/lint": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@lezer/javascript": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-markdown": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.5.0.tgz",
|
||||
"integrity": "sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.7.1",
|
||||
"@codemirror/lang-html": "^6.0.0",
|
||||
"@codemirror/language": "^6.3.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@lezer/common": "^1.2.1",
|
||||
"@lezer/markdown": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/language": {
|
||||
"version": "6.12.4",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz",
|
||||
"integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.23.0",
|
||||
"@lezer/common": "^1.5.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0",
|
||||
"style-mod": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lint": {
|
||||
"version": "6.9.7",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.7.tgz",
|
||||
"integrity": "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.42.0",
|
||||
"crelt": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/search": {
|
||||
"version": "6.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.1.tgz",
|
||||
"integrity": "sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.37.0",
|
||||
"crelt": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/state": {
|
||||
"version": "6.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.1.tgz",
|
||||
"integrity": "sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/theme-one-dark": {
|
||||
"version": "6.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz",
|
||||
"integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@lezer/highlight": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.43.6",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.6.tgz",
|
||||
"integrity": "sha512-EVunGSYN1wz1p75WY1s3Xg7t3i8Yol0kGZGizNdX9BUFgMFILYVe8/u6EVpo7Ff5PwbZuILb4QAq7IZoKzIEQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.7.0",
|
||||
"crelt": "^1.0.6",
|
||||
"style-mod": "^4.1.0",
|
||||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@colors/colors": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
|
||||
|
|
@ -3142,6 +3300,73 @@
|
|||
"url": "https://opencollective.com/js-sdsl"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/common": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz",
|
||||
"integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@lezer/css": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.4.tgz",
|
||||
"integrity": "sha512-N+tn9tej2hPvyKgHEApMOQfHczDJCwxrRFS3SPn9QjYN+uwHvEDnCgKRrb3mxDYxRS8sKMM8fhC3+lc04Abz5Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/highlight": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
|
||||
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/html": {
|
||||
"version": "1.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz",
|
||||
"integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/javascript": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz",
|
||||
"integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.1.3",
|
||||
"@lezer/lr": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/lr": {
|
||||
"version": "1.4.10",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz",
|
||||
"integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/markdown": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.7.0.tgz",
|
||||
"integrity": "sha512-zZDdfKkXl1CBYet/nL2jCskQXC+8DpbhZubbTEGqbqpkaBC4w03F5Kt9ZLDyqbadqIvvZ7VrVhd6d6qgurzAew==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.5.0",
|
||||
"@lezer/highlight": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/geojson-rewind": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz",
|
||||
|
|
@ -3237,6 +3462,12 @@
|
|||
"integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@marijn/find-cluster-break": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.3.tgz",
|
||||
"integrity": "sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@markdoc/markdoc": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@markdoc/markdoc/-/markdoc-0.5.4.tgz",
|
||||
|
|
@ -7569,6 +7800,21 @@
|
|||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/codemirror": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
|
||||
"integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/commands": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/lint": "^6.0.0",
|
||||
"@codemirror/search": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
|
|
@ -7821,6 +8067,12 @@
|
|||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/crelt": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz",
|
||||
"integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cron-parser": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz",
|
||||
|
|
@ -15953,6 +16205,12 @@
|
|||
"url": "https://github.com/sponsors/Borewit"
|
||||
}
|
||||
},
|
||||
"node_modules/style-mod": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
||||
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/style-to-js": {
|
||||
"version": "1.1.21",
|
||||
"resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz",
|
||||
|
|
@ -17031,6 +17289,12 @@
|
|||
"pbf": "^3.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wasm-feature-detect": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz",
|
||||
|
|
|
|||
|
|
@ -74,6 +74,10 @@
|
|||
"@adonisjs/transmit-client": "1.1.0",
|
||||
"@adonisjs/vite": "4.0.0",
|
||||
"@chonkiejs/core": "0.0.7",
|
||||
"@codemirror/lang-markdown": "6.5.0",
|
||||
"@codemirror/state": "6.7.1",
|
||||
"@codemirror/theme-one-dark": "6.1.3",
|
||||
"@codemirror/view": "6.43.6",
|
||||
"@headlessui/react": "2.2.9",
|
||||
"@inertiajs/react": "2.3.13",
|
||||
"@markdoc/markdoc": "0.5.4",
|
||||
|
|
@ -96,6 +100,7 @@
|
|||
"better-sqlite3": "12.6.2",
|
||||
"bullmq": "5.77.6",
|
||||
"cheerio": "1.2.0",
|
||||
"codemirror": "6.0.2",
|
||||
"compression": "1.8.1",
|
||||
"dockerode": "4.0.9",
|
||||
"edge.js": "6.4.0",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import DownloadsController from '#controllers/downloads_controller'
|
|||
import EasySetupController from '#controllers/easy_setup_controller'
|
||||
import HomeController from '#controllers/home_controller'
|
||||
import MapsController from '#controllers/maps_controller'
|
||||
import NomadMdController from '#controllers/nomad_md_controller'
|
||||
import OllamaController from '#controllers/ollama_controller'
|
||||
import RagController from '#controllers/rag_controller'
|
||||
import SettingsController from '#controllers/settings_controller'
|
||||
|
|
@ -127,6 +128,13 @@ router
|
|||
})
|
||||
.prefix('/api/ollama')
|
||||
|
||||
router
|
||||
.group(() => {
|
||||
router.get('/nomad-md', [NomadMdController, 'show'])
|
||||
router.put('/nomad-md', [NomadMdController, 'update'])
|
||||
})
|
||||
.prefix('/api/ai')
|
||||
|
||||
router
|
||||
.group(() => {
|
||||
router.get('/', [ChatsController, 'index'])
|
||||
|
|
|
|||
Loading…
Reference in New Issue