diff --git a/admin/app/controllers/nomad_md_controller.ts b/admin/app/controllers/nomad_md_controller.ts new file mode 100644 index 0000000..6f788ed --- /dev/null +++ b/admin/app/controllers/nomad_md_controller.ts @@ -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' }) + } +} diff --git a/admin/app/controllers/ollama_controller.ts b/admin/app/controllers/ollama_controller.ts index 9174616..b506953 100644 --- a/admin/app/controllers/ollama_controller.ts +++ b/admin/app/controllers/ollama_controller.ts @@ -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) diff --git a/admin/app/services/nomad_md_service.ts b/admin/app/services/nomad_md_service.ts new file mode 100644 index 0000000..d34e08e --- /dev/null +++ b/admin/app/services/nomad_md_service.ts @@ -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 { + 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 { + 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 { + const content = (await this.read()).trim() + return content.length > 0 ? content : null + } +} diff --git a/admin/app/validators/nomad_md.ts b/admin/app/validators/nomad_md.ts new file mode 100644 index 0000000..982dc8b --- /dev/null +++ b/admin/app/validators/nomad_md.ts @@ -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(), + }) +) diff --git a/admin/inertia/components/MarkdownEditor.tsx b/admin/inertia/components/MarkdownEditor.tsx new file mode 100644 index 0000000..f6f72d4 --- /dev/null +++ b/admin/inertia/components/MarkdownEditor.tsx @@ -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(null) + const viewRef = useRef(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
+} diff --git a/admin/inertia/components/chat/ChatSidebar.tsx b/admin/inertia/components/chat/ChatSidebar.tsx index 07e59fa..49b100d 100644 --- a/admin/inertia/components/chat/ChatSidebar.tsx +++ b/admin/inertia/components/chat/ChatSidebar.tsx @@ -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 + { + setIsNomadMdModalOpen(true) + }} + icon="IconFileDescription" + variant="primary" + size="sm" + fullWidth + > + NOMAD.md + {sessions.length > 0 && ( )} + {isNomadMdModalOpen && ( + setIsNomadMdModalOpen(false)} + /> + )}
) } diff --git a/admin/inertia/components/chat/NomadMdModal.tsx b/admin/inertia/components/chat/NomadMdModal.tsx new file mode 100644 index 0000000..e2eb40b --- /dev/null +++ b/admin/inertia/components/chat/NomadMdModal.tsx @@ -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 + + + +## 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(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 ( +
+
+
+
+

NOMAD.md

+

+ Custom instructions passed to {assistantName} as a system prompt on every chat. +

+
+ +
+ +
+ {isLoading || content === null ? ( +
Loading…
+ ) : ( +
+ +
+ )} +

+ Tip: this file is also stored on disk at{' '} + storage/NOMAD.md and can be edited directly. +

+
+ +
+ + Cancel + + content !== null && saveMutation.mutate(content)} + loading={saveMutation.isPending} + disabled={content === null} + > + Save + +
+
+
+ ) +} diff --git a/admin/inertia/lib/api.ts b/admin/inertia/lib/api.ts index 9782e9e..28e9bf2 100644 --- a/admin/inertia/lib/api.ts +++ b/admin/inertia/lib/api.ts @@ -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<{ diff --git a/admin/package-lock.json b/admin/package-lock.json index 0a18db6..21c3d5a 100644 --- a/admin/package-lock.json +++ b/admin/package-lock.json @@ -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", diff --git a/admin/package.json b/admin/package.json index 18254d8..edd4604 100644 --- a/admin/package.json +++ b/admin/package.json @@ -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", diff --git a/admin/start/routes.ts b/admin/start/routes.ts index ca068cc..06e1b53 100644 --- a/admin/start/routes.ts +++ b/admin/start/routes.ts @@ -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'])