From d6891a56287cc0573ad8dd875431cadacd773277 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Mon, 21 Oct 2024 09:57:04 +0700 Subject: [PATCH 01/11] fix: api playground text overlap button and search hub model (#3833) * fix: button api playground overlap button and search hub model * fix: update overlap text responsive tab and conditional render GPU menu --- joi/src/core/Tabs/styles.scss | 1 + web/screens/Hub/index.tsx | 22 ++++++++++--------- .../LocalServerLeftPanel/index.tsx | 5 +++-- web/screens/Settings/Advanced/index.tsx | 7 ++---- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/joi/src/core/Tabs/styles.scss b/joi/src/core/Tabs/styles.scss index ce3df013b2..932b8431af 100644 --- a/joi/src/core/Tabs/styles.scss +++ b/joi/src/core/Tabs/styles.scss @@ -35,6 +35,7 @@ flex: 1; height: 38px; display: flex; + white-space: nowrap; color: hsla(var(--text-secondary)); align-items: center; justify-content: center; diff --git a/web/screens/Hub/index.tsx b/web/screens/Hub/index.tsx index 37adb717c7..8148a6bb5c 100644 --- a/web/screens/Hub/index.tsx +++ b/web/screens/Hub/index.tsx @@ -96,17 +96,19 @@ const HubScreen = () => { {!filteredModels.length ? ( ) : ( -
- { + setSortSelected(value) + }} + options={sortMenus} + /> +
+ + )} - diff --git a/web/screens/LocalServer/LocalServerLeftPanel/index.tsx b/web/screens/LocalServer/LocalServerLeftPanel/index.tsx index ef2c2d76c5..6f5de80ecd 100644 --- a/web/screens/LocalServer/LocalServerLeftPanel/index.tsx +++ b/web/screens/LocalServer/LocalServerLeftPanel/index.tsx @@ -127,9 +127,10 @@ const LocalServerLeftPanel = () => { {serverEnabled ? 'Stop' : 'Start'} Server {serverEnabled && ( - )} diff --git a/web/screens/Settings/Advanced/index.tsx b/web/screens/Settings/Advanced/index.tsx index 2c444371a8..bbfe7274cd 100644 --- a/web/screens/Settings/Advanced/index.tsx +++ b/web/screens/Settings/Advanced/index.tsx @@ -211,9 +211,6 @@ const Advanced = () => { saveSettings({ gpusInUse: updatedGpusInUse }) } - const gpuSelectionPlaceHolder = - gpuList.length > 0 ? 'Select GPU' : "You don't have any compatible GPU" - /** * Handle click outside */ @@ -240,7 +237,7 @@ const Advanced = () => { {/* CPU / GPU switching */} - {!isMac && ( + {!isMac && gpuList.length > 0 && (
@@ -326,7 +323,7 @@ const Advanced = () => { value={selectedGpu.join() || ''} className="w-full cursor-pointer" readOnly - placeholder={gpuSelectionPlaceHolder} + placeholder="" suffixIcon={ Date: Mon, 21 Oct 2024 12:54:30 +0700 Subject: [PATCH 02/11] fix: correct eos token of llava models --- extensions/inference-nitro-extension/package.json | 2 +- .../resources/models/llava-13b/model.json | 5 +++-- .../resources/models/llava-7b/model.json | 5 +++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/extensions/inference-nitro-extension/package.json b/extensions/inference-nitro-extension/package.json index 42c31938e8..15ceaf5662 100644 --- a/extensions/inference-nitro-extension/package.json +++ b/extensions/inference-nitro-extension/package.json @@ -1,7 +1,7 @@ { "name": "@janhq/inference-cortex-extension", "productName": "Cortex Inference Engine", - "version": "1.0.19", + "version": "1.0.20", "description": "This extension embeds cortex.cpp, a lightweight inference engine written in C++. See https://jan.ai.\nAdditional dependencies could be installed to run without Cuda Toolkit installation.", "main": "dist/index.js", "node": "dist/node/index.cjs.js", diff --git a/extensions/inference-nitro-extension/resources/models/llava-13b/model.json b/extensions/inference-nitro-extension/resources/models/llava-13b/model.json index caca33b7e0..6d94fd2724 100644 --- a/extensions/inference-nitro-extension/resources/models/llava-13b/model.json +++ b/extensions/inference-nitro-extension/resources/models/llava-13b/model.json @@ -12,7 +12,7 @@ "id": "llava-13b", "object": "model", "name": "LlaVa 13B Q4", - "version": "1.1", + "version": "1.2", "description": "LlaVa can bring vision understanding to Jan", "format": "gguf", "settings": { @@ -24,7 +24,8 @@ "mmproj": "mmproj-model-f16.gguf" }, "parameters": { - "max_tokens": 4096 + "max_tokens": 4096, + "stop": [""] }, "metadata": { "author": "liuhaotian", diff --git a/extensions/inference-nitro-extension/resources/models/llava-7b/model.json b/extensions/inference-nitro-extension/resources/models/llava-7b/model.json index b61ec38c2c..1fdd75247b 100644 --- a/extensions/inference-nitro-extension/resources/models/llava-7b/model.json +++ b/extensions/inference-nitro-extension/resources/models/llava-7b/model.json @@ -12,7 +12,7 @@ "id": "llava-7b", "object": "model", "name": "LlaVa 7B", - "version": "1.1", + "version": "1.2", "description": "LlaVa can bring vision understanding to Jan", "format": "gguf", "settings": { @@ -24,7 +24,8 @@ "mmproj": "mmproj-model-f16.gguf" }, "parameters": { - "max_tokens": 4096 + "max_tokens": 4096, + "stop": [""] }, "metadata": { "author": "liuhaotian", From 6b4e556a7a6d178f6f808fcfabc9b6bf6c952b47 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Mon, 21 Oct 2024 22:20:10 +0700 Subject: [PATCH 03/11] fix: context-lenght-value (#3854) --- web/containers/ModelSetting/SettingComponent.tsx | 1 + web/containers/SliderRightPanel/index.tsx | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/web/containers/ModelSetting/SettingComponent.tsx b/web/containers/ModelSetting/SettingComponent.tsx index 43df16430d..ac45b0f063 100644 --- a/web/containers/ModelSetting/SettingComponent.tsx +++ b/web/containers/ModelSetting/SettingComponent.tsx @@ -25,6 +25,7 @@ const SettingComponent: React.FC = ({ case 'slider': { const { min, max, step, value } = data.controllerProps as SliderComponentProps + return ( setShowTooltip({ max: false, min: false }), null, []) + useEffect(() => { + setVal(value.toString()) + }, [value]) + return (
From 4c562c3e12c57f47312e38c9d69f2bc7cce5f83f Mon Sep 17 00:00:00 2001 From: Srihari Thyagarajan Date: Mon, 21 Oct 2024 23:53:02 +0530 Subject: [PATCH 04/11] Update broken/outdated hyperlink --- extensions/inference-martian-extension/resources/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/inference-martian-extension/resources/settings.json b/extensions/inference-martian-extension/resources/settings.json index 2341ad6cd7..6825099f5e 100644 --- a/extensions/inference-martian-extension/resources/settings.json +++ b/extensions/inference-martian-extension/resources/settings.json @@ -14,7 +14,7 @@ { "key": "chat-completions-endpoint", "title": "Chat Completions Endpoint", - "description": "The endpoint to use for chat completions. See the [Martian API documentation](https://docs.withmartian.com/martian-model-router/api-reference/get-chat-completions) for more information.", + "description": "The endpoint to use for chat completions. See the [Martian API documentation](https://docs.withmartian.com/martian-model-router/getting-started/quickstart-integrating-martian-into-your-codebase) for more information.", "controllerType": "input", "controllerProps": { "placeholder": "https://withmartian.com/api/openai/v1/chat/completions", From b14f54e866791baed4a01f4c782031a21c495e82 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Tue, 22 Oct 2024 15:44:13 +0700 Subject: [PATCH 05/11] fix: inconsistent state of downloading multimodal (#3862) --- extensions/model-extension/package.json | 2 +- extensions/model-extension/src/index.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/extensions/model-extension/package.json b/extensions/model-extension/package.json index 9a406dcf42..3a694e5a02 100644 --- a/extensions/model-extension/package.json +++ b/extensions/model-extension/package.json @@ -1,7 +1,7 @@ { "name": "@janhq/model-extension", "productName": "Model Management", - "version": "1.0.33", + "version": "1.0.34", "description": "Model Management Extension provides model exploration and seamless downloads", "main": "dist/index.js", "node": "dist/node/index.cjs.js", diff --git a/extensions/model-extension/src/index.ts b/extensions/model-extension/src/index.ts index 6d26d576c7..7e7c12469a 100644 --- a/extensions/model-extension/src/index.ts +++ b/extensions/model-extension/src/index.ts @@ -411,7 +411,8 @@ export default class JanModelExtension extends ModelExtension { .toLowerCase() .includes(JanModelExtension._tensorRtEngineFormat) ) - })?.length > 0 // TODO: find better way (can use basename to check the file name with source url) + // Check if the number of matched files equals the number of sources + })?.length >= model.sources.length ) }) From 9867634b5ab909a74fccde82aa45f3717ebb27fd Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Tue, 22 Oct 2024 15:44:34 +0700 Subject: [PATCH 06/11] feat: support markdown on user message (#3848) * feat: enable render markdown for user message * wip: slate * style: update style rich text editor * chore: finalize richText editor * chore: fix model dropdown selector * chore: fix change format file syntax highlight * chore: fix missing element during the test * fix: adjust onPrompChange include on fake textarea --- web/containers/ModelDropdown/index.tsx | 2 +- web/package.json | 5 +- .../ChatInput/RichTextEditor.tsx | 408 ++++++++++++++++++ .../ThreadCenterPanel/ChatInput/index.tsx | 85 +--- .../ThreadCenterPanel/EditChatInput/index.tsx | 15 - .../SimpleTextMessage/index.tsx | 38 +- 6 files changed, 438 insertions(+), 115 deletions(-) create mode 100644 web/screens/Thread/ThreadCenterPanel/ChatInput/RichTextEditor.tsx diff --git a/web/containers/ModelDropdown/index.tsx b/web/containers/ModelDropdown/index.tsx index 0ac016a609..59f19586ae 100644 --- a/web/containers/ModelDropdown/index.tsx +++ b/web/containers/ModelDropdown/index.tsx @@ -306,7 +306,7 @@ const ModelDropdown = ({ className={twMerge('relative', disabled && 'pointer-events-none')} data-testid="model-selector" > -
+
{chatInputMode ? ( + +const RichTextEditor = ({ + className, + style, + disabled, + placeholder, + spellCheck, +}: RichTextEditorProps) => { + const [editor] = useState(() => withHistory(withReact(createEditor()))) + const currentLanguage = useRef('plaintext') + const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom) + const textareaRef = useRef(null) + const activeThreadId = useAtomValue(getActiveThreadIdAtom) + const activeSettingInputBox = useAtomValue(activeSettingInputBoxAtom) + const messages = useAtomValue(getCurrentChatMessagesAtom) + const { sendChatMessage } = useSendChatMessage() + const { stopInference } = useActiveModel() + + // The decorate function identifies code blocks and marks the ranges + const decorate = useCallback( + (entry: [any, any]) => { + const ranges: any[] = [] + const [node, path] = entry + + if (Editor.isBlock(editor, node) && node.type === 'paragraph') { + node.children.forEach((child: { text: any }, childIndex: number) => { + const text = child.text + + // Match bold text pattern *text* + const boldMatches = [...text.matchAll(/(\*.*?\*)/g)] // Find bold patterns + boldMatches.forEach((match) => { + const startOffset = match.index + 1 || 0 + const length = match[0].length - 2 + + ranges.push({ + anchor: { path: [...path, childIndex], offset: startOffset }, + focus: { + path: [...path, childIndex], + offset: startOffset + length, + }, + format: 'italic', + className: 'italic', + }) + }) + }) + } + + if (Editor.isBlock(editor, node) && node.type === 'paragraph') { + node.children.forEach((child: { text: any }, childIndex: number) => { + const text = child.text + + // Match bold text pattern **text** + const boldMatches = [...text.matchAll(/(\*\*.*?\*\*)/g)] // Find bold patterns + boldMatches.forEach((match) => { + const startOffset = match.index + 2 || 0 + const length = match[0].length - 4 + + ranges.push({ + anchor: { path: [...path, childIndex], offset: startOffset }, + focus: { + path: [...path, childIndex], + offset: startOffset + length, + }, + format: 'bold', + className: 'font-bold', + }) + }) + }) + } + + if (Editor.isBlock(editor, node) && node.type === 'code') { + node.children.forEach((child: { text: any }, childIndex: number) => { + const text = child.text + + // Match code block start and end + const startMatch = text.match(/^```(\w*)$/) + const endMatch = text.match(/^```$/) + const inlineMatch = text.match(/^`([^`]+)`$/) // Match inline code + + if (startMatch) { + // If it's the start of a code block, store the language + currentLanguage.current = startMatch[1] || 'plaintext' + } else if (endMatch) { + // Reset language when code block ends + currentLanguage.current = 'plaintext' + } else if (inlineMatch) { + // Apply syntax highlighting to inline code + const codeContent = inlineMatch[1] // Get the content within the backticks + try { + hljs.highlight(codeContent, { + language: + currentLanguage.current.length > 1 + ? currentLanguage.current + : 'plaintext', + }).value + } catch (err) { + hljs.highlight(codeContent, { + language: 'javascript', + }).value + } + + // Calculate the range for the inline code + const length = codeContent.length + ranges.push({ + anchor: { + path: [...path, childIndex], + offset: inlineMatch.index + 1, + }, + focus: { + path: [...path, childIndex], + offset: inlineMatch.index + 1 + length, + }, + type: 'code', + code: true, + language: currentLanguage.current, + className: '', // Specify class name if needed + }) + } else if (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 + + let highlighted = '' + highlighted = hljs.highlightAuto(codeContent).value + try { + highlighted = hljs.highlight(codeContent, { + language: + currentLanguage.current.length > 1 + ? currentLanguage.current + : 'plaintext', + }).value + } catch (err) { + highlighted = hljs.highlight(codeContent, { + language: 'javascript', + }).value + } + + const parser = new DOMParser() + const doc = parser.parseFromString(highlighted, 'text/html') + + 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: leadingSpaces.length, + }, + 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 + ? (childNode as HTMLElement).className + : '' + + ranges.push({ + anchor: { + path: [...path, childIndex], + offset: slateTextIndex + leadingSpaces.length, + }, + focus: { + path: [...path, childIndex], + offset: slateTextIndex + leadingSpaces.length + length, + }, + type: 'code', + code: true, + language: currentLanguage.current, + className, + }) + + slateTextIndex += length + }) + } else { + ranges.push({ + anchor: { path: [...path, childIndex], offset: 0 }, + focus: { path: [...path, childIndex], offset: text.length }, + type: 'paragraph', // Treat as a paragraph + code: false, + }) + } + }) + } + + return ranges + }, + [editor] + ) + + // RenderLeaf applies the decoration styles + const renderLeaf = useCallback( + ({ attributes, children, leaf }: RenderLeafProps) => { + if (leaf.format === 'italic') { + return ( + + {children} + + ) + } + if (leaf.format === 'bold') { + return ( + + {children} + + ) + } + if (leaf.code) { + // Apply syntax highlighting to code blocks + return ( + + {children} + + ) + } + + return {children} + }, + [] + ) + + useEffect(() => { + if (textareaRef.current) { + textareaRef.current.focus() + } + }, [activeThreadId]) + + useEffect(() => { + if (textareaRef.current?.clientHeight) { + textareaRef.current.style.height = activeSettingInputBox + ? '100px' + : '40px' + textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px' + textareaRef.current.style.overflow = + textareaRef.current.clientHeight >= 390 ? 'auto' : 'hidden' + } + }, [textareaRef.current?.clientHeight, currentPrompt, activeSettingInputBox]) + + const onStopInferenceClick = async () => { + stopInference() + } + + const resetEditor = useCallback(() => { + Transforms.delete(editor, { + at: { + anchor: Editor.start(editor, []), + focus: Editor.end(editor, []), + }, + }) + + // Adjust the height of the textarea to its initial state + if (textareaRef.current) { + textareaRef.current.style.height = '40px' // Reset to the initial height or your desired height + textareaRef.current.style.overflow = 'hidden' // Reset overflow style + } + + // Ensure the editor re-renders decorations + editor.onChange() + }, [editor]) + + const handleKeyDown = useCallback( + (event: React.KeyboardEvent) => { + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault() + if (messages[messages.length - 1]?.status !== MessageStatus.Pending) { + sendChatMessage(currentPrompt) + resetEditor() + } else onStopInferenceClick() + } + + if (event.key === '`') { + // Determine whether any of the currently selected blocks are code blocks. + const [match] = Editor.nodes(editor, { + match: (n) => + Element.isElement(n) && (n as CustomElement).type === 'code', + }) + // Toggle the block type dependsing on whether there's already a match. + Transforms.setNodes( + editor, + { type: match ? 'paragraph' : 'code' }, + { match: (n) => Element.isElement(n) && Editor.isBlock(editor, n) } + ) + } + + if (event.key === 'Tab') { + const [match] = Editor.nodes(editor, { + match: (n) => { + return (n as CustomElement).type === 'code' + }, + mode: 'lowest', + }) + + if (match) { + event.preventDefault() + // Insert a tab character + Editor.insertText(editor, ' ') // Insert 2 spaces + } + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [currentPrompt, editor, messages] + ) + + return ( + { + const combinedText = value + .map((block) => { + if ('children' in block) { + return block.children.map((child) => child.text).join('') + } + return '' + }) + .join('\n') + + setCurrentPrompt(combinedText) + }} + > + + + ) +} + +export default RichTextEditor diff --git a/web/screens/Thread/ThreadCenterPanel/ChatInput/index.tsx b/web/screens/Thread/ThreadCenterPanel/ChatInput/index.tsx index a7c5ad1216..83a68fa8a9 100644 --- a/web/screens/Thread/ThreadCenterPanel/ChatInput/index.tsx +++ b/web/screens/Thread/ThreadCenterPanel/ChatInput/index.tsx @@ -29,6 +29,8 @@ import { isLocalEngine } from '@/utils/modelEngine' import FileUploadPreview from '../FileUploadPreview' import ImageUploadPreview from '../ImageUploadPreview' +import RichTextEditor from './RichTextEditor' + import { showRightPanelAtom } from '@/helpers/atoms/App.atom' import { experimentalFeatureEnabledAtom } from '@/helpers/atoms/AppConfig.atom' import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom' @@ -40,7 +42,6 @@ import { getActiveThreadIdAtom, isGeneratingResponseAtom, threadStatesAtom, - waitingToSendMessage, } from '@/helpers/atoms/Thread.atom' import { activeTabThreadRightPanelAtom } from '@/helpers/atoms/ThreadRightPanel.atom' @@ -48,7 +49,6 @@ const ChatInput = () => { const activeThread = useAtomValue(activeThreadAtom) const { stateModel } = useActiveModel() const messages = useAtomValue(getCurrentChatMessagesAtom) - // const [activeSetting, setActiveSetting] = useState(false) const spellCheck = useAtomValue(spellCheckAtom) const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom) @@ -59,7 +59,6 @@ const ChatInput = () => { const selectedModel = useAtomValue(selectedModelAtom) const activeThreadId = useAtomValue(getActiveThreadIdAtom) - const [isWaitingToSend, setIsWaitingToSend] = useAtom(waitingToSendMessage) const [fileUpload, setFileUpload] = useAtom(fileUploadAtom) const [showAttacmentMenus, setShowAttacmentMenus] = useState(false) const textareaRef = useRef(null) @@ -78,52 +77,15 @@ const ChatInput = () => { (threadState) => threadState.waitingForResponse ) - const onPromptChange = (e: React.ChangeEvent) => { - setCurrentPrompt(e.target.value) - } - const refAttachmentMenus = useClickOutside(() => setShowAttacmentMenus(false)) const [showRightPanel, setShowRightPanel] = useAtom(showRightPanelAtom) - useEffect(() => { - if (isWaitingToSend && activeThreadId) { - setIsWaitingToSend(false) - sendChatMessage(currentPrompt) - } - }, [ - activeThreadId, - isWaitingToSend, - currentPrompt, - setIsWaitingToSend, - sendChatMessage, - ]) - useEffect(() => { if (textareaRef.current) { textareaRef.current.focus() } }, [activeThreadId]) - useEffect(() => { - if (textareaRef.current?.clientHeight) { - textareaRef.current.style.height = activeSettingInputBox - ? '100px' - : '40px' - textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px' - textareaRef.current.style.overflow = - textareaRef.current.clientHeight >= 390 ? 'auto' : 'hidden' - } - }, [textareaRef.current?.clientHeight, currentPrompt, activeSettingInputBox]) - - const onKeyDown = async (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) { - e.preventDefault() - if (messages[messages.length - 1]?.status !== MessageStatus.Pending) - sendChatMessage(currentPrompt) - else onStopInferenceClick() - } - } - const onStopInferenceClick = async () => { stopInference() } @@ -163,22 +125,25 @@ const ChatInput = () => {
{renderPreview(fileUpload)} -