diff --git a/extensions/model-extension/src/cortex.ts b/extensions/model-extension/src/cortex.ts index 5b7d1e36b..b7111c859 100644 --- a/extensions/model-extension/src/cortex.ts +++ b/extensions/model-extension/src/cortex.ts @@ -123,10 +123,10 @@ export class CortexAPI implements ICortexAPI { * @param model * @returns */ - updateModel(model: object): Promise { + updateModel(model: Partial): Promise { return this.queue.add(() => ky - .patch(`${API_URL}/v1/models/${model}`, { json: { model } }) + .patch(`${API_URL}/v1/models/${model.id}`, { json: { ...model } }) .json() .then() ) diff --git a/extensions/model-extension/src/index.ts b/extensions/model-extension/src/index.ts index 8f50bd5d0..e5fc7ccf6 100644 --- a/extensions/model-extension/src/index.ts +++ b/extensions/model-extension/src/index.ts @@ -193,7 +193,13 @@ export default class JanModelExtension extends ModelExtension { ]) // Copied models : model.sources[0].url, // Symlink models, model.name - ) + ).then((e) => { + this.updateModel({ + id: model.id, + ...model.settings, + ...model.parameters, + } as Partial) + }) ) ) diff --git a/web/containers/Providers/EventListener.tsx b/web/containers/Providers/EventListener.tsx index 9535bbfa6..5cb0debab 100644 --- a/web/containers/Providers/EventListener.tsx +++ b/web/containers/Providers/EventListener.tsx @@ -2,7 +2,17 @@ import { PropsWithChildren, useCallback, useEffect } from 'react' import React from 'react' -import { DownloadEvent, events, DownloadState, ModelEvent } from '@janhq/core' +import { + DownloadEvent, + events, + DownloadState, + ModelEvent, + ExtensionTypeEnum, + ModelExtension, + ModelManager, + Model, +} from '@janhq/core' + import { useSetAtom } from 'jotai' import { setDownloadStateAtom } from '@/hooks/useDownloadState' @@ -18,6 +28,7 @@ import EventHandler from './EventHandler' import ModelImportListener from './ModelImportListener' import QuickAskListener from './QuickAskListener' +import { extensionManager } from '@/extension' import { InstallingExtensionState, removeInstallingExtensionAtom, @@ -83,12 +94,24 @@ const EventListenerWrapper = ({ children }: PropsWithChildren) => { ) const onFileDownloadSuccess = useCallback( - (state: DownloadState) => { + async (state: DownloadState) => { console.debug('onFileDownloadSuccess', state) if (state.downloadType !== 'extension') { + // Update model metadata accordingly + const model = ModelManager.instance().models.get(state.modelId) + if (model) { + await extensionManager + .get(ExtensionTypeEnum.Model) + ?.updateModel({ + id: model.id, + ...model.settings, + ...model.parameters, + } as Partial) + .catch((e) => console.debug(e)) + } state.downloadState = 'end' setDownloadState(state) - if (state.percent !== 0) removeDownloadingModel(state.modelId) + removeDownloadingModel(state.modelId) } events.emit(ModelEvent.OnModelsUpdate, {}) }, diff --git a/web/hooks/useModels.ts b/web/hooks/useModels.ts index b8b680715..400e02793 100644 --- a/web/hooks/useModels.ts +++ b/web/hooks/useModels.ts @@ -35,10 +35,6 @@ const useModels = () => { const localModels = (await getModels()).map((e) => ({ ...e, name: ModelManager.instance().models.get(e.id)?.name ?? e.id, - settings: - ModelManager.instance().models.get(e.id)?.settings ?? e.settings, - parameters: - ModelManager.instance().models.get(e.id)?.parameters ?? e.parameters, metadata: ModelManager.instance().models.get(e.id)?.metadata ?? e.metadata, })) diff --git a/web/screens/Thread/ThreadCenterPanel/ChatInput/RichTextEditor.tsx b/web/screens/Thread/ThreadCenterPanel/ChatInput/RichTextEditor.tsx index bca808e28..70fecb8a9 100644 --- a/web/screens/Thread/ThreadCenterPanel/ChatInput/RichTextEditor.tsx +++ b/web/screens/Thread/ThreadCenterPanel/ChatInput/RichTextEditor.tsx @@ -69,6 +69,9 @@ const RichTextEditor = ({ }: RichTextEditorProps) => { const [editor] = useState(() => withHistory(withReact(createEditor()))) const currentLanguage = useRef('plaintext') + const hasStartBackticks = useRef(false) + const hasEndBackticks = useRef(false) + const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom) const textareaRef = useRef(null) const activeThreadId = useAtomValue(getActiveThreadIdAtom) @@ -133,20 +136,31 @@ const RichTextEditor = ({ node.children.forEach((child: { text: any }, childIndex: number) => { const text = child.text + const codeBlockStartRegex = /```(\w*)/g + const matches = [...currentPrompt.matchAll(codeBlockStartRegex)] + + if (matches.length % 2 !== 0) { + hasEndBackticks.current = false + } + // Match code block start and end - const startMatch = text.match(/^```(\w*)$/) + const lang = text.match(/^```(\w*)$/) const endMatch = text.match(/^```$/) - if (startMatch) { + if (lang) { // If it's the start of a code block, store the language - currentLanguage.current = startMatch[1] || 'plaintext' + currentLanguage.current = lang[1] || 'plaintext' } else if (endMatch) { // Reset language when code block ends currentLanguage.current = 'plaintext' - } else if (currentLanguage.current !== 'plaintext') { + } else if ( + hasStartBackticks.current && + hasEndBackticks.current && + currentLanguage.current !== 'plaintext' + ) { // Highlight entire code line if in a code block - const leadingSpaces = text.match(/^\s*/)?.[0] ?? '' // Capture leading spaces - const codeContent = text.trimStart() // Remove leading spaces for highlighting + + const codeContent = text.trim() // Remove leading spaces for highlighting let highlighted = '' highlighted = hljs.highlightAuto(codeContent).value @@ -168,21 +182,9 @@ const RichTextEditor = ({ let slateTextIndex = 0 - // Adjust to include leading spaces in the ranges and preserve formatting - ranges.push({ - anchor: { path: [...path, childIndex], offset: 0 }, - focus: { - path: [...path, childIndex], - offset: slateTextIndex, - }, - type: 'code', - code: true, - language: currentLanguage.current, - className: '', // No class for leading spaces - }) - doc.body.childNodes.forEach((childNode) => { const childText = childNode.textContent || '' + const length = childText.length const className = childNode.nodeType === Node.ELEMENT_NODE @@ -192,11 +194,11 @@ const RichTextEditor = ({ ranges.push({ anchor: { path: [...path, childIndex], - offset: slateTextIndex + leadingSpaces.length, + offset: slateTextIndex, }, focus: { path: [...path, childIndex], - offset: slateTextIndex + leadingSpaces.length + length, + offset: slateTextIndex + length, }, type: 'code', code: true, @@ -220,7 +222,7 @@ const RichTextEditor = ({ return ranges }, - [editor] + [currentPrompt, editor] ) // RenderLeaf applies the decoration styles @@ -340,9 +342,20 @@ const RichTextEditor = ({ currentLanguage.current = 'plaintext' } const hasCodeBlockStart = combinedText.match(/^```(\w*)/m) + const hasCodeBlockEnd = combinedText.match(/^```$/m) + // Set language to plaintext if no code block with language identifier is found if (!hasCodeBlockStart) { currentLanguage.current = 'plaintext' + hasStartBackticks.current = false + } else { + hasStartBackticks.current = true + } + if (!hasCodeBlockEnd) { + currentLanguage.current = 'plaintext' + hasEndBackticks.current = false + } else { + hasEndBackticks.current = true } }} >