Merge branch 'dev' into allow-assistant-message-edits
This commit is contained in:
commit
c07f5dba03
@ -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 = {}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
643
docs/.astro/collections/docs.schema.json
Normal file
643
docs/.astro/collections/docs.schema.json
Normal file
@ -0,0 +1,643 @@
|
||||
{
|
||||
"$ref": "#/definitions/docs",
|
||||
"definitions": {
|
||||
"docs": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"editUrl": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
{
|
||||
"type": "boolean"
|
||||
}
|
||||
],
|
||||
"default": true
|
||||
},
|
||||
"head": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tag": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"title",
|
||||
"base",
|
||||
"link",
|
||||
"style",
|
||||
"meta",
|
||||
"script",
|
||||
"noscript",
|
||||
"template"
|
||||
]
|
||||
},
|
||||
"attrs": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"not": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"tag"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"default": []
|
||||
},
|
||||
"tableOfContents": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"minHeadingLevel": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 6,
|
||||
"default": 2
|
||||
},
|
||||
"maxHeadingLevel": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 6,
|
||||
"default": 3
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "boolean"
|
||||
}
|
||||
],
|
||||
"default": {
|
||||
"minHeadingLevel": 2,
|
||||
"maxHeadingLevel": 3
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"doc",
|
||||
"splash"
|
||||
],
|
||||
"default": "doc"
|
||||
},
|
||||
"hero": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"tagline": {
|
||||
"type": "string"
|
||||
},
|
||||
"image": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"alt": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"file": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"file"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"alt": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"dark": {
|
||||
"type": "string"
|
||||
},
|
||||
"light": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"dark",
|
||||
"light"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"html": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"html"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"actions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"link": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"primary",
|
||||
"secondary",
|
||||
"minimal"
|
||||
],
|
||||
"default": "primary"
|
||||
},
|
||||
"icon": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"up-caret",
|
||||
"down-caret",
|
||||
"right-caret",
|
||||
"left-caret",
|
||||
"up-arrow",
|
||||
"down-arrow",
|
||||
"right-arrow",
|
||||
"left-arrow",
|
||||
"bars",
|
||||
"translate",
|
||||
"pencil",
|
||||
"pen",
|
||||
"document",
|
||||
"add-document",
|
||||
"setting",
|
||||
"external",
|
||||
"download",
|
||||
"cloud-download",
|
||||
"moon",
|
||||
"sun",
|
||||
"laptop",
|
||||
"open-book",
|
||||
"information",
|
||||
"magnifier",
|
||||
"forward-slash",
|
||||
"close",
|
||||
"error",
|
||||
"warning",
|
||||
"approve-check-circle",
|
||||
"approve-check",
|
||||
"rocket",
|
||||
"star",
|
||||
"puzzle",
|
||||
"list-format",
|
||||
"random",
|
||||
"comment",
|
||||
"comment-alt",
|
||||
"heart",
|
||||
"github",
|
||||
"gitlab",
|
||||
"bitbucket",
|
||||
"codePen",
|
||||
"farcaster",
|
||||
"discord",
|
||||
"gitter",
|
||||
"twitter",
|
||||
"x.com",
|
||||
"mastodon",
|
||||
"codeberg",
|
||||
"youtube",
|
||||
"threads",
|
||||
"linkedin",
|
||||
"twitch",
|
||||
"azureDevOps",
|
||||
"microsoftTeams",
|
||||
"instagram",
|
||||
"stackOverflow",
|
||||
"telegram",
|
||||
"rss",
|
||||
"facebook",
|
||||
"email",
|
||||
"phone",
|
||||
"reddit",
|
||||
"patreon",
|
||||
"signal",
|
||||
"slack",
|
||||
"matrix",
|
||||
"hackerOne",
|
||||
"openCollective",
|
||||
"blueSky",
|
||||
"discourse",
|
||||
"zulip",
|
||||
"pinterest",
|
||||
"tiktok",
|
||||
"astro",
|
||||
"alpine",
|
||||
"pnpm",
|
||||
"biome",
|
||||
"bun",
|
||||
"mdx",
|
||||
"apple",
|
||||
"linux",
|
||||
"homebrew",
|
||||
"nix",
|
||||
"starlight",
|
||||
"pkl",
|
||||
"node",
|
||||
"cloudflare",
|
||||
"vercel",
|
||||
"netlify",
|
||||
"deno",
|
||||
"jsr",
|
||||
"nostr",
|
||||
"backstage",
|
||||
"confluence",
|
||||
"jira",
|
||||
"storybook",
|
||||
"vscode",
|
||||
"jetbrains",
|
||||
"zed",
|
||||
"vim",
|
||||
"figma",
|
||||
"sketch",
|
||||
"npm",
|
||||
"sourcehut",
|
||||
"substack",
|
||||
"seti:folder",
|
||||
"seti:bsl",
|
||||
"seti:mdo",
|
||||
"seti:salesforce",
|
||||
"seti:asm",
|
||||
"seti:bicep",
|
||||
"seti:bazel",
|
||||
"seti:c",
|
||||
"seti:c-sharp",
|
||||
"seti:html",
|
||||
"seti:cpp",
|
||||
"seti:clojure",
|
||||
"seti:coldfusion",
|
||||
"seti:config",
|
||||
"seti:crystal",
|
||||
"seti:crystal_embedded",
|
||||
"seti:json",
|
||||
"seti:css",
|
||||
"seti:csv",
|
||||
"seti:xls",
|
||||
"seti:cu",
|
||||
"seti:cake",
|
||||
"seti:cake_php",
|
||||
"seti:d",
|
||||
"seti:word",
|
||||
"seti:elixir",
|
||||
"seti:elixir_script",
|
||||
"seti:hex",
|
||||
"seti:elm",
|
||||
"seti:favicon",
|
||||
"seti:f-sharp",
|
||||
"seti:git",
|
||||
"seti:go",
|
||||
"seti:godot",
|
||||
"seti:gradle",
|
||||
"seti:grails",
|
||||
"seti:graphql",
|
||||
"seti:hacklang",
|
||||
"seti:haml",
|
||||
"seti:mustache",
|
||||
"seti:haskell",
|
||||
"seti:haxe",
|
||||
"seti:jade",
|
||||
"seti:java",
|
||||
"seti:javascript",
|
||||
"seti:jinja",
|
||||
"seti:julia",
|
||||
"seti:karma",
|
||||
"seti:kotlin",
|
||||
"seti:dart",
|
||||
"seti:liquid",
|
||||
"seti:livescript",
|
||||
"seti:lua",
|
||||
"seti:markdown",
|
||||
"seti:argdown",
|
||||
"seti:info",
|
||||
"seti:clock",
|
||||
"seti:maven",
|
||||
"seti:nim",
|
||||
"seti:github",
|
||||
"seti:notebook",
|
||||
"seti:nunjucks",
|
||||
"seti:npm",
|
||||
"seti:ocaml",
|
||||
"seti:odata",
|
||||
"seti:perl",
|
||||
"seti:php",
|
||||
"seti:pipeline",
|
||||
"seti:pddl",
|
||||
"seti:plan",
|
||||
"seti:happenings",
|
||||
"seti:powershell",
|
||||
"seti:prisma",
|
||||
"seti:pug",
|
||||
"seti:puppet",
|
||||
"seti:purescript",
|
||||
"seti:python",
|
||||
"seti:react",
|
||||
"seti:rescript",
|
||||
"seti:R",
|
||||
"seti:ruby",
|
||||
"seti:rust",
|
||||
"seti:sass",
|
||||
"seti:spring",
|
||||
"seti:slim",
|
||||
"seti:smarty",
|
||||
"seti:sbt",
|
||||
"seti:scala",
|
||||
"seti:ethereum",
|
||||
"seti:stylus",
|
||||
"seti:svelte",
|
||||
"seti:swift",
|
||||
"seti:db",
|
||||
"seti:terraform",
|
||||
"seti:tex",
|
||||
"seti:default",
|
||||
"seti:twig",
|
||||
"seti:typescript",
|
||||
"seti:tsconfig",
|
||||
"seti:vala",
|
||||
"seti:vite",
|
||||
"seti:vue",
|
||||
"seti:wasm",
|
||||
"seti:wat",
|
||||
"seti:xml",
|
||||
"seti:yml",
|
||||
"seti:prolog",
|
||||
"seti:zig",
|
||||
"seti:zip",
|
||||
"seti:wgt",
|
||||
"seti:illustrator",
|
||||
"seti:photoshop",
|
||||
"seti:pdf",
|
||||
"seti:font",
|
||||
"seti:image",
|
||||
"seti:svg",
|
||||
"seti:sublime",
|
||||
"seti:code-search",
|
||||
"seti:shell",
|
||||
"seti:video",
|
||||
"seti:audio",
|
||||
"seti:windows",
|
||||
"seti:jenkins",
|
||||
"seti:babel",
|
||||
"seti:bower",
|
||||
"seti:docker",
|
||||
"seti:code-climate",
|
||||
"seti:eslint",
|
||||
"seti:firebase",
|
||||
"seti:firefox",
|
||||
"seti:gitlab",
|
||||
"seti:grunt",
|
||||
"seti:gulp",
|
||||
"seti:ionic",
|
||||
"seti:platformio",
|
||||
"seti:rollup",
|
||||
"seti:stylelint",
|
||||
"seti:yarn",
|
||||
"seti:webpack",
|
||||
"seti:lock",
|
||||
"seti:license",
|
||||
"seti:makefile",
|
||||
"seti:heroku",
|
||||
"seti:todo",
|
||||
"seti:ignored"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^\\<svg"
|
||||
}
|
||||
]
|
||||
},
|
||||
"attrs": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": [
|
||||
"string",
|
||||
"number",
|
||||
"boolean"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"text",
|
||||
"link"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"default": []
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"lastUpdated": {
|
||||
"anyOf": [
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"format": "date"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "unix-time"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "boolean"
|
||||
}
|
||||
]
|
||||
},
|
||||
"prev": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"link": {
|
||||
"type": "string"
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"next": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"link": {
|
||||
"type": "string"
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"sidebar": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"order": {
|
||||
"type": "number"
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"badge": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"variant": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"note",
|
||||
"danger",
|
||||
"success",
|
||||
"caution",
|
||||
"tip",
|
||||
"default"
|
||||
],
|
||||
"default": "default"
|
||||
},
|
||||
"class": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"text"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"attrs": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"not": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"default": {}
|
||||
},
|
||||
"banner": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"content"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"pagefind": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"draft": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"$schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||
}
|
||||
1
docs/.astro/content-assets.mjs
Normal file
1
docs/.astro/content-assets.mjs
Normal file
@ -0,0 +1 @@
|
||||
export default new Map();
|
||||
1
docs/.astro/content-modules.mjs
Normal file
1
docs/.astro/content-modules.mjs
Normal file
@ -0,0 +1 @@
|
||||
export default new Map();
|
||||
164
docs/.astro/content.d.ts
vendored
Normal file
164
docs/.astro/content.d.ts
vendored
Normal file
@ -0,0 +1,164 @@
|
||||
declare module 'astro:content' {
|
||||
export interface RenderResult {
|
||||
Content: import('astro/runtime/server/index.js').AstroComponentFactory;
|
||||
headings: import('astro').MarkdownHeading[];
|
||||
remarkPluginFrontmatter: Record<string, any>;
|
||||
}
|
||||
interface Render {
|
||||
'.md': Promise<RenderResult>;
|
||||
}
|
||||
|
||||
export interface RenderedContent {
|
||||
html: string;
|
||||
metadata?: {
|
||||
imagePaths: Array<string>;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'astro:content' {
|
||||
type Flatten<T> = T extends { [K: string]: infer U } ? U : never;
|
||||
|
||||
export type CollectionKey = keyof AnyEntryMap;
|
||||
export type CollectionEntry<C extends CollectionKey> = Flatten<AnyEntryMap[C]>;
|
||||
|
||||
export type ContentCollectionKey = keyof ContentEntryMap;
|
||||
export type DataCollectionKey = keyof DataEntryMap;
|
||||
|
||||
type AllValuesOf<T> = T extends any ? T[keyof T] : never;
|
||||
type ValidContentEntrySlug<C extends keyof ContentEntryMap> = AllValuesOf<
|
||||
ContentEntryMap[C]
|
||||
>['slug'];
|
||||
|
||||
export type ReferenceDataEntry<
|
||||
C extends CollectionKey,
|
||||
E extends keyof DataEntryMap[C] = string,
|
||||
> = {
|
||||
collection: C;
|
||||
id: E;
|
||||
};
|
||||
export type ReferenceContentEntry<
|
||||
C extends keyof ContentEntryMap,
|
||||
E extends ValidContentEntrySlug<C> | (string & {}) = string,
|
||||
> = {
|
||||
collection: C;
|
||||
slug: E;
|
||||
};
|
||||
|
||||
/** @deprecated Use `getEntry` instead. */
|
||||
export function getEntryBySlug<
|
||||
C extends keyof ContentEntryMap,
|
||||
E extends ValidContentEntrySlug<C> | (string & {}),
|
||||
>(
|
||||
collection: C,
|
||||
// Note that this has to accept a regular string too, for SSR
|
||||
entrySlug: E,
|
||||
): E extends ValidContentEntrySlug<C>
|
||||
? Promise<CollectionEntry<C>>
|
||||
: Promise<CollectionEntry<C> | undefined>;
|
||||
|
||||
/** @deprecated Use `getEntry` instead. */
|
||||
export function getDataEntryById<C extends keyof DataEntryMap, E extends keyof DataEntryMap[C]>(
|
||||
collection: C,
|
||||
entryId: E,
|
||||
): Promise<CollectionEntry<C>>;
|
||||
|
||||
export function getCollection<C extends keyof AnyEntryMap, E extends CollectionEntry<C>>(
|
||||
collection: C,
|
||||
filter?: (entry: CollectionEntry<C>) => entry is E,
|
||||
): Promise<E[]>;
|
||||
export function getCollection<C extends keyof AnyEntryMap>(
|
||||
collection: C,
|
||||
filter?: (entry: CollectionEntry<C>) => unknown,
|
||||
): Promise<CollectionEntry<C>[]>;
|
||||
|
||||
export function getEntry<
|
||||
C extends keyof ContentEntryMap,
|
||||
E extends ValidContentEntrySlug<C> | (string & {}),
|
||||
>(
|
||||
entry: ReferenceContentEntry<C, E>,
|
||||
): E extends ValidContentEntrySlug<C>
|
||||
? Promise<CollectionEntry<C>>
|
||||
: Promise<CollectionEntry<C> | undefined>;
|
||||
export function getEntry<
|
||||
C extends keyof DataEntryMap,
|
||||
E extends keyof DataEntryMap[C] | (string & {}),
|
||||
>(
|
||||
entry: ReferenceDataEntry<C, E>,
|
||||
): E extends keyof DataEntryMap[C]
|
||||
? Promise<DataEntryMap[C][E]>
|
||||
: Promise<CollectionEntry<C> | undefined>;
|
||||
export function getEntry<
|
||||
C extends keyof ContentEntryMap,
|
||||
E extends ValidContentEntrySlug<C> | (string & {}),
|
||||
>(
|
||||
collection: C,
|
||||
slug: E,
|
||||
): E extends ValidContentEntrySlug<C>
|
||||
? Promise<CollectionEntry<C>>
|
||||
: Promise<CollectionEntry<C> | undefined>;
|
||||
export function getEntry<
|
||||
C extends keyof DataEntryMap,
|
||||
E extends keyof DataEntryMap[C] | (string & {}),
|
||||
>(
|
||||
collection: C,
|
||||
id: E,
|
||||
): E extends keyof DataEntryMap[C]
|
||||
? string extends keyof DataEntryMap[C]
|
||||
? Promise<DataEntryMap[C][E]> | undefined
|
||||
: Promise<DataEntryMap[C][E]>
|
||||
: Promise<CollectionEntry<C> | undefined>;
|
||||
|
||||
/** Resolve an array of entry references from the same collection */
|
||||
export function getEntries<C extends keyof ContentEntryMap>(
|
||||
entries: ReferenceContentEntry<C, ValidContentEntrySlug<C>>[],
|
||||
): Promise<CollectionEntry<C>[]>;
|
||||
export function getEntries<C extends keyof DataEntryMap>(
|
||||
entries: ReferenceDataEntry<C, keyof DataEntryMap[C]>[],
|
||||
): Promise<CollectionEntry<C>[]>;
|
||||
|
||||
export function render<C extends keyof AnyEntryMap>(
|
||||
entry: AnyEntryMap[C][string],
|
||||
): Promise<RenderResult>;
|
||||
|
||||
export function reference<C extends keyof AnyEntryMap>(
|
||||
collection: C,
|
||||
): import('astro/zod').ZodEffects<
|
||||
import('astro/zod').ZodString,
|
||||
C extends keyof ContentEntryMap
|
||||
? ReferenceContentEntry<C, ValidContentEntrySlug<C>>
|
||||
: ReferenceDataEntry<C, keyof DataEntryMap[C]>
|
||||
>;
|
||||
// Allow generic `string` to avoid excessive type errors in the config
|
||||
// if `dev` is not running to update as you edit.
|
||||
// Invalid collection names will be caught at build time.
|
||||
export function reference<C extends string>(
|
||||
collection: C,
|
||||
): import('astro/zod').ZodEffects<import('astro/zod').ZodString, never>;
|
||||
|
||||
type ReturnTypeOrOriginal<T> = T extends (...args: any[]) => infer R ? R : T;
|
||||
type InferEntrySchema<C extends keyof AnyEntryMap> = import('astro/zod').infer<
|
||||
ReturnTypeOrOriginal<Required<ContentConfig['collections'][C]>['schema']>
|
||||
>;
|
||||
|
||||
type ContentEntryMap = {
|
||||
|
||||
};
|
||||
|
||||
type DataEntryMap = {
|
||||
"docs": Record<string, {
|
||||
id: string;
|
||||
body?: string;
|
||||
collection: "docs";
|
||||
data: any;
|
||||
rendered?: RenderedContent;
|
||||
filePath?: string;
|
||||
}>;
|
||||
|
||||
};
|
||||
|
||||
type AnyEntryMap = ContentEntryMap & DataEntryMap;
|
||||
|
||||
export type ContentConfig = typeof import("../src/content.config.mjs");
|
||||
}
|
||||
1
docs/.astro/data-store.json
Normal file
1
docs/.astro/data-store.json
Normal file
@ -0,0 +1 @@
|
||||
[["Map",1,2],"meta::meta",["Map",3,4,5,6],"astro-version","5.9.3","astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"compressHTML\":true,\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"static\",\"scopedStyleStrategy\":\"attribute\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":true,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":4321,\"streaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"domains\":[],\"remotePatterns\":[],\"experimentalDefaultStyles\":true},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":true},\"security\":{\"checkOrigin\":true},\"env\":{\"schema\":{},\"validateSecrets\":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"responsiveImages\":false,\"headingIdCompat\":false,\"preserveScriptOrder\":false,\"csp\":false},\"legacy\":{\"collections\":false}}"]
|
||||
5
docs/.astro/settings.json
Normal file
5
docs/.astro/settings.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"_variables": {
|
||||
"lastUpdateCheck": 1750832446593
|
||||
}
|
||||
}
|
||||
2
docs/.astro/types.d.ts
vendored
Normal file
2
docs/.astro/types.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/// <reference types="astro/client" />
|
||||
/// <reference path="content.d.ts" />
|
||||
BIN
docs/public/assets/images/changelog/jan_nano_128.gif
Normal file
BIN
docs/public/assets/images/changelog/jan_nano_128.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.7 MiB |
BIN
docs/public/assets/images/changelog/jn128.gif
Normal file
BIN
docs/public/assets/images/changelog/jn128.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 MiB |
BIN
docs/public/assets/videos/jan-nano-demo.mp4
Normal file
BIN
docs/public/assets/videos/jan-nano-demo.mp4
Normal file
Binary file not shown.
@ -11,6 +11,21 @@
|
||||
"type": "page",
|
||||
"title": "Documentation"
|
||||
},
|
||||
"cortex": {
|
||||
"type": "page",
|
||||
"title": "Cortex",
|
||||
"display": "hidden"
|
||||
},
|
||||
"integrations": {
|
||||
"type": "page",
|
||||
"title": "Integrations",
|
||||
"display": "hidden"
|
||||
},
|
||||
"platforms": {
|
||||
"type": "page",
|
||||
"title": "Platforms",
|
||||
"display": "hidden"
|
||||
},
|
||||
"changelog": {
|
||||
"type": "page",
|
||||
"title": "Changelog",
|
||||
|
||||
21
docs/src/pages/changelog/2025-06-26-jan-nano-mcp.mdx
Normal file
21
docs/src/pages/changelog/2025-06-26-jan-nano-mcp.mdx
Normal file
@ -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"
|
||||
|
||||
<ChangelogHeader title="Jan v0.6.3 brings with it MCP and our latest model!" date="2025-06-26" ogImage="/assets/images/changelog/jn128.gif" />
|
||||
|
||||
## 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).
|
||||
BIN
docs/src/pages/docs/_assets/jan-nano-bench.png
Normal file
BIN
docs/src/pages/docs/_assets/jan-nano-bench.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 205 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 614 KiB After Width: | Height: | Size: 231 KiB |
@ -1,4 +1,8 @@
|
||||
{
|
||||
"-- Switcher": {
|
||||
"type": "separator",
|
||||
"title": "Switcher"
|
||||
},
|
||||
"index": "Overview",
|
||||
"how-to-separator": {
|
||||
"title": "HOW TO",
|
||||
@ -6,8 +10,7 @@
|
||||
},
|
||||
"desktop": "Install 👋 Jan",
|
||||
"threads": "Start Chatting",
|
||||
"manage-models": "Manage Models",
|
||||
"menlo-models": "Menlo Models",
|
||||
"jan-models": "Use Jan Models",
|
||||
"assistants": "Create Assistants",
|
||||
|
||||
"tutorials-separators": {
|
||||
@ -16,8 +19,7 @@
|
||||
},
|
||||
"quickstart": "Quickstart",
|
||||
"remote-models": "Connect to Remote Models",
|
||||
"server-examples": "Provide AI to Tools",
|
||||
"mcp": "Model Context Protocol",
|
||||
"server-examples": "Integrations",
|
||||
|
||||
"explanation-separator": {
|
||||
"title": "EXPLANATION",
|
||||
@ -26,18 +28,25 @@
|
||||
"llama-cpp": "Local AI Engine",
|
||||
"api-server": "Server Overview",
|
||||
"data-folder": "Jan Data Folder",
|
||||
"privacy": "Privacy",
|
||||
"privacy-policy": {
|
||||
"type": "page",
|
||||
"display": "hidden",
|
||||
"title": "Privacy Policy"
|
||||
},
|
||||
|
||||
"advanced-separator": {
|
||||
"title": "ADVANCED",
|
||||
"type": "separator"
|
||||
},
|
||||
"manage-models": "Manage Models",
|
||||
"mcp": "Model Context Protocol",
|
||||
|
||||
"reference-separator": {
|
||||
"title": "REFERENCE",
|
||||
"type": "separator"
|
||||
},
|
||||
"settings": "Settings",
|
||||
"troubleshooting": "Troubleshooting",
|
||||
"model-parameters": "Model Parameters"
|
||||
"model-parameters": "Model Parameters",
|
||||
"privacy": "Privacy"
|
||||
}
|
||||
|
||||
139
docs/src/pages/docs/jan-models/jan-nano-128.mdx
Normal file
139
docs/src/pages/docs/jan-models/jan-nano-128.mdx
Normal file
@ -0,0 +1,139 @@
|
||||
---
|
||||
title: Jan Nano 128k
|
||||
description: Jan Models
|
||||
keywords:
|
||||
[
|
||||
Jan,
|
||||
Jan Models,
|
||||
Jan Model,
|
||||
Jan Model List,
|
||||
Menlo Models,
|
||||
Menlo Model,
|
||||
Jan-Nano-Gguf,
|
||||
ReZero,
|
||||
Model Context Protocol,
|
||||
MCP,
|
||||
]
|
||||
---
|
||||
|
||||
import { Callout } from 'nextra/components'
|
||||
|
||||
# Jan-Nano-128k
|
||||
|
||||
> Enabling deeper research through extended context understanding.
|
||||
|
||||
Jan-Nano-128k represents a notable advancement in compact language models for different applications. Building upon the
|
||||
success of Jan-Nano-32k, this enhanced version features a native 128k context window that enables deeper, more comprehensive
|
||||
research capabilities without the performance degradation typically associated with context extension methods.
|
||||
|
||||
You can have a look at all of our models, and download them from the HuggingFace [Menlo Models page](https://huggingface.co/Menlo).
|
||||
|
||||
**Key Improvements:**
|
||||
|
||||
- 🔍 Deeper Research: Extended context allows for processing entire research papers, lengthy documents, and complex multi-turn conversations
|
||||
- ⚡ Native 128k Window: Built to handle long contexts efficiently, maintaining performance across the full context range
|
||||
- 📈 Enhanced Performance: Unlike traditional context extension methods, Jan-Nano-128k's performance remains consistent with longer contexts
|
||||
|
||||
This model maintains full compatibility with Model Context Protocol (MCP) servers while dramatically expanding the scope of research
|
||||
tasks it can handle in a single session.
|
||||
|
||||
|
||||
## Why Jan-Nano-128k?
|
||||
|
||||
Most small models hit a wall at 8-32k tokens. Jan-Nano-128k goes beyond this limitation with a native 128k context window—that's roughly
|
||||
300 pages of text or an entire novel's worth of information processed simultaneously.
|
||||
|
||||
Unlike YaRN or PI methods that retrofit models beyond their limits and degrade performance, Jan-Nano-128k was architecturally rewired for
|
||||
128k contexts from the ground up. The result: an inverse scaling behavior where performance actually improves with longer contexts,
|
||||
maintaining consistent accuracy from 1k to 128k tokens as the model leverages more information for synthesis.
|
||||
|
||||
|
||||
<Callout type="info">
|
||||
**Position Interpolation (PI):** A method that extends a model's context by scaling down position indices to fit within the original context
|
||||
window. For example, to extend a 4k model to 32k, PI compresses the 32k positions into the original 4k range by dividing each position by 8.
|
||||
|
||||
**YaRN (Yet another RoPE extensioN method):** A more sophisticated context extension method that preserves frequently occurring tokens while
|
||||
selectively scaling others. YaRN divides position embeddings into frequency groups and applies different scaling factors to each, resulting
|
||||
in more efficient training and better performance than PI.
|
||||
|
||||
The key difference is that PI applies uniform scaling across all dimensions, while YaRN uses targeted interpolation based on frequency analysis—preserving
|
||||
high-frequency information that's crucial for distinguishing nearby tokens while interpolating lower frequencies more aggressively.
|
||||
</Callout>
|
||||
|
||||
**Applications unlocked:**
|
||||
- **Academic**: Extract key findings from 50+ papers simultaneously
|
||||
- **Legal**: Pinpoint relevant clauses across thousand-page contracts
|
||||
- **Code**: Trace specific functions through massive codebases
|
||||
- **Business**: Distill insights from quarters of financial data
|
||||
- **Content**: Maintain narrative coherence across book-length outputs
|
||||
|
||||
**MCP Usage:** Jan-Nano-128k doesn't memorize, it orchestrates. With MCP integration, it becomes a research conductor that fetches dozens
|
||||
of sources, holds everything in active memory, extracts precisely what's needed, and synthesizes findings across a marathon research session. It's
|
||||
not about understanding every word; it's about finding the needle in a haystack of haystacks.
|
||||
|
||||
## Evaluation
|
||||
|
||||
Jan-Nano-128k has been rigorously evaluated on the SimpleQA benchmark using our MCP-based methodology, demonstrating superior performance compared to its predecessor:
|
||||
|
||||

|
||||
|
||||
**Key findings:**
|
||||
- 15% improvement over Jan-Nano-32k on complex multi-document tasks
|
||||
- Consistent performance across all context lengths (no cliff at 64k like other extended models)
|
||||
- Superior citation accuracy when handling 10+ sources simultaneously
|
||||
|
||||
## 🖥️ How to Run Locally
|
||||
|
||||
### Demo
|
||||
|
||||
<video width="100%" controls>
|
||||
<source src="/assets/videos/jan-nano-demo.mp4" type="video/mp4" />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
|
||||
### Quick Start Guide
|
||||
|
||||
1. **Download Jan**
|
||||
2. **Download Jan-Nano-128k**
|
||||
3. **Enable MCP**, the serper or the exa MCPs work very well with Jan-Nano-128k
|
||||
4. **Start researching**
|
||||
|
||||
### Usage
|
||||
|
||||
Deploy using VLLM:
|
||||
|
||||
```bash
|
||||
vllm serve Menlo/Jan-nano-128k \
|
||||
--host 0.0.0.0 \
|
||||
--port 1234 \
|
||||
--enable-auto-tool-choice \
|
||||
--tool-call-parser hermes \
|
||||
--rope-scaling '{"rope_type":"yarn","factor":3.2,"original_max_position_embeddings":40960}' --max-model-len 131072
|
||||
```
|
||||
|
||||
Or with `llama-server` from `llama.cpp`:
|
||||
|
||||
```bash
|
||||
llama-server ... --rope-scaling yarn --rope-scale 3.2 --yarn-orig-ctx 40960
|
||||
```
|
||||
|
||||
**Note:** The chat template is included in the tokenizer. For troubleshooting, download the [Non-think chat template](https://qwen.readthedocs.io/en/latest/_downloads/c101120b5bebcc2f12ec504fc93a965e/qwen3_nonthinking.jinja).
|
||||
|
||||
### Recommended Sampling Parameters
|
||||
|
||||
```yaml
|
||||
Temperature: 0.7
|
||||
Top-p: 0.8
|
||||
Top-k: 20
|
||||
Min-p: 0.0
|
||||
```
|
||||
|
||||
### Hardware Requirements
|
||||
- **Minimum**: 16GB RAM for Q4 quantization
|
||||
- **Recommended**: 24GB RAM for Q8 quantization
|
||||
- **Optimal**: 32GB+ RAM for full precision
|
||||
|
||||
## 🤝 Community & Support
|
||||
- **Discussions**: [HuggingFace Community](https://huggingface.co/Menlo/Jan-nano-128k/discussions)
|
||||
- **Issues**: [GitHub Repository](https://github.com/menloresearch/deep-research/issues)
|
||||
- **Discord**: Join our research community for tips and best practices
|
||||
@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Jan Nano
|
||||
title: Jan Nano 32k
|
||||
description: Jan-Nano-Gguf Model
|
||||
keywords:
|
||||
[
|
||||
@ -20,15 +20,26 @@ import { Callout } from 'nextra/components'
|
||||
|
||||
# Jan Nano
|
||||
|
||||
Jan-Nano is a compact 4-billion parameter language model specifically designed and trained for deep
|
||||
research tasks. This model has been optimized to work seamlessly with Model Context Protocol (MCP) servers,
|
||||
enabling efficient integration with various research tools and data sources.
|
||||

|
||||
|
||||
## Why Jan Nano?
|
||||
|
||||
Most language models face a fundamental tradeoff where powerful capabilities require a lot of computational resources. Jan
|
||||
Nano breaks this constraint through a focused design philosophy where instead of trying to know everything, it excels at
|
||||
knowing how to find anything.
|
||||
|
||||
|
||||
## What is Jan Nano?
|
||||
|
||||
Jan Nano is a compact 4-billion parameter language model specifically designed and trained for deep research tasks.
|
||||
This model has been optimized to work seamlessly with Model Context Protocol (MCP) servers, enabling efficient integration
|
||||
with various research tools and data sources.
|
||||
|
||||
The model and its different model variants are fully supported by Jan.
|
||||
|
||||
<Callout type="info">
|
||||
Jan-Nano can be used by Jan's stable version but its true capabilities shine in Jan's beta version, which
|
||||
offers MCP support. You can download Jan's beta version from [here](https://jan.ai/docs/desktop/beta).
|
||||
To use Jan-Nano, you will need to use a search engine via MCP. You can enable MCP in the **Settings**
|
||||
tab under **Advanced Settings**.
|
||||
</Callout>
|
||||
|
||||
|
||||
@ -45,29 +56,29 @@ The model and its different model variants are fully supported by Jan.
|
||||
- RTX 30/40 series or newer
|
||||
|
||||
|
||||
## Using Jan-Nano
|
||||
## Using Jan-Nano-32k
|
||||
|
||||
### Step 1
|
||||
Download Jan Beta from [here](https://jan.ai/docs/desktop/beta).
|
||||
**Step 1**
|
||||
Download Jan from [here](https://jan.ai/docs/desktop/).
|
||||
|
||||
### Step 2
|
||||
**Step 2**
|
||||
Go to the Hub Tab, search for Jan-Nano-Gguf, and click on the download button to the best model size for your system.
|
||||
|
||||

|
||||
|
||||
### Step 3
|
||||
**Step 3**
|
||||
Go to **Settings** > **Model Providers** > **Llama.cpp** click on the pencil icon and enable tool use for Jan-Nano-Gguf.
|
||||
|
||||
### Step 4
|
||||
**Step 4**
|
||||
To take advantage of Jan-Nano's full capabilities, you need to enable MCP support. We're going to use it with Serper's
|
||||
API. You can get a free API key from [here](https://serper.dev/). Sign up and they will immediately generate one for you.
|
||||
|
||||
### Step 5
|
||||
**Step 5**
|
||||
Add the serper MCP to Jan via the **Settings** > **MCP Servers** tab.
|
||||
|
||||

|
||||
|
||||
### Step 6
|
||||
**Step 6**
|
||||
Open up a new chat and ask Jan-Nano to search the web for you.
|
||||
|
||||

|
||||
@ -21,6 +21,49 @@ import { Callout, Steps } from 'nextra/components'
|
||||
|
||||
# Using the Model Context Protocol (MCP) in Jan
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph "What is MCP?"
|
||||
You[You using Jan Desktop]
|
||||
Claude[Jan AI Assistant]
|
||||
|
||||
subgraph "Your Connected Tools"
|
||||
Files[📁 Your Files<br/>Documents, folders,<br/>text files]
|
||||
Database[📊 Your Data<br/>Spreadsheets,<br/>databases]
|
||||
WebServices[🌐 Online Services<br/>GitHub, Slack,<br/>Google Drive]
|
||||
Custom[🔧 Custom Tools<br/>Special programs<br/>you've added]
|
||||
end
|
||||
|
||||
subgraph "What Jan Can Do"
|
||||
Read[Read & Understand<br/>- View your files<br/>- Check your data<br/>- See updates]
|
||||
Action[Take Actions<br/>- Search for info<br/>- Create content<br/>- Run commands]
|
||||
Templates[Use Templates<br/>- Common tasks<br/>- Saved prompts<br/>- Workflows]
|
||||
end
|
||||
end
|
||||
|
||||
You --> Claude
|
||||
Claude -->|"Can I see this file?"| Files
|
||||
Claude -->|"What's in my database?"| Database
|
||||
Claude -->|"Check my GitHub"| WebServices
|
||||
Claude -->|"Run this tool"| Custom
|
||||
|
||||
Files --> Read
|
||||
Database --> Read
|
||||
WebServices --> Action
|
||||
Custom --> Templates
|
||||
|
||||
style You fill:transparent
|
||||
style Claude fill:transparent
|
||||
style Files fill:transparent
|
||||
style Database fill:transparent
|
||||
style WebServices fill:transparent
|
||||
style Custom fill:transparent
|
||||
style Read fill:transparent
|
||||
style Action fill:transparent
|
||||
style Templates fill:transparent
|
||||
```
|
||||
|
||||
|
||||
Jan now supports the **Model Context Protocol (MCP)**, an open standard designed to allow language models to
|
||||
interact with external tools and data sources.
|
||||
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
{
|
||||
"overview": {
|
||||
"title": "Overview",
|
||||
"href": "/docs/menlo-models/overview"
|
||||
},
|
||||
"jan-nano": {
|
||||
"title": "Jan Nano",
|
||||
"href": "/docs/menlo-models/jan-nano"
|
||||
}
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
---
|
||||
title: Overview
|
||||
description: Jan Models
|
||||
keywords:
|
||||
[
|
||||
Jan,
|
||||
Jan Models,
|
||||
Jan Model,
|
||||
Jan Model List,
|
||||
Menlo Models,
|
||||
Menlo Model,
|
||||
Jan-Nano-Gguf,
|
||||
ReZero,
|
||||
Model Context Protocol,
|
||||
MCP,
|
||||
]
|
||||
---
|
||||
|
||||
# Menlo Models
|
||||
|
||||
At Menlo, we have focused on creating a series of models that are optimized for all sorts of tasks, including
|
||||
web search, deep research, robotic control, and using MCPs. Our latest model, Jan-Nano-Gguf, is available in Jan
|
||||
right now providing excellent results on tasks that use MCPs.
|
||||
|
||||
You can have a look at all of our models, and download them from the HuggingFace [Menlo Models page](https://huggingface.co/Menlo).
|
||||
|
||||
## Jan-Nano-Gguf (Available in Jan right now 🚀)
|
||||
|
||||

|
||||
|
||||
Jan-Nano-Gguf is a 4-billion parameter model that is optimized for deep research tasks. It has been trained on a
|
||||
variety of datasets and is designed to be used with the Model Context Protocol (MCP) servers.
|
||||
|
||||
|
||||
## ReZero
|
||||
|
||||
ReZero (Retry-Zero) is a reinforcement learning framework that improves RAG systems by rewarding LLMs for retrying
|
||||
failed queries. Traditional RAG approaches struggle when initial searches fail, but ReZero encourages persistence and
|
||||
alternative strategies. This increases accuracy from 25% to 46.88% in complex information-seeking tasks.
|
||||
|
||||
@ -1,70 +0,0 @@
|
||||
---
|
||||
title: Open Interpreter
|
||||
description: A step-by-step guide on integrating Jan with Open Interpreter.
|
||||
keywords:
|
||||
[
|
||||
Jan,
|
||||
Customizable Intelligence, LLM,
|
||||
local AI,
|
||||
privacy focus,
|
||||
free and open source,
|
||||
private and offline,
|
||||
conversational AI,
|
||||
no-subscription fee,
|
||||
large language models,
|
||||
Open Interpreter integration,
|
||||
Open Interpreter,
|
||||
]
|
||||
---
|
||||
|
||||
import { Callout, Steps } from 'nextra/components'
|
||||
|
||||
# Open Interpreter
|
||||
|
||||
## Integrate Open Interpreter with Jan
|
||||
|
||||
[Open Interpreter](https://github.com/KillianLucas/open-interpreter/) lets LLMs run code (Python, Javascript, Shell, and more) locally. After installing, you can chat with Open Interpreter through a ChatGPT-like interface in your terminal by running `interpreter`. To integrate Open Interpreter with Jan, follow the steps below:
|
||||
|
||||
<Steps>
|
||||
|
||||
### Step 1: Install Open Interpreter
|
||||
|
||||
1. Install Open Interpreter by running:
|
||||
|
||||
```bash
|
||||
pip install open-interpreter
|
||||
```
|
||||
|
||||
2. A Rust compiler is required to install Open Interpreter. If not already installed, run the following command or go to [this page](https://rustup.rs/) if you are running on Windows:
|
||||
|
||||
```bash
|
||||
sudo apt install rustc
|
||||
```
|
||||
|
||||
<Callout type='info'>
|
||||
The Rust compiler is necessary for building some native extensions that Open Interpreter requires.
|
||||
</Callout>
|
||||
|
||||
### Step 2: Configure Jan's Local API Server
|
||||
|
||||
Before using Open Interpreter, configure the model in `Settings` > `My Model` for Jan and activate its local API server.
|
||||
|
||||
#### Enabling Jan API Server
|
||||
|
||||
1. Click the `<>` button to access the **Local API Server** section in Jan.
|
||||
|
||||
2. Configure the server settings, including **IP Port**, **Cross-Origin-Resource-Sharing (CORS)**, and **Verbose Server Logs**.
|
||||
|
||||
3. Click **Start Server**.
|
||||
|
||||
### Step 3: Set the Open Interpreter Environment
|
||||
|
||||
1. For integration, provide the API Base (`http://localhost:1337/v1`) and the model ID (e.g., `mistral-ins-7b-q4`) when running Open Interpreter. For example, see the code below:
|
||||
|
||||
```zsh
|
||||
interpreter --api_base http://localhost:1337/v1 --model mistral-ins-7b-q4
|
||||
```
|
||||
|
||||
> **Open Interpreter is now ready for use!**
|
||||
|
||||
</Steps>
|
||||
9
docs/src/pages/platforms/_meta.json
Normal file
9
docs/src/pages/platforms/_meta.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"-- Switcher": {
|
||||
"type": "separator",
|
||||
"title": "Switcher"
|
||||
},
|
||||
"index": {
|
||||
"display": "hidden"
|
||||
}
|
||||
}
|
||||
87
docs/src/pages/platforms/index.mdx
Normal file
87
docs/src/pages/platforms/index.mdx
Normal file
@ -0,0 +1,87 @@
|
||||
---
|
||||
title: Coming Soon
|
||||
description: Exciting new features and platforms are on the way. Stay tuned for Jan Web, Jan Mobile, and our API Platform.
|
||||
keywords:
|
||||
[
|
||||
Jan,
|
||||
Customizable Intelligence, LLM,
|
||||
local AI,
|
||||
privacy focus,
|
||||
free and open source,
|
||||
private and offline,
|
||||
conversational AI,
|
||||
no-subscription fee,
|
||||
large language models,
|
||||
coming soon,
|
||||
Jan Web,
|
||||
Jan Mobile,
|
||||
API Platform,
|
||||
]
|
||||
---
|
||||
|
||||
import { Callout } from 'nextra/components'
|
||||
|
||||
<div className="text-center py-12">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent mb-4 py-2">
|
||||
🚀 Coming Soon
|
||||
</h1>
|
||||
<p className="text-xl text-gray-600 dark:text-gray-300 max-w-2xl mx-auto">
|
||||
We're working on the next stage of Jan - making our local assistant more powerful and available in more platforms.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-4xl mx-auto mb-12">
|
||||
<div className="p-6 border border-gray-200 dark:border-gray-700 rounded-lg bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20">
|
||||
<div className="text-3xl mb-3">🌐</div>
|
||||
<h3 className="text-lg font-semibold mb-2">Jan Web</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Access Jan directly from your browser with our powerful web interface
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-6 border border-gray-200 dark:border-gray-700 rounded-lg bg-gradient-to-br from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20">
|
||||
<div className="text-3xl mb-3">📱</div>
|
||||
<h3 className="text-lg font-semibold mb-2">Jan Mobile</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Take Jan on the go with our native mobile applications
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-6 border border-gray-200 dark:border-gray-700 rounded-lg bg-gradient-to-br from-purple-50 to-pink-50 dark:from-purple-900/20 dark:to-pink-900/20">
|
||||
<div className="text-3xl mb-3">⚡</div>
|
||||
<h3 className="text-lg font-semibold mb-2">API Platform</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Integrate Jan's capabilities into your applications with our API
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Callout type="info">
|
||||
**Stay Updated**: Follow our [GitHub repository](https://github.com/menloresearch/jan) and join our [Discord community](https://discord.com/invite/FTk2MvZwJH) for the latest updates on these exciting releases!
|
||||
</Callout>
|
||||
|
||||
<div className="mt-12">
|
||||
<h2 className="text-2xl font-semibold mb-6">What to Expect</h2>
|
||||
<div className="text-left max-w-2xl mx-auto space-y-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-green-500 text-xl">✓</span>
|
||||
<div>
|
||||
<strong>Seamless Experience:</strong> Unified interface across all platforms
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-green-500 text-xl">✓</span>
|
||||
<div>
|
||||
<strong>Privacy First:</strong> Same privacy-focused approach you trust
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-green-500 text-xl">✓</span>
|
||||
<div>
|
||||
<strong>Developer Friendly:</strong> Robust APIs and comprehensive documentation
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -65,6 +65,54 @@ const config: DocsThemeConfig = {
|
||||
</div>
|
||||
),
|
||||
},
|
||||
sidebar: {
|
||||
titleComponent: ({ type, title }) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const { asPath } = useRouter()
|
||||
if (type === 'separator' && title === 'Switcher') {
|
||||
return (
|
||||
<div className="-mx-2 hidden md:block">
|
||||
{[
|
||||
{ title: 'Jan', path: '/docs', Icon: LibraryBig },
|
||||
{
|
||||
title: 'Jan Web',
|
||||
path: '/platforms',
|
||||
Icon: BrainCircuit,
|
||||
},
|
||||
{ title: 'Jan Mobile', path: '/platforms', Icon: Blocks },
|
||||
{
|
||||
title: 'API Platform',
|
||||
path: '/platforms',
|
||||
Icon: Computer,
|
||||
},
|
||||
].map((item) =>
|
||||
asPath.startsWith(item.path) ? (
|
||||
<div
|
||||
key={item.path}
|
||||
className="group mb-3 flex flex-row items-center gap-3 nx-text-primary-800 dark:nx-text-primary-600"
|
||||
>
|
||||
<item.Icon className="w-7 h-7 p-1 border border-gray-200 dark:border-gray-700 rounded nx-bg-primary-100 dark:nx-bg-primary-400/10" />
|
||||
{item.title}
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
href={item.path}
|
||||
key={item.path}
|
||||
className="group mb-3 flex flex-row items-center gap-3 text-gray-500 hover:text-primary/100"
|
||||
>
|
||||
<item.Icon className="w-7 h-7 p-1 border rounded border-gray-200 dark:border-gray-700" />
|
||||
{item.title}
|
||||
</Link>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return title
|
||||
},
|
||||
defaultMenuCollapseLevel: 1,
|
||||
toggleButton: true,
|
||||
},
|
||||
toc: {
|
||||
backToTop: true,
|
||||
},
|
||||
@ -83,14 +131,14 @@ const config: DocsThemeConfig = {
|
||||
name="description"
|
||||
content={
|
||||
frontMatter?.description ||
|
||||
`Run LLMs like Mistral or Llama2 locally and offline on your computer, or connect to remote AI APIs like OpenAI’s GPT-4 or Groq.`
|
||||
`Run LLMs like Mistral or Llama2 locally and offline on your computer, or connect to remote AI APIs like OpenAI's GPT-4 or Groq.`
|
||||
}
|
||||
/>
|
||||
<meta
|
||||
name="og:description"
|
||||
content={
|
||||
frontMatter?.description ||
|
||||
`Run LLMs like Mistral or Llama2 locally and offline on your computer, or connect to remote AI APIs like OpenAI’s GPT-4 or Groq.`
|
||||
`Run LLMs like Mistral or Llama2 locally and offline on your computer, or connect to remote AI APIs like OpenAI's GPT-4 or Groq.`
|
||||
}
|
||||
/>
|
||||
<link
|
||||
|
||||
@ -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
|
||||
|
||||
@ -28,7 +28,7 @@ type Data<T> = {
|
||||
/**
|
||||
* Defaul mode sources
|
||||
*/
|
||||
const defaultModelSources = ['Menlo/Jan-nano-gguf']
|
||||
const defaultModelSources = ['Menlo/Jan-nano-gguf', 'Menlo/Jan-nano-128k-gguf']
|
||||
|
||||
/**
|
||||
* A extension for models
|
||||
|
||||
@ -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/<locale>/<file>.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/<locale>/<file>.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()
|
||||
main()
|
||||
|
||||
@ -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/<locale>/<file>.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/<locale>/<file>.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()
|
||||
findMissingTranslations()
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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) => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<OutOfContextModal />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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 = ({
|
||||
</span>
|
||||
|
||||
<div className="flex-1"></div>
|
||||
{!isProd && capabilities.length > 0 && (
|
||||
{capabilities.length > 0 && (
|
||||
<div className="flex-shrink-0 -mr-1.5">
|
||||
<Capabilities capabilities={capabilities} />
|
||||
</div>
|
||||
|
||||
@ -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 (
|
||||
<div className="flex h-full w-44 shrink-0 px-1.5 pt-3 border-r border-main-view-fg/5">
|
||||
<div className="flex flex-col gap-1 w-full text-main-view-fg/90 font-medium">
|
||||
|
||||
@ -147,7 +147,6 @@ export const ThreadContent = memo(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
streamTools?: any
|
||||
contextOverflowModal?: React.ReactNode | null
|
||||
showContextOverflowModal?: () => Promise<unknown>
|
||||
updateMessage: (item: ThreadMessage, message: string) => void
|
||||
}
|
||||
) => {
|
||||
@ -199,10 +198,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])
|
||||
|
||||
|
||||
@ -65,7 +65,7 @@ export function CortexFailureDialog() {
|
||||
<DialogFooter className="flex gap-2">
|
||||
<Button
|
||||
asChild
|
||||
variant="default"
|
||||
variant="link"
|
||||
className="bg-transparent border border-main-view-fg/20 hover:bg-main-view-fg/4"
|
||||
onClick={() => {
|
||||
setShowDialog(false)
|
||||
|
||||
@ -35,7 +35,16 @@ export const DialogDeleteModel = ({
|
||||
const removeModel = async () => {
|
||||
deleteModelCache(selectedModelId)
|
||||
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(
|
||||
t('providers:deleteModel.title', { modelId: selectedModel?.id }),
|
||||
{
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { t } from 'i18next'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@ -8,108 +7,67 @@ import {
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
|
||||
import { ReactNode, useCallback, useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useContextSizeApproval } from '@/hooks/useModelContextApproval'
|
||||
import { useTranslation } from '@/i18n'
|
||||
|
||||
export function useOutOfContextPromiseModal() {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [modalProps, setModalProps] = useState<{
|
||||
resolveRef:
|
||||
| ((value: 'ctx_len' | 'context_shift' | undefined) => void)
|
||||
| null
|
||||
}>({
|
||||
resolveRef: null,
|
||||
})
|
||||
// Function to open the modal and return a Promise
|
||||
const showModal = useCallback(() => {
|
||||
return new Promise((resolve) => {
|
||||
setModalProps({
|
||||
resolveRef: resolve,
|
||||
})
|
||||
setIsOpen(true)
|
||||
})
|
||||
}, [])
|
||||
export default function OutOfContextPromiseModal() {
|
||||
const { t } = useTranslation()
|
||||
const { isModalOpen, modalProps, setModalOpen } = useContextSizeApproval()
|
||||
if (!modalProps) {
|
||||
return null
|
||||
}
|
||||
const { onApprove, onDeny } = modalProps
|
||||
|
||||
const PromiseModal = useCallback((): ReactNode => {
|
||||
if (!isOpen) {
|
||||
return null
|
||||
const handleContextLength = () => {
|
||||
onApprove('ctx_len')
|
||||
}
|
||||
|
||||
const handleContextShift = () => {
|
||||
onApprove('context_shift')
|
||||
}
|
||||
|
||||
const handleDialogOpen = (open: boolean) => {
|
||||
setModalOpen(open)
|
||||
if (!open) {
|
||||
onDeny()
|
||||
}
|
||||
}
|
||||
|
||||
const handleContextLength = () => {
|
||||
setIsOpen(false)
|
||||
if (modalProps.resolveRef) {
|
||||
modalProps.resolveRef('ctx_len')
|
||||
}
|
||||
}
|
||||
|
||||
const handleContextShift = () => {
|
||||
setIsOpen(false)
|
||||
if (modalProps.resolveRef) {
|
||||
modalProps.resolveRef('context_shift')
|
||||
}
|
||||
}
|
||||
const handleCancel = () => {
|
||||
setIsOpen(false)
|
||||
if (modalProps.resolveRef) {
|
||||
modalProps.resolveRef(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onOpenChange={(open) => {
|
||||
setIsOpen(open)
|
||||
if (!open) handleCancel()
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{t('outOfContextError.title', 'Out of context error')}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogDescription>
|
||||
{t(
|
||||
'outOfContextError.description',
|
||||
'This chat is reaching the AI’s memory limit, like a whiteboard filling up. We can expand the memory window (called context size) so it remembers more, but it may use more of your computer’s memory. We can also truncate the input, which means it will forget some of the chat history to make room for new messages.'
|
||||
)}
|
||||
<br />
|
||||
<br />
|
||||
{t(
|
||||
'outOfContextError.increaseContextSizeDescription',
|
||||
'Do you want to increase the context size?'
|
||||
)}
|
||||
</DialogDescription>
|
||||
<DialogFooter className="flex gap-2">
|
||||
<Button
|
||||
variant="default"
|
||||
className="bg-transparent border border-main-view-fg/20 hover:bg-main-view-fg/4"
|
||||
onClick={() => {
|
||||
handleContextShift()
|
||||
setIsOpen(false)
|
||||
}}
|
||||
>
|
||||
{t('outOfContextError.truncateInput', 'Truncate Input')}
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
onClick={() => {
|
||||
handleContextLength()
|
||||
setIsOpen(false)
|
||||
}}
|
||||
>
|
||||
<span className="text-main-view-fg/70">
|
||||
{t(
|
||||
'outOfContextError.increaseContextSize',
|
||||
'Increase Context Size'
|
||||
)}
|
||||
</span>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}, [isOpen, modalProps])
|
||||
return { showModal, PromiseModal }
|
||||
return (
|
||||
<Dialog open={isModalOpen} onOpenChange={handleDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('model-errors:title')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogDescription>
|
||||
{t('model-errors:description')}
|
||||
<br />
|
||||
<br />
|
||||
{t('model-errors:increaseContextSizeDescription')}
|
||||
</DialogDescription>
|
||||
<DialogFooter className="flex gap-2">
|
||||
<Button
|
||||
variant="link"
|
||||
className="bg-transparent border border-main-view-fg/20 hover:bg-main-view-fg/4"
|
||||
onClick={() => {
|
||||
handleContextShift()
|
||||
}}
|
||||
>
|
||||
{t('model-errors:truncateInput')}
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
onClick={() => {
|
||||
handleContextLength()
|
||||
}}
|
||||
>
|
||||
<span className="text-main-view-fg/70">
|
||||
{t('model-errors:increaseContextSize')}
|
||||
</span>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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({
|
||||
<InputControl
|
||||
type={controllerProps.type}
|
||||
placeholder={controllerProps.placeholder}
|
||||
value={(controllerProps.value as string) || ''}
|
||||
value={
|
||||
typeof controllerProps.value === 'number'
|
||||
? controllerProps.value.toString()
|
||||
: (controllerProps.value as string) || ''
|
||||
}
|
||||
inputActions={controllerProps.input_actions}
|
||||
className={className}
|
||||
onChange={(newValue) => onChange(newValue)}
|
||||
|
||||
@ -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<unknown>,
|
||||
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,
|
||||
]
|
||||
|
||||
@ -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<LeftPanelStoreState>()(
|
||||
(set) => ({
|
||||
currentLanguage: 'en',
|
||||
spellCheckChatInput: true,
|
||||
experimentalFeatures: false,
|
||||
setExperimentalFeatures: (value) => set({ experimentalFeatures: value }),
|
||||
setSpellCheckChatInput: (value) => set({ spellCheckChatInput: 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[]
|
||||
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<ModelProviderState>()(
|
||||
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<ModelProviderState>()(
|
||||
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<ModelProviderState>()(
|
||||
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<ModelProviderState>()(
|
||||
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) => ({
|
||||
|
||||
@ -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()) &&
|
||||
|
||||
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 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() {
|
||||
{/* <TanStackRouterDevtools position="bottom-right" /> */}
|
||||
<CortexFailureDialog />
|
||||
<ToolApproval />
|
||||
<OutOfContextPromiseModal />
|
||||
</TranslationProvider>
|
||||
</Fragment>
|
||||
)
|
||||
|
||||
@ -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<string | undefined>()
|
||||
const [isCopied, setIsCopied] = useState(false)
|
||||
@ -239,6 +244,20 @@ function General() {
|
||||
/>
|
||||
</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 */}
|
||||
<Card title={t('common:dataFolder')}>
|
||||
<CardItem
|
||||
|
||||
@ -39,7 +39,6 @@ import { toast } from 'sonner'
|
||||
import { ActiveModel } from '@/types/models'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { predefinedProviders } from '@/mock/data'
|
||||
import { isProd } from '@/lib/version'
|
||||
|
||||
// as route.threadsDetail
|
||||
export const Route = createFileRoute('/settings/providers/$providerName')({
|
||||
@ -462,19 +461,15 @@ function ProviderDetail() {
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="font-medium">{model.id}</h1>
|
||||
{!isProd && (
|
||||
<Capabilities capabilities={capabilities} />
|
||||
)}
|
||||
<Capabilities capabilities={capabilities} />
|
||||
</div>
|
||||
}
|
||||
actions={
|
||||
<div className="flex items-center gap-1">
|
||||
{!isProd && (
|
||||
<DialogEditModel
|
||||
provider={provider}
|
||||
modelId={model.id}
|
||||
/>
|
||||
)}
|
||||
<DialogEditModel
|
||||
provider={provider}
|
||||
modelId={model.id}
|
||||
/>
|
||||
{model.settings && (
|
||||
<ModelSetting
|
||||
provider={provider}
|
||||
|
||||
@ -20,7 +20,6 @@ import { useAppState } from '@/hooks/useAppState'
|
||||
import DropdownAssistant from '@/containers/DropdownAssistant'
|
||||
import { useAssistant } from '@/hooks/useAssistant'
|
||||
import { useAppearance } from '@/hooks/useAppearance'
|
||||
import { useOutOfContextPromiseModal } from '@/containers/dialogs/OutOfContextDialog'
|
||||
import { ContentType, ThreadMessage } from '@janhq/core'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
import { useChat } from '@/hooks/useChat'
|
||||
@ -55,8 +54,6 @@ function ThreadDetail() {
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
||||
const isFirstRender = useRef(true)
|
||||
const messagesCount = useMemo(() => messages?.length ?? 0, [messages])
|
||||
const { showModal, PromiseModal: OutOfContextModal } =
|
||||
useOutOfContextPromiseModal()
|
||||
|
||||
// Function to check scroll position and scrollbar presence
|
||||
const checkScrollState = () => {
|
||||
@ -223,7 +220,7 @@ function ThreadDetail() {
|
||||
const generateAIResponse = () => {
|
||||
const latestUserMessage = messages[messages.length - 1]
|
||||
if (latestUserMessage?.content?.[0]?.text?.value) {
|
||||
sendMessage(latestUserMessage.content[0].text.value, undefined, false)
|
||||
sendMessage(latestUserMessage.content[0].text.value, false)
|
||||
}
|
||||
}
|
||||
|
||||
@ -231,8 +228,6 @@ function ThreadDetail() {
|
||||
|
||||
if (!messages || !threadModel) return null
|
||||
|
||||
const contextOverflowModalComponent = <OutOfContextModal />
|
||||
|
||||
const showScrollToBottomBtn = !isAtBottom && hasScrollbar
|
||||
const showGenerateAIResponseBtn =
|
||||
messages[messages.length - 1]?.role === 'user' && !streamingContent
|
||||
@ -283,8 +278,6 @@ function ThreadDetail() {
|
||||
}
|
||||
index={index}
|
||||
updateMessage={updateMessage}
|
||||
showContextOverflowModal={showModal}
|
||||
contextOverflowModal={contextOverflowModalComponent}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -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<ModelProvider[]> => {
|
||||
const engines = !localStorage.getItem('migration_completed')
|
||||
@ -148,7 +150,7 @@ export const getProviders = async (): Promise<ModelProvider[]> => {
|
||||
...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<ModelProvider[]> => {
|
||||
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<string[]> Array of model IDs
|
||||
*/
|
||||
export const fetchModelsFromProvider = async (
|
||||
provider: ModelProvider
|
||||
): Promise<string[]> => {
|
||||
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<string, string> = {
|
||||
'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[]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user