Sync Release/v0.6.0 into dev (#5293)
* chore: enable shortcut zoom (#5261) * chore: enable shortcut zoom * chore: update shortcut setting * fix: thinking block (#5263) * Merge pull request #5262 from menloresearch/chore/sync-new-hub-data chore: sync new hub data * ✨enhancement: model run improvement (#5268) * fix: mcp tool error handling * fix: error message * fix: trigger download from recommend model * fix: can't scroll hub * fix: show progress * ✨enhancement: prompt users to increase context size * ✨enhancement: rearrange action buttons for a better UX * 🔧chore: clean up logics --------- Co-authored-by: Faisal Amir <urmauur@gmail.com> * fix: glitch download from onboarding (#5269) * ✨enhancement: Model sources should not be hard coded from frontend (#5270) * 🐛fix: default onboarding model should use recommended quantizations (#5273) * 🐛fix: default onboarding model should use recommended quantizations * ✨enhancement: show context shift option in provider settings * 🔧chore: wording * 🔧 config: add to gitignore * 🐛fix: Jan-nano repo name changed (#5274) * 🚧 wip: disable showSpeedToken in ChatInput * 🐛 fix: commented out the wrong import * fix: masking value MCP env field (#5276) * ✨ feat: add token speed to each message that persist * ♻️ refactor: to follow prettier convention * 🐛 fix: exclude deleted field * 🧹 clean: all the missed console.log * ✨enhancement: out of context troubleshooting (#5275) * ✨enhancement: out of context troubleshooting * 🔧refactor: clean up * ✨enhancement: add setting chat width container (#5289) * ✨enhancement: add setting conversation width * ✨enahncement: cleanup log and change improve accesibility * ✨enahcement: move const beta version * 🐛fix: optional additional_information gpu (#5291) * 🐛fix: showing release notes for beta and prod (#5292) * 🐛fix: showing release notes for beta and prod * ♻️refactor: make an utils env * ♻️refactor: hide MCP for production * ♻️refactor: simplify the boolean expression fetch release note --------- Co-authored-by: Faisal Amir <urmauur@gmail.com> Co-authored-by: LazyYuuki <huy2840@gmail.com> Co-authored-by: Bui Quang Huy <34532913+LazyYuuki@users.noreply.github.com>
This commit is contained in:
parent
38dedc2fb8
commit
035cc0f79c
4
.gitignore
vendored
4
.gitignore
vendored
@ -45,3 +45,7 @@ src-tauri/icons
|
||||
!src-tauri/icons/icon.png
|
||||
src-tauri/gen/apple
|
||||
src-tauri/resources/bin
|
||||
|
||||
# Helper tools
|
||||
.opencode
|
||||
OpenCode.md
|
||||
|
||||
@ -75,7 +75,7 @@ export default class JanAssistantExtension extends AssistantExtension {
|
||||
'Jan is a helpful desktop assistant that can reason through complex tasks and use tools to complete them on the user’s behalf.',
|
||||
model: '*',
|
||||
instructions:
|
||||
'Jan is a helpful desktop assistant that can reason through complex tasks and use tools to complete them on the user’s behalf. Respond naturally and concisely, take actions when needed, and guide the user toward their goals.',
|
||||
'You have access to a set of tools to help you answer the user’s question. You can use only one tool per message, and you’ll receive the result of that tool in the user’s next response. To complete a task, use tools step by step—each step should be guided by the outcome of the previous one.\nTool Usage Rules:\n1. Always provide the correct values as arguments when using tools. Do not pass variable names—use actual values instead.\n2. You may perform multiple tool steps to complete a task.\n3. Avoid repeating a tool call with exactly the same parameters to prevent infinite loops.',
|
||||
tools: [
|
||||
{
|
||||
type: 'retrieval',
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"name": "@janhq/download-extension",
|
||||
"productName": "Download Manager",
|
||||
"version": "1.0.0",
|
||||
"description": "Handle downloads",
|
||||
"description": "Download and manage files and AI models in Jan.",
|
||||
"main": "dist/index.js",
|
||||
"author": "Jan <service@jan.ai>",
|
||||
"license": "AGPL-3.0",
|
||||
|
||||
@ -8,6 +8,15 @@
|
||||
"value": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "context_shift",
|
||||
"title": "Context Shift",
|
||||
"description": "Automatically shifts the context window when the model is unable to process the entire prompt, ensuring that the most relevant information is always included.",
|
||||
"controllerType": "checkbox",
|
||||
"controllerProps": {
|
||||
"value": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "cont_batching",
|
||||
"title": "Continuous Batching",
|
||||
|
||||
@ -37,6 +37,7 @@ enum Settings {
|
||||
cpu_threads = 'cpu_threads',
|
||||
huggingfaceToken = 'hugging-face-access-token',
|
||||
auto_unload_models = 'auto_unload_models',
|
||||
context_shift = 'context_shift',
|
||||
}
|
||||
|
||||
type LoadedModelResponse = { data: { engine: string; id: string }[] }
|
||||
@ -62,6 +63,8 @@ export default class JanInferenceCortexExtension extends LocalOAIEngine {
|
||||
cache_type: string = 'q8'
|
||||
cpu_threads?: number
|
||||
auto_unload_models: boolean = true
|
||||
reasoning_budget = -1 // Default reasoning budget in seconds
|
||||
context_shift = true
|
||||
/**
|
||||
* The URL for making inference requests.
|
||||
*/
|
||||
@ -127,6 +130,10 @@ export default class JanInferenceCortexExtension extends LocalOAIEngine {
|
||||
true
|
||||
)
|
||||
this.flash_attn = await this.getSetting<boolean>(Settings.flash_attn, true)
|
||||
this.context_shift = await this.getSetting<boolean>(
|
||||
Settings.context_shift,
|
||||
true
|
||||
)
|
||||
this.use_mmap = await this.getSetting<boolean>(Settings.use_mmap, true)
|
||||
if (this.caching_enabled)
|
||||
this.cache_type = await this.getSetting<string>(Settings.cache_type, 'q8')
|
||||
@ -208,6 +215,8 @@ export default class JanInferenceCortexExtension extends LocalOAIEngine {
|
||||
this.updateCortexConfig({ huggingface_token: value })
|
||||
} else if (key === Settings.auto_unload_models) {
|
||||
this.auto_unload_models = value as boolean
|
||||
} else if (key === Settings.context_shift && typeof value === 'boolean') {
|
||||
this.context_shift = value
|
||||
}
|
||||
}
|
||||
|
||||
@ -230,8 +239,6 @@ export default class JanInferenceCortexExtension extends LocalOAIEngine {
|
||||
|
||||
const loadedModels = await this.activeModels()
|
||||
|
||||
console.log('Loaded models:', loadedModels)
|
||||
|
||||
// This is to avoid loading the same model multiple times
|
||||
if (loadedModels.some((e: { id: string }) => e.id === model.id)) {
|
||||
console.log(`Model ${model.id} already loaded`)
|
||||
@ -269,6 +276,12 @@ export default class JanInferenceCortexExtension extends LocalOAIEngine {
|
||||
...(this.cont_batching && this.n_parallel && this.n_parallel > 1
|
||||
? { cont_batching: this.cont_batching }
|
||||
: {}),
|
||||
...(model.id.toLowerCase().includes('jan-nano')
|
||||
? { reasoning_budget: 0 }
|
||||
: { reasoning_budget: this.reasoning_budget }),
|
||||
...(this.context_shift === false
|
||||
? { 'no-context-shift': true }
|
||||
: {}),
|
||||
},
|
||||
timeout: false,
|
||||
signal,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -25,6 +25,11 @@ type Data<T> = {
|
||||
data: T[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Defaul mode sources
|
||||
*/
|
||||
const defaultModelSources = ['Menlo/Jan-nano-gguf']
|
||||
|
||||
/**
|
||||
* A extension for models
|
||||
*/
|
||||
@ -286,6 +291,8 @@ export default class JanModelExtension extends ModelExtension {
|
||||
const sources = await this.apiInstance()
|
||||
.then((api) => api.get('v1/models/sources').json<Data<ModelSource>>())
|
||||
.then((e) => (typeof e === 'object' ? (e.data as ModelSource[]) : []))
|
||||
// Deprecated source - filter out from legacy sources
|
||||
.then((e) => e.filter((x) => x.id.toLowerCase() !== 'menlo/jan-nano'))
|
||||
.catch(() => [])
|
||||
return sources.concat(
|
||||
DEFAULT_MODEL_SOURCES.filter((e) => !sources.some((x) => x.id === e.id))
|
||||
@ -396,6 +403,11 @@ export default class JanModelExtension extends ModelExtension {
|
||||
fetchModelsHub = async () => {
|
||||
const models = await this.fetchModels()
|
||||
|
||||
defaultModelSources.forEach((model) => {
|
||||
this.addSource(model).catch((e) => {
|
||||
console.debug(`Failed to add default model source ${model}:`, e)
|
||||
})
|
||||
})
|
||||
return this.apiInstance()
|
||||
.then((api) =>
|
||||
api
|
||||
@ -403,7 +415,7 @@ export default class JanModelExtension extends ModelExtension {
|
||||
.json<Data<string>>()
|
||||
.then(async (e) => {
|
||||
await Promise.all(
|
||||
e.data?.map((model) => {
|
||||
[...(e.data ?? []), ...defaultModelSources].map((model) => {
|
||||
if (
|
||||
!models.some(
|
||||
(e) => 'modelSource' in e && e.modelSource === model
|
||||
|
||||
@ -2,16 +2,13 @@
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "enables the default permissions",
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"windows": ["main"],
|
||||
"remote": {
|
||||
"urls": [
|
||||
"http://*"
|
||||
]
|
||||
"urls": ["http://*"]
|
||||
},
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:webview:allow-set-webview-zoom",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-set-theme",
|
||||
"shell:allow-spawn",
|
||||
@ -81,9 +78,7 @@
|
||||
{
|
||||
"identifier": "opener:allow-open-url",
|
||||
"description": "opens the default permissions for the core module",
|
||||
"windows": [
|
||||
"*"
|
||||
],
|
||||
"windows": ["*"],
|
||||
"allow": [
|
||||
{
|
||||
"url": "https://*"
|
||||
@ -98,4 +93,4 @@
|
||||
},
|
||||
"store:default"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -377,7 +377,12 @@ pub async fn call_tool(
|
||||
});
|
||||
|
||||
return match timeout(MCP_TOOL_CALL_TIMEOUT, tool_call).await {
|
||||
Ok(result) => result.map_err(|e| e.to_string()),
|
||||
Ok(result) => {
|
||||
match result {
|
||||
Ok(ok_result) => Ok(ok_result),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
Err(_) => Err(format!(
|
||||
"Tool call '{}' timed out after {} seconds",
|
||||
tool_name,
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
"macOSPrivateApi": true,
|
||||
"windows": [
|
||||
{
|
||||
"zoomHotkeysEnabled": true,
|
||||
"label": "main",
|
||||
"title": "Jan",
|
||||
"width": 1024,
|
||||
|
||||
13
web-app/src/components/ui/skeleton.tsx
Normal file
13
web-app/src/components/ui/skeleton.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Skeleton({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="skeleton"
|
||||
className={cn('bg-main-view-fg/10', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
@ -21,7 +21,6 @@ import {
|
||||
IconTool,
|
||||
IconCodeCircle2,
|
||||
IconPlayerStopFilled,
|
||||
IconBrandSpeedtest,
|
||||
IconX,
|
||||
} from '@tabler/icons-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -36,6 +35,7 @@ import { ModelLoader } from '@/containers/loaders/ModelLoader'
|
||||
import DropdownToolsAvailable from '@/containers/DropdownToolsAvailable'
|
||||
import { getConnectedServers } from '@/services/mcp'
|
||||
import { stopAllModels } from '@/services/models'
|
||||
import { useOutOfContextPromiseModal } from './dialogs/OutOfContextDialog'
|
||||
|
||||
type ChatInputProps = {
|
||||
className?: string
|
||||
@ -44,12 +44,7 @@ type ChatInputProps = {
|
||||
initialMessage?: boolean
|
||||
}
|
||||
|
||||
const ChatInput = ({
|
||||
model,
|
||||
className,
|
||||
showSpeedToken = true,
|
||||
initialMessage,
|
||||
}: ChatInputProps) => {
|
||||
const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
const [isFocused, setIsFocused] = useState(false)
|
||||
const [rows, setRows] = useState(1)
|
||||
@ -59,7 +54,9 @@ const ChatInput = ({
|
||||
const { currentThreadId } = useThreads()
|
||||
const { t } = useTranslation()
|
||||
const { spellCheckChatInput } = useGeneralSetting()
|
||||
const { tokenSpeed } = useAppState()
|
||||
|
||||
const { showModal, PromiseModal: OutOfContextModal } =
|
||||
useOutOfContextPromiseModal()
|
||||
const maxRows = 10
|
||||
|
||||
const { selectedModel } = useModelProvider()
|
||||
@ -110,7 +107,7 @@ const ChatInput = ({
|
||||
return
|
||||
}
|
||||
setMessage('')
|
||||
sendMessage(prompt)
|
||||
sendMessage(prompt, showModal)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@ -556,15 +553,6 @@ const ChatInput = ({
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showSpeedToken && (
|
||||
<div className="flex items-center gap-1 text-main-view-fg/60 text-xs">
|
||||
<IconBrandSpeedtest size={18} />
|
||||
<span>
|
||||
{Math.round(tokenSpeed?.tokenSpeed ?? 0)} tokens/sec
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{streamingContent ? (
|
||||
@ -611,6 +599,7 @@ const ChatInput = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<OutOfContextModal />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
61
web-app/src/containers/ChatWidthSwitcher.tsx
Normal file
61
web-app/src/containers/ChatWidthSwitcher.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { useAppearance } from '@/hooks/useAppearance'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { IconCircleCheckFilled } from '@tabler/icons-react'
|
||||
|
||||
export function ChatWidthSwitcher() {
|
||||
const { chatWidth, setChatWidth } = useAppearance()
|
||||
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
className={cn(
|
||||
'w-full overflow-hidden border border-main-view-fg/10 rounded-md my-2 pb-2 cursor-pointer',
|
||||
chatWidth === 'compact' && 'border-accent'
|
||||
)}
|
||||
onClick={() => setChatWidth('compact')}
|
||||
>
|
||||
<div className="flex items-center justify-between px-4 py-2 bg-main-view-fg/10">
|
||||
<span className="font-medium text-xs font-sans">Compact Width</span>
|
||||
{chatWidth === 'compact' && (
|
||||
<IconCircleCheckFilled className="size-4 text-accent" />
|
||||
)}
|
||||
</div>
|
||||
<div className="overflow-auto p-2">
|
||||
<div className="flex flex-col px-10 gap-2 mt-2">
|
||||
<Skeleton className="h-2 w-full rounded-full" />
|
||||
<Skeleton className="h-2 w-full rounded-full" />
|
||||
<Skeleton className="h-2 w-full rounded-full" />
|
||||
<div className="bg-main-view-fg/10 h-8 px-4 w-full flex-shrink-0 border-none resize-none outline-0 rounded-2xl flex items-center">
|
||||
<span className="text-main-view-fg/50">Ask me anything...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
className={cn(
|
||||
'w-full overflow-hidden border border-main-view-fg/10 rounded-md my-2 pb-2 cursor-pointer',
|
||||
chatWidth === 'full' && 'border-accent'
|
||||
)}
|
||||
onClick={() => setChatWidth('full')}
|
||||
>
|
||||
<div className="flex items-center justify-between px-4 py-2 bg-main-view-fg/10">
|
||||
<span className="font-medium text-xs font-sans">Full Width</span>
|
||||
{chatWidth === 'full' && (
|
||||
<IconCircleCheckFilled className="size-4 text-accent" />
|
||||
)}
|
||||
</div>
|
||||
<div className="overflow-auto p-2">
|
||||
<div className="flex flex-col gap-2 mt-2">
|
||||
<Skeleton className="h-2 w-full rounded-full" />
|
||||
<Skeleton className="h-2 w-full rounded-full" />
|
||||
<Skeleton className="h-2 w-full rounded-full" />
|
||||
<div className="bg-main-view-fg/10 h-8 px-4 w-full flex-shrink-0 border-none resize-none outline-0 rounded-2xl flex items-center">
|
||||
<span className="text-main-view-fg/50">Ask me anything...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -2,6 +2,7 @@ import { Link, useMatches } from '@tanstack/react-router'
|
||||
import { route } from '@/constants/routes'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useModelProvider } from '@/hooks/useModelProvider'
|
||||
import { isProd } from '@/lib/version'
|
||||
|
||||
const menuSettings = [
|
||||
{
|
||||
@ -24,10 +25,15 @@ const menuSettings = [
|
||||
title: 'Hardware',
|
||||
route: route.settings.hardware,
|
||||
},
|
||||
{
|
||||
title: 'MCP Servers',
|
||||
route: route.settings.mcp_servers,
|
||||
},
|
||||
// Only show MCP Servers in non-production environment
|
||||
...(!isProd
|
||||
? [
|
||||
{
|
||||
title: 'MCP Servers',
|
||||
route: route.settings.mcp_servers,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: 'Local API Server',
|
||||
route: route.settings.local_api_server,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { ThreadMessage } from '@janhq/core'
|
||||
import { RenderMarkdown } from './RenderMarkdown'
|
||||
import { Fragment, memo, useCallback, useMemo, useState } from 'react'
|
||||
import React, { Fragment, memo, useCallback, useMemo, useState } from 'react'
|
||||
import {
|
||||
IconCopy,
|
||||
IconCopyCheck,
|
||||
@ -34,6 +34,9 @@ import {
|
||||
} from '@/components/ui/tooltip'
|
||||
import { formatDate } from '@/utils/formatDate'
|
||||
import { AvatarEmoji } from '@/containers/AvatarEmoji'
|
||||
|
||||
import TokenSpeedIndicator from '@/containers/TokenSpeedIndicator'
|
||||
|
||||
import CodeEditor from '@uiw/react-textarea-code-editor'
|
||||
import '@uiw/react-textarea-code-editor/dist.css'
|
||||
|
||||
@ -79,6 +82,8 @@ export const ThreadContent = memo(
|
||||
showAssistant?: boolean
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
streamTools?: any
|
||||
contextOverflowModal?: React.ReactNode | null
|
||||
showContextOverflowModal?: () => Promise<unknown>
|
||||
}
|
||||
) => {
|
||||
const [message, setMessage] = useState(item.content?.[0]?.text?.value || '')
|
||||
@ -129,7 +134,10 @@ export const ThreadContent = memo(
|
||||
}
|
||||
if (toSendMessage) {
|
||||
deleteMessage(toSendMessage.thread_id, toSendMessage.id ?? '')
|
||||
sendMessage(toSendMessage.content?.[0]?.text?.value || '')
|
||||
sendMessage(
|
||||
toSendMessage.content?.[0]?.text?.value || '',
|
||||
item.showContextOverflowModal
|
||||
)
|
||||
}
|
||||
}, [deleteMessage, getMessages, item, sendMessage])
|
||||
|
||||
@ -162,15 +170,25 @@ export const ThreadContent = memo(
|
||||
const editMessage = useCallback(
|
||||
(messageId: string) => {
|
||||
const threadMessages = getMessages(item.thread_id)
|
||||
|
||||
const index = threadMessages.findIndex((msg) => msg.id === messageId)
|
||||
if (index === -1) return
|
||||
|
||||
// Delete all messages after the edited message
|
||||
for (let i = threadMessages.length - 1; i >= index; i--) {
|
||||
deleteMessage(threadMessages[i].thread_id, threadMessages[i].id)
|
||||
}
|
||||
sendMessage(message)
|
||||
|
||||
sendMessage(message, item.showContextOverflowModal)
|
||||
},
|
||||
[deleteMessage, getMessages, item.thread_id, message, sendMessage]
|
||||
[
|
||||
deleteMessage,
|
||||
getMessages,
|
||||
item.thread_id,
|
||||
message,
|
||||
sendMessage,
|
||||
item.showContextOverflowModal,
|
||||
]
|
||||
)
|
||||
|
||||
const isToolCalls =
|
||||
@ -184,7 +202,7 @@ export const ThreadContent = memo(
|
||||
| undefined
|
||||
|
||||
return (
|
||||
<Fragment key={item.id}>
|
||||
<Fragment>
|
||||
{item.content?.[0]?.text && item.role === 'user' && (
|
||||
<div className="w-full">
|
||||
<div className="flex justify-end w-full h-full text-start break-words whitespace-normal">
|
||||
@ -341,95 +359,100 @@ export const ThreadContent = memo(
|
||||
|
||||
{!isToolCalls && (
|
||||
<div className="flex items-center gap-2 mt-2 text-main-view-fg/60 text-xs">
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-2',
|
||||
item.isLastMessage &&
|
||||
streamingContent &&
|
||||
'opacity-0 visibility-hidden pointer-events-none'
|
||||
)}
|
||||
>
|
||||
<CopyButton text={item.content?.[0]?.text.value || ''} />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
className="flex items-center gap-1 hover:text-accent transition-colors cursor-pointer group relative"
|
||||
onClick={() => {
|
||||
removeMessage()
|
||||
}}
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Delete</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Dialog>
|
||||
<DialogTrigger>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="outline-0 focus:outline-0 flex items-center gap-1 hover:text-accent transition-colors cursor-pointer group relative">
|
||||
<IconInfoCircle size={16} />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Metadata</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Message Metadata</DialogTitle>
|
||||
<div className="space-y-2">
|
||||
<div className="border border-main-view-fg/10 rounded-md overflow-hidden">
|
||||
<CodeEditor
|
||||
value={JSON.stringify(
|
||||
item.metadata || {},
|
||||
null,
|
||||
2
|
||||
)}
|
||||
language="json"
|
||||
readOnly
|
||||
style={{
|
||||
fontFamily: 'ui-monospace',
|
||||
backgroundColor: 'transparent',
|
||||
height: '100%',
|
||||
}}
|
||||
className="w-full h-full !text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="mt-2 flex items-center">
|
||||
<DialogClose asChild>
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="hover:no-underline"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{item.isLastMessage && (
|
||||
<div className={cn('flex items-center gap-2')}>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-2',
|
||||
item.isLastMessage && streamingContent && 'hidden'
|
||||
)}
|
||||
>
|
||||
<CopyButton text={item.content?.[0]?.text.value || ''} />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
className="flex items-center gap-1 hover:text-accent transition-colors cursor-pointer group relative"
|
||||
onClick={regenerate}
|
||||
onClick={() => {
|
||||
removeMessage()
|
||||
}}
|
||||
>
|
||||
<IconRefresh size={16} />
|
||||
<IconTrash size={16} />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Regenerate</p>
|
||||
<p>Delete</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Dialog>
|
||||
<DialogTrigger>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="outline-0 focus:outline-0 flex items-center gap-1 hover:text-accent transition-colors cursor-pointer group relative">
|
||||
<IconInfoCircle size={16} />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Metadata</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Message Metadata</DialogTitle>
|
||||
<div className="space-y-2">
|
||||
<div className="border border-main-view-fg/10 rounded-md overflow-hidden">
|
||||
<CodeEditor
|
||||
value={JSON.stringify(
|
||||
item.metadata || {},
|
||||
null,
|
||||
2
|
||||
)}
|
||||
language="json"
|
||||
readOnly
|
||||
style={{
|
||||
fontFamily: 'ui-monospace',
|
||||
backgroundColor: 'transparent',
|
||||
height: '100%',
|
||||
}}
|
||||
className="w-full h-full !text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="mt-2 flex items-center">
|
||||
<DialogClose asChild>
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="hover:no-underline"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{item.isLastMessage && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
className="flex items-center gap-1 hover:text-accent transition-colors cursor-pointer group relative"
|
||||
onClick={regenerate}
|
||||
>
|
||||
<IconRefresh size={16} />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Regenerate</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<TokenSpeedIndicator
|
||||
streaming={Boolean(item.isLastMessage && streamingContent)}
|
||||
metadata={item.metadata}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -445,6 +468,7 @@ export const ThreadContent = memo(
|
||||
{image.detail && <p className="text-sm mt-1">{image.detail}</p>}
|
||||
</div>
|
||||
)}
|
||||
{item.contextOverflowModal && item.contextOverflowModal}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
31
web-app/src/containers/TokenSpeedIndicator.tsx
Normal file
31
web-app/src/containers/TokenSpeedIndicator.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { useAppState } from '@/hooks/useAppState'
|
||||
import { Gauge } from 'lucide-react'
|
||||
|
||||
interface TokenSpeedIndicatorProps {
|
||||
metadata?: Record<string, unknown>
|
||||
streaming?: boolean
|
||||
}
|
||||
|
||||
export const TokenSpeedIndicator = ({
|
||||
metadata,
|
||||
streaming,
|
||||
}: TokenSpeedIndicatorProps) => {
|
||||
const { tokenSpeed } = useAppState()
|
||||
const persistedTokenSpeed = (metadata?.tokenSpeed as { tokenSpeed: number })
|
||||
?.tokenSpeed
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1 text-main-view-fg/60 text-xs">
|
||||
<Gauge size={16} />
|
||||
|
||||
<span>
|
||||
{Math.round(
|
||||
streaming ? Number(tokenSpeed?.tokenSpeed) : persistedTokenSpeed
|
||||
)}
|
||||
tokens/sec
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TokenSpeedIndicator
|
||||
@ -7,6 +7,7 @@ import { useState, useEffect } from 'react'
|
||||
import { useReleaseNotes } from '@/hooks/useReleaseNotes'
|
||||
import { RenderMarkdown } from '../RenderMarkdown'
|
||||
import { cn, isDev } from '@/lib/utils'
|
||||
import { isNightly, isBeta } from '@/lib/version'
|
||||
|
||||
const DialogAppUpdater = () => {
|
||||
const {
|
||||
@ -22,16 +23,13 @@ const DialogAppUpdater = () => {
|
||||
setRemindMeLater(true)
|
||||
}
|
||||
|
||||
const beta = VERSION.includes('beta')
|
||||
const nightly = VERSION.includes('-')
|
||||
|
||||
const { release, fetchLatestRelease } = useReleaseNotes()
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDev()) {
|
||||
fetchLatestRelease(beta ? true : false)
|
||||
fetchLatestRelease(isBeta)
|
||||
}
|
||||
}, [beta, fetchLatestRelease])
|
||||
}, [fetchLatestRelease])
|
||||
|
||||
// Check for updates when component mounts
|
||||
useEffect(() => {
|
||||
@ -71,7 +69,7 @@ const DialogAppUpdater = () => {
|
||||
<div className="text-base font-medium">
|
||||
New Version: Jan {updateState.updateInfo?.version}
|
||||
</div>
|
||||
<div className="mt-1 text-main-view-fg/70 font-normal">
|
||||
<div className="mt-1 text-main-view-fg/70 font-normal mb-2">
|
||||
There's a new app update available to download.
|
||||
</div>
|
||||
</div>
|
||||
@ -79,9 +77,9 @@ const DialogAppUpdater = () => {
|
||||
</div>
|
||||
|
||||
{showReleaseNotes && (
|
||||
<div className="max-h-[500px] py-2 overflow-y-scroll px-4 text-sm font-normal leading-relaxed">
|
||||
{nightly ? (
|
||||
<p className="mt-2 text-sm font-normal">
|
||||
<div className="max-h-[500px] p-4 w-[400px] overflow-y-scroll text-sm font-normal leading-relaxed">
|
||||
{isNightly && !isBeta ? (
|
||||
<p className="text-sm font-normal">
|
||||
You are using a nightly build. This version is built from
|
||||
the latest development branch and may not have release
|
||||
notes.
|
||||
|
||||
115
web-app/src/containers/dialogs/OutOfContextDialog.tsx
Normal file
115
web-app/src/containers/dialogs/OutOfContextDialog.tsx
Normal file
@ -0,0 +1,115 @@
|
||||
import { t } from 'i18next'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
|
||||
import { ReactNode, useCallback, useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
export function useOutOfContextPromiseModal() {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [modalProps, setModalProps] = useState<{
|
||||
resolveRef:
|
||||
| ((value: 'ctx_len' | 'context_shift' | undefined) => void)
|
||||
| null
|
||||
}>({
|
||||
resolveRef: null,
|
||||
})
|
||||
// Function to open the modal and return a Promise
|
||||
const showModal = useCallback(() => {
|
||||
return new Promise((resolve) => {
|
||||
setModalProps({
|
||||
resolveRef: resolve,
|
||||
})
|
||||
setIsOpen(true)
|
||||
})
|
||||
}, [])
|
||||
|
||||
const PromiseModal = useCallback((): ReactNode => {
|
||||
if (!isOpen) {
|
||||
return null
|
||||
}
|
||||
|
||||
const handleContextLength = () => {
|
||||
setIsOpen(false)
|
||||
if (modalProps.resolveRef) {
|
||||
modalProps.resolveRef('ctx_len')
|
||||
}
|
||||
}
|
||||
|
||||
const handleContextShift = () => {
|
||||
setIsOpen(false)
|
||||
if (modalProps.resolveRef) {
|
||||
modalProps.resolveRef('context_shift')
|
||||
}
|
||||
}
|
||||
const handleCancel = () => {
|
||||
setIsOpen(false)
|
||||
if (modalProps.resolveRef) {
|
||||
modalProps.resolveRef(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onOpenChange={(open) => {
|
||||
setIsOpen(open)
|
||||
if (!open) handleCancel()
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{t('outOfContextError.title', 'Out of context error')}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogDescription>
|
||||
{t(
|
||||
'outOfContextError.description',
|
||||
'This chat is reaching the AI’s memory limit, like a whiteboard filling up. We can expand the memory window (called context size) so it remembers more, but it may use more of your computer’s memory. We can also truncate the input, which means it will forget some of the chat history to make room for new messages.'
|
||||
)}
|
||||
<br />
|
||||
<br />
|
||||
{t(
|
||||
'outOfContextError.increaseContextSizeDescription',
|
||||
'Do you want to increase the context size?'
|
||||
)}
|
||||
</DialogDescription>
|
||||
<DialogFooter className="flex gap-2">
|
||||
<Button
|
||||
variant="default"
|
||||
className="bg-transparent border border-main-view-fg/20 hover:bg-main-view-fg/4"
|
||||
onClick={() => {
|
||||
handleContextShift()
|
||||
setIsOpen(false)
|
||||
}}
|
||||
>
|
||||
{t('outOfContextError.truncateInput', 'Truncate Input')}
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
onClick={() => {
|
||||
handleContextLength()
|
||||
setIsOpen(false)
|
||||
}}
|
||||
>
|
||||
<span className="text-main-view-fg/70">
|
||||
{t(
|
||||
'outOfContextError.increaseContextSize',
|
||||
'Increase Context Size'
|
||||
)}
|
||||
</span>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}, [isOpen, modalProps])
|
||||
return { showModal, PromiseModal }
|
||||
}
|
||||
@ -12,6 +12,7 @@ type AppState = {
|
||||
abortControllers: Record<string, AbortController>
|
||||
tokenSpeed?: TokenSpeed
|
||||
currentToolCall?: ChatCompletionMessageToolCall
|
||||
showOutOfContextDialog?: boolean
|
||||
setServerStatus: (value: 'running' | 'stopped' | 'pending') => void
|
||||
updateStreamingContent: (content: ThreadMessage | undefined) => void
|
||||
updateCurrentToolCall: (
|
||||
@ -22,6 +23,7 @@ type AppState = {
|
||||
setAbortController: (threadId: string, controller: AbortController) => void
|
||||
updateTokenSpeed: (message: ThreadMessage) => void
|
||||
resetTokenSpeed: () => void
|
||||
setOutOfContextDialog: (show: boolean) => void
|
||||
}
|
||||
|
||||
export const useAppState = create<AppState>()((set) => ({
|
||||
@ -99,4 +101,9 @@ export const useAppState = create<AppState>()((set) => ({
|
||||
set({
|
||||
tokenSpeed: undefined,
|
||||
}),
|
||||
setOutOfContextDialog: (show) => {
|
||||
set(() => ({
|
||||
showOutOfContextDialog: show,
|
||||
}))
|
||||
},
|
||||
}))
|
||||
|
||||
@ -6,8 +6,10 @@ import { rgb, oklch, formatCss } from 'culori'
|
||||
import { useTheme } from './useTheme'
|
||||
|
||||
export type FontSize = '14px' | '15px' | '16px' | '18px'
|
||||
export type ChatWidth = 'full' | 'compact'
|
||||
|
||||
interface AppearanceState {
|
||||
chatWidth: ChatWidth
|
||||
fontSize: FontSize
|
||||
appBgColor: RgbaColor
|
||||
appMainViewBgColor: RgbaColor
|
||||
@ -19,6 +21,7 @@ interface AppearanceState {
|
||||
appAccentTextColor: string
|
||||
appDestructiveTextColor: string
|
||||
appLeftPanelTextColor: string
|
||||
setChatWidth: (size: ChatWidth) => void
|
||||
setFontSize: (size: FontSize) => void
|
||||
setAppBgColor: (color: RgbaColor) => void
|
||||
setAppMainViewBgColor: (color: RgbaColor) => void
|
||||
@ -129,6 +132,7 @@ export const useAppearance = create<AppearanceState>()(
|
||||
persist(
|
||||
(set) => {
|
||||
return {
|
||||
chatWidth: 'compact',
|
||||
fontSize: defaultFontSize,
|
||||
appBgColor: defaultAppBgColor,
|
||||
appMainViewBgColor: defaultAppMainViewBgColor,
|
||||
@ -270,6 +274,10 @@ export const useAppearance = create<AppearanceState>()(
|
||||
})
|
||||
},
|
||||
|
||||
setChatWidth: (value: ChatWidth) => {
|
||||
set({ chatWidth: value })
|
||||
},
|
||||
|
||||
setFontSize: (size: FontSize) => {
|
||||
// Update CSS variable
|
||||
document.documentElement.style.setProperty('--font-size-base', size)
|
||||
|
||||
@ -21,7 +21,7 @@ export const defaultAssistant: Assistant = {
|
||||
description:
|
||||
'Jan is a helpful desktop assistant that can reason through complex tasks and use tools to complete them on the user’s behalf.',
|
||||
instructions:
|
||||
'Jan is a helpful desktop assistant that can reason through complex tasks and use tools to complete them on the user’s behalf. Respond naturally and concisely, take actions when needed, and guide the user toward their goals.',
|
||||
'You have access to a set of tools to help you answer the user’s question. You can use only one tool per message, and you’ll receive the result of that tool in the user’s next response. To complete a task, use tools step by step—each step should be guided by the outcome of the previous one.\nTool Usage Rules:\n1. Always provide the correct values as arguments when using tools. Do not pass variable names—use actual values instead.\n2. You may perform multiple tool steps to complete a task.\n3. Avoid repeating a tool call with exactly the same parameters to prevent infinite loops.',
|
||||
}
|
||||
|
||||
export const useAssistant = create<AssistantState>()((set, get) => ({
|
||||
|
||||
@ -24,10 +24,12 @@ import { getTools } from '@/services/mcp'
|
||||
import { MCPTool } from '@/types/completion'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
import { SystemEvent } from '@/types/events'
|
||||
import { stopModel, startModel } from '@/services/models'
|
||||
import { stopModel, startModel, stopAllModels } from '@/services/models'
|
||||
|
||||
import { useToolApproval } from '@/hooks/useToolApproval'
|
||||
import { useToolAvailable } from '@/hooks/useToolAvailable'
|
||||
import { OUT_OF_CONTEXT_SIZE } from '@/utils/error'
|
||||
import { updateSettings } from '@/services/providers'
|
||||
|
||||
export const useChat = () => {
|
||||
const { prompt, setPrompt } = usePrompt()
|
||||
@ -41,6 +43,7 @@ export const useChat = () => {
|
||||
setAbortController,
|
||||
} = useAppState()
|
||||
const { currentAssistant } = useAssistant()
|
||||
const { updateProvider } = useModelProvider()
|
||||
|
||||
const { approvedTools, showApprovalModal, allowAllMCPPermissions } =
|
||||
useToolApproval()
|
||||
@ -108,12 +111,131 @@ export const useChat = () => {
|
||||
currentAssistant,
|
||||
])
|
||||
|
||||
const restartModel = useCallback(
|
||||
async (
|
||||
provider: ProviderObject,
|
||||
modelId: string,
|
||||
abortController: AbortController
|
||||
) => {
|
||||
await stopAllModels()
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
updateLoadingModel(true)
|
||||
await startModel(provider, modelId, abortController).catch(console.error)
|
||||
updateLoadingModel(false)
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
},
|
||||
[updateLoadingModel]
|
||||
)
|
||||
|
||||
const increaseModelContextSize = useCallback(
|
||||
async (
|
||||
modelId: string,
|
||||
provider: ProviderObject,
|
||||
controller: AbortController
|
||||
) => {
|
||||
/**
|
||||
* Should increase the context size of the model by 2x
|
||||
* If the context size is not set or too low, it defaults to 8192.
|
||||
*/
|
||||
const model = provider.models.find((m) => m.id === modelId)
|
||||
if (!model) return undefined
|
||||
const ctxSize = Math.max(
|
||||
model.settings?.ctx_len?.controller_props.value
|
||||
? typeof model.settings.ctx_len.controller_props.value === 'string'
|
||||
? parseInt(model.settings.ctx_len.controller_props.value as string)
|
||||
: (model.settings.ctx_len.controller_props.value as number)
|
||||
: 16384,
|
||||
16384
|
||||
)
|
||||
const updatedModel = {
|
||||
...model,
|
||||
settings: {
|
||||
...model.settings,
|
||||
ctx_len: {
|
||||
...(model.settings?.ctx_len != null ? model.settings?.ctx_len : {}),
|
||||
controller_props: {
|
||||
...(model.settings?.ctx_len?.controller_props ?? {}),
|
||||
value: ctxSize * 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Find the model index in the provider's models array
|
||||
const modelIndex = provider.models.findIndex((m) => m.id === model.id)
|
||||
|
||||
if (modelIndex !== -1) {
|
||||
// Create a copy of the provider's models array
|
||||
const updatedModels = [...provider.models]
|
||||
|
||||
// Update the specific model in the array
|
||||
updatedModels[modelIndex] = updatedModel as Model
|
||||
|
||||
// Update the provider with the new models array
|
||||
updateProvider(provider.provider, {
|
||||
models: updatedModels,
|
||||
})
|
||||
}
|
||||
const updatedProvider = getProviderByName(provider.provider)
|
||||
if (updatedProvider)
|
||||
await restartModel(updatedProvider, model.id, controller)
|
||||
|
||||
console.log(
|
||||
updatedProvider?.models.find((e) => e.id === model.id)?.settings
|
||||
?.ctx_len?.controller_props.value
|
||||
)
|
||||
return updatedProvider
|
||||
},
|
||||
[getProviderByName, restartModel, updateProvider]
|
||||
)
|
||||
const toggleOnContextShifting = useCallback(
|
||||
async (
|
||||
modelId: string,
|
||||
provider: ProviderObject,
|
||||
controller: AbortController
|
||||
) => {
|
||||
const providerName = provider.provider
|
||||
const newSettings = [...provider.settings]
|
||||
const settingKey = 'context_shift'
|
||||
// Handle different value types by forcing the type
|
||||
// Use type assertion to bypass type checking
|
||||
const settingIndex = provider.settings.findIndex(
|
||||
(s) => s.key === settingKey
|
||||
)
|
||||
;(
|
||||
newSettings[settingIndex].controller_props as {
|
||||
value: string | boolean | number
|
||||
}
|
||||
).value = true
|
||||
|
||||
// Create update object with updated settings
|
||||
const updateObj: Partial<ModelProvider> = {
|
||||
settings: newSettings,
|
||||
}
|
||||
|
||||
await updateSettings(providerName, updateObj.settings ?? [])
|
||||
updateProvider(providerName, {
|
||||
...provider,
|
||||
...updateObj,
|
||||
})
|
||||
const updatedProvider = getProviderByName(providerName)
|
||||
if (updatedProvider)
|
||||
await restartModel(updatedProvider, modelId, controller)
|
||||
return updatedProvider
|
||||
},
|
||||
[updateProvider, getProviderByName, restartModel]
|
||||
)
|
||||
|
||||
const sendMessage = useCallback(
|
||||
async (message: string) => {
|
||||
async (
|
||||
message: string,
|
||||
showModal?: () => Promise<unknown>,
|
||||
troubleshooting = true
|
||||
) => {
|
||||
const activeThread = await getCurrentThread()
|
||||
|
||||
resetTokenSpeed()
|
||||
const activeProvider = currentProviderId
|
||||
let activeProvider = currentProviderId
|
||||
? getProviderByName(currentProviderId)
|
||||
: provider
|
||||
if (!activeThread || !activeProvider) return
|
||||
@ -121,7 +243,9 @@ export const useChat = () => {
|
||||
const abortController = new AbortController()
|
||||
setAbortController(activeThread.id, abortController)
|
||||
updateStreamingContent(emptyThreadContent)
|
||||
addMessage(newUserThreadContent(activeThread.id, message))
|
||||
// Do not add new message on retry
|
||||
if (troubleshooting)
|
||||
addMessage(newUserThreadContent(activeThread.id, message))
|
||||
updateThreadTimestamp(activeThread.id)
|
||||
setPrompt('')
|
||||
try {
|
||||
@ -154,7 +278,11 @@ export const useChat = () => {
|
||||
|
||||
// TODO: Later replaced by Agent setup?
|
||||
const followUpWithToolUse = true
|
||||
while (!isCompleted && !abortController.signal.aborted) {
|
||||
while (
|
||||
!isCompleted &&
|
||||
!abortController.signal.aborted &&
|
||||
activeProvider
|
||||
) {
|
||||
const completion = await sendCompletion(
|
||||
activeThread,
|
||||
activeProvider,
|
||||
@ -173,48 +301,90 @@ export const useChat = () => {
|
||||
let accumulatedText = ''
|
||||
const currentCall: ChatCompletionMessageToolCall | null = null
|
||||
const toolCalls: ChatCompletionMessageToolCall[] = []
|
||||
if (isCompletionResponse(completion)) {
|
||||
accumulatedText = completion.choices[0]?.message?.content || ''
|
||||
if (completion.choices[0]?.message?.tool_calls) {
|
||||
toolCalls.push(...completion.choices[0].message.tool_calls)
|
||||
}
|
||||
} else {
|
||||
for await (const part of completion) {
|
||||
const delta = part.choices[0]?.delta?.content || ''
|
||||
try {
|
||||
if (isCompletionResponse(completion)) {
|
||||
accumulatedText = completion.choices[0]?.message?.content || ''
|
||||
if (completion.choices[0]?.message?.tool_calls) {
|
||||
toolCalls.push(...completion.choices[0].message.tool_calls)
|
||||
}
|
||||
} else {
|
||||
for await (const part of completion) {
|
||||
// Error message
|
||||
if (!part.choices) {
|
||||
throw new Error(
|
||||
'message' in part
|
||||
? (part.message as string)
|
||||
: (JSON.stringify(part) ?? '')
|
||||
)
|
||||
}
|
||||
const delta = part.choices[0]?.delta?.content || ''
|
||||
|
||||
if (part.choices[0]?.delta?.tool_calls) {
|
||||
const calls = extractToolCall(part, currentCall, toolCalls)
|
||||
const currentContent = newAssistantThreadContent(
|
||||
activeThread.id,
|
||||
accumulatedText,
|
||||
{
|
||||
tool_calls: calls.map((e) => ({
|
||||
...e,
|
||||
state: 'pending',
|
||||
})),
|
||||
}
|
||||
)
|
||||
updateStreamingContent(currentContent)
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
if (part.choices[0]?.delta?.tool_calls) {
|
||||
const calls = extractToolCall(part, currentCall, toolCalls)
|
||||
const currentContent = newAssistantThreadContent(
|
||||
activeThread.id,
|
||||
accumulatedText,
|
||||
{
|
||||
tool_calls: calls.map((e) => ({
|
||||
...e,
|
||||
state: 'pending',
|
||||
})),
|
||||
}
|
||||
)
|
||||
updateStreamingContent(currentContent)
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
}
|
||||
if (delta) {
|
||||
accumulatedText += delta
|
||||
// Create a new object each time to avoid reference issues
|
||||
// Use a timeout to prevent React from batching updates too quickly
|
||||
const currentContent = newAssistantThreadContent(
|
||||
activeThread.id,
|
||||
accumulatedText,
|
||||
{
|
||||
tool_calls: toolCalls.map((e) => ({
|
||||
...e,
|
||||
state: 'pending',
|
||||
})),
|
||||
}
|
||||
)
|
||||
updateStreamingContent(currentContent)
|
||||
updateTokenSpeed(currentContent)
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
}
|
||||
}
|
||||
if (delta) {
|
||||
accumulatedText += delta
|
||||
// Create a new object each time to avoid reference issues
|
||||
// Use a timeout to prevent React from batching updates too quickly
|
||||
const currentContent = newAssistantThreadContent(
|
||||
activeThread.id,
|
||||
accumulatedText,
|
||||
{
|
||||
tool_calls: toolCalls.map((e) => ({
|
||||
...e,
|
||||
state: 'pending',
|
||||
})),
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error && typeof error === 'object' && 'message' in error
|
||||
? error.message
|
||||
: error
|
||||
if (
|
||||
typeof errorMessage === 'string' &&
|
||||
errorMessage.includes(OUT_OF_CONTEXT_SIZE) &&
|
||||
selectedModel &&
|
||||
troubleshooting
|
||||
) {
|
||||
const method = await showModal?.()
|
||||
if (method === 'ctx_len') {
|
||||
/// Increase context size
|
||||
activeProvider = await increaseModelContextSize(
|
||||
selectedModel.id,
|
||||
activeProvider,
|
||||
abortController
|
||||
)
|
||||
updateStreamingContent(currentContent)
|
||||
updateTokenSpeed(currentContent)
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
}
|
||||
continue
|
||||
} else if (method === 'context_shift' && selectedModel?.id) {
|
||||
/// Enable context_shift
|
||||
activeProvider = await toggleOnContextShifting(
|
||||
selectedModel?.id,
|
||||
activeProvider,
|
||||
abortController
|
||||
)
|
||||
continue
|
||||
} else throw error
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
// TODO: Remove this check when integrating new llama.cpp extension
|
||||
@ -231,8 +401,12 @@ export const useChat = () => {
|
||||
// Create a final content object for adding to the thread
|
||||
const finalContent = newAssistantThreadContent(
|
||||
activeThread.id,
|
||||
accumulatedText
|
||||
accumulatedText,
|
||||
{
|
||||
tokenSpeed: useAppState.getState().tokenSpeed,
|
||||
}
|
||||
)
|
||||
|
||||
builder.addAssistantMessage(accumulatedText, undefined, toolCalls)
|
||||
const updatedMessage = await postMessageProcessing(
|
||||
toolCalls,
|
||||
@ -252,9 +426,12 @@ export const useChat = () => {
|
||||
if (!followUpWithToolUse) availableTools = []
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
`Error sending message: ${error && typeof error === 'object' && 'message' in error ? error.message : error}`
|
||||
)
|
||||
const errorMessage =
|
||||
error && typeof error === 'object' && 'message' in error
|
||||
? error.message
|
||||
: error
|
||||
|
||||
toast.error(`Error sending message: ${errorMessage}`)
|
||||
console.error('Error sending message:', error)
|
||||
} finally {
|
||||
updateLoadingModel(false)
|
||||
@ -274,7 +451,8 @@ export const useChat = () => {
|
||||
updateThreadTimestamp,
|
||||
setPrompt,
|
||||
selectedModel,
|
||||
currentAssistant,
|
||||
currentAssistant?.instructions,
|
||||
currentAssistant.parameters,
|
||||
tools,
|
||||
updateLoadingModel,
|
||||
getDisabledToolsForThread,
|
||||
@ -282,6 +460,8 @@ export const useChat = () => {
|
||||
allowAllMCPPermissions,
|
||||
showApprovalModal,
|
||||
updateTokenSpeed,
|
||||
increaseModelContextSize,
|
||||
toggleOnContextShifting,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@ -134,7 +134,8 @@ export const sendCompletion = async (
|
||||
thread.model.id &&
|
||||
!(thread.model.id in Object.values(models).flat()) &&
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
!tokenJS.extendedModelExist(providerName as any, thread.model?.id)
|
||||
!tokenJS.extendedModelExist(providerName as any, thread.model?.id) &&
|
||||
provider.provider !== 'llama.cpp'
|
||||
) {
|
||||
try {
|
||||
tokenJS.extendModelList(
|
||||
@ -323,7 +324,7 @@ export const postMessageProcessing = async (
|
||||
? await showModal(toolCall.function.name, message.thread_id)
|
||||
: true)
|
||||
|
||||
const result = approved
|
||||
let result = approved
|
||||
? await callTool({
|
||||
toolName: toolCall.function.name,
|
||||
arguments: toolCall.function.arguments.length
|
||||
@ -335,7 +336,7 @@ export const postMessageProcessing = async (
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error calling tool ${toolCall.function.name}: ${e.message}`,
|
||||
text: `Error calling tool ${toolCall.function.name}: ${e.message ?? e}`,
|
||||
},
|
||||
],
|
||||
error: true,
|
||||
@ -350,7 +351,16 @@ export const postMessageProcessing = async (
|
||||
],
|
||||
}
|
||||
|
||||
if ('error' in result && result.error) break
|
||||
if (typeof result === 'string') {
|
||||
result = {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: result,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
message.metadata = {
|
||||
...(message.metadata ?? {}),
|
||||
|
||||
5
web-app/src/lib/version.ts
Normal file
5
web-app/src/lib/version.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { isDev } from './utils'
|
||||
|
||||
export const isNightly = VERSION.includes('-')
|
||||
export const isBeta = VERSION.includes('beta')
|
||||
export const isProd = !isNightly && !isBeta && !isDev
|
||||
@ -23,6 +23,7 @@
|
||||
"reset": "Reset",
|
||||
"search": "Search",
|
||||
"name": "Name",
|
||||
"cancel": "Cancel",
|
||||
|
||||
"placeholder": {
|
||||
"chatInput": "Ask me anything..."
|
||||
|
||||
@ -49,6 +49,7 @@ type ModelProps = {
|
||||
type SearchParams = {
|
||||
repo: string
|
||||
}
|
||||
const defaultModelQuantizations = ['iq4_xs.gguf', 'q4_k_m.gguf']
|
||||
|
||||
export const Route = createFileRoute(route.hub as any)({
|
||||
component: Hub,
|
||||
@ -77,6 +78,8 @@ function Hub() {
|
||||
const addModelSourceTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
|
||||
null
|
||||
)
|
||||
const downloadButtonRef = useRef<HTMLButtonElement>(null)
|
||||
const hasTriggeredDownload = useRef(false)
|
||||
|
||||
const { getProviderByName } = useModelProvider()
|
||||
const llamaProvider = getProviderByName('llama.cpp')
|
||||
@ -196,7 +199,8 @@ function Hub() {
|
||||
const navigate = useNavigate()
|
||||
|
||||
const isRecommendedModel = useCallback((modelId: string) => {
|
||||
return (extractModelName(modelId) === 'Jan-nano') as boolean
|
||||
return (extractModelName(modelId)?.toLowerCase() ===
|
||||
'jan-nano-gguf') as boolean
|
||||
}, [])
|
||||
|
||||
const handleUseModel = useCallback(
|
||||
@ -217,7 +221,10 @@ function Hub() {
|
||||
|
||||
const DownloadButtonPlaceholder = useMemo(() => {
|
||||
return ({ model }: ModelProps) => {
|
||||
const modelId = model.models[0]?.id
|
||||
const modelId =
|
||||
model.models.find((e) =>
|
||||
defaultModelQuantizations.some((m) => e.id.toLowerCase().includes(m))
|
||||
)?.id ?? model.models[0]?.id
|
||||
const isDownloading = downloadProcesses.some((e) => e.id === modelId)
|
||||
const downloadProgress =
|
||||
downloadProcesses.find((e) => e.id === modelId)?.progress || 0
|
||||
@ -233,17 +240,14 @@ function Hub() {
|
||||
isRecommended && 'hub-download-button-step'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-2 w-20 ',
|
||||
!isDownloading && 'opacity-0 visibility-hidden w-0'
|
||||
)}
|
||||
>
|
||||
<Progress value={downloadProgress * 100} />
|
||||
<span className="text-xs text-center text-main-view-fg/70">
|
||||
{Math.round(downloadProgress * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
{isDownloading && !isDownloaded && (
|
||||
<div className={cn('flex items-center gap-2 w-20')}>
|
||||
<Progress value={downloadProgress * 100} />
|
||||
<span className="text-xs text-center text-main-view-fg/70">
|
||||
{Math.round(downloadProgress * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isDownloaded ? (
|
||||
<Button size="sm" onClick={() => handleUseModel(modelId)}>
|
||||
Use
|
||||
@ -253,6 +257,7 @@ function Hub() {
|
||||
size="sm"
|
||||
onClick={() => downloadModel(modelId)}
|
||||
className={cn(isDownloading && 'hidden')}
|
||||
ref={isRecommended ? downloadButtonRef : undefined}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
@ -265,6 +270,7 @@ function Hub() {
|
||||
llamaProvider?.models,
|
||||
handleUseModel,
|
||||
isRecommendedModel,
|
||||
downloadButtonRef,
|
||||
])
|
||||
|
||||
const { step } = useSearch({ from: Route.id })
|
||||
@ -285,13 +291,20 @@ function Hub() {
|
||||
const handleJoyrideCallback = (data: CallBackProps) => {
|
||||
const { status, index } = data
|
||||
|
||||
if (status === STATUS.FINISHED && !isDownloading && isLastStep) {
|
||||
if (
|
||||
status === STATUS.FINISHED &&
|
||||
!isDownloading &&
|
||||
isLastStep &&
|
||||
!hasTriggeredDownload.current
|
||||
) {
|
||||
const recommendedModel = filteredModels.find((model) =>
|
||||
isRecommendedModel(model.metadata?.id)
|
||||
)
|
||||
if (recommendedModel && recommendedModel.models[0]?.id) {
|
||||
downloadModel(recommendedModel.models[0].id)
|
||||
|
||||
if (downloadButtonRef.current) {
|
||||
hasTriggeredDownload.current = true
|
||||
downloadButtonRef.current.click()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -412,7 +425,7 @@ function Hub() {
|
||||
</div>
|
||||
</div>
|
||||
</HeaderPage>
|
||||
<div className="p-4 w-full h-[calc(100%-32px)] overflow-y-auto first-step-setup-local-provider">
|
||||
<div className="p-4 w-full h-[calc(100%-32px)] !overflow-y-auto first-step-setup-local-provider">
|
||||
<div className="flex flex-col h-full justify-between gap-4 gap-y-3 w-4/5 mx-auto">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center">
|
||||
@ -452,7 +465,15 @@ function Hub() {
|
||||
</Link>
|
||||
<div className="shrink-0 space-x-3 flex items-center">
|
||||
<span className="text-main-view-fg/70 font-medium text-xs">
|
||||
{toGigabytes(model.models?.[0]?.size)}
|
||||
{toGigabytes(
|
||||
(
|
||||
model.models.find((m) =>
|
||||
defaultModelQuantizations.some((e) =>
|
||||
m.id.toLowerCase().includes(e)
|
||||
)
|
||||
) ?? model.models?.[0]
|
||||
)?.size
|
||||
)}
|
||||
</span>
|
||||
<DownloadButtonPlaceholder model={model} />
|
||||
</div>
|
||||
|
||||
@ -18,6 +18,7 @@ import CodeBlockStyleSwitcher from '@/containers/CodeBlockStyleSwitcher'
|
||||
import { LineNumbersSwitcher } from '@/containers/LineNumbersSwitcher'
|
||||
import { CodeBlockExample } from '@/containers/CodeBlockExample'
|
||||
import { toast } from 'sonner'
|
||||
import { ChatWidthSwitcher } from '@/containers/ChatWidthSwitcher'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const Route = createFileRoute(route.settings.appearance as any)({
|
||||
@ -98,6 +99,15 @@ function Appareances() {
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* Chat Message */}
|
||||
<Card>
|
||||
<CardItem
|
||||
title="Chat Width"
|
||||
description="Choose the width of the chat area to customize your conversation view."
|
||||
/>
|
||||
<ChatWidthSwitcher />
|
||||
</Card>
|
||||
|
||||
{/* Codeblock */}
|
||||
<Card>
|
||||
<CardItem
|
||||
|
||||
@ -97,7 +97,7 @@ function SortableGPUItem({ gpu, index }: { gpu: GPU; index: number }) {
|
||||
title="Driver Version"
|
||||
actions={
|
||||
<span className="text-main-view-fg/80">
|
||||
{gpu.additional_information.driver_version}
|
||||
{gpu.additional_information?.driver_version}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
@ -105,7 +105,7 @@ function SortableGPUItem({ gpu, index }: { gpu: GPU; index: number }) {
|
||||
title="Compute Capability"
|
||||
actions={
|
||||
<span className="text-main-view-fg/80">
|
||||
{gpu.additional_information.compute_cap}
|
||||
{gpu.additional_information?.compute_cap}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
|
||||
@ -22,6 +22,13 @@ import { useToolApproval } from '@/hooks/useToolApproval'
|
||||
import { toast } from 'sonner'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
// Function to mask sensitive values
|
||||
const maskSensitiveValue = (value: string) => {
|
||||
if (!value) return value
|
||||
if (value.length <= 8) return '*'.repeat(value.length)
|
||||
return value.slice(0, 4) + '*'.repeat(value.length - 8) + value.slice(-4)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const Route = createFileRoute(route.settings.mcp_servers as any)({
|
||||
component: MCPServers,
|
||||
@ -322,7 +329,10 @@ function MCPServers() {
|
||||
<div className="break-all">
|
||||
Env:{' '}
|
||||
{Object.entries(config.env)
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.map(
|
||||
([key, value]) =>
|
||||
`${key}=${maskSensitiveValue(value)}`
|
||||
)
|
||||
.join(', ')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
getActiveModels,
|
||||
importModel,
|
||||
startModel,
|
||||
stopAllModels,
|
||||
stopModel,
|
||||
} from '@/services/models'
|
||||
import {
|
||||
@ -299,6 +300,8 @@ function ProviderDetail() {
|
||||
...provider,
|
||||
...updateObj,
|
||||
})
|
||||
|
||||
stopAllModels()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -47,6 +47,28 @@ function Shortcuts() {
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<CardItem
|
||||
title="Zoom In"
|
||||
description="Increase the zoom level"
|
||||
actions={
|
||||
<div className="flex items-center justify-center px-3 py-1 bg-main-view-fg/5 rounded-md">
|
||||
<span className="font-medium">
|
||||
<PlatformMetaKey /> +
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<CardItem
|
||||
title="Zoom Out"
|
||||
description="Decrease the zoom level"
|
||||
actions={
|
||||
<div className="flex items-center justify-center px-3 py-1 bg-main-view-fg/5 rounded-md">
|
||||
<span className="font-medium">
|
||||
<PlatformMetaKey /> -
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* Chat */}
|
||||
|
||||
@ -18,6 +18,7 @@ import { useAppState } from '@/hooks/useAppState'
|
||||
import DropdownAssistant from '@/containers/DropdownAssistant'
|
||||
import { useAssistant } from '@/hooks/useAssistant'
|
||||
import { useAppearance } from '@/hooks/useAppearance'
|
||||
import { useOutOfContextPromiseModal } from '@/containers/dialogs/OutOfContextDialog'
|
||||
|
||||
// as route.threadsDetail
|
||||
export const Route = createFileRoute('/threads/$threadId')({
|
||||
@ -34,7 +35,7 @@ function ThreadDetail() {
|
||||
const { setCurrentAssistant, assistants } = useAssistant()
|
||||
const { setMessages } = useMessages()
|
||||
const { streamingContent } = useAppState()
|
||||
const { appMainViewBgColor } = useAppearance()
|
||||
const { appMainViewBgColor, chatWidth } = useAppearance()
|
||||
|
||||
const { messages } = useMessages(
|
||||
useShallow((state) => ({
|
||||
@ -47,6 +48,8 @@ function ThreadDetail() {
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
||||
const isFirstRender = useRef(true)
|
||||
const messagesCount = useMemo(() => messages?.length ?? 0, [messages])
|
||||
const { showModal, PromiseModal: OutOfContextModal } =
|
||||
useOutOfContextPromiseModal()
|
||||
|
||||
// Function to check scroll position and scrollbar presence
|
||||
const checkScrollState = () => {
|
||||
@ -193,6 +196,8 @@ function ThreadDetail() {
|
||||
|
||||
if (!messages || !threadModel) return null
|
||||
|
||||
const contextOverflowModalComponent = <OutOfContextModal />
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<HeaderPage>
|
||||
@ -208,7 +213,12 @@ function ThreadDetail() {
|
||||
'flex flex-col h-full w-full overflow-auto px-4 pt-4 pb-3'
|
||||
)}
|
||||
>
|
||||
<div className="w-4/6 mx-auto flex max-w-full flex-col grow">
|
||||
<div
|
||||
className={cn(
|
||||
'w-4/6 mx-auto flex max-w-full flex-col grow',
|
||||
chatWidth === 'compact' ? 'w-4/6' : 'w-full'
|
||||
)}
|
||||
>
|
||||
{messages &&
|
||||
messages.map((item, index) => {
|
||||
// Only pass isLastMessage to the last message in the array
|
||||
@ -233,6 +243,8 @@ function ThreadDetail() {
|
||||
))
|
||||
}
|
||||
index={index}
|
||||
showContextOverflowModal={showModal}
|
||||
contextOverflowModal={contextOverflowModalComponent}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
@ -240,7 +252,12 @@ function ThreadDetail() {
|
||||
<StreamingContent threadId={threadId} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-4/6 mx-auto pt-2 pb-3 shrink-0 relative">
|
||||
<div
|
||||
className={cn(
|
||||
' mx-auto pt-2 pb-3 shrink-0 relative',
|
||||
chatWidth === 'compact' ? 'w-4/6' : 'w-full px-3'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'absolute z-0 -top-6 h-8 py-1 flex w-full justify-center pointer-events-none opacity-0 visibility-hidden',
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { ExtensionManager } from '@/lib/extension'
|
||||
import { normalizeProvider } from '@/lib/models'
|
||||
import { hardcodedModel } from '@/utils/models'
|
||||
import { EngineManager, ExtensionTypeEnum, ModelExtension } from '@janhq/core'
|
||||
import { Model as CoreModel } from '@janhq/core'
|
||||
|
||||
@ -24,7 +23,7 @@ export const fetchModelSources = async (): Promise<any[]> => {
|
||||
ExtensionTypeEnum.Model
|
||||
)
|
||||
|
||||
if (!extension) return [hardcodedModel]
|
||||
if (!extension) return []
|
||||
|
||||
try {
|
||||
const sources = await extension.getSources()
|
||||
@ -34,10 +33,10 @@ export const fetchModelSources = async (): Promise<any[]> => {
|
||||
}))
|
||||
|
||||
// Prepend the hardcoded model to the sources
|
||||
return [hardcodedModel, ...mappedSources]
|
||||
return [...mappedSources]
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch model sources:', error)
|
||||
return [hardcodedModel]
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,7 +50,7 @@ export const fetchModelHub = async (): Promise<any[]> => {
|
||||
?.fetchModelsHub()
|
||||
|
||||
// Prepend the hardcoded model to the hub data
|
||||
return hubData ? [hardcodedModel, ...hubData] : [hardcodedModel]
|
||||
return hubData ? [...hubData] : []
|
||||
}
|
||||
|
||||
/**
|
||||
@ -297,7 +296,8 @@ export const startModel = async (
|
||||
normalizeProvider(provider.provider)
|
||||
)
|
||||
const modelObj = provider.models.find((m) => m.id === model)
|
||||
if (providerObj && modelObj)
|
||||
|
||||
if (providerObj && modelObj) {
|
||||
return providerObj?.loadModel(
|
||||
{
|
||||
id: modelObj.id,
|
||||
@ -310,6 +310,7 @@ export const startModel = async (
|
||||
},
|
||||
abortController
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
2
web-app/src/utils/error.ts
Normal file
2
web-app/src/utils/error.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export const OUT_OF_CONTEXT_SIZE =
|
||||
'the request exceeds the available context size.'
|
||||
@ -1,87 +0,0 @@
|
||||
export const hardcodedModel = {
|
||||
author: 'Menlo',
|
||||
id: 'https://huggingface.co/Menlo/Jan-nano',
|
||||
metadata: {
|
||||
'_id': '68492cd9cada68b1d11ca1bd',
|
||||
'author': 'Menlo',
|
||||
'cardData': {
|
||||
license: 'apache-2.0',
|
||||
pipeline_tag: 'text-generation',
|
||||
},
|
||||
'createdAt': '2025-06-11T07:14:33.000Z',
|
||||
'description':
|
||||
'---\nlicense: apache-2.0\npipeline_tag: text-generation\n---\n# Jan Nano\n\n\n\n\n\n## Overview\n\nJan Nano is a fine-tuned language model built on top of the Qwen3 architecture. Developed as part of the Jan ecosystem, it balances compact size and extended context length, making it ideal for efficient, high-quality text generation in local or embedded environments.\n\n## Features\n\n- **Tool Use**: Excellent function calling and tool integration\n- **Research**: Enhanced research and information processing capabilities\n- **Small Model**: VRAM efficient for local deployment\n\n## Use it with Jan (UI)\n\n1. Install **Jan** using [Quickstart](https://jan.ai/docs/quickstart)',
|
||||
'disabled': false,
|
||||
'downloads': 0,
|
||||
'gated': false,
|
||||
'gguf': {
|
||||
architecture: 'qwen3',
|
||||
bos_token: '<|endoftext|>',
|
||||
chat_template:
|
||||
"{%- if tools %}\n {{- '<|im_start|>system\\n' }}\n {%- if messages[0].role == 'system' %}\n {{- messages[0].content + '\\n\\n' }}\n {%- endif %}\n {{- \"# Tools\\n\\nYou may call one or more functions to assist with the user query.\\n\\nYou are provided with function signatures within <tools></tools> XML tags:\\n<tools>\" }}\n {%- for tool in tools %}\n {{- \"\\n\" }}\n {{- tool | tojson }}\n {%- endfor %}\n {{- \"\\n</tools>\\n\\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\\n<tool_call>\\n{\\\"name\\\": <function-name>, \\\"arguments\\\": <args-json-object>}\\n</tool_call><|im_end|>\\n\" }}\n{%- else %}\n {%- if messages[0].role == 'system' %}\n {{- '<|im_start|>system\\n' + messages[0].content + '<|im_end|>\\n' }}\n {%- endif %}\n{%- endif %}\n{%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}\n{%- for message in messages[::-1] %}\n {%- set index = (messages|length - 1) - loop.index0 %}\n {%- if ns.multi_step_tool and message.role == \"user\" and message.content is string and not(message.content.startswith('<tool_response>') and message.content.endswith('</tool_response>')) %}\n {%- set ns.multi_step_tool = false %}\n {%- set ns.last_query_index = index %}\n {%- endif %}\n{%- endfor %}\n{%- for message in messages %}\n {%- if message.content is string %}\n {%- set content = message.content %}\n {%- else %}\n {%- set content = '' %}\n {%- endif %}\n {%- if (message.role == \"user\") or (message.role == \"system\" and not loop.first) %}\n {{- '<|im_start|>' + message.role + '\\n' + content + '<|im_end|>' + '\\n' }}\n {%- elif message.role == \"assistant\" %}\n {%- set reasoning_content = '' %}\n {%- if message.reasoning_content is string %}\n {%- set reasoning_content = message.reasoning_content %}\n {%- else %}\n {%- if '</think>' in content %}\n {%- set reasoning_content = content.split('</think>')[0].rstrip('\\n').split('<think>')[-1].lstrip('\\n') %}\n {%- set content = content.split('</think>')[-1].lstrip('\\n') %}\n {%- endif %}\n {%- endif %}\n {%- if loop.index0 > ns.last_query_index %}\n {%- if loop.last or (not loop.last and reasoning_content) %}\n {{- '<|im_start|>' + message.role + '\\n<think>\\n' + reasoning_content.strip('\\n') + '\\n</think>\\n\\n' + content.lstrip('\\n') }}\n {%- else %}\n {{- '<|im_start|>' + message.role + '\\n' + content }}\n {%- endif %}\n {%- else %}\n {{- '<|im_start|>' + message.role + '\\n' + content }}\n {%- endif %}\n {%- if message.tool_calls %}\n {%- for tool_call in message.tool_calls %}\n {%- if (loop.first and content) or (not loop.first) %}\n {{- '\\n' }}\n {%- endif %}\n {%- if tool_call.function %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {{- '<tool_call>\\n{\"name\": \"' }}\n {{- tool_call.name }}\n {{- '\", \"arguments\": ' }}\n {%- if tool_call.arguments is string %}\n {{- tool_call.arguments }}\n {%- else %}\n {{- tool_call.arguments | tojson }}\n {%- endif %}\n {{- '}\\n</tool_call>' }}\n {%- endfor %}\n {%- endif %}\n {{- '<|im_end|>\\n' }}\n {%- elif message.role == \"tool\" %}\n {%- if loop.first or (messages[loop.index0 - 1].role != \"tool\") %}\n {{- '<|im_start|>user' }}\n {%- endif %}\n {{- '\\n<tool_response>\\n' }}\n {{- content }}\n {{- '\\n</tool_response>' }}\n {%- if loop.last or (messages[loop.index0 + 1].role != \"tool\") %}\n {{- '<|im_end|>\\n' }}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n<think>\\n\\n</think>\\n\\n' }}\n{%- endif %}",
|
||||
context_length: 40960,
|
||||
eos_token: '<|im_end|>',
|
||||
total: 4022468096,
|
||||
},
|
||||
'id': 'Menlo/Jan-nano',
|
||||
'lastModified': '2025-06-11T10:42:16.000Z',
|
||||
'likes': 2,
|
||||
'model-index': null,
|
||||
'modelId': 'Menlo/Jan-nano',
|
||||
'pipeline_tag': 'text-generation',
|
||||
'private': false,
|
||||
'sha': 'f05b9e798d3cb66394a25d2a45cdc77fd1d5a3ba',
|
||||
'siblings': [
|
||||
{
|
||||
rfilename: '.gitattributes',
|
||||
size: 1681,
|
||||
},
|
||||
{
|
||||
rfilename: 'Jan-nano_q4_k_m.gguf',
|
||||
size: 2497280288,
|
||||
},
|
||||
{
|
||||
rfilename: 'Jan-nano_q8_0.gguf',
|
||||
size: 4280400640,
|
||||
},
|
||||
{
|
||||
rfilename: 'README.md',
|
||||
size: 776,
|
||||
},
|
||||
],
|
||||
'spaces': [],
|
||||
'tags': [
|
||||
'gguf',
|
||||
'text-generation',
|
||||
'license:apache-2.0',
|
||||
'endpoints_compatible',
|
||||
'region:us',
|
||||
'conversational',
|
||||
],
|
||||
'usedStorage': 11772241536,
|
||||
'widgetData': [
|
||||
{
|
||||
text: 'Hi, what can you help me with?',
|
||||
},
|
||||
{
|
||||
text: 'What is 84 * 3 / 2?',
|
||||
},
|
||||
{
|
||||
text: 'Tell me an interesting fact about the universe!',
|
||||
},
|
||||
{
|
||||
text: 'Explain quantum computing in simple terms.',
|
||||
},
|
||||
],
|
||||
},
|
||||
models: [
|
||||
{
|
||||
id: 'Menlo:Jan-nano:Jan-nano_q4_k_m.gguf',
|
||||
size: 2497280288,
|
||||
},
|
||||
{
|
||||
id: 'Menlo:Jan-nano:Jan-nano_q8_0.gguf',
|
||||
size: 4280400640,
|
||||
},
|
||||
],
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user