diff --git a/core/src/browser/models/utils.ts b/core/src/browser/models/utils.ts index 2ac243b6a..192b838da 100644 --- a/core/src/browser/models/utils.ts +++ b/core/src/browser/models/utils.ts @@ -15,6 +15,8 @@ export const validationRules: { [key: string]: (value: any) => boolean } = { stop: (value: any) => Array.isArray(value) && value.every((v) => typeof v === 'string'), frequency_penalty: (value: any) => typeof value === 'number' && value >= 0 && value <= 1, presence_penalty: (value: any) => typeof value === 'number' && value >= 0 && value <= 1, + repeat_last_n: (value: any) => typeof value === 'number', + repeat_penalty: (value: any) => typeof value === 'number', ctx_len: (value: any) => Number.isInteger(value) && value >= 0, ngl: (value: any) => Number.isInteger(value), @@ -126,6 +128,14 @@ export const extractModelLoadParams = ( vision_model: undefined, text_model: undefined, engine: undefined, + top_p: undefined, + top_k: undefined, + min_p: undefined, + temperature: undefined, + repeat_penalty: undefined, + repeat_last_n: undefined, + presence_penalty: undefined, + frequency_penalty: undefined, } const settingParams: ModelSettingParams = {} diff --git a/core/src/types/model/modelEntity.ts b/core/src/types/model/modelEntity.ts index 83d8a864c..beb529c40 100644 --- a/core/src/types/model/modelEntity.ts +++ b/core/src/types/model/modelEntity.ts @@ -121,6 +121,14 @@ export type ModelSettingParams = { vision_model?: boolean text_model?: boolean engine?: boolean + top_p?: number + top_k?: number + min_p?: number + temperature?: number + repeat_penalty?: number + repeat_last_n?: number + presence_penalty?: number + frequency_penalty?: number } /** diff --git a/docs/public/assets/images/changelog/jan_nano_128.gif b/docs/public/assets/images/changelog/jan_nano_128.gif new file mode 100644 index 000000000..a439667da Binary files /dev/null and b/docs/public/assets/images/changelog/jan_nano_128.gif differ diff --git a/docs/public/assets/images/changelog/jn128.gif b/docs/public/assets/images/changelog/jn128.gif new file mode 100644 index 000000000..90b77b48f Binary files /dev/null and b/docs/public/assets/images/changelog/jn128.gif differ diff --git a/docs/src/pages/docs/_assets/jan-nano-demo.mp4 b/docs/public/assets/videos/jan-nano-demo.mp4 similarity index 100% rename from docs/src/pages/docs/_assets/jan-nano-demo.mp4 rename to docs/public/assets/videos/jan-nano-demo.mp4 diff --git a/docs/src/pages/changelog/2025-06-26-jan-nano-mcp.mdx b/docs/src/pages/changelog/2025-06-26-jan-nano-mcp.mdx new file mode 100644 index 000000000..19c3d5f8e --- /dev/null +++ b/docs/src/pages/changelog/2025-06-26-jan-nano-mcp.mdx @@ -0,0 +1,21 @@ +--- +title: "Jan v0.6.3 brings new features and models!" +version: 0.6.3 +description: "Unlocking MCP for everyone and bringing our latest model to Jan!" +date: 2025-06-26 +ogImage: "/assets/images/changelog/jn128.gif" +--- + +import ChangelogHeader from "@/components/Changelog/ChangelogHeader" + + + +## Highlights 🎉 + +- We have added Model Context Protocol (MCP) support to the stable build of Jan. It needs to be enabled in the General Settings tab. +- Jan now supports Menlo's latest model, Jan-Nano-128k. +- Some hot fixes and improvements. + +Update your Jan or [download the latest](https://jan.ai/). + +For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.6.3). \ No newline at end of file diff --git a/docs/src/pages/docs/_assets/serper-mcp.png b/docs/src/pages/docs/_assets/serper-mcp.png index 8a8808d2e..20c9fad99 100644 Binary files a/docs/src/pages/docs/_assets/serper-mcp.png and b/docs/src/pages/docs/_assets/serper-mcp.png differ diff --git a/docs/src/pages/docs/jan-models/jan-nano-128.mdx b/docs/src/pages/docs/jan-models/jan-nano-128.mdx index c1a1e3d4a..de619fae2 100644 --- a/docs/src/pages/docs/jan-models/jan-nano-128.mdx +++ b/docs/src/pages/docs/jan-models/jan-nano-128.mdx @@ -87,7 +87,7 @@ Jan-Nano-128k has been rigorously evaluated on the SimpleQA benchmark using our ### Demo diff --git a/extensions/inference-cortex-extension/src/index.ts b/extensions/inference-cortex-extension/src/index.ts index dd78e2d62..73a95d360 100644 --- a/extensions/inference-cortex-extension/src/index.ts +++ b/extensions/inference-cortex-extension/src/index.ts @@ -280,7 +280,7 @@ export default class JanInferenceCortexExtension extends LocalOAIEngine { ...(model.id.toLowerCase().includes('jan-nano') ? { reasoning_budget: 0 } : { reasoning_budget: this.reasoning_budget }), - ...(this.context_shift === false + ...(this.context_shift !== true // explicit true required to enable context shift ? { 'no-context-shift': true } : {}), ...(modelSettings.ngl === -1 || modelSettings.ngl === undefined diff --git a/extensions/model-extension/src/index.ts b/extensions/model-extension/src/index.ts index f0f0589df..669051114 100644 --- a/extensions/model-extension/src/index.ts +++ b/extensions/model-extension/src/index.ts @@ -28,7 +28,7 @@ type Data = { /** * Defaul mode sources */ -const defaultModelSources = ['Menlo/Jan-nano-gguf'] +const defaultModelSources = ['Menlo/Jan-nano-gguf', 'Menlo/Jan-nano-128k-gguf'] /** * A extension for models diff --git a/scripts/find-missing-i18n-key.js b/scripts/find-missing-i18n-key.js index bff64f97b..84db0127c 100644 --- a/scripts/find-missing-i18n-key.js +++ b/scripts/find-missing-i18n-key.js @@ -10,24 +10,24 @@ * --help Show this help message */ -const fs = require("fs") -const path = require("path") +const fs = require('fs') +const path = require('path') // Parse command-line arguments const args = process.argv.slice(2).reduce((acc, arg) => { - if (arg === "--help") { - acc.help = true - } else if (arg.startsWith("--locale=")) { - acc.locale = arg.split("=")[1] - } else if (arg.startsWith("--file=")) { - acc.file = arg.split("=")[1] - } - return acc + if (arg === '--help') { + acc.help = true + } else if (arg.startsWith('--locale=')) { + acc.locale = arg.split('=')[1] + } else if (arg.startsWith('--file=')) { + acc.file = arg.split('=')[1] + } + return acc }, {}) // Display help information if (args.help) { - console.log(` + console.log(` Find missing i18n translations in Jan A useful script to identify whether the i18n keys used in component files exist in all language files. @@ -43,264 +43,303 @@ Options: Output: - Generate a report of missing translations `) - process.exit(0) + process.exit(0) } // Directories to traverse and their corresponding locales const DIRS = { - components: { - path: path.join(__dirname, "../web-app/src/components"), - localesDir: path.join(__dirname, "../web-app/src/locales"), - }, - containers: { - path: path.join(__dirname, "../web-app/src/containers"), - localesDir: path.join(__dirname, "../web-app/src/locales"), - }, - routes: { - path: path.join(__dirname, "../web-app/src/routes"), - localesDir: path.join(__dirname, "../web-app/src/locales"), - }, + components: { + path: path.join(__dirname, '../web-app/src/components'), + localesDir: path.join(__dirname, '../web-app/src/locales'), + }, + containers: { + path: path.join(__dirname, '../web-app/src/containers'), + localesDir: path.join(__dirname, '../web-app/src/locales'), + }, + routes: { + path: path.join(__dirname, '../web-app/src/routes'), + localesDir: path.join(__dirname, '../web-app/src/locales'), + }, } // Regular expressions to match i18n keys const i18nPatterns = [ - /{t\("([^"]+)"\)}/g, // Match {t("key")} format - /i18nKey="([^"]+)"/g, // Match i18nKey="key" format - /\bt\(\s*["']([^"']+)["']\s*(?:,\s*[^)]+)?\)/g, // Match t("key") format with optional parameters - simplified and more robust + /{t\("([^"]+)"\)}/g, // Match {t("key")} format + /i18nKey="([^"]+)"/g, // Match i18nKey="key" format + /\bt\(\s*["']([^"']+)["']\s*(?:,\s*[^)]+)?\)/g, // Match t("key") format with optional parameters - simplified and more robust ] // Get all language directories for a specific locales directory function getLocaleDirs(localesDir) { - try { - const allLocales = fs.readdirSync(localesDir).filter((file) => { - const stats = fs.statSync(path.join(localesDir, file)) - return stats.isDirectory() // Do not exclude any language directories - }) + try { + const allLocales = fs.readdirSync(localesDir).filter((file) => { + const stats = fs.statSync(path.join(localesDir, file)) + return stats.isDirectory() // Do not exclude any language directories + }) - // Filter to a specific language if specified - return args.locale ? allLocales.filter((locale) => locale === args.locale) : allLocales - } catch (error) { - if (error.code === "ENOENT") { - console.warn(`Warning: Locales directory not found: ${localesDir}`) - return [] - } - throw error - } + // Filter to a specific language if specified + return args.locale + ? allLocales.filter((locale) => locale === args.locale) + : allLocales + } catch (error) { + if (error.code === 'ENOENT') { + console.warn(`Warning: Locales directory not found: ${localesDir}`) + return [] + } + throw error + } } // Get the value from JSON by path function getValueByPath(obj, path) { - const parts = path.split(".") - let current = obj + const parts = path.split('.') + let current = obj - for (const part of parts) { - if (current === undefined || current === null) { - return undefined - } - current = current[part] - } + for (const part of parts) { + if (current === undefined || current === null) { + return undefined + } + current = current[part] + } - return current + return current } // Check if the key exists in all language files, return a list of missing language files function checkKeyInLocales(key, localeDirs, localesDir) { - // Handle namespace:key format (e.g., "common:save" or "settings:general") - let namespace, keyPath + // Handle namespace:key format (e.g., "common:save" or "settings:general") + let namespace, keyPath - if (key.includes(":")) { - [namespace, keyPath] = key.split(":", 2) - } else if (key.includes(".")) { - // Handle namespace.key format - const parts = key.split(".") + if (key.includes(':')) { + ;[namespace, keyPath] = key.split(':', 2) + } else if (key.includes('.')) { + // Handle namespace.key format + const parts = key.split('.') - // Check if the first part is a known namespace - const knownNamespaces = ['common', 'settings', 'systemMonitor', 'chat', 'hub', 'providers', 'assistants', 'mcpServers', 'mcp-servers', 'toolApproval', 'tool-approval', 'updater', 'setup', 'logs', 'provider'] + // Check if the first part is a known namespace + const knownNamespaces = [ + 'common', + 'settings', + 'systemMonitor', + 'chat', + 'hub', + 'providers', + 'assistants', + 'mcpServers', + 'mcp-servers', + 'toolApproval', + 'tool-approval', + 'updater', + 'setup', + 'logs', + 'provider', + 'model-errors', + ] - if (knownNamespaces.includes(parts[0])) { - namespace = parts[0] - keyPath = parts.slice(1).join(".") - } else { - // Default to common namespace if no known namespace is found - namespace = "common" - keyPath = key - } - } else { - // No dots, default to common namespace - namespace = "common" - keyPath = key - } + if (knownNamespaces.includes(parts[0])) { + namespace = parts[0] + keyPath = parts.slice(1).join('.') + } else { + // Default to common namespace if no known namespace is found + namespace = 'common' + keyPath = key + } + } else { + // No dots, default to common namespace + namespace = 'common' + keyPath = key + } - const missingLocales = [] + const missingLocales = [] - // Map namespace to actual filename - const namespaceToFile = { - 'systemMonitor': 'system-monitor', - 'mcpServers': 'mcp-servers', - 'mcp-servers': 'mcp-servers', - 'toolApproval': 'tool-approval', - 'tool-approval': 'tool-approval' - } + // Map namespace to actual filename + const namespaceToFile = { + 'systemMonitor': 'system-monitor', + 'mcpServers': 'mcp-servers', + 'mcp-servers': 'mcp-servers', + 'toolApproval': 'tool-approval', + 'tool-approval': 'tool-approval', + 'model-errors': 'model-errors', + } - const fileName = namespaceToFile[namespace] || namespace + const fileName = namespaceToFile[namespace] || namespace - localeDirs.forEach((locale) => { - const filePath = path.join(localesDir, locale, `${fileName}.json`) - if (!fs.existsSync(filePath)) { - missingLocales.push(`${locale}/${fileName}.json`) - return - } + localeDirs.forEach((locale) => { + const filePath = path.join(localesDir, locale, `${fileName}.json`) + if (!fs.existsSync(filePath)) { + missingLocales.push(`${locale}/${fileName}.json`) + return + } - try { - const json = JSON.parse(fs.readFileSync(filePath, "utf8")) + try { + const json = JSON.parse(fs.readFileSync(filePath, 'utf8')) - // Jan's localization files have flat structure - // e.g., common.json has { "save": "Save", "cancel": "Cancel" } - // not nested like { "common": { "save": "Save" } } - const valueToCheck = getValueByPath(json, keyPath) + // Jan's localization files have flat structure + // e.g., common.json has { "save": "Save", "cancel": "Cancel" } + // not nested like { "common": { "save": "Save" } } + const valueToCheck = getValueByPath(json, keyPath) - if (valueToCheck === undefined) { - missingLocales.push(`${locale}/${fileName}.json`) - } - } catch (error) { - console.warn(`Warning: Could not parse ${filePath}: ${error.message}`) - missingLocales.push(`${locale}/${fileName}.json`) - } - }) + if (valueToCheck === undefined) { + missingLocales.push(`${locale}/${fileName}.json`) + } + } catch (error) { + console.warn(`Warning: Could not parse ${filePath}: ${error.message}`) + missingLocales.push(`${locale}/${fileName}.json`) + } + }) - return missingLocales + return missingLocales } // Recursively traverse the directory function findMissingI18nKeys() { - const results = [] + const results = [] - function walk(dir, baseDir, localeDirs, localesDir) { - if (!fs.existsSync(dir)) { - console.warn(`Warning: Directory not found: ${dir}`) - return - } + function walk(dir, baseDir, localeDirs, localesDir) { + if (!fs.existsSync(dir)) { + console.warn(`Warning: Directory not found: ${dir}`) + return + } - const files = fs.readdirSync(dir) + const files = fs.readdirSync(dir) - for (const file of files) { - const filePath = path.join(dir, file) - const stat = fs.statSync(filePath) + for (const file of files) { + const filePath = path.join(dir, file) + const stat = fs.statSync(filePath) - // Exclude test files, __mocks__ directory, and node_modules - if (filePath.includes(".test.") || - filePath.includes("__mocks__") || - filePath.includes("node_modules") || - filePath.includes(".spec.")) { - continue - } + // Exclude test files, __mocks__ directory, and node_modules + if ( + filePath.includes('.test.') || + filePath.includes('__mocks__') || + filePath.includes('node_modules') || + filePath.includes('.spec.') + ) { + continue + } - if (stat.isDirectory()) { - walk(filePath, baseDir, localeDirs, localesDir) // Recursively traverse subdirectories - } else if (stat.isFile() && [".ts", ".tsx", ".js", ".jsx"].includes(path.extname(filePath))) { - const content = fs.readFileSync(filePath, "utf8") + if (stat.isDirectory()) { + walk(filePath, baseDir, localeDirs, localesDir) // Recursively traverse subdirectories + } else if ( + stat.isFile() && + ['.ts', '.tsx', '.js', '.jsx'].includes(path.extname(filePath)) + ) { + const content = fs.readFileSync(filePath, 'utf8') - // Match all i18n keys - for (const pattern of i18nPatterns) { - let match - while ((match = pattern.exec(content)) !== null) { - const key = match[1] + // Match all i18n keys + for (const pattern of i18nPatterns) { + let match + while ((match = pattern.exec(content)) !== null) { + const key = match[1] - // Skip empty keys or keys that look like variables/invalid - if (!key || - key.includes("${") || - key.includes("{{") || - key.startsWith("$") || - key.length < 2 || - key === "." || - key === "," || - key === "-" || - !/^[a-zA-Z]/.test(key)) { - continue - } + // Skip empty keys or keys that look like variables/invalid + if ( + !key || + key.includes('${') || + key.includes('{{') || + key.startsWith('$') || + key.length < 2 || + key === '.' || + key === ',' || + key === '-' || + !/^[a-zA-Z]/.test(key) + ) { + continue + } - const missingLocales = checkKeyInLocales(key, localeDirs, localesDir) - if (missingLocales.length > 0) { - results.push({ - key, - missingLocales, - file: path.relative(baseDir, filePath), - }) - } - } - } - } - } - } + const missingLocales = checkKeyInLocales( + key, + localeDirs, + localesDir + ) + if (missingLocales.length > 0) { + results.push({ + key, + missingLocales, + file: path.relative(baseDir, filePath), + }) + } + } + } + } + } + } - // Walk through all directories - Object.entries(DIRS).forEach(([name, config]) => { - const localeDirs = getLocaleDirs(config.localesDir) - if (localeDirs.length > 0) { - console.log(`\nChecking ${name} directory with ${localeDirs.length} languages: ${localeDirs.join(", ")}`) - walk(config.path, config.path, localeDirs, config.localesDir) - } - }) + // Walk through all directories + Object.entries(DIRS).forEach(([name, config]) => { + const localeDirs = getLocaleDirs(config.localesDir) + if (localeDirs.length > 0) { + console.log( + `\nChecking ${name} directory with ${ + localeDirs.length + } languages: ${localeDirs.join(', ')}` + ) + walk(config.path, config.path, localeDirs, config.localesDir) + } + }) - return results + return results } // Execute and output the results function main() { - try { - if (args.locale) { - // Check if the specified locale exists in the locales directory - const localesDir = path.join(__dirname, "../web-app/src/locales") - const localeDirs = getLocaleDirs(localesDir) + try { + if (args.locale) { + // Check if the specified locale exists in the locales directory + const localesDir = path.join(__dirname, '../web-app/src/locales') + const localeDirs = getLocaleDirs(localesDir) - if (!localeDirs.includes(args.locale)) { - console.error(`Error: Language '${args.locale}' not found in ${localesDir}`) - process.exit(1) - } - } + if (!localeDirs.includes(args.locale)) { + console.error( + `Error: Language '${args.locale}' not found in ${localesDir}` + ) + process.exit(1) + } + } - const missingKeys = findMissingI18nKeys() + const missingKeys = findMissingI18nKeys() - if (missingKeys.length === 0) { - console.log("\n✅ All i18n keys are present!") - return - } + if (missingKeys.length === 0) { + console.log('\n✅ All i18n keys are present!') + return + } - console.log("\nMissing i18n keys:\n") + console.log('\nMissing i18n keys:\n') - // Group by file for better readability - const groupedByFile = {} - missingKeys.forEach(({ key, missingLocales, file }) => { - if (!groupedByFile[file]) { - groupedByFile[file] = [] - } - groupedByFile[file].push({ key, missingLocales }) - }) + // Group by file for better readability + const groupedByFile = {} + missingKeys.forEach(({ key, missingLocales, file }) => { + if (!groupedByFile[file]) { + groupedByFile[file] = [] + } + groupedByFile[file].push({ key, missingLocales }) + }) - Object.entries(groupedByFile).forEach(([file, keys]) => { - console.log(`📁 File: ${file}`) - keys.forEach(({ key, missingLocales }) => { - console.log(` 🔑 Key: ${key}`) - console.log(" ❌ Missing in:") - missingLocales.forEach((locale) => console.log(` - ${locale}`)) - console.log("") - }) - console.log("-------------------") - }) + Object.entries(groupedByFile).forEach(([file, keys]) => { + console.log(`📁 File: ${file}`) + keys.forEach(({ key, missingLocales }) => { + console.log(` 🔑 Key: ${key}`) + console.log(' ❌ Missing in:') + missingLocales.forEach((locale) => console.log(` - ${locale}`)) + console.log('') + }) + console.log('-------------------') + }) - console.log("\n💡 To fix missing translations:") - console.log("1. Add the missing keys to the appropriate locale files") - console.log("2. Use yq commands for efficient updates:") - console.log(" yq -i '.namespace.key = \"Translation\"' web-app/src/locales//.json") - console.log("3. Run this script again to verify all keys are present") + console.log('\n💡 To fix missing translations:') + console.log('1. Add the missing keys to the appropriate locale files') + console.log('2. Use yq commands for efficient updates:') + console.log( + ' yq -i \'.namespace.key = "Translation"\' web-app/src/locales//.json' + ) + console.log('3. Run this script again to verify all keys are present') - // Exit code 1 indicates missing keys - process.exit(1) - } catch (error) { - console.error("Error:", error.message) - console.error(error.stack) - process.exit(1) - } + // Exit code 1 indicates missing keys + process.exit(1) + } catch (error) { + console.error('Error:', error.message) + console.error(error.stack) + process.exit(1) + } } -main() \ No newline at end of file +main() diff --git a/scripts/find-missing-translations.js b/scripts/find-missing-translations.js index 5e307f250..8fa5c4cc7 100644 --- a/scripts/find-missing-translations.js +++ b/scripts/find-missing-translations.js @@ -10,27 +10,24 @@ * --help Show this help message */ -const fs = require("fs") -const path = require("path") +const fs = require('fs') +const path = require('path') // Process command line arguments -const args = process.argv.slice(2).reduce( - (acc, arg) => { - if (arg === "--help") { - acc.help = true - } else if (arg.startsWith("--locale=")) { - acc.locale = arg.split("=")[1] - } else if (arg.startsWith("--file=")) { - acc.file = arg.split("=")[1] - } - return acc - }, - {} -) +const args = process.argv.slice(2).reduce((acc, arg) => { + if (arg === '--help') { + acc.help = true + } else if (arg.startsWith('--locale=')) { + acc.locale = arg.split('=')[1] + } else if (arg.startsWith('--file=')) { + acc.file = arg.split('=')[1] + } + return acc +}, {}) // Show help if requested if (args.help) { - console.log(` + console.log(` Find Missing Translations for Jan A utility script to identify missing translations across locale files. @@ -47,207 +44,225 @@ Options: Output: - Generates a report of missing translations for the web-app `) - process.exit(0) + process.exit(0) } // Path to the locales directory -const LOCALES_DIR = path.join(__dirname, "../web-app/src/locales") +const LOCALES_DIR = path.join(__dirname, '../web-app/src/locales') // Recursively find all keys in an object -function findKeys(obj, parentKey = "") { - let keys = [] +function findKeys(obj, parentKey = '') { + let keys = [] - for (const [key, value] of Object.entries(obj)) { - const currentKey = parentKey ? `${parentKey}.${key}` : key + for (const [key, value] of Object.entries(obj)) { + const currentKey = parentKey ? `${parentKey}.${key}` : key - if (typeof value === "object" && value !== null) { - // If value is an object, recurse - keys = [...keys, ...findKeys(value, currentKey)] - } else { - // If value is a primitive, add the key - keys.push(currentKey) - } - } + if (typeof value === 'object' && value !== null) { + // If value is an object, recurse + keys = [...keys, ...findKeys(value, currentKey)] + } else { + // If value is a primitive, add the key + keys.push(currentKey) + } + } - return keys + return keys } // Get value at a dotted path in an object function getValueAtPath(obj, path) { - const parts = path.split(".") - let current = obj + const parts = path.split('.') + let current = obj - for (const part of parts) { - if (current === undefined || current === null) { - return undefined - } - current = current[part] - } + for (const part of parts) { + if (current === undefined || current === null) { + return undefined + } + current = current[part] + } - return current + return current } // Function to check translations function checkTranslations() { - // Get all locale directories (or filter to the specified locale) - const allLocales = fs.readdirSync(LOCALES_DIR).filter((item) => { - const stats = fs.statSync(path.join(LOCALES_DIR, item)) - return stats.isDirectory() && item !== "en" // Exclude English as it's our source - }) + // Get all locale directories (or filter to the specified locale) + const allLocales = fs.readdirSync(LOCALES_DIR).filter((item) => { + const stats = fs.statSync(path.join(LOCALES_DIR, item)) + return stats.isDirectory() && item !== 'en' // Exclude English as it's our source + }) - // Filter to the specified locale if provided - const locales = args.locale ? allLocales.filter((locale) => locale === args.locale) : allLocales + // Filter to the specified locale if provided + const locales = args.locale + ? allLocales.filter((locale) => locale === args.locale) + : allLocales - if (args.locale && locales.length === 0) { - console.error(`Error: Locale '${args.locale}' not found in ${LOCALES_DIR}`) - process.exit(1) - } + if (args.locale && locales.length === 0) { + console.error(`Error: Locale '${args.locale}' not found in ${LOCALES_DIR}`) + process.exit(1) + } - console.log(`Checking ${locales.length} non-English locale(s): ${locales.join(", ")}`) + console.log( + `Checking ${locales.length} non-English locale(s): ${locales.join(', ')}` + ) - // Get all English JSON files - const englishDir = path.join(LOCALES_DIR, "en") - let englishFiles = fs.readdirSync(englishDir).filter((file) => file.endsWith(".json") && !file.startsWith(".")) + // Get all English JSON files + const englishDir = path.join(LOCALES_DIR, 'en') + let englishFiles = fs + .readdirSync(englishDir) + .filter((file) => file.endsWith('.json') && !file.startsWith('.')) - // Filter to the specified file if provided - if (args.file) { - if (!englishFiles.includes(args.file)) { - console.error(`Error: File '${args.file}' not found in ${englishDir}`) - process.exit(1) - } - englishFiles = englishFiles.filter((file) => file === args.file) - } + // Filter to the specified file if provided + if (args.file) { + if (!englishFiles.includes(args.file)) { + console.error(`Error: File '${args.file}' not found in ${englishDir}`) + process.exit(1) + } + englishFiles = englishFiles.filter((file) => file === args.file) + } - // Load file contents - let englishFileContents + // Load file contents + let englishFileContents - try { - englishFileContents = englishFiles.map((file) => ({ - name: file, - content: JSON.parse(fs.readFileSync(path.join(englishDir, file), "utf8")), - })) - } catch (e) { - console.error(`Error: File '${englishDir}' is not a valid JSON file`) - process.exit(1) - } + try { + englishFileContents = englishFiles.map((file) => ({ + name: file, + content: JSON.parse(fs.readFileSync(path.join(englishDir, file), 'utf8')), + })) + } catch (e) { + console.error(`Error: File '${englishDir}' is not a valid JSON file`) + process.exit(1) + } - console.log( - `Checking ${englishFileContents.length} translation file(s): ${englishFileContents.map((f) => f.name).join(", ")}` - ) + console.log( + `Checking ${ + englishFileContents.length + } translation file(s): ${englishFileContents.map((f) => f.name).join(', ')}` + ) - // Results object to store missing translations - const missingTranslations = {} + // Results object to store missing translations + const missingTranslations = {} - // For each locale, check for missing translations - for (const locale of locales) { - missingTranslations[locale] = {} + // For each locale, check for missing translations + for (const locale of locales) { + missingTranslations[locale] = {} - for (const { name, content: englishContent } of englishFileContents) { - const localeFilePath = path.join(LOCALES_DIR, locale, name) + for (const { name, content: englishContent } of englishFileContents) { + const localeFilePath = path.join(LOCALES_DIR, locale, name) - // Check if the file exists in the locale - if (!fs.existsSync(localeFilePath)) { - missingTranslations[locale][name] = { file: "File is missing entirely" } - continue - } + // Check if the file exists in the locale + if (!fs.existsSync(localeFilePath)) { + missingTranslations[locale][name] = { file: 'File is missing entirely' } + continue + } - // Load the locale file - let localeContent + // Load the locale file + let localeContent - try { - localeContent = JSON.parse(fs.readFileSync(localeFilePath, "utf8")) - } catch (e) { - console.error(`Error: File '${localeFilePath}' is not a valid JSON file`) - process.exit(1) - } + try { + localeContent = JSON.parse(fs.readFileSync(localeFilePath, 'utf8')) + } catch (e) { + console.error( + `Error: File '${localeFilePath}' is not a valid JSON file` + ) + process.exit(1) + } - // Find all keys in the English file - const englishKeys = findKeys(englishContent) + // Find all keys in the English file + const englishKeys = findKeys(englishContent) - // Check for missing keys in the locale file - const missingKeys = [] + // Check for missing keys in the locale file + const missingKeys = [] - for (const key of englishKeys) { - const englishValue = getValueAtPath(englishContent, key) - const localeValue = getValueAtPath(localeContent, key) + for (const key of englishKeys) { + const englishValue = getValueAtPath(englishContent, key) + const localeValue = getValueAtPath(localeContent, key) - if (localeValue === undefined) { - missingKeys.push({ - key, - englishValue, - }) - } - } + if (localeValue === undefined) { + missingKeys.push({ + key, + englishValue, + }) + } + } - if (missingKeys.length > 0) { - missingTranslations[locale][name] = missingKeys - } - } - } + if (missingKeys.length > 0) { + missingTranslations[locale][name] = missingKeys + } + } + } - return outputResults(missingTranslations) + return outputResults(missingTranslations) } // Function to output results function outputResults(missingTranslations) { - let hasMissingTranslations = false + let hasMissingTranslations = false - console.log(`\nMissing Translations Report:\n`) + console.log(`\nMissing Translations Report:\n`) - for (const [locale, files] of Object.entries(missingTranslations)) { - if (Object.keys(files).length === 0) { - console.log(`✅ ${locale}: No missing translations`) - continue - } + for (const [locale, files] of Object.entries(missingTranslations)) { + if (Object.keys(files).length === 0) { + console.log(`✅ ${locale}: No missing translations`) + continue + } - hasMissingTranslations = true - console.log(`📝 ${locale}:`) + hasMissingTranslations = true + console.log(`📝 ${locale}:`) - for (const [fileName, missingItems] of Object.entries(files)) { - if (missingItems.file) { - console.log(` - ${fileName}: ${missingItems.file}`) - continue - } + for (const [fileName, missingItems] of Object.entries(files)) { + if (missingItems.file) { + console.log(` - ${fileName}: ${missingItems.file}`) + continue + } - console.log(` - ${fileName}: ${missingItems.length} missing translations`) + console.log( + ` - ${fileName}: ${missingItems.length} missing translations` + ) - for (const { key, englishValue } of missingItems) { - console.log(` ${key}: "${englishValue}"`) - } - } + for (const { key, englishValue } of missingItems) { + console.log(` ${key}: "${englishValue}"`) + } + } - console.log("") - } + console.log('') + } - return hasMissingTranslations + return hasMissingTranslations } // Main function to find missing translations function findMissingTranslations() { - try { - console.log("Starting translation check for Jan web-app...") + try { + console.log('Starting translation check for Jan web-app...') - const hasMissingTranslations = checkTranslations() + const hasMissingTranslations = checkTranslations() - // Summary - if (!hasMissingTranslations) { - console.log("\n✅ All translations are complete!") - } else { - console.log("\n✏️ To add missing translations:") - console.log("1. Add the missing keys to the corresponding locale files") - console.log("2. Translate the English values to the appropriate language") - console.log("3. You can use yq commands to update JSON files efficiently:") - console.log(" yq -i '.namespace.key = \"Translation\"' web-app/src/locales//.json") - console.log("4. Run this script again to verify all translations are complete") - // Exit with error code to fail CI checks - process.exit(1) - } - } catch (error) { - console.error("Error:", error.message) - console.error(error.stack) - process.exit(1) - } + // Summary + if (!hasMissingTranslations) { + console.log('\n✅ All translations are complete!') + } else { + console.log('\n✏️ To add missing translations:') + console.log('1. Add the missing keys to the corresponding locale files') + console.log('2. Translate the English values to the appropriate language') + console.log( + '3. You can use yq commands to update JSON files efficiently:' + ) + console.log( + ' yq -i \'.namespace.key = "Translation"\' web-app/src/locales//.json' + ) + console.log( + '4. Run this script again to verify all translations are complete' + ) + // Exit with error code to fail CI checks + process.exit(1) + } + } catch (error) { + console.error('Error:', error.message) + console.error(error.stack) + process.exit(1) + } } // Run the main function -findMissingTranslations() \ No newline at end of file +findMissingTranslations() diff --git a/web-app/package.json b/web-app/package.json index 18e3a7d7d..0272d1151 100644 --- a/web-app/package.json +++ b/web-app/package.json @@ -32,6 +32,7 @@ "@tauri-apps/api": "^2.5.0", "@tauri-apps/plugin-deep-link": "~2", "@tauri-apps/plugin-dialog": "^2.2.1", + "@tauri-apps/plugin-http": "^2.2.1", "@tauri-apps/plugin-opener": "^2.2.7", "@tauri-apps/plugin-os": "^2.2.1", "@tauri-apps/plugin-updater": "^2.7.1", diff --git a/web-app/src/containers/ChatInput.tsx b/web-app/src/containers/ChatInput.tsx index 53de2d726..2d16b2eee 100644 --- a/web-app/src/containers/ChatInput.tsx +++ b/web-app/src/containers/ChatInput.tsx @@ -35,7 +35,6 @@ 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 @@ -55,8 +54,6 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => { const { t } = useTranslation() const { spellCheckChatInput } = useGeneralSetting() - const { showModal, PromiseModal: OutOfContextModal } = - useOutOfContextPromiseModal() const maxRows = 10 const { selectedModel } = useModelProvider() @@ -107,7 +104,7 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => { return } setMessage('') - sendMessage(prompt, showModal) + sendMessage(prompt) } useEffect(() => { @@ -599,7 +596,6 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => { )} - ) } diff --git a/web-app/src/containers/DropdownModelProvider.tsx b/web-app/src/containers/DropdownModelProvider.tsx index 14a53c127..924b6014a 100644 --- a/web-app/src/containers/DropdownModelProvider.tsx +++ b/web-app/src/containers/DropdownModelProvider.tsx @@ -16,7 +16,6 @@ import { ModelSetting } from '@/containers/ModelSetting' import ProvidersAvatar from '@/containers/ProvidersAvatar' import { Fzf } from 'fzf' import { localStorageKey } from '@/constants/localStorage' -import { isProd } from '@/lib/version' import { useTranslation } from '@/i18n/react-i18next-compat' type DropdownModelProviderProps = { @@ -398,7 +397,7 @@ const DropdownModelProvider = ({
- {!isProd && capabilities.length > 0 && ( + {capabilities.length > 0 && (
diff --git a/web-app/src/containers/SettingsMenu.tsx b/web-app/src/containers/SettingsMenu.tsx index 4ab21f80b..5d5e3ef5b 100644 --- a/web-app/src/containers/SettingsMenu.tsx +++ b/web-app/src/containers/SettingsMenu.tsx @@ -2,55 +2,12 @@ import { Link, useMatches } from '@tanstack/react-router' import { route } from '@/constants/routes' import { useTranslation } from '@/i18n/react-i18next-compat' import { useModelProvider } from '@/hooks/useModelProvider' -import { isProd } from '@/lib/version' - -const menuSettings = [ - { - title: 'common:general', - route: route.settings.general, - }, - { - title: 'common:appearance', - route: route.settings.appearance, - }, - { - title: 'common:privacy', - route: route.settings.privacy, - }, - { - title: 'common:keyboardShortcuts', - route: route.settings.shortcuts, - }, - { - title: 'common:hardware', - route: route.settings.hardware, - }, - // Only show MCP Servers in non-production environment - ...(!isProd - ? [ - { - title: 'common:mcp-servers', - route: route.settings.mcp_servers, - }, - ] - : []), - { - title: 'common:local_api_server', - route: route.settings.local_api_server, - }, - { - title: 'common:https_proxy', - route: route.settings.https_proxy, - }, - { - title: 'common:extensions', - route: route.settings.extensions, - }, -] +import { useGeneralSetting } from '@/hooks/useGeneralSetting' const SettingsMenu = () => { const { t } = useTranslation() const { providers } = useModelProvider() + const { experimentalFeatures } = useGeneralSetting() const firstItemProvider = providers.length > 0 ? providers[0].provider : 'llama.cpp' const matches = useMatches() @@ -60,6 +17,50 @@ const SettingsMenu = () => { 'providerName' in match.params ) + const menuSettings = [ + { + title: 'common:general', + route: route.settings.general, + }, + { + title: 'common:appearance', + route: route.settings.appearance, + }, + { + title: 'common:privacy', + route: route.settings.privacy, + }, + { + title: 'common:keyboardShortcuts', + route: route.settings.shortcuts, + }, + { + title: 'common:hardware', + route: route.settings.hardware, + }, + // Only show MCP Servers when experimental features are enabled + ...(experimentalFeatures + ? [ + { + title: 'common:mcp-servers', + route: route.settings.mcp_servers, + }, + ] + : []), + { + title: 'common:local_api_server', + route: route.settings.local_api_server, + }, + { + title: 'common:https_proxy', + route: route.settings.https_proxy, + }, + { + title: 'common:extensions', + route: route.settings.extensions, + }, + ] + return (
diff --git a/web-app/src/containers/ThreadContent.tsx b/web-app/src/containers/ThreadContent.tsx index 27bcf10d1..80a864c67 100644 --- a/web-app/src/containers/ThreadContent.tsx +++ b/web-app/src/containers/ThreadContent.tsx @@ -85,7 +85,6 @@ export const ThreadContent = memo( // eslint-disable-next-line @typescript-eslint/no-explicit-any streamTools?: any contextOverflowModal?: React.ReactNode | null - showContextOverflowModal?: () => Promise } ) => { const [message, setMessage] = useState(item.content?.[0]?.text?.value || '') @@ -137,10 +136,7 @@ export const ThreadContent = memo( } if (toSendMessage) { deleteMessage(toSendMessage.thread_id, toSendMessage.id ?? '') - sendMessage( - toSendMessage.content?.[0]?.text?.value || '', - item.showContextOverflowModal - ) + sendMessage(toSendMessage.content?.[0]?.text?.value || '') } }, [deleteMessage, getMessages, item, sendMessage]) @@ -182,16 +178,9 @@ export const ThreadContent = memo( deleteMessage(threadMessages[i].thread_id, threadMessages[i].id) } - sendMessage(message, item.showContextOverflowModal) + sendMessage(message) }, - [ - deleteMessage, - getMessages, - item.thread_id, - message, - sendMessage, - item.showContextOverflowModal, - ] + [deleteMessage, getMessages, item.thread_id, message, sendMessage] ) const isToolCalls = diff --git a/web-app/src/containers/dialogs/CortexFailureDialog.tsx b/web-app/src/containers/dialogs/CortexFailureDialog.tsx index bdd2aff2e..b28281f54 100644 --- a/web-app/src/containers/dialogs/CortexFailureDialog.tsx +++ b/web-app/src/containers/dialogs/CortexFailureDialog.tsx @@ -65,7 +65,7 @@ export function CortexFailureDialog() { - - - - - ) - }, [isOpen, modalProps]) - return { showModal, PromiseModal } + return ( + + + + {t('model-errors:title')} + + + {t('model-errors:description')} +
+
+ {t('model-errors:increaseContextSizeDescription')} +
+ + + + +
+
+ ) } diff --git a/web-app/src/containers/dynamicControllerSetting/index.tsx b/web-app/src/containers/dynamicControllerSetting/index.tsx index d0c7ee30a..4c2115399 100644 --- a/web-app/src/containers/dynamicControllerSetting/index.tsx +++ b/web-app/src/containers/dynamicControllerSetting/index.tsx @@ -10,7 +10,13 @@ type DynamicControllerProps = { title?: string className?: string description?: string - controllerType: 'input' | 'checkbox' | 'dropdown' | 'textarea' | 'slider' | string + controllerType: + | 'input' + | 'checkbox' + | 'dropdown' + | 'textarea' + | 'slider' + | string controllerProps: { value?: string | boolean | number placeholder?: string @@ -36,7 +42,11 @@ export function DynamicControllerSetting({ onChange(newValue)} diff --git a/web-app/src/hooks/useChat.ts b/web-app/src/hooks/useChat.ts index 2c8f9fd2a..3e9dd6363 100644 --- a/web-app/src/hooks/useChat.ts +++ b/web-app/src/hooks/useChat.ts @@ -30,6 +30,7 @@ import { useToolApproval } from '@/hooks/useToolApproval' import { useToolAvailable } from '@/hooks/useToolAvailable' import { OUT_OF_CONTEXT_SIZE } from '@/utils/error' import { updateSettings } from '@/services/providers' +import { useContextSizeApproval } from './useModelContextApproval' export const useChat = () => { const { prompt, setPrompt } = usePrompt() @@ -47,6 +48,8 @@ export const useChat = () => { const { approvedTools, showApprovalModal, allowAllMCPPermissions } = useToolApproval() + const { showApprovalModal: showIncreaseContextSizeModal } = + useContextSizeApproval() const { getDisabledToolsForThread } = useToolAvailable() const { getProviderByName, selectedModel, selectedProvider } = @@ -223,11 +226,7 @@ export const useChat = () => { ) const sendMessage = useCallback( - async ( - message: string, - showModal?: () => Promise, - troubleshooting = true - ) => { + async (message: string, troubleshooting = true) => { const activeThread = await getCurrentThread() resetTokenSpeed() @@ -361,7 +360,7 @@ export const useChat = () => { selectedModel && troubleshooting ) { - const method = await showModal?.() + const method = await showIncreaseContextSizeModal() if (method === 'ctx_len') { /// Increase context size activeProvider = await increaseModelContextSize( @@ -447,8 +446,7 @@ export const useChat = () => { updateThreadTimestamp, setPrompt, selectedModel, - currentAssistant?.instructions, - currentAssistant.parameters, + currentAssistant, tools, updateLoadingModel, getDisabledToolsForThread, @@ -456,6 +454,7 @@ export const useChat = () => { allowAllMCPPermissions, showApprovalModal, updateTokenSpeed, + showIncreaseContextSizeModal, increaseModelContextSize, toggleOnContextShifting, ] diff --git a/web-app/src/hooks/useGeneralSetting.ts b/web-app/src/hooks/useGeneralSetting.ts index 5335f245c..6f4a36fa4 100644 --- a/web-app/src/hooks/useGeneralSetting.ts +++ b/web-app/src/hooks/useGeneralSetting.ts @@ -5,6 +5,8 @@ import { localStorageKey } from '@/constants/localStorage' type LeftPanelStoreState = { currentLanguage: Language spellCheckChatInput: boolean + experimentalFeatures: boolean + setExperimentalFeatures: (value: boolean) => void setSpellCheckChatInput: (value: boolean) => void setCurrentLanguage: (value: Language) => void } @@ -14,6 +16,8 @@ export const useGeneralSetting = create()( (set) => ({ currentLanguage: 'en', spellCheckChatInput: true, + experimentalFeatures: false, + setExperimentalFeatures: (value) => set({ experimentalFeatures: value }), setSpellCheckChatInput: (value) => set({ spellCheckChatInput: value }), setCurrentLanguage: (value) => set({ currentLanguage: value }), }), diff --git a/web-app/src/hooks/useModelContextApproval.ts b/web-app/src/hooks/useModelContextApproval.ts new file mode 100644 index 000000000..92abe6ba6 --- /dev/null +++ b/web-app/src/hooks/useModelContextApproval.ts @@ -0,0 +1,53 @@ +import { create } from 'zustand' + +export type ApprovalModalProps = { + onApprove: (method: 'ctx_len' | 'context_shift') => void + onDeny: () => void +} + +type ApprovalState = { + // Modal state + isModalOpen: boolean + modalProps: ApprovalModalProps | null + + showApprovalModal: () => Promise<'ctx_len' | 'context_shift' | undefined> + closeModal: () => void + setModalOpen: (open: boolean) => void +} + +export const useContextSizeApproval = create()((set, get) => ({ + isModalOpen: false, + modalProps: null, + + showApprovalModal: async () => { + return new Promise<'ctx_len' | 'context_shift' | undefined>((resolve) => { + set({ + isModalOpen: true, + modalProps: { + onApprove: (method) => { + get().closeModal() + resolve(method) + }, + onDeny: () => { + get().closeModal() + resolve(undefined) + }, + }, + }) + }) + }, + + closeModal: () => { + set({ + isModalOpen: false, + modalProps: null, + }) + }, + + setModalOpen: (open: boolean) => { + set({ isModalOpen: open }) + if (!open) { + get().closeModal() + } + }, +})) diff --git a/web-app/src/hooks/useModelProvider.ts b/web-app/src/hooks/useModelProvider.ts index c87645a1e..e2f26b1f7 100644 --- a/web-app/src/hooks/useModelProvider.ts +++ b/web-app/src/hooks/useModelProvider.ts @@ -6,6 +6,7 @@ type ModelProviderState = { providers: ModelProvider[] selectedProvider: string selectedModel: Model | null + deletedModels: string[] getModelBy: (modelId: string) => Model | undefined setProviders: (providers: ModelProvider[]) => void getProviderByName: (providerName: string) => ModelProvider | undefined @@ -25,6 +26,7 @@ export const useModelProvider = create()( providers: [], selectedProvider: 'llama.cpp', selectedModel: null, + deletedModels: [], getModelBy: (modelId: string) => { const provider = get().providers.find( (provider) => provider.provider === get().selectedProvider @@ -35,6 +37,11 @@ export const useModelProvider = create()( setProviders: (providers) => set((state) => { const existingProviders = state.providers + // Ensure deletedModels is always an array + const currentDeletedModels = Array.isArray(state.deletedModels) + ? state.deletedModels + : [] + const updatedProviders = providers.map((provider) => { const existingProvider = existingProviders.find( (x) => x.provider === provider.provider @@ -43,7 +50,9 @@ export const useModelProvider = create()( const mergedModels = [ ...models, ...(provider?.models ?? []).filter( - (e) => !models.some((m) => m.id === e.id) + (e) => + !models.some((m) => m.id === e.id) && + !currentDeletedModels.includes(e.id) ), ] return { @@ -118,17 +127,25 @@ export const useModelProvider = create()( return modelObject }, deleteModel: (modelId: string) => { - set((state) => ({ - providers: state.providers.map((provider) => { - const models = provider.models.filter( - (model) => model.id !== modelId - ) - return { - ...provider, - models, - } - }), - })) + set((state) => { + // Ensure deletedModels is always an array + const currentDeletedModels = Array.isArray(state.deletedModels) + ? state.deletedModels + : [] + + return { + providers: state.providers.map((provider) => { + const models = provider.models.filter( + (model) => model.id !== modelId + ) + return { + ...provider, + models, + } + }), + deletedModels: [...currentDeletedModels, modelId], + } + }) }, addProvider: (provider: ModelProvider) => { set((state) => ({ diff --git a/web-app/src/lib/completion.ts b/web-app/src/lib/completion.ts index 24daec3cd..5ffd4fa4b 100644 --- a/web-app/src/lib/completion.ts +++ b/web-app/src/lib/completion.ts @@ -7,6 +7,7 @@ import { ModelManager, } from '@janhq/core' import { invoke } from '@tauri-apps/api/core' +import { fetch as fetchTauri } from '@tauri-apps/plugin-http' import { ChatCompletionMessageParam, ChatCompletionTool, @@ -15,7 +16,13 @@ import { models, StreamCompletionResponse, TokenJS, + ConfigOptions, } from 'token.js' + +// Extended config options to include custom fetch function +type ExtendedConfigOptions = ConfigOptions & { + fetch?: typeof fetch +} import { ulid } from 'ulidx' import { normalizeProvider } from './models' import { MCPTool } from '@/types/completion' @@ -129,7 +136,9 @@ export const sendCompletion = async ( apiKey: provider.api_key ?? (await invoke('app_token')), // TODO: Retrieve from extension settings baseURL: provider.base_url, - }) + // Use Tauri's fetch to avoid CORS issues only for openai-compatible provider + ...(providerName === 'openai-compatible' && { fetch: fetchTauri }), + } as ExtendedConfigOptions) if ( thread.model.id && !(thread.model.id in Object.values(models).flat()) && diff --git a/web-app/src/locales/en/model-errors.json b/web-app/src/locales/en/model-errors.json new file mode 100644 index 000000000..307d4ee3d --- /dev/null +++ b/web-app/src/locales/en/model-errors.json @@ -0,0 +1,7 @@ +{ + "title": "Out of context error", + "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.", + "increaseContextSizeDescription": "Do you want to increase the context size?", + "truncateInput": "Truncate Input", + "increaseContextSize": "Increase Context Size" +} diff --git a/web-app/src/locales/id/model-errors.json b/web-app/src/locales/id/model-errors.json new file mode 100644 index 000000000..869e935ee --- /dev/null +++ b/web-app/src/locales/id/model-errors.json @@ -0,0 +1,7 @@ +{ + "title": "Kesalahan kehabisan konteks", + "description": "Obrolan ini hampir mencapai batas memori AI, seperti papan tulis yang mulai penuh. Kita bisa memperluas jendela memori (disebut ukuran konteks) agar AI dapat mengingat lebih banyak, tetapi ini mungkin akan menggunakan lebih banyak memori komputer Anda. Kita juga bisa memotong input, artinya sebagian riwayat obrolan akan dilupakan untuk memberi ruang pada pesan baru.", + "increaseContextSizeDescription": "Apakah Anda ingin memperbesar ukuran konteks?", + "truncateInput": "Potong Input", + "increaseContextSize": "Perbesar Ukuran Konteks" +} diff --git a/web-app/src/locales/vn/model-errors.json b/web-app/src/locales/vn/model-errors.json new file mode 100644 index 000000000..73f51d7da --- /dev/null +++ b/web-app/src/locales/vn/model-errors.json @@ -0,0 +1,7 @@ +{ + "title": "Lỗi vượt quá ngữ cảnh", + "description": "Cuộc trò chuyện này đang đạt đến giới hạn bộ nhớ của AI, giống như một bảng trắng sắp đầy. Chúng ta có thể mở rộng cửa sổ bộ nhớ (gọi là kích thước ngữ cảnh) để AI nhớ được nhiều hơn, nhưng điều này có thể sử dụng nhiều bộ nhớ máy tính của bạn hơn. Chúng ta cũng có thể cắt bớt đầu vào, nghĩa là AI sẽ quên một phần lịch sử trò chuyện để dành chỗ cho các tin nhắn mới.", + "increaseContextSizeDescription": "Bạn có muốn tăng kích thước ngữ cảnh không?", + "truncateInput": "Cắt bớt đầu vào", + "increaseContextSize": "Tăng kích thước ngữ cảnh" +} diff --git a/web-app/src/locales/zh-CN/model-errors.json b/web-app/src/locales/zh-CN/model-errors.json new file mode 100644 index 000000000..952082a16 --- /dev/null +++ b/web-app/src/locales/zh-CN/model-errors.json @@ -0,0 +1,7 @@ +{ + "title": "超出上下文错误", + "description": "此对话已接近 AI 的记忆上限,就像白板快被填满一样。我们可以扩展记忆窗口(称为上下文大小),这样它能记住更多内容,但可能会占用你电脑更多内存。我们也可以截断输入,这意味着它会遗忘部分聊天记录,为新消息腾出空间。", + "increaseContextSizeDescription": "你想要增加上下文大小吗?", + "truncateInput": "截断输入", + "increaseContextSize": "增加上下文大小" +} diff --git a/web-app/src/locales/zh-TW/model-errors.json b/web-app/src/locales/zh-TW/model-errors.json new file mode 100644 index 000000000..90cf16f77 --- /dev/null +++ b/web-app/src/locales/zh-TW/model-errors.json @@ -0,0 +1,7 @@ +{ + "title": "超出上下文錯誤", + "description": "此對話已接近 AI 的記憶上限,就像白板快被填滿一樣。我們可以擴大記憶視窗(稱為上下文大小),讓它能記住更多內容,但這可能會佔用你電腦更多記憶體。我們也可以截斷輸入,這表示它會忘記部分對話歷史,以便為新訊息騰出空間。", + "increaseContextSizeDescription": "你想要增加上下文大小嗎?", + "truncateInput": "截斷輸入", + "increaseContextSize": "增加上下文大小" +} diff --git a/web-app/src/routes/__root.tsx b/web-app/src/routes/__root.tsx index 1014c2aa1..dc94a86e1 100644 --- a/web-app/src/routes/__root.tsx +++ b/web-app/src/routes/__root.tsx @@ -19,6 +19,7 @@ import { useLeftPanel } from '@/hooks/useLeftPanel' import { cn } from '@/lib/utils' import ToolApproval from '@/containers/dialogs/ToolApproval' import { TranslationProvider } from '@/i18n/TranslationContext' +import OutOfContextPromiseModal from '@/containers/dialogs/OutOfContextDialog' export const Route = createRootRoute({ component: RootLayout, @@ -96,6 +97,7 @@ function RootLayout() { {/* */} + ) diff --git a/web-app/src/routes/settings/general.tsx b/web-app/src/routes/settings/general.tsx index 9bd82b842..c0a3fa5a1 100644 --- a/web-app/src/routes/settings/general.tsx +++ b/web-app/src/routes/settings/general.tsx @@ -53,6 +53,12 @@ export const Route = createFileRoute(route.settings.general as any)({ function General() { const { t } = useTranslation() + const { + spellCheckChatInput, + setSpellCheckChatInput, + experimentalFeatures, + setExperimentalFeatures, + } = useGeneralSetting() const openFileTitle = (): string => { if (IS_MACOS) { @@ -63,7 +69,6 @@ function General() { return t('settings:general.openContainingFolder') } } - const { spellCheckChatInput, setSpellCheckChatInput } = useGeneralSetting() const { checkForUpdate } = useAppUpdater() const [janDataFolder, setJanDataFolder] = useState() const [isCopied, setIsCopied] = useState(false) @@ -239,6 +244,20 @@ function General() { /> + {/* Advanced */} + + setExperimentalFeatures(e)} + /> + } + /> + + {/* Data folder */}

{model.id}

- {!isProd && ( - - )} +
} actions={
- {!isProd && ( - - )} + {model.settings && ( (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 = () => { @@ -198,8 +195,6 @@ function ThreadDetail() { if (!messages || !threadModel) return null - const contextOverflowModalComponent = - return (
@@ -245,8 +240,6 @@ function ThreadDetail() { )) } index={index} - showContextOverflowModal={showModal} - contextOverflowModal={contextOverflowModalComponent} />
) diff --git a/web-app/src/services/providers.ts b/web-app/src/services/providers.ts index 6bd2b63f0..c279620f2 100644 --- a/web-app/src/services/providers.ts +++ b/web-app/src/services/providers.ts @@ -13,6 +13,8 @@ import { import { modelSettings } from '@/lib/predefined' import { fetchModels } from './models' import { ExtensionManager } from '@/lib/extension' +import { fetch as fetchTauri } from '@tauri-apps/plugin-http' + export const getProviders = async (): Promise => { const engines = !localStorage.getItem('migration_completed') @@ -148,7 +150,7 @@ export const getProviders = async (): Promise => { ...setting, controller_props: { ...setting.controller_props, - value: value ?? setting.controller_props.value, + value: value, }, } return acc @@ -163,26 +165,35 @@ export const getProviders = async (): Promise => { return runtimeProviders.concat(builtinProviders as ModelProvider[]) } + /** * Fetches models from a provider's API endpoint + * Always uses Tauri's HTTP client to bypass CORS issues * @param provider The provider object containing base_url and api_key * @returns Promise Array of model IDs */ export const fetchModelsFromProvider = async ( provider: ModelProvider ): Promise => { - if (!provider.base_url || !provider.api_key) { - throw new Error('Provider must have base_url and api_key configured') + if (!provider.base_url) { + throw new Error('Provider must have base_url configured') } try { - const response = await fetch(`${provider.base_url}/models`, { + const headers: Record = { + 'Content-Type': 'application/json', + } + + // Only add authentication headers if API key is provided + if (provider.api_key) { + headers['x-api-key'] = provider.api_key + headers['Authorization'] = `Bearer ${provider.api_key}` + } + + // Always use Tauri's fetch to avoid CORS issues + const response = await fetchTauri(`${provider.base_url}/models`, { method: 'GET', - headers: { - 'x-api-key': provider.api_key, - 'Authorization': `Bearer ${provider.api_key}`, - 'Content-Type': 'application/json', - }, + headers, }) if (!response.ok) { @@ -213,6 +224,14 @@ export const fetchModelsFromProvider = async ( } } catch (error) { console.error('Error fetching models from provider:', error) + + // Provide helpful error message + if (error instanceof Error && error.message.includes('fetch')) { + throw new Error( + `Cannot connect to ${provider.provider} at ${provider.base_url}. Please check that the service is running and accessible.` + ) + } + throw error } } @@ -235,7 +254,10 @@ export const updateSettings = async ( ...setting, controllerProps: { ...setting.controller_props, - value: setting.controller_props.value ?? '', + value: + setting.controller_props.value !== undefined + ? setting.controller_props.value + : '', }, controllerType: setting.controller_type, })) as SettingComponentProps[]