Merge pull request #5550 from menloresearch/release/v0.6.3
sync: release/v0.6.3 into dev
This commit is contained in:
commit
094d4a8879
@ -15,6 +15,8 @@ export const validationRules: { [key: string]: (value: any) => boolean } = {
|
|||||||
stop: (value: any) => Array.isArray(value) && value.every((v) => typeof v === 'string'),
|
stop: (value: any) => Array.isArray(value) && value.every((v) => typeof v === 'string'),
|
||||||
frequency_penalty: (value: any) => typeof value === 'number' && value >= 0 && value <= 1,
|
frequency_penalty: (value: any) => typeof value === 'number' && value >= 0 && value <= 1,
|
||||||
presence_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,
|
ctx_len: (value: any) => Number.isInteger(value) && value >= 0,
|
||||||
ngl: (value: any) => Number.isInteger(value),
|
ngl: (value: any) => Number.isInteger(value),
|
||||||
@ -126,6 +128,14 @@ export const extractModelLoadParams = (
|
|||||||
vision_model: undefined,
|
vision_model: undefined,
|
||||||
text_model: undefined,
|
text_model: undefined,
|
||||||
engine: 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 = {}
|
const settingParams: ModelSettingParams = {}
|
||||||
|
|
||||||
|
|||||||
@ -121,6 +121,14 @@ export type ModelSettingParams = {
|
|||||||
vision_model?: boolean
|
vision_model?: boolean
|
||||||
text_model?: boolean
|
text_model?: boolean
|
||||||
engine?: 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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -280,7 +280,7 @@ export default class JanInferenceCortexExtension extends LocalOAIEngine {
|
|||||||
...(model.id.toLowerCase().includes('jan-nano')
|
...(model.id.toLowerCase().includes('jan-nano')
|
||||||
? { reasoning_budget: 0 }
|
? { reasoning_budget: 0 }
|
||||||
: { reasoning_budget: this.reasoning_budget }),
|
: { reasoning_budget: this.reasoning_budget }),
|
||||||
...(this.context_shift === false
|
...(this.context_shift !== true // explicit true required to enable context shift
|
||||||
? { 'no-context-shift': true }
|
? { 'no-context-shift': true }
|
||||||
: {}),
|
: {}),
|
||||||
...(modelSettings.ngl === -1 || modelSettings.ngl === undefined
|
...(modelSettings.ngl === -1 || modelSettings.ngl === undefined
|
||||||
|
|||||||
@ -28,7 +28,7 @@ type Data<T> = {
|
|||||||
/**
|
/**
|
||||||
* Defaul mode sources
|
* Defaul mode sources
|
||||||
*/
|
*/
|
||||||
const defaultModelSources = ['Menlo/Jan-nano-gguf']
|
const defaultModelSources = ['Menlo/Jan-nano-gguf', 'Menlo/Jan-nano-128k-gguf']
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A extension for models
|
* A extension for models
|
||||||
|
|||||||
@ -10,17 +10,17 @@
|
|||||||
* --help Show this help message
|
* --help Show this help message
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const fs = require("fs")
|
const fs = require('fs')
|
||||||
const path = require("path")
|
const path = require('path')
|
||||||
|
|
||||||
// Parse command-line arguments
|
// Parse command-line arguments
|
||||||
const args = process.argv.slice(2).reduce((acc, arg) => {
|
const args = process.argv.slice(2).reduce((acc, arg) => {
|
||||||
if (arg === "--help") {
|
if (arg === '--help') {
|
||||||
acc.help = true
|
acc.help = true
|
||||||
} else if (arg.startsWith("--locale=")) {
|
} else if (arg.startsWith('--locale=')) {
|
||||||
acc.locale = arg.split("=")[1]
|
acc.locale = arg.split('=')[1]
|
||||||
} else if (arg.startsWith("--file=")) {
|
} else if (arg.startsWith('--file=')) {
|
||||||
acc.file = arg.split("=")[1]
|
acc.file = arg.split('=')[1]
|
||||||
}
|
}
|
||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
@ -49,16 +49,16 @@ Output:
|
|||||||
// Directories to traverse and their corresponding locales
|
// Directories to traverse and their corresponding locales
|
||||||
const DIRS = {
|
const DIRS = {
|
||||||
components: {
|
components: {
|
||||||
path: path.join(__dirname, "../web-app/src/components"),
|
path: path.join(__dirname, '../web-app/src/components'),
|
||||||
localesDir: path.join(__dirname, "../web-app/src/locales"),
|
localesDir: path.join(__dirname, '../web-app/src/locales'),
|
||||||
},
|
},
|
||||||
containers: {
|
containers: {
|
||||||
path: path.join(__dirname, "../web-app/src/containers"),
|
path: path.join(__dirname, '../web-app/src/containers'),
|
||||||
localesDir: path.join(__dirname, "../web-app/src/locales"),
|
localesDir: path.join(__dirname, '../web-app/src/locales'),
|
||||||
},
|
},
|
||||||
routes: {
|
routes: {
|
||||||
path: path.join(__dirname, "../web-app/src/routes"),
|
path: path.join(__dirname, '../web-app/src/routes'),
|
||||||
localesDir: path.join(__dirname, "../web-app/src/locales"),
|
localesDir: path.join(__dirname, '../web-app/src/locales'),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,9 +78,11 @@ function getLocaleDirs(localesDir) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Filter to a specific language if specified
|
// Filter to a specific language if specified
|
||||||
return args.locale ? allLocales.filter((locale) => locale === args.locale) : allLocales
|
return args.locale
|
||||||
|
? allLocales.filter((locale) => locale === args.locale)
|
||||||
|
: allLocales
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code === "ENOENT") {
|
if (error.code === 'ENOENT') {
|
||||||
console.warn(`Warning: Locales directory not found: ${localesDir}`)
|
console.warn(`Warning: Locales directory not found: ${localesDir}`)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@ -90,7 +92,7 @@ function getLocaleDirs(localesDir) {
|
|||||||
|
|
||||||
// Get the value from JSON by path
|
// Get the value from JSON by path
|
||||||
function getValueByPath(obj, path) {
|
function getValueByPath(obj, path) {
|
||||||
const parts = path.split(".")
|
const parts = path.split('.')
|
||||||
let current = obj
|
let current = obj
|
||||||
|
|
||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
@ -108,26 +110,43 @@ function checkKeyInLocales(key, localeDirs, localesDir) {
|
|||||||
// Handle namespace:key format (e.g., "common:save" or "settings:general")
|
// Handle namespace:key format (e.g., "common:save" or "settings:general")
|
||||||
let namespace, keyPath
|
let namespace, keyPath
|
||||||
|
|
||||||
if (key.includes(":")) {
|
if (key.includes(':')) {
|
||||||
[namespace, keyPath] = key.split(":", 2)
|
;[namespace, keyPath] = key.split(':', 2)
|
||||||
} else if (key.includes(".")) {
|
} else if (key.includes('.')) {
|
||||||
// Handle namespace.key format
|
// Handle namespace.key format
|
||||||
const parts = key.split(".")
|
const parts = key.split('.')
|
||||||
|
|
||||||
// Check if the first part is a known namespace
|
// 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']
|
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])) {
|
if (knownNamespaces.includes(parts[0])) {
|
||||||
namespace = parts[0]
|
namespace = parts[0]
|
||||||
keyPath = parts.slice(1).join(".")
|
keyPath = parts.slice(1).join('.')
|
||||||
} else {
|
} else {
|
||||||
// Default to common namespace if no known namespace is found
|
// Default to common namespace if no known namespace is found
|
||||||
namespace = "common"
|
namespace = 'common'
|
||||||
keyPath = key
|
keyPath = key
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No dots, default to common namespace
|
// No dots, default to common namespace
|
||||||
namespace = "common"
|
namespace = 'common'
|
||||||
keyPath = key
|
keyPath = key
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,7 +158,8 @@ function checkKeyInLocales(key, localeDirs, localesDir) {
|
|||||||
'mcpServers': 'mcp-servers',
|
'mcpServers': 'mcp-servers',
|
||||||
'mcp-servers': 'mcp-servers',
|
'mcp-servers': 'mcp-servers',
|
||||||
'toolApproval': 'tool-approval',
|
'toolApproval': 'tool-approval',
|
||||||
'tool-approval': 'tool-approval'
|
'tool-approval': 'tool-approval',
|
||||||
|
'model-errors': 'model-errors',
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileName = namespaceToFile[namespace] || namespace
|
const fileName = namespaceToFile[namespace] || namespace
|
||||||
@ -152,7 +172,7 @@ function checkKeyInLocales(key, localeDirs, localesDir) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const json = JSON.parse(fs.readFileSync(filePath, "utf8"))
|
const json = JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
||||||
|
|
||||||
// Jan's localization files have flat structure
|
// Jan's localization files have flat structure
|
||||||
// e.g., common.json has { "save": "Save", "cancel": "Cancel" }
|
// e.g., common.json has { "save": "Save", "cancel": "Cancel" }
|
||||||
@ -188,17 +208,22 @@ function findMissingI18nKeys() {
|
|||||||
const stat = fs.statSync(filePath)
|
const stat = fs.statSync(filePath)
|
||||||
|
|
||||||
// Exclude test files, __mocks__ directory, and node_modules
|
// Exclude test files, __mocks__ directory, and node_modules
|
||||||
if (filePath.includes(".test.") ||
|
if (
|
||||||
filePath.includes("__mocks__") ||
|
filePath.includes('.test.') ||
|
||||||
filePath.includes("node_modules") ||
|
filePath.includes('__mocks__') ||
|
||||||
filePath.includes(".spec.")) {
|
filePath.includes('node_modules') ||
|
||||||
|
filePath.includes('.spec.')
|
||||||
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stat.isDirectory()) {
|
if (stat.isDirectory()) {
|
||||||
walk(filePath, baseDir, localeDirs, localesDir) // Recursively traverse subdirectories
|
walk(filePath, baseDir, localeDirs, localesDir) // Recursively traverse subdirectories
|
||||||
} else if (stat.isFile() && [".ts", ".tsx", ".js", ".jsx"].includes(path.extname(filePath))) {
|
} else if (
|
||||||
const content = fs.readFileSync(filePath, "utf8")
|
stat.isFile() &&
|
||||||
|
['.ts', '.tsx', '.js', '.jsx'].includes(path.extname(filePath))
|
||||||
|
) {
|
||||||
|
const content = fs.readFileSync(filePath, 'utf8')
|
||||||
|
|
||||||
// Match all i18n keys
|
// Match all i18n keys
|
||||||
for (const pattern of i18nPatterns) {
|
for (const pattern of i18nPatterns) {
|
||||||
@ -207,19 +232,25 @@ function findMissingI18nKeys() {
|
|||||||
const key = match[1]
|
const key = match[1]
|
||||||
|
|
||||||
// Skip empty keys or keys that look like variables/invalid
|
// Skip empty keys or keys that look like variables/invalid
|
||||||
if (!key ||
|
if (
|
||||||
key.includes("${") ||
|
!key ||
|
||||||
key.includes("{{") ||
|
key.includes('${') ||
|
||||||
key.startsWith("$") ||
|
key.includes('{{') ||
|
||||||
|
key.startsWith('$') ||
|
||||||
key.length < 2 ||
|
key.length < 2 ||
|
||||||
key === "." ||
|
key === '.' ||
|
||||||
key === "," ||
|
key === ',' ||
|
||||||
key === "-" ||
|
key === '-' ||
|
||||||
!/^[a-zA-Z]/.test(key)) {
|
!/^[a-zA-Z]/.test(key)
|
||||||
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const missingLocales = checkKeyInLocales(key, localeDirs, localesDir)
|
const missingLocales = checkKeyInLocales(
|
||||||
|
key,
|
||||||
|
localeDirs,
|
||||||
|
localesDir
|
||||||
|
)
|
||||||
if (missingLocales.length > 0) {
|
if (missingLocales.length > 0) {
|
||||||
results.push({
|
results.push({
|
||||||
key,
|
key,
|
||||||
@ -237,7 +268,11 @@ function findMissingI18nKeys() {
|
|||||||
Object.entries(DIRS).forEach(([name, config]) => {
|
Object.entries(DIRS).forEach(([name, config]) => {
|
||||||
const localeDirs = getLocaleDirs(config.localesDir)
|
const localeDirs = getLocaleDirs(config.localesDir)
|
||||||
if (localeDirs.length > 0) {
|
if (localeDirs.length > 0) {
|
||||||
console.log(`\nChecking ${name} directory with ${localeDirs.length} languages: ${localeDirs.join(", ")}`)
|
console.log(
|
||||||
|
`\nChecking ${name} directory with ${
|
||||||
|
localeDirs.length
|
||||||
|
} languages: ${localeDirs.join(', ')}`
|
||||||
|
)
|
||||||
walk(config.path, config.path, localeDirs, config.localesDir)
|
walk(config.path, config.path, localeDirs, config.localesDir)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -250,11 +285,13 @@ function main() {
|
|||||||
try {
|
try {
|
||||||
if (args.locale) {
|
if (args.locale) {
|
||||||
// Check if the specified locale exists in the locales directory
|
// Check if the specified locale exists in the locales directory
|
||||||
const localesDir = path.join(__dirname, "../web-app/src/locales")
|
const localesDir = path.join(__dirname, '../web-app/src/locales')
|
||||||
const localeDirs = getLocaleDirs(localesDir)
|
const localeDirs = getLocaleDirs(localesDir)
|
||||||
|
|
||||||
if (!localeDirs.includes(args.locale)) {
|
if (!localeDirs.includes(args.locale)) {
|
||||||
console.error(`Error: Language '${args.locale}' not found in ${localesDir}`)
|
console.error(
|
||||||
|
`Error: Language '${args.locale}' not found in ${localesDir}`
|
||||||
|
)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -262,11 +299,11 @@ function main() {
|
|||||||
const missingKeys = findMissingI18nKeys()
|
const missingKeys = findMissingI18nKeys()
|
||||||
|
|
||||||
if (missingKeys.length === 0) {
|
if (missingKeys.length === 0) {
|
||||||
console.log("\n✅ All i18n keys are present!")
|
console.log('\n✅ All i18n keys are present!')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("\nMissing i18n keys:\n")
|
console.log('\nMissing i18n keys:\n')
|
||||||
|
|
||||||
// Group by file for better readability
|
// Group by file for better readability
|
||||||
const groupedByFile = {}
|
const groupedByFile = {}
|
||||||
@ -281,23 +318,25 @@ function main() {
|
|||||||
console.log(`📁 File: ${file}`)
|
console.log(`📁 File: ${file}`)
|
||||||
keys.forEach(({ key, missingLocales }) => {
|
keys.forEach(({ key, missingLocales }) => {
|
||||||
console.log(` 🔑 Key: ${key}`)
|
console.log(` 🔑 Key: ${key}`)
|
||||||
console.log(" ❌ Missing in:")
|
console.log(' ❌ Missing in:')
|
||||||
missingLocales.forEach((locale) => console.log(` - ${locale}`))
|
missingLocales.forEach((locale) => console.log(` - ${locale}`))
|
||||||
console.log("")
|
console.log('')
|
||||||
})
|
})
|
||||||
console.log("-------------------")
|
console.log('-------------------')
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log("\n💡 To fix missing translations:")
|
console.log('\n💡 To fix missing translations:')
|
||||||
console.log("1. Add the missing keys to the appropriate locale files")
|
console.log('1. Add the missing keys to the appropriate locale files')
|
||||||
console.log("2. Use yq commands for efficient updates:")
|
console.log('2. Use yq commands for efficient updates:')
|
||||||
console.log(" yq -i '.namespace.key = \"Translation\"' web-app/src/locales/<locale>/<file>.json")
|
console.log(
|
||||||
console.log("3. Run this script again to verify all keys are present")
|
' yq -i \'.namespace.key = "Translation"\' web-app/src/locales/<locale>/<file>.json'
|
||||||
|
)
|
||||||
|
console.log('3. Run this script again to verify all keys are present')
|
||||||
|
|
||||||
// Exit code 1 indicates missing keys
|
// Exit code 1 indicates missing keys
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error:", error.message)
|
console.error('Error:', error.message)
|
||||||
console.error(error.stack)
|
console.error(error.stack)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,23 +10,20 @@
|
|||||||
* --help Show this help message
|
* --help Show this help message
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const fs = require("fs")
|
const fs = require('fs')
|
||||||
const path = require("path")
|
const path = require('path')
|
||||||
|
|
||||||
// Process command line arguments
|
// Process command line arguments
|
||||||
const args = process.argv.slice(2).reduce(
|
const args = process.argv.slice(2).reduce((acc, arg) => {
|
||||||
(acc, arg) => {
|
if (arg === '--help') {
|
||||||
if (arg === "--help") {
|
|
||||||
acc.help = true
|
acc.help = true
|
||||||
} else if (arg.startsWith("--locale=")) {
|
} else if (arg.startsWith('--locale=')) {
|
||||||
acc.locale = arg.split("=")[1]
|
acc.locale = arg.split('=')[1]
|
||||||
} else if (arg.startsWith("--file=")) {
|
} else if (arg.startsWith('--file=')) {
|
||||||
acc.file = arg.split("=")[1]
|
acc.file = arg.split('=')[1]
|
||||||
}
|
}
|
||||||
return acc
|
return acc
|
||||||
},
|
}, {})
|
||||||
{}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Show help if requested
|
// Show help if requested
|
||||||
if (args.help) {
|
if (args.help) {
|
||||||
@ -51,16 +48,16 @@ Output:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Path to the locales directory
|
// 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
|
// Recursively find all keys in an object
|
||||||
function findKeys(obj, parentKey = "") {
|
function findKeys(obj, parentKey = '') {
|
||||||
let keys = []
|
let keys = []
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(obj)) {
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
const currentKey = parentKey ? `${parentKey}.${key}` : key
|
const currentKey = parentKey ? `${parentKey}.${key}` : key
|
||||||
|
|
||||||
if (typeof value === "object" && value !== null) {
|
if (typeof value === 'object' && value !== null) {
|
||||||
// If value is an object, recurse
|
// If value is an object, recurse
|
||||||
keys = [...keys, ...findKeys(value, currentKey)]
|
keys = [...keys, ...findKeys(value, currentKey)]
|
||||||
} else {
|
} else {
|
||||||
@ -74,7 +71,7 @@ function findKeys(obj, parentKey = "") {
|
|||||||
|
|
||||||
// Get value at a dotted path in an object
|
// Get value at a dotted path in an object
|
||||||
function getValueAtPath(obj, path) {
|
function getValueAtPath(obj, path) {
|
||||||
const parts = path.split(".")
|
const parts = path.split('.')
|
||||||
let current = obj
|
let current = obj
|
||||||
|
|
||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
@ -92,22 +89,28 @@ function checkTranslations() {
|
|||||||
// Get all locale directories (or filter to the specified locale)
|
// Get all locale directories (or filter to the specified locale)
|
||||||
const allLocales = fs.readdirSync(LOCALES_DIR).filter((item) => {
|
const allLocales = fs.readdirSync(LOCALES_DIR).filter((item) => {
|
||||||
const stats = fs.statSync(path.join(LOCALES_DIR, item))
|
const stats = fs.statSync(path.join(LOCALES_DIR, item))
|
||||||
return stats.isDirectory() && item !== "en" // Exclude English as it's our source
|
return stats.isDirectory() && item !== 'en' // Exclude English as it's our source
|
||||||
})
|
})
|
||||||
|
|
||||||
// Filter to the specified locale if provided
|
// Filter to the specified locale if provided
|
||||||
const locales = args.locale ? allLocales.filter((locale) => locale === args.locale) : allLocales
|
const locales = args.locale
|
||||||
|
? allLocales.filter((locale) => locale === args.locale)
|
||||||
|
: allLocales
|
||||||
|
|
||||||
if (args.locale && locales.length === 0) {
|
if (args.locale && locales.length === 0) {
|
||||||
console.error(`Error: Locale '${args.locale}' not found in ${LOCALES_DIR}`)
|
console.error(`Error: Locale '${args.locale}' not found in ${LOCALES_DIR}`)
|
||||||
process.exit(1)
|
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
|
// Get all English JSON files
|
||||||
const englishDir = path.join(LOCALES_DIR, "en")
|
const englishDir = path.join(LOCALES_DIR, 'en')
|
||||||
let englishFiles = fs.readdirSync(englishDir).filter((file) => file.endsWith(".json") && !file.startsWith("."))
|
let englishFiles = fs
|
||||||
|
.readdirSync(englishDir)
|
||||||
|
.filter((file) => file.endsWith('.json') && !file.startsWith('.'))
|
||||||
|
|
||||||
// Filter to the specified file if provided
|
// Filter to the specified file if provided
|
||||||
if (args.file) {
|
if (args.file) {
|
||||||
@ -124,7 +127,7 @@ function checkTranslations() {
|
|||||||
try {
|
try {
|
||||||
englishFileContents = englishFiles.map((file) => ({
|
englishFileContents = englishFiles.map((file) => ({
|
||||||
name: file,
|
name: file,
|
||||||
content: JSON.parse(fs.readFileSync(path.join(englishDir, file), "utf8")),
|
content: JSON.parse(fs.readFileSync(path.join(englishDir, file), 'utf8')),
|
||||||
}))
|
}))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Error: File '${englishDir}' is not a valid JSON file`)
|
console.error(`Error: File '${englishDir}' is not a valid JSON file`)
|
||||||
@ -132,7 +135,9 @@ function checkTranslations() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`Checking ${englishFileContents.length} translation file(s): ${englishFileContents.map((f) => f.name).join(", ")}`
|
`Checking ${
|
||||||
|
englishFileContents.length
|
||||||
|
} translation file(s): ${englishFileContents.map((f) => f.name).join(', ')}`
|
||||||
)
|
)
|
||||||
|
|
||||||
// Results object to store missing translations
|
// Results object to store missing translations
|
||||||
@ -147,7 +152,7 @@ function checkTranslations() {
|
|||||||
|
|
||||||
// Check if the file exists in the locale
|
// Check if the file exists in the locale
|
||||||
if (!fs.existsSync(localeFilePath)) {
|
if (!fs.existsSync(localeFilePath)) {
|
||||||
missingTranslations[locale][name] = { file: "File is missing entirely" }
|
missingTranslations[locale][name] = { file: 'File is missing entirely' }
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,9 +160,11 @@ function checkTranslations() {
|
|||||||
let localeContent
|
let localeContent
|
||||||
|
|
||||||
try {
|
try {
|
||||||
localeContent = JSON.parse(fs.readFileSync(localeFilePath, "utf8"))
|
localeContent = JSON.parse(fs.readFileSync(localeFilePath, 'utf8'))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Error: File '${localeFilePath}' is not a valid JSON file`)
|
console.error(
|
||||||
|
`Error: File '${localeFilePath}' is not a valid JSON file`
|
||||||
|
)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -209,14 +216,16 @@ function outputResults(missingTranslations) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(` - ${fileName}: ${missingItems.length} missing translations`)
|
console.log(
|
||||||
|
` - ${fileName}: ${missingItems.length} missing translations`
|
||||||
|
)
|
||||||
|
|
||||||
for (const { key, englishValue } of missingItems) {
|
for (const { key, englishValue } of missingItems) {
|
||||||
console.log(` ${key}: "${englishValue}"`)
|
console.log(` ${key}: "${englishValue}"`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("")
|
console.log('')
|
||||||
}
|
}
|
||||||
|
|
||||||
return hasMissingTranslations
|
return hasMissingTranslations
|
||||||
@ -225,25 +234,31 @@ function outputResults(missingTranslations) {
|
|||||||
// Main function to find missing translations
|
// Main function to find missing translations
|
||||||
function findMissingTranslations() {
|
function findMissingTranslations() {
|
||||||
try {
|
try {
|
||||||
console.log("Starting translation check for Jan web-app...")
|
console.log('Starting translation check for Jan web-app...')
|
||||||
|
|
||||||
const hasMissingTranslations = checkTranslations()
|
const hasMissingTranslations = checkTranslations()
|
||||||
|
|
||||||
// Summary
|
// Summary
|
||||||
if (!hasMissingTranslations) {
|
if (!hasMissingTranslations) {
|
||||||
console.log("\n✅ All translations are complete!")
|
console.log('\n✅ All translations are complete!')
|
||||||
} else {
|
} else {
|
||||||
console.log("\n✏️ To add missing translations:")
|
console.log('\n✏️ To add missing translations:')
|
||||||
console.log("1. Add the missing keys to the corresponding locale files")
|
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('2. Translate the English values to the appropriate language')
|
||||||
console.log("3. You can use yq commands to update JSON files efficiently:")
|
console.log(
|
||||||
console.log(" yq -i '.namespace.key = \"Translation\"' web-app/src/locales/<locale>/<file>.json")
|
'3. You can use yq commands to update JSON files efficiently:'
|
||||||
console.log("4. Run this script again to verify all translations are complete")
|
)
|
||||||
|
console.log(
|
||||||
|
' yq -i \'.namespace.key = "Translation"\' web-app/src/locales/<locale>/<file>.json'
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
'4. Run this script again to verify all translations are complete'
|
||||||
|
)
|
||||||
// Exit with error code to fail CI checks
|
// Exit with error code to fail CI checks
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error:", error.message)
|
console.error('Error:', error.message)
|
||||||
console.error(error.stack)
|
console.error(error.stack)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,6 +32,7 @@
|
|||||||
"@tauri-apps/api": "^2.5.0",
|
"@tauri-apps/api": "^2.5.0",
|
||||||
"@tauri-apps/plugin-deep-link": "~2",
|
"@tauri-apps/plugin-deep-link": "~2",
|
||||||
"@tauri-apps/plugin-dialog": "^2.2.1",
|
"@tauri-apps/plugin-dialog": "^2.2.1",
|
||||||
|
"@tauri-apps/plugin-http": "^2.2.1",
|
||||||
"@tauri-apps/plugin-opener": "^2.2.7",
|
"@tauri-apps/plugin-opener": "^2.2.7",
|
||||||
"@tauri-apps/plugin-os": "^2.2.1",
|
"@tauri-apps/plugin-os": "^2.2.1",
|
||||||
"@tauri-apps/plugin-updater": "^2.7.1",
|
"@tauri-apps/plugin-updater": "^2.7.1",
|
||||||
|
|||||||
@ -35,7 +35,6 @@ import { ModelLoader } from '@/containers/loaders/ModelLoader'
|
|||||||
import DropdownToolsAvailable from '@/containers/DropdownToolsAvailable'
|
import DropdownToolsAvailable from '@/containers/DropdownToolsAvailable'
|
||||||
import { getConnectedServers } from '@/services/mcp'
|
import { getConnectedServers } from '@/services/mcp'
|
||||||
import { stopAllModels } from '@/services/models'
|
import { stopAllModels } from '@/services/models'
|
||||||
import { useOutOfContextPromiseModal } from './dialogs/OutOfContextDialog'
|
|
||||||
|
|
||||||
type ChatInputProps = {
|
type ChatInputProps = {
|
||||||
className?: string
|
className?: string
|
||||||
@ -55,8 +54,6 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { spellCheckChatInput } = useGeneralSetting()
|
const { spellCheckChatInput } = useGeneralSetting()
|
||||||
|
|
||||||
const { showModal, PromiseModal: OutOfContextModal } =
|
|
||||||
useOutOfContextPromiseModal()
|
|
||||||
const maxRows = 10
|
const maxRows = 10
|
||||||
|
|
||||||
const { selectedModel } = useModelProvider()
|
const { selectedModel } = useModelProvider()
|
||||||
@ -107,7 +104,7 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
setMessage('')
|
setMessage('')
|
||||||
sendMessage(prompt, showModal)
|
sendMessage(prompt)
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -599,7 +596,6 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<OutOfContextModal />
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,6 @@ import { ModelSetting } from '@/containers/ModelSetting'
|
|||||||
import ProvidersAvatar from '@/containers/ProvidersAvatar'
|
import ProvidersAvatar from '@/containers/ProvidersAvatar'
|
||||||
import { Fzf } from 'fzf'
|
import { Fzf } from 'fzf'
|
||||||
import { localStorageKey } from '@/constants/localStorage'
|
import { localStorageKey } from '@/constants/localStorage'
|
||||||
import { isProd } from '@/lib/version'
|
|
||||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||||
|
|
||||||
type DropdownModelProviderProps = {
|
type DropdownModelProviderProps = {
|
||||||
@ -398,7 +397,7 @@ const DropdownModelProvider = ({
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className="flex-1"></div>
|
<div className="flex-1"></div>
|
||||||
{!isProd && capabilities.length > 0 && (
|
{capabilities.length > 0 && (
|
||||||
<div className="flex-shrink-0 -mr-1.5">
|
<div className="flex-shrink-0 -mr-1.5">
|
||||||
<Capabilities capabilities={capabilities} />
|
<Capabilities capabilities={capabilities} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,9 +2,22 @@ import { Link, useMatches } from '@tanstack/react-router'
|
|||||||
import { route } from '@/constants/routes'
|
import { route } from '@/constants/routes'
|
||||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||||
import { useModelProvider } from '@/hooks/useModelProvider'
|
import { useModelProvider } from '@/hooks/useModelProvider'
|
||||||
import { isProd } from '@/lib/version'
|
import { useGeneralSetting } from '@/hooks/useGeneralSetting'
|
||||||
|
|
||||||
const menuSettings = [
|
const SettingsMenu = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { providers } = useModelProvider()
|
||||||
|
const { experimentalFeatures } = useGeneralSetting()
|
||||||
|
const firstItemProvider =
|
||||||
|
providers.length > 0 ? providers[0].provider : 'llama.cpp'
|
||||||
|
const matches = useMatches()
|
||||||
|
const isActive = matches.some(
|
||||||
|
(match) =>
|
||||||
|
match.routeId === '/settings/providers/$providerName' &&
|
||||||
|
'providerName' in match.params
|
||||||
|
)
|
||||||
|
|
||||||
|
const menuSettings = [
|
||||||
{
|
{
|
||||||
title: 'common:general',
|
title: 'common:general',
|
||||||
route: route.settings.general,
|
route: route.settings.general,
|
||||||
@ -25,8 +38,8 @@ const menuSettings = [
|
|||||||
title: 'common:hardware',
|
title: 'common:hardware',
|
||||||
route: route.settings.hardware,
|
route: route.settings.hardware,
|
||||||
},
|
},
|
||||||
// Only show MCP Servers in non-production environment
|
// Only show MCP Servers when experimental features are enabled
|
||||||
...(!isProd
|
...(experimentalFeatures
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
title: 'common:mcp-servers',
|
title: 'common:mcp-servers',
|
||||||
@ -46,19 +59,7 @@ const menuSettings = [
|
|||||||
title: 'common:extensions',
|
title: 'common:extensions',
|
||||||
route: route.settings.extensions,
|
route: route.settings.extensions,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const SettingsMenu = () => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const { providers } = useModelProvider()
|
|
||||||
const firstItemProvider =
|
|
||||||
providers.length > 0 ? providers[0].provider : 'llama.cpp'
|
|
||||||
const matches = useMatches()
|
|
||||||
const isActive = matches.some(
|
|
||||||
(match) =>
|
|
||||||
match.routeId === '/settings/providers/$providerName' &&
|
|
||||||
'providerName' in match.params
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-44 shrink-0 px-1.5 pt-3 border-r border-main-view-fg/5">
|
<div className="flex h-full w-44 shrink-0 px-1.5 pt-3 border-r border-main-view-fg/5">
|
||||||
|
|||||||
@ -85,7 +85,6 @@ export const ThreadContent = memo(
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
streamTools?: any
|
streamTools?: any
|
||||||
contextOverflowModal?: React.ReactNode | null
|
contextOverflowModal?: React.ReactNode | null
|
||||||
showContextOverflowModal?: () => Promise<unknown>
|
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
const [message, setMessage] = useState(item.content?.[0]?.text?.value || '')
|
const [message, setMessage] = useState(item.content?.[0]?.text?.value || '')
|
||||||
@ -137,10 +136,7 @@ export const ThreadContent = memo(
|
|||||||
}
|
}
|
||||||
if (toSendMessage) {
|
if (toSendMessage) {
|
||||||
deleteMessage(toSendMessage.thread_id, toSendMessage.id ?? '')
|
deleteMessage(toSendMessage.thread_id, toSendMessage.id ?? '')
|
||||||
sendMessage(
|
sendMessage(toSendMessage.content?.[0]?.text?.value || '')
|
||||||
toSendMessage.content?.[0]?.text?.value || '',
|
|
||||||
item.showContextOverflowModal
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}, [deleteMessage, getMessages, item, sendMessage])
|
}, [deleteMessage, getMessages, item, sendMessage])
|
||||||
|
|
||||||
@ -182,16 +178,9 @@ export const ThreadContent = memo(
|
|||||||
deleteMessage(threadMessages[i].thread_id, threadMessages[i].id)
|
deleteMessage(threadMessages[i].thread_id, threadMessages[i].id)
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessage(message, item.showContextOverflowModal)
|
sendMessage(message)
|
||||||
},
|
},
|
||||||
[
|
[deleteMessage, getMessages, item.thread_id, message, sendMessage]
|
||||||
deleteMessage,
|
|
||||||
getMessages,
|
|
||||||
item.thread_id,
|
|
||||||
message,
|
|
||||||
sendMessage,
|
|
||||||
item.showContextOverflowModal,
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const isToolCalls =
|
const isToolCalls =
|
||||||
|
|||||||
@ -65,7 +65,7 @@ export function CortexFailureDialog() {
|
|||||||
<DialogFooter className="flex gap-2">
|
<DialogFooter className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
asChild
|
asChild
|
||||||
variant="default"
|
variant="link"
|
||||||
className="bg-transparent border border-main-view-fg/20 hover:bg-main-view-fg/4"
|
className="bg-transparent border border-main-view-fg/20 hover:bg-main-view-fg/4"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowDialog(false)
|
setShowDialog(false)
|
||||||
|
|||||||
@ -35,7 +35,16 @@ export const DialogDeleteModel = ({
|
|||||||
const removeModel = async () => {
|
const removeModel = async () => {
|
||||||
deleteModelCache(selectedModelId)
|
deleteModelCache(selectedModelId)
|
||||||
deleteModel(selectedModelId).then(() => {
|
deleteModel(selectedModelId).then(() => {
|
||||||
getProviders().then(setProviders)
|
getProviders().then((providers) => {
|
||||||
|
// Filter out the deleted model from all providers
|
||||||
|
const filteredProviders = providers.map((provider) => ({
|
||||||
|
...provider,
|
||||||
|
models: provider.models.filter(
|
||||||
|
(model) => model.id !== selectedModelId
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
setProviders(filteredProviders)
|
||||||
|
})
|
||||||
toast.success(
|
toast.success(
|
||||||
t('providers:deleteModel.title', { modelId: selectedModel?.id }),
|
t('providers:deleteModel.title', { modelId: selectedModel?.id }),
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { t } from 'i18next'
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@ -8,108 +7,67 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@/components/ui/dialog'
|
} from '@/components/ui/dialog'
|
||||||
|
|
||||||
import { ReactNode, useCallback, useState } from 'react'
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { useContextSizeApproval } from '@/hooks/useModelContextApproval'
|
||||||
|
import { useTranslation } from '@/i18n'
|
||||||
|
|
||||||
export function useOutOfContextPromiseModal() {
|
export default function OutOfContextPromiseModal() {
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const { t } = useTranslation()
|
||||||
const [modalProps, setModalProps] = useState<{
|
const { isModalOpen, modalProps, setModalOpen } = useContextSizeApproval()
|
||||||
resolveRef:
|
if (!modalProps) {
|
||||||
| ((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
|
return null
|
||||||
}
|
}
|
||||||
|
const { onApprove, onDeny } = modalProps
|
||||||
|
|
||||||
const handleContextLength = () => {
|
const handleContextLength = () => {
|
||||||
setIsOpen(false)
|
onApprove('ctx_len')
|
||||||
if (modalProps.resolveRef) {
|
|
||||||
modalProps.resolveRef('ctx_len')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleContextShift = () => {
|
const handleContextShift = () => {
|
||||||
setIsOpen(false)
|
onApprove('context_shift')
|
||||||
if (modalProps.resolveRef) {
|
|
||||||
modalProps.resolveRef('context_shift')
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
const handleCancel = () => {
|
const handleDialogOpen = (open: boolean) => {
|
||||||
setIsOpen(false)
|
setModalOpen(open)
|
||||||
if (modalProps.resolveRef) {
|
if (!open) {
|
||||||
modalProps.resolveRef(undefined)
|
onDeny()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog open={isModalOpen} onOpenChange={handleDialogOpen}>
|
||||||
open={isOpen}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
setIsOpen(open)
|
|
||||||
if (!open) handleCancel()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>{t('model-errors:title')}</DialogTitle>
|
||||||
{t('outOfContextError.title', 'Out of context error')}
|
|
||||||
</DialogTitle>
|
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{t(
|
{t('model-errors:description')}
|
||||||
'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 />
|
||||||
<br />
|
<br />
|
||||||
{t(
|
{t('model-errors:increaseContextSizeDescription')}
|
||||||
'outOfContextError.increaseContextSizeDescription',
|
|
||||||
'Do you want to increase the context size?'
|
|
||||||
)}
|
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
<DialogFooter className="flex gap-2">
|
<DialogFooter className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="link"
|
||||||
className="bg-transparent border border-main-view-fg/20 hover:bg-main-view-fg/4"
|
className="bg-transparent border border-main-view-fg/20 hover:bg-main-view-fg/4"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleContextShift()
|
handleContextShift()
|
||||||
setIsOpen(false)
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('outOfContextError.truncateInput', 'Truncate Input')}
|
{t('model-errors:truncateInput')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
asChild
|
asChild
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleContextLength()
|
handleContextLength()
|
||||||
setIsOpen(false)
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="text-main-view-fg/70">
|
<span className="text-main-view-fg/70">
|
||||||
{t(
|
{t('model-errors:increaseContextSize')}
|
||||||
'outOfContextError.increaseContextSize',
|
|
||||||
'Increase Context Size'
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}, [isOpen, modalProps])
|
|
||||||
return { showModal, PromiseModal }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,13 @@ type DynamicControllerProps = {
|
|||||||
title?: string
|
title?: string
|
||||||
className?: string
|
className?: string
|
||||||
description?: string
|
description?: string
|
||||||
controllerType: 'input' | 'checkbox' | 'dropdown' | 'textarea' | 'slider' | string
|
controllerType:
|
||||||
|
| 'input'
|
||||||
|
| 'checkbox'
|
||||||
|
| 'dropdown'
|
||||||
|
| 'textarea'
|
||||||
|
| 'slider'
|
||||||
|
| string
|
||||||
controllerProps: {
|
controllerProps: {
|
||||||
value?: string | boolean | number
|
value?: string | boolean | number
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
@ -36,7 +42,11 @@ export function DynamicControllerSetting({
|
|||||||
<InputControl
|
<InputControl
|
||||||
type={controllerProps.type}
|
type={controllerProps.type}
|
||||||
placeholder={controllerProps.placeholder}
|
placeholder={controllerProps.placeholder}
|
||||||
value={(controllerProps.value as string) || ''}
|
value={
|
||||||
|
typeof controllerProps.value === 'number'
|
||||||
|
? controllerProps.value.toString()
|
||||||
|
: (controllerProps.value as string) || ''
|
||||||
|
}
|
||||||
inputActions={controllerProps.input_actions}
|
inputActions={controllerProps.input_actions}
|
||||||
className={className}
|
className={className}
|
||||||
onChange={(newValue) => onChange(newValue)}
|
onChange={(newValue) => onChange(newValue)}
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import { useToolApproval } from '@/hooks/useToolApproval'
|
|||||||
import { useToolAvailable } from '@/hooks/useToolAvailable'
|
import { useToolAvailable } from '@/hooks/useToolAvailable'
|
||||||
import { OUT_OF_CONTEXT_SIZE } from '@/utils/error'
|
import { OUT_OF_CONTEXT_SIZE } from '@/utils/error'
|
||||||
import { updateSettings } from '@/services/providers'
|
import { updateSettings } from '@/services/providers'
|
||||||
|
import { useContextSizeApproval } from './useModelContextApproval'
|
||||||
|
|
||||||
export const useChat = () => {
|
export const useChat = () => {
|
||||||
const { prompt, setPrompt } = usePrompt()
|
const { prompt, setPrompt } = usePrompt()
|
||||||
@ -47,6 +48,8 @@ export const useChat = () => {
|
|||||||
|
|
||||||
const { approvedTools, showApprovalModal, allowAllMCPPermissions } =
|
const { approvedTools, showApprovalModal, allowAllMCPPermissions } =
|
||||||
useToolApproval()
|
useToolApproval()
|
||||||
|
const { showApprovalModal: showIncreaseContextSizeModal } =
|
||||||
|
useContextSizeApproval()
|
||||||
const { getDisabledToolsForThread } = useToolAvailable()
|
const { getDisabledToolsForThread } = useToolAvailable()
|
||||||
|
|
||||||
const { getProviderByName, selectedModel, selectedProvider } =
|
const { getProviderByName, selectedModel, selectedProvider } =
|
||||||
@ -223,11 +226,7 @@ export const useChat = () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const sendMessage = useCallback(
|
const sendMessage = useCallback(
|
||||||
async (
|
async (message: string, troubleshooting = true) => {
|
||||||
message: string,
|
|
||||||
showModal?: () => Promise<unknown>,
|
|
||||||
troubleshooting = true
|
|
||||||
) => {
|
|
||||||
const activeThread = await getCurrentThread()
|
const activeThread = await getCurrentThread()
|
||||||
|
|
||||||
resetTokenSpeed()
|
resetTokenSpeed()
|
||||||
@ -361,7 +360,7 @@ export const useChat = () => {
|
|||||||
selectedModel &&
|
selectedModel &&
|
||||||
troubleshooting
|
troubleshooting
|
||||||
) {
|
) {
|
||||||
const method = await showModal?.()
|
const method = await showIncreaseContextSizeModal()
|
||||||
if (method === 'ctx_len') {
|
if (method === 'ctx_len') {
|
||||||
/// Increase context size
|
/// Increase context size
|
||||||
activeProvider = await increaseModelContextSize(
|
activeProvider = await increaseModelContextSize(
|
||||||
@ -447,8 +446,7 @@ export const useChat = () => {
|
|||||||
updateThreadTimestamp,
|
updateThreadTimestamp,
|
||||||
setPrompt,
|
setPrompt,
|
||||||
selectedModel,
|
selectedModel,
|
||||||
currentAssistant?.instructions,
|
currentAssistant,
|
||||||
currentAssistant.parameters,
|
|
||||||
tools,
|
tools,
|
||||||
updateLoadingModel,
|
updateLoadingModel,
|
||||||
getDisabledToolsForThread,
|
getDisabledToolsForThread,
|
||||||
@ -456,6 +454,7 @@ export const useChat = () => {
|
|||||||
allowAllMCPPermissions,
|
allowAllMCPPermissions,
|
||||||
showApprovalModal,
|
showApprovalModal,
|
||||||
updateTokenSpeed,
|
updateTokenSpeed,
|
||||||
|
showIncreaseContextSizeModal,
|
||||||
increaseModelContextSize,
|
increaseModelContextSize,
|
||||||
toggleOnContextShifting,
|
toggleOnContextShifting,
|
||||||
]
|
]
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import { localStorageKey } from '@/constants/localStorage'
|
|||||||
type LeftPanelStoreState = {
|
type LeftPanelStoreState = {
|
||||||
currentLanguage: Language
|
currentLanguage: Language
|
||||||
spellCheckChatInput: boolean
|
spellCheckChatInput: boolean
|
||||||
|
experimentalFeatures: boolean
|
||||||
|
setExperimentalFeatures: (value: boolean) => void
|
||||||
setSpellCheckChatInput: (value: boolean) => void
|
setSpellCheckChatInput: (value: boolean) => void
|
||||||
setCurrentLanguage: (value: Language) => void
|
setCurrentLanguage: (value: Language) => void
|
||||||
}
|
}
|
||||||
@ -14,6 +16,8 @@ export const useGeneralSetting = create<LeftPanelStoreState>()(
|
|||||||
(set) => ({
|
(set) => ({
|
||||||
currentLanguage: 'en',
|
currentLanguage: 'en',
|
||||||
spellCheckChatInput: true,
|
spellCheckChatInput: true,
|
||||||
|
experimentalFeatures: false,
|
||||||
|
setExperimentalFeatures: (value) => set({ experimentalFeatures: value }),
|
||||||
setSpellCheckChatInput: (value) => set({ spellCheckChatInput: value }),
|
setSpellCheckChatInput: (value) => set({ spellCheckChatInput: value }),
|
||||||
setCurrentLanguage: (value) => set({ currentLanguage: value }),
|
setCurrentLanguage: (value) => set({ currentLanguage: value }),
|
||||||
}),
|
}),
|
||||||
|
|||||||
53
web-app/src/hooks/useModelContextApproval.ts
Normal file
53
web-app/src/hooks/useModelContextApproval.ts
Normal file
@ -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<ApprovalState>()((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()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}))
|
||||||
@ -6,6 +6,7 @@ type ModelProviderState = {
|
|||||||
providers: ModelProvider[]
|
providers: ModelProvider[]
|
||||||
selectedProvider: string
|
selectedProvider: string
|
||||||
selectedModel: Model | null
|
selectedModel: Model | null
|
||||||
|
deletedModels: string[]
|
||||||
getModelBy: (modelId: string) => Model | undefined
|
getModelBy: (modelId: string) => Model | undefined
|
||||||
setProviders: (providers: ModelProvider[]) => void
|
setProviders: (providers: ModelProvider[]) => void
|
||||||
getProviderByName: (providerName: string) => ModelProvider | undefined
|
getProviderByName: (providerName: string) => ModelProvider | undefined
|
||||||
@ -25,6 +26,7 @@ export const useModelProvider = create<ModelProviderState>()(
|
|||||||
providers: [],
|
providers: [],
|
||||||
selectedProvider: 'llama.cpp',
|
selectedProvider: 'llama.cpp',
|
||||||
selectedModel: null,
|
selectedModel: null,
|
||||||
|
deletedModels: [],
|
||||||
getModelBy: (modelId: string) => {
|
getModelBy: (modelId: string) => {
|
||||||
const provider = get().providers.find(
|
const provider = get().providers.find(
|
||||||
(provider) => provider.provider === get().selectedProvider
|
(provider) => provider.provider === get().selectedProvider
|
||||||
@ -35,6 +37,11 @@ export const useModelProvider = create<ModelProviderState>()(
|
|||||||
setProviders: (providers) =>
|
setProviders: (providers) =>
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const existingProviders = state.providers
|
const existingProviders = state.providers
|
||||||
|
// Ensure deletedModels is always an array
|
||||||
|
const currentDeletedModels = Array.isArray(state.deletedModels)
|
||||||
|
? state.deletedModels
|
||||||
|
: []
|
||||||
|
|
||||||
const updatedProviders = providers.map((provider) => {
|
const updatedProviders = providers.map((provider) => {
|
||||||
const existingProvider = existingProviders.find(
|
const existingProvider = existingProviders.find(
|
||||||
(x) => x.provider === provider.provider
|
(x) => x.provider === provider.provider
|
||||||
@ -43,7 +50,9 @@ export const useModelProvider = create<ModelProviderState>()(
|
|||||||
const mergedModels = [
|
const mergedModels = [
|
||||||
...models,
|
...models,
|
||||||
...(provider?.models ?? []).filter(
|
...(provider?.models ?? []).filter(
|
||||||
(e) => !models.some((m) => m.id === e.id)
|
(e) =>
|
||||||
|
!models.some((m) => m.id === e.id) &&
|
||||||
|
!currentDeletedModels.includes(e.id)
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
return {
|
return {
|
||||||
@ -118,7 +127,13 @@ export const useModelProvider = create<ModelProviderState>()(
|
|||||||
return modelObject
|
return modelObject
|
||||||
},
|
},
|
||||||
deleteModel: (modelId: string) => {
|
deleteModel: (modelId: string) => {
|
||||||
set((state) => ({
|
set((state) => {
|
||||||
|
// Ensure deletedModels is always an array
|
||||||
|
const currentDeletedModels = Array.isArray(state.deletedModels)
|
||||||
|
? state.deletedModels
|
||||||
|
: []
|
||||||
|
|
||||||
|
return {
|
||||||
providers: state.providers.map((provider) => {
|
providers: state.providers.map((provider) => {
|
||||||
const models = provider.models.filter(
|
const models = provider.models.filter(
|
||||||
(model) => model.id !== modelId
|
(model) => model.id !== modelId
|
||||||
@ -128,7 +143,9 @@ export const useModelProvider = create<ModelProviderState>()(
|
|||||||
models,
|
models,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
}))
|
deletedModels: [...currentDeletedModels, modelId],
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
addProvider: (provider: ModelProvider) => {
|
addProvider: (provider: ModelProvider) => {
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
ModelManager,
|
ModelManager,
|
||||||
} from '@janhq/core'
|
} from '@janhq/core'
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
import { fetch as fetchTauri } from '@tauri-apps/plugin-http'
|
||||||
import {
|
import {
|
||||||
ChatCompletionMessageParam,
|
ChatCompletionMessageParam,
|
||||||
ChatCompletionTool,
|
ChatCompletionTool,
|
||||||
@ -15,7 +16,13 @@ import {
|
|||||||
models,
|
models,
|
||||||
StreamCompletionResponse,
|
StreamCompletionResponse,
|
||||||
TokenJS,
|
TokenJS,
|
||||||
|
ConfigOptions,
|
||||||
} from 'token.js'
|
} from 'token.js'
|
||||||
|
|
||||||
|
// Extended config options to include custom fetch function
|
||||||
|
type ExtendedConfigOptions = ConfigOptions & {
|
||||||
|
fetch?: typeof fetch
|
||||||
|
}
|
||||||
import { ulid } from 'ulidx'
|
import { ulid } from 'ulidx'
|
||||||
import { normalizeProvider } from './models'
|
import { normalizeProvider } from './models'
|
||||||
import { MCPTool } from '@/types/completion'
|
import { MCPTool } from '@/types/completion'
|
||||||
@ -129,7 +136,9 @@ export const sendCompletion = async (
|
|||||||
apiKey: provider.api_key ?? (await invoke('app_token')),
|
apiKey: provider.api_key ?? (await invoke('app_token')),
|
||||||
// TODO: Retrieve from extension settings
|
// TODO: Retrieve from extension settings
|
||||||
baseURL: provider.base_url,
|
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 (
|
if (
|
||||||
thread.model.id &&
|
thread.model.id &&
|
||||||
!(thread.model.id in Object.values(models).flat()) &&
|
!(thread.model.id in Object.values(models).flat()) &&
|
||||||
|
|||||||
7
web-app/src/locales/en/model-errors.json
Normal file
7
web-app/src/locales/en/model-errors.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
7
web-app/src/locales/id/model-errors.json
Normal file
7
web-app/src/locales/id/model-errors.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
7
web-app/src/locales/vn/model-errors.json
Normal file
7
web-app/src/locales/vn/model-errors.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
7
web-app/src/locales/zh-CN/model-errors.json
Normal file
7
web-app/src/locales/zh-CN/model-errors.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"title": "超出上下文错误",
|
||||||
|
"description": "此对话已接近 AI 的记忆上限,就像白板快被填满一样。我们可以扩展记忆窗口(称为上下文大小),这样它能记住更多内容,但可能会占用你电脑更多内存。我们也可以截断输入,这意味着它会遗忘部分聊天记录,为新消息腾出空间。",
|
||||||
|
"increaseContextSizeDescription": "你想要增加上下文大小吗?",
|
||||||
|
"truncateInput": "截断输入",
|
||||||
|
"increaseContextSize": "增加上下文大小"
|
||||||
|
}
|
||||||
7
web-app/src/locales/zh-TW/model-errors.json
Normal file
7
web-app/src/locales/zh-TW/model-errors.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"title": "超出上下文錯誤",
|
||||||
|
"description": "此對話已接近 AI 的記憶上限,就像白板快被填滿一樣。我們可以擴大記憶視窗(稱為上下文大小),讓它能記住更多內容,但這可能會佔用你電腦更多記憶體。我們也可以截斷輸入,這表示它會忘記部分對話歷史,以便為新訊息騰出空間。",
|
||||||
|
"increaseContextSizeDescription": "你想要增加上下文大小嗎?",
|
||||||
|
"truncateInput": "截斷輸入",
|
||||||
|
"increaseContextSize": "增加上下文大小"
|
||||||
|
}
|
||||||
@ -19,6 +19,7 @@ import { useLeftPanel } from '@/hooks/useLeftPanel'
|
|||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import ToolApproval from '@/containers/dialogs/ToolApproval'
|
import ToolApproval from '@/containers/dialogs/ToolApproval'
|
||||||
import { TranslationProvider } from '@/i18n/TranslationContext'
|
import { TranslationProvider } from '@/i18n/TranslationContext'
|
||||||
|
import OutOfContextPromiseModal from '@/containers/dialogs/OutOfContextDialog'
|
||||||
|
|
||||||
export const Route = createRootRoute({
|
export const Route = createRootRoute({
|
||||||
component: RootLayout,
|
component: RootLayout,
|
||||||
@ -96,6 +97,7 @@ function RootLayout() {
|
|||||||
{/* <TanStackRouterDevtools position="bottom-right" /> */}
|
{/* <TanStackRouterDevtools position="bottom-right" /> */}
|
||||||
<CortexFailureDialog />
|
<CortexFailureDialog />
|
||||||
<ToolApproval />
|
<ToolApproval />
|
||||||
|
<OutOfContextPromiseModal />
|
||||||
</TranslationProvider>
|
</TranslationProvider>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -53,6 +53,12 @@ export const Route = createFileRoute(route.settings.general as any)({
|
|||||||
|
|
||||||
function General() {
|
function General() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const {
|
||||||
|
spellCheckChatInput,
|
||||||
|
setSpellCheckChatInput,
|
||||||
|
experimentalFeatures,
|
||||||
|
setExperimentalFeatures,
|
||||||
|
} = useGeneralSetting()
|
||||||
|
|
||||||
const openFileTitle = (): string => {
|
const openFileTitle = (): string => {
|
||||||
if (IS_MACOS) {
|
if (IS_MACOS) {
|
||||||
@ -63,7 +69,6 @@ function General() {
|
|||||||
return t('settings:general.openContainingFolder')
|
return t('settings:general.openContainingFolder')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const { spellCheckChatInput, setSpellCheckChatInput } = useGeneralSetting()
|
|
||||||
const { checkForUpdate } = useAppUpdater()
|
const { checkForUpdate } = useAppUpdater()
|
||||||
const [janDataFolder, setJanDataFolder] = useState<string | undefined>()
|
const [janDataFolder, setJanDataFolder] = useState<string | undefined>()
|
||||||
const [isCopied, setIsCopied] = useState(false)
|
const [isCopied, setIsCopied] = useState(false)
|
||||||
@ -239,6 +244,20 @@ function General() {
|
|||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Advanced */}
|
||||||
|
<Card title="Advanced">
|
||||||
|
<CardItem
|
||||||
|
title="Experimental Features"
|
||||||
|
description="Enable experimental features. They may be unstable or change at any time."
|
||||||
|
actions={
|
||||||
|
<Switch
|
||||||
|
checked={experimentalFeatures}
|
||||||
|
onCheckedChange={(e) => setExperimentalFeatures(e)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* Data folder */}
|
{/* Data folder */}
|
||||||
<Card title={t('common:dataFolder')}>
|
<Card title={t('common:dataFolder')}>
|
||||||
<CardItem
|
<CardItem
|
||||||
|
|||||||
@ -39,7 +39,6 @@ import { toast } from 'sonner'
|
|||||||
import { ActiveModel } from '@/types/models'
|
import { ActiveModel } from '@/types/models'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { predefinedProviders } from '@/mock/data'
|
import { predefinedProviders } from '@/mock/data'
|
||||||
import { isProd } from '@/lib/version'
|
|
||||||
|
|
||||||
// as route.threadsDetail
|
// as route.threadsDetail
|
||||||
export const Route = createFileRoute('/settings/providers/$providerName')({
|
export const Route = createFileRoute('/settings/providers/$providerName')({
|
||||||
@ -462,19 +461,15 @@ function ProviderDetail() {
|
|||||||
title={
|
title={
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h1 className="font-medium">{model.id}</h1>
|
<h1 className="font-medium">{model.id}</h1>
|
||||||
{!isProd && (
|
|
||||||
<Capabilities capabilities={capabilities} />
|
<Capabilities capabilities={capabilities} />
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
actions={
|
actions={
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
{!isProd && (
|
|
||||||
<DialogEditModel
|
<DialogEditModel
|
||||||
provider={provider}
|
provider={provider}
|
||||||
modelId={model.id}
|
modelId={model.id}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
{model.settings && (
|
{model.settings && (
|
||||||
<ModelSetting
|
<ModelSetting
|
||||||
provider={provider}
|
provider={provider}
|
||||||
|
|||||||
@ -18,7 +18,6 @@ import { useAppState } from '@/hooks/useAppState'
|
|||||||
import DropdownAssistant from '@/containers/DropdownAssistant'
|
import DropdownAssistant from '@/containers/DropdownAssistant'
|
||||||
import { useAssistant } from '@/hooks/useAssistant'
|
import { useAssistant } from '@/hooks/useAssistant'
|
||||||
import { useAppearance } from '@/hooks/useAppearance'
|
import { useAppearance } from '@/hooks/useAppearance'
|
||||||
import { useOutOfContextPromiseModal } from '@/containers/dialogs/OutOfContextDialog'
|
|
||||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||||
|
|
||||||
// as route.threadsDetail
|
// as route.threadsDetail
|
||||||
@ -50,8 +49,6 @@ function ThreadDetail() {
|
|||||||
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
||||||
const isFirstRender = useRef(true)
|
const isFirstRender = useRef(true)
|
||||||
const messagesCount = useMemo(() => messages?.length ?? 0, [messages])
|
const messagesCount = useMemo(() => messages?.length ?? 0, [messages])
|
||||||
const { showModal, PromiseModal: OutOfContextModal } =
|
|
||||||
useOutOfContextPromiseModal()
|
|
||||||
|
|
||||||
// Function to check scroll position and scrollbar presence
|
// Function to check scroll position and scrollbar presence
|
||||||
const checkScrollState = () => {
|
const checkScrollState = () => {
|
||||||
@ -198,8 +195,6 @@ function ThreadDetail() {
|
|||||||
|
|
||||||
if (!messages || !threadModel) return null
|
if (!messages || !threadModel) return null
|
||||||
|
|
||||||
const contextOverflowModalComponent = <OutOfContextModal />
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<HeaderPage>
|
<HeaderPage>
|
||||||
@ -245,8 +240,6 @@ function ThreadDetail() {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
index={index}
|
index={index}
|
||||||
showContextOverflowModal={showModal}
|
|
||||||
contextOverflowModal={contextOverflowModalComponent}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -13,6 +13,8 @@ import {
|
|||||||
import { modelSettings } from '@/lib/predefined'
|
import { modelSettings } from '@/lib/predefined'
|
||||||
import { fetchModels } from './models'
|
import { fetchModels } from './models'
|
||||||
import { ExtensionManager } from '@/lib/extension'
|
import { ExtensionManager } from '@/lib/extension'
|
||||||
|
import { fetch as fetchTauri } from '@tauri-apps/plugin-http'
|
||||||
|
|
||||||
|
|
||||||
export const getProviders = async (): Promise<ModelProvider[]> => {
|
export const getProviders = async (): Promise<ModelProvider[]> => {
|
||||||
const engines = !localStorage.getItem('migration_completed')
|
const engines = !localStorage.getItem('migration_completed')
|
||||||
@ -148,7 +150,7 @@ export const getProviders = async (): Promise<ModelProvider[]> => {
|
|||||||
...setting,
|
...setting,
|
||||||
controller_props: {
|
controller_props: {
|
||||||
...setting.controller_props,
|
...setting.controller_props,
|
||||||
value: value ?? setting.controller_props.value,
|
value: value,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return acc
|
return acc
|
||||||
@ -163,26 +165,35 @@ export const getProviders = async (): Promise<ModelProvider[]> => {
|
|||||||
return runtimeProviders.concat(builtinProviders as ModelProvider[])
|
return runtimeProviders.concat(builtinProviders as ModelProvider[])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches models from a provider's API endpoint
|
* 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
|
* @param provider The provider object containing base_url and api_key
|
||||||
* @returns Promise<string[]> Array of model IDs
|
* @returns Promise<string[]> Array of model IDs
|
||||||
*/
|
*/
|
||||||
export const fetchModelsFromProvider = async (
|
export const fetchModelsFromProvider = async (
|
||||||
provider: ModelProvider
|
provider: ModelProvider
|
||||||
): Promise<string[]> => {
|
): Promise<string[]> => {
|
||||||
if (!provider.base_url || !provider.api_key) {
|
if (!provider.base_url) {
|
||||||
throw new Error('Provider must have base_url and api_key configured')
|
throw new Error('Provider must have base_url configured')
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${provider.base_url}/models`, {
|
const headers: Record<string, string> = {
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'x-api-key': provider.api_key,
|
|
||||||
'Authorization': `Bearer ${provider.api_key}`,
|
|
||||||
'Content-Type': 'application/json',
|
'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,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@ -213,6 +224,14 @@ export const fetchModelsFromProvider = async (
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching models from provider:', 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
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -235,7 +254,10 @@ export const updateSettings = async (
|
|||||||
...setting,
|
...setting,
|
||||||
controllerProps: {
|
controllerProps: {
|
||||||
...setting.controller_props,
|
...setting.controller_props,
|
||||||
value: setting.controller_props.value ?? '',
|
value:
|
||||||
|
setting.controller_props.value !== undefined
|
||||||
|
? setting.controller_props.value
|
||||||
|
: '',
|
||||||
},
|
},
|
||||||
controllerType: setting.controller_type,
|
controllerType: setting.controller_type,
|
||||||
})) as SettingComponentProps[]
|
})) as SettingComponentProps[]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user