Merge branch 'dev' into release/v0.6.3
# Conflicts: # web-app/src/containers/DropdownModelProvider.tsx # web-app/src/containers/SettingsMenu.tsx # web-app/src/containers/dialogs/DeleteModel.tsx # web-app/src/routes/__root.tsx # web-app/src/routes/settings/general.tsx # web-app/src/routes/threads/$threadId.tsx
17
.github/dependabot.yaml
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference#package-ecosystem-
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "src-tauri"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directories:
|
||||
- "/"
|
||||
- "core"
|
||||
- "docs"
|
||||
- "extensions"
|
||||
- "extensions/*"
|
||||
- "web-app"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
1
.gitignore
vendored
@ -49,3 +49,4 @@ src-tauri/resources/bin
|
||||
# Helper tools
|
||||
.opencode
|
||||
OpenCode.md
|
||||
archive/
|
||||
50
README.md
@ -89,7 +89,7 @@ For those who enjoy the scenic route:
|
||||
- Make ≥ 3.81
|
||||
- Rust (for Tauri)
|
||||
|
||||
### Quick Start
|
||||
### Run with Make
|
||||
|
||||
```bash
|
||||
git clone https://github.com/menloresearch/jan
|
||||
@ -99,34 +99,44 @@ make dev
|
||||
|
||||
This handles everything: installs dependencies, builds core components, and launches the app.
|
||||
|
||||
### Alternative Commands
|
||||
**Available make targets:**
|
||||
- `make dev` - Full development setup and launch
|
||||
- `make build` - Production build
|
||||
- `make test` - Run tests and linting
|
||||
- `make clean` - Delete everything and start fresh
|
||||
|
||||
If you prefer the verbose approach:
|
||||
### Run with Mise (easier)
|
||||
|
||||
You can also run with [mise](https://mise.jdx.dev/), which is a bit easier as it ensures Node.js, Rust, and other dependency versions are automatically managed:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/menloresearch/jan
|
||||
cd jan
|
||||
|
||||
# Install mise (if not already installed)
|
||||
curl https://mise.run | sh
|
||||
|
||||
# Install tools and start development
|
||||
mise install # installs Node.js, Rust, and other tools
|
||||
mise dev # runs the full development setup
|
||||
```
|
||||
|
||||
**Available mise commands:**
|
||||
- `mise dev` - Full development setup and launch
|
||||
- `mise build` - Production build
|
||||
- `mise test` - Run tests and linting
|
||||
- `mise clean` - Delete everything and start fresh
|
||||
- `mise tasks` - List all available tasks
|
||||
|
||||
### Manual Commands
|
||||
|
||||
```bash
|
||||
# Setup and development
|
||||
yarn install
|
||||
yarn build:core
|
||||
yarn build:extensions
|
||||
yarn dev
|
||||
|
||||
# Production build
|
||||
yarn build
|
||||
|
||||
# Clean slate (when things inevitably break)
|
||||
make clean
|
||||
```
|
||||
|
||||
### Available Make Targets
|
||||
|
||||
- `make dev` - Full development setup and launch (recommended)
|
||||
- `make dev-tauri` - Tauri development (deprecated, use `make dev`)
|
||||
- `make build` - Production build
|
||||
- `make install-and-build` - Install dependencies and build core/extensions
|
||||
- `make test` - Run tests and linting
|
||||
- `make lint` - Check your code doesn't offend the linters
|
||||
- `make clean` - Nuclear option: delete everything and start fresh
|
||||
|
||||
## System Requirements
|
||||
|
||||
**Minimum specs for a decent experience:**
|
||||
|
||||
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
@ -0,0 +1 @@
|
||||
export default new Map();
|
||||
1
docs/.astro/content-modules.mjs
Normal file
@ -0,0 +1 @@
|
||||
export default new Map();
|
||||
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
@ -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
@ -0,0 +1,5 @@
|
||||
{
|
||||
"_variables": {
|
||||
"lastUpdateCheck": 1750832446593
|
||||
}
|
||||
}
|
||||
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-v0.6.1-ui-revamp.png
Normal file
|
After Width: | Height: | Size: 817 KiB |
@ -57,7 +57,7 @@ const Changelog = () => {
|
||||
<p className="text-base mt-2 leading-relaxed">
|
||||
Latest release updates from the Jan team. Check out our
|
||||
<a
|
||||
href="https://github.com/orgs/menloresearch/projects/5/views/52"
|
||||
href="https://github.com/orgs/menloresearch/projects/30"
|
||||
className="text-blue-600 dark:text-blue-400 cursor-pointer"
|
||||
>
|
||||
Roadmap
|
||||
|
||||
@ -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-19-jan-ui-revamp.mdx
Normal file
@ -0,0 +1,21 @@
|
||||
---
|
||||
title: "Jan v0.6.1 is here: It's a whole new vibe!"
|
||||
version: 0.6.1
|
||||
description: "Are you ready for the sexiest UI ever?"
|
||||
date: 2025-06-19
|
||||
ogImage: "/assets/images/changelog/jan-v0.6.1-ui-revamp.png"
|
||||
---
|
||||
|
||||
import ChangelogHeader from "@/components/Changelog/ChangelogHeader"
|
||||
|
||||
<ChangelogHeader title="Jan v0.6.1 is here: It's a whole new vibe!" date="2025-06-19" ogImage="/assets/images/changelog/jan-v0.6.1-ui-revamp.png" />
|
||||
|
||||
## Highlights 🎉
|
||||
|
||||
- Jan's been redesigned to be faster, cleaner, and easier to use.
|
||||
- You can now create assistants with custom instructions and settings from a dedicated tab.
|
||||
- You can now use Jan with Menlo's models.
|
||||
|
||||
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.1).
|
||||
BIN
docs/src/pages/docs/_assets/api-server2.png
Normal file
|
After Width: | Height: | Size: 306 KiB |
BIN
docs/src/pages/docs/_assets/assistant-dropdown-updated.png
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
docs/src/pages/docs/_assets/jan-app-new.png
Normal file
|
After Width: | Height: | Size: 343 KiB |
BIN
docs/src/pages/docs/_assets/jan-nano-bench.png
Normal file
|
After Width: | Height: | Size: 205 KiB |
BIN
docs/src/pages/docs/_assets/jan-nano-demo.gif
Normal file
|
After Width: | Height: | Size: 22 MiB |
BIN
docs/src/pages/docs/_assets/jan-nano-demo.mp4
Normal file
BIN
docs/src/pages/docs/_assets/jan-nano0.png
Normal file
|
After Width: | Height: | Size: 4.4 MiB |
BIN
docs/src/pages/docs/_assets/jan-nano1.png
Normal file
|
After Width: | Height: | Size: 819 KiB |
BIN
docs/src/pages/docs/_assets/serper-mcp.png
Normal file
|
After Width: | Height: | Size: 614 KiB |
BIN
docs/src/pages/docs/_assets/threads-context-menu-updated.png
Normal file
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 149 KiB |
BIN
docs/src/pages/docs/_assets/threads-new-chat-updated.png
Normal file
|
After Width: | Height: | Size: 149 KiB |
@ -1,4 +1,8 @@
|
||||
{
|
||||
"-- Switcher": {
|
||||
"type": "separator",
|
||||
"title": "Switcher"
|
||||
},
|
||||
"index": "Overview",
|
||||
"how-to-separator": {
|
||||
"title": "HOW TO",
|
||||
@ -6,7 +10,7 @@
|
||||
},
|
||||
"desktop": "Install 👋 Jan",
|
||||
"threads": "Start Chatting",
|
||||
"manage-models": "Manage Models",
|
||||
"jan-models": "Use Jan Models",
|
||||
"assistants": "Create Assistants",
|
||||
|
||||
"tutorials-separators": {
|
||||
@ -15,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",
|
||||
@ -25,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"
|
||||
}
|
||||
|
||||
@ -42,7 +42,9 @@ as well after downloading it from [here](https://github.com/ggml-org/llama.cpp).
|
||||
2. Add an API Key (it can be anything) or fully configure the server at [Server Settings](/docs/api-server#server-settings)
|
||||
3. Click **Start Server** button
|
||||
4. Wait for the confirmation message in the logs panel, your server is ready when you see: `JAN API listening at: http://127.0.0.1:1337`
|
||||
5. Make sure you add an API key, this can be anything you want, a word like "testing" or even a combination of numbers and letters.
|
||||
|
||||

|
||||
|
||||
### Step 2: Test Server
|
||||
The easiest way to test your server is through the API Playground:
|
||||
@ -50,8 +52,25 @@ The easiest way to test your server is through the API Playground:
|
||||
2. Select a model from the dropdown menu in Jan interface
|
||||
3. Try a simple request
|
||||
4. View the response in real-time
|
||||
5. When you send requests from another app, you need to add the API key in the request headers.
|
||||
|
||||
### Step 3: Use the API
|
||||
|
||||
```sh
|
||||
curl http://127.0.0.1:1337/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer testing-something" \ # here you need to add your API key
|
||||
-d '{
|
||||
"model": "jan-nano-gguf",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Write a one-sentence bedtime story about a unicorn."
|
||||
}
|
||||
]
|
||||
}'
|
||||
|
||||
```
|
||||
</Steps>
|
||||
|
||||
|
||||
@ -108,6 +127,8 @@ Enable **Verbose Server Logs** for detailed error messages.
|
||||
- Verify your JSON request format is correct
|
||||
- Verify firewall settings
|
||||
- Look for detailed error messages in the logs
|
||||
- Make sure you add an API key, this can be anything you want, a word like "testing" or even a combination of numbers and letters.
|
||||
- Use the API Key in the request headers when sending requests from another app.
|
||||
|
||||
**2. CORS Errors in Web Apps**
|
||||
- Enable CORS in server settings if using from a webpage
|
||||
|
||||
@ -77,7 +77,7 @@ Provide examples when explaining complex topics.
|
||||
You can quickly switch between assistants, or create and edit them, directly from the Chat screen using the
|
||||
assistant dropdown menu at the top:
|
||||
|
||||

|
||||

|
||||
|
||||
- Click the assistant's name (e.g., "Travel Planner") at the top of the Chat screen to open the dropdown menu.
|
||||
- The dropdown lists all of your assistants. Click on any of the assistants available to switch to it for the
|
||||
|
||||
@ -22,10 +22,10 @@ import FAQBox from '@/components/FaqBox'
|
||||
|
||||
# Jan
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
Jan is an AI chat application that runs 100% offline on your desktop and (*soon*) on mobile. Our goal is to
|
||||
Jan is a ChatGPT alternative that runs 100% offline on your desktop and (*soon*) on mobile. Our goal is to
|
||||
make it easy for anyone, with or without coding skills, to download and use AI models with full control and
|
||||
[privacy](https://www.reuters.com/legal/legalindustry/privacy-paradox-with-ai-2023-10-31/).
|
||||
|
||||
@ -39,10 +39,10 @@ add it to Jan via the configuration's page and start talking to your favorite mo
|
||||
|
||||
### Features
|
||||
|
||||
- Download popular open-source LLMs (Llama3, Gemma3, Qwen3, and more) from the HuggingFace [Model Hub](./docs/models/manage-models.mdx)
|
||||
- Download popular open-source LLMs (Llama3, Gemma3, Qwen3, and more) from the HuggingFace [Model Hub](./docs/manage-models.mdx)
|
||||
or import any GGUF files (the model format used by llama.cpp) available locally
|
||||
- Connect to [cloud services](/docs/remote-models/openai) (OpenAI, Anthropic, Mistral, Groq, etc.)
|
||||
- [Chat](./docs/threads.mdx) with AI models & [customize their parameters](./docs/models/model-parameters.mdx) via our
|
||||
- [Chat](./docs/threads.mdx) with AI models & [customize their parameters](/docs/model-parameters.mdx) via our
|
||||
intuitive interface
|
||||
- Use our [local API server](https://jan.ai/api-reference) with an OpenAI-equivalent API to power other apps.
|
||||
|
||||
@ -86,7 +86,7 @@ Jan is built on the shoulders of many open-source projects like:
|
||||
Jan supports all major operating systems,
|
||||
- [Mac](/docs/desktop/mac#compatibility)
|
||||
- [Windows](/docs/desktop/windows#compatibility)
|
||||
- [Linux](docs/desktop/linux).
|
||||
- [Linux](/docs/desktop/linux)
|
||||
|
||||
Hardware compatibility includes:
|
||||
- NVIDIA GPUs (CUDA)
|
||||
@ -111,7 +111,7 @@ Jan is built on the shoulders of many open-source projects like:
|
||||
</FAQBox>
|
||||
|
||||
<FAQBox title="What models can I use with Jan?">
|
||||
- Download optimized models from the [Jan Hub](/docs/models/manage-models#1-download-from-jan-hub-recommended)
|
||||
- Download optimized models from the [Jan Hub](/docs/manage-models)
|
||||
- Import GGUF models from Hugging Face or your local files
|
||||
- Connect to cloud providers like OpenAI, Anthropic, Mistral and Groq (requires your own API keys)
|
||||
</FAQBox>
|
||||
|
||||
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/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
|
||||
136
docs/src/pages/docs/jan-models/jan-nano-32.mdx
Normal file
@ -0,0 +1,136 @@
|
||||
---
|
||||
title: Jan Nano 32k
|
||||
description: Jan-Nano-Gguf Model
|
||||
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
|
||||
|
||||

|
||||
|
||||
## 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">
|
||||
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>
|
||||
|
||||
|
||||
## System Requirements
|
||||
|
||||
- Minimum Requirements:
|
||||
- 8GB RAM (with iQ4_XS quantization)
|
||||
- 12GB VRAM (for Q8 quantization)
|
||||
- CUDA-compatible GPU
|
||||
- Recommended Setup:
|
||||
- 16GB+ RAM
|
||||
- 16GB+ VRAM
|
||||
- Latest CUDA drivers
|
||||
- RTX 30/40 series or newer
|
||||
|
||||
|
||||
## Using Jan-Nano-32k
|
||||
|
||||
**Step 1**
|
||||
Download Jan from [here](https://jan.ai/docs/desktop/).
|
||||
|
||||
**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**
|
||||
Go to **Settings** > **Model Providers** > **Llama.cpp** click on the pencil icon and enable tool use for Jan-Nano-Gguf.
|
||||
|
||||
**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**
|
||||
Add the serper MCP to Jan via the **Settings** > **MCP Servers** tab.
|
||||
|
||||

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

|
||||
|
||||
## Queries to Try
|
||||
|
||||
Here are some example queries to showcase Jan-Nano's web search capabilities:
|
||||
|
||||
1. **Current Events**: What are the latest developments in renewable energy adoption in Germany and Denmark?
|
||||
2. **International Business**: What is the current status of Tesla's Gigafactory in Berlin and how has it impacted the local economy?
|
||||
3. **Technology Trends**: What are the newest AI developments from Google, Microsoft, and Meta that were announced this week?
|
||||
4. **Global Weather**: What's the current weather forecast for Tokyo, Japan for the next 5 days?
|
||||
5. **Stock Market**: What are the current stock prices for Apple, Samsung, and Huawei, and how have they performed this month?
|
||||
6. **Sports Updates**: What are the latest results from the Premier League matches played this weekend?
|
||||
7. **Scientific Research**: What are the most recent findings about climate change impacts in the Arctic region?
|
||||
8. **Cultural Events**: What major music festivals are happening in Europe this summer and who are the headliners?
|
||||
9. **Health & Medicine**: What are the latest developments in mRNA vaccine technology and its applications beyond COVID-19?
|
||||
10. **Space Exploration**: What are the current missions being conducted by NASA, ESA, and China's space program?
|
||||
|
||||
|
||||
## FAQ
|
||||
|
||||
- What are the recommended GGUF quantizations?
|
||||
- Q8 GGUF is recommended for best performance
|
||||
- iQ4_XS GGUF for very limited VRAM setups
|
||||
- Avoid Q4_0 and Q4_K_M as they show significant performance degradation
|
||||
|
||||
- Can I run this on a laptop with 8GB RAM?
|
||||
- Yes, but use the recommended quantizations (iQ4_XS)
|
||||
- Note that performance may be limited with Q4 quantizations
|
||||
|
||||
- How much did the training cost?
|
||||
- Training was done on internal A6000 clusters
|
||||
- Estimated cost on RunPod would be under $100 using H200
|
||||
- Hardware used:
|
||||
- 8xA6000 for training code
|
||||
- 4xA6000 for vllm server (inferencing)
|
||||
|
||||
- What frontend should I use?
|
||||
- Jan Beta (recommended) - Minimalistic and polished interface
|
||||
- Download link: https://jan.ai/docs/desktop/beta
|
||||
|
||||
- Getting Jinja errors in LM Studio?
|
||||
- Use Qwen3 template from other LM Studio compatible models
|
||||
- Disable “thinking” and add the required system prompt
|
||||
- Fix coming soon in future GGUF releases
|
||||
- Having model loading issues in Jan?
|
||||
- Use latest beta version: Jan-beta_0.5.18-rc6-beta
|
||||
- Ensure proper CUDA support for your GPU
|
||||
- Check VRAM requirements match your quantization choice
|
||||
|
||||
## Resources
|
||||
|
||||
- [Jan-Nano Model on Hugging Face](https://huggingface.co/Menlo/Jan-nano)
|
||||
- [Jan-Nano GGUF on Hugging Face](https://huggingface.co/Menlo/Jan-nano-gguf)
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -91,7 +91,7 @@ Install all required dependencies and drivers before enabling GPU acceleration.
|
||||
### Step 4: Customize Assistant Instructions
|
||||
|
||||
With your model ready to roll, you can tailor how it responds by tweaking instructions or model configurations
|
||||
in [Assistant.](/docs/assistants).
|
||||
through the [Assistants feature](/docs/assistants).
|
||||
|
||||
<br/>
|
||||
|
||||
@ -104,7 +104,7 @@ these is that you can use them no matter which model you choose.
|
||||
|
||||
<br/>
|
||||
|
||||

|
||||

|
||||
|
||||
<br/>
|
||||
|
||||
|
||||
@ -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>
|
||||
@ -33,7 +33,7 @@ bottom left of Jan.
|
||||
2. Select your preferred model in **Model Selector** in input field & start chatting.
|
||||
|
||||
<br/>
|
||||

|
||||

|
||||
|
||||
## View Your Chat History
|
||||
|
||||
@ -51,7 +51,7 @@ thread and a context menu will pop up with the favorite option for you to click
|
||||
- **Recents**: See your most recently accessed threads for quick navigation.
|
||||
|
||||
<br/>
|
||||

|
||||

|
||||
|
||||
|
||||
## Edit a Chat Title
|
||||
@ -61,7 +61,7 @@ thread and a context menu will pop up with the favorite option for you to click
|
||||
4. Add new title & save
|
||||
|
||||
<br/>
|
||||

|
||||

|
||||
|
||||
## Delete Thread
|
||||
|
||||
@ -77,7 +77,7 @@ When you want to completely remove a thread:
|
||||
|
||||
|
||||
<br/>
|
||||

|
||||

|
||||
|
||||
### Delete all threads at once
|
||||
|
||||
|
||||
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
@ -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
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
"rimraf": "^3.0.2",
|
||||
"rolldown": "1.0.0-beta.1",
|
||||
"run-script-os": "^1.1.6",
|
||||
"typescript": "5.3.3",
|
||||
"typescript": "5.8.3",
|
||||
"vitest": "^3.0.6"
|
||||
},
|
||||
"files": [
|
||||
|
||||
215
mise.toml
Normal file
@ -0,0 +1,215 @@
|
||||
[tools]
|
||||
node = "20"
|
||||
rust = "1.85.1"
|
||||
sccache = "latest"
|
||||
|
||||
[env]
|
||||
_.path = ['./node_modules/.bin']
|
||||
RUSTC_WRAPPER="sccache"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CORE SETUP AND CONFIGURATION TASKS
|
||||
# ============================================================================
|
||||
|
||||
[tasks.config-yarn]
|
||||
description = "Configure yarn version and settings"
|
||||
run = [
|
||||
"corepack enable",
|
||||
"corepack prepare yarn@4.5.3 --activate",
|
||||
"yarn --version",
|
||||
"yarn config set -H enableImmutableInstalls false"
|
||||
]
|
||||
|
||||
[tasks.install]
|
||||
description = "Install dependencies"
|
||||
depends = ["config-yarn"]
|
||||
run = '''
|
||||
#!/usr/bin/env bash
|
||||
# Skip install on Windows per Makefile logic
|
||||
if [[ "$OSTYPE" != "msys" && "$OSTYPE" != "win32" ]]; then
|
||||
yarn install
|
||||
fi
|
||||
'''
|
||||
sources = ['package.json', 'yarn.lock']
|
||||
outputs = ['node_modules']
|
||||
|
||||
[tasks.build-core]
|
||||
description = "Build core package"
|
||||
depends = ["install"]
|
||||
run = "yarn build:core"
|
||||
sources = ['core/**/*']
|
||||
outputs = ['core/dist']
|
||||
|
||||
[tasks.build-extensions]
|
||||
description = "Build extensions"
|
||||
depends = ["build-core"]
|
||||
run = "yarn build:extensions"
|
||||
sources = ['extensions/**/*']
|
||||
outputs = ['pre-install/*.tgz']
|
||||
|
||||
[tasks.install-and-build]
|
||||
description = "Install dependencies and build core and extensions (matches Makefile)"
|
||||
depends = ["build-extensions"]
|
||||
|
||||
# ============================================================================
|
||||
# DEVELOPMENT TASKS
|
||||
# ============================================================================
|
||||
|
||||
[tasks.dev]
|
||||
description = "Start development server (matches Makefile)"
|
||||
depends = ["install-and-build"]
|
||||
run = [
|
||||
"yarn install:cortex",
|
||||
"yarn download:bin",
|
||||
"yarn copy:lib",
|
||||
"yarn dev"
|
||||
]
|
||||
|
||||
[tasks.dev-tauri]
|
||||
description = "Start development server with Tauri (DEPRECATED - matches Makefile)"
|
||||
depends = ["install-and-build"]
|
||||
run = [
|
||||
"yarn install:cortex",
|
||||
"yarn download:bin",
|
||||
"yarn copy:lib",
|
||||
"yarn dev:tauri"
|
||||
]
|
||||
|
||||
# ============================================================================
|
||||
# BUILD TASKS
|
||||
# ============================================================================
|
||||
|
||||
[tasks.build]
|
||||
description = "Build complete application (matches Makefile)"
|
||||
depends = ["install-and-build"]
|
||||
run = "yarn build"
|
||||
|
||||
[tasks.build-tauri]
|
||||
description = "Build Tauri application (DEPRECATED - matches Makefile)"
|
||||
depends = ["install-and-build"]
|
||||
run = [
|
||||
"yarn copy:lib",
|
||||
"yarn build"
|
||||
]
|
||||
|
||||
[tasks.build-and-publish]
|
||||
description = "Build and publish the application (matches Makefile)"
|
||||
depends = ["install-and-build"]
|
||||
run = "yarn build"
|
||||
|
||||
# ============================================================================
|
||||
# QUALITY ASSURANCE TASKS
|
||||
# ============================================================================
|
||||
|
||||
[tasks.lint]
|
||||
description = "Run linting (matches Makefile)"
|
||||
depends = ["build-extensions"]
|
||||
run = "yarn lint"
|
||||
|
||||
[tasks.test]
|
||||
description = "Run test suite (matches Makefile)"
|
||||
depends = ["lint"]
|
||||
run = "yarn test"
|
||||
|
||||
# ============================================================================
|
||||
# PARALLEL-FRIENDLY QUALITY ASSURANCE TASKS
|
||||
# ============================================================================
|
||||
|
||||
[tasks.lint-only]
|
||||
description = "Run linting only (parallel-friendly)"
|
||||
depends = ["build-extensions"]
|
||||
run = "yarn lint"
|
||||
hide = true
|
||||
|
||||
[tasks.test-only]
|
||||
description = "Run tests only (parallel-friendly)"
|
||||
depends = ["build-extensions"]
|
||||
run = "yarn test"
|
||||
hide = true
|
||||
|
||||
[tasks.qa-parallel]
|
||||
description = "Run linting and testing in parallel"
|
||||
depends = ["lint-only", "test-only"]
|
||||
|
||||
# ============================================================================
|
||||
# UTILITY TASKS
|
||||
# ============================================================================
|
||||
|
||||
[tasks.clean]
|
||||
description = "Clean all build artifacts and dependencies (cross-platform - matches Makefile)"
|
||||
run = '''
|
||||
#!/usr/bin/env bash
|
||||
echo "Cleaning build artifacts and dependencies..."
|
||||
|
||||
# Platform detection and cleanup (matches Makefile exactly)
|
||||
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then
|
||||
# Windows cleanup using PowerShell (matches Makefile)
|
||||
powershell -Command "Get-ChildItem -Path . -Include node_modules, .next, dist, build, out, .turbo, .yarn -Recurse -Directory | Remove-Item -Recurse -Force" 2>/dev/null || true
|
||||
powershell -Command "Get-ChildItem -Path . -Include package-lock.json, tsconfig.tsbuildinfo -Recurse -File | Remove-Item -Recurse -Force" 2>/dev/null || true
|
||||
powershell -Command "Remove-Item -Recurse -Force ./pre-install/*.tgz" 2>/dev/null || true
|
||||
powershell -Command "Remove-Item -Recurse -Force ./extensions/*/*.tgz" 2>/dev/null || true
|
||||
powershell -Command "Remove-Item -Recurse -Force ./electron/pre-install/*.tgz" 2>/dev/null || true
|
||||
powershell -Command "Remove-Item -Recurse -Force ./src-tauri/resources" 2>/dev/null || true
|
||||
powershell -Command "Remove-Item -Recurse -Force ./src-tauri/target" 2>/dev/null || true
|
||||
powershell -Command "if (Test-Path \"\$(\$env:USERPROFILE)\\jan\\extensions\\\") { Remove-Item -Path \"\$(\$env:USERPROFILE)\\jan\\extensions\" -Recurse -Force }" 2>/dev/null || true
|
||||
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
# Linux cleanup (matches Makefile)
|
||||
find . -name "node_modules" -type d -prune -exec rm -rf '{}' + 2>/dev/null || true
|
||||
find . -name ".next" -type d -exec rm -rf '{}' + 2>/dev/null || true
|
||||
find . -name "dist" -type d -exec rm -rf '{}' + 2>/dev/null || true
|
||||
find . -name "build" -type d -exec rm -rf '{}' + 2>/dev/null || true
|
||||
find . -name "out" -type d -exec rm -rf '{}' + 2>/dev/null || true
|
||||
find . -name ".turbo" -type d -exec rm -rf '{}' + 2>/dev/null || true
|
||||
find . -name ".yarn" -type d -exec rm -rf '{}' + 2>/dev/null || true
|
||||
find . -name "package-lock.json" -type f -exec rm -rf '{}' + 2>/dev/null || true
|
||||
rm -rf ./pre-install/*.tgz 2>/dev/null || true
|
||||
rm -rf ./extensions/*/*.tgz 2>/dev/null || true
|
||||
rm -rf ./electron/pre-install/*.tgz 2>/dev/null || true
|
||||
rm -rf ./src-tauri/resources 2>/dev/null || true
|
||||
rm -rf ./src-tauri/target 2>/dev/null || true
|
||||
rm -rf ~/jan/extensions 2>/dev/null || true
|
||||
rm -rf "~/.cache/jan*" 2>/dev/null || true
|
||||
else
|
||||
# macOS cleanup (matches Makefile)
|
||||
find . -name "node_modules" -type d -prune -exec rm -rf '{}' + 2>/dev/null || true
|
||||
find . -name ".next" -type d -exec rm -rf '{}' + 2>/dev/null || true
|
||||
find . -name "dist" -type d -exec rm -rf '{}' + 2>/dev/null || true
|
||||
find . -name "build" -type d -exec rm -rf '{}' + 2>/dev/null || true
|
||||
find . -name "out" -type d -exec rm -rf '{}' + 2>/dev/null || true
|
||||
find . -name ".turbo" -type d -exec rm -rf '{}' + 2>/dev/null || true
|
||||
find . -name ".yarn" -type d -exec rm -rf '{}' + 2>/dev/null || true
|
||||
find . -name "package-lock.json" -type f -exec rm -rf '{}' + 2>/dev/null || true
|
||||
rm -rf ./pre-install/*.tgz 2>/dev/null || true
|
||||
rm -rf ./extensions/*/*.tgz 2>/dev/null || true
|
||||
rm -rf ./electron/pre-install/*.tgz 2>/dev/null || true
|
||||
rm -rf ./src-tauri/resources 2>/dev/null || true
|
||||
rm -rf ./src-tauri/target 2>/dev/null || true
|
||||
rm -rf ~/jan/extensions 2>/dev/null || true
|
||||
rm -rf ~/Library/Caches/jan* 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo "Clean completed!"
|
||||
'''
|
||||
|
||||
[tasks.all]
|
||||
description = "Default target - shows available commands (matches Makefile)"
|
||||
run = "echo 'Specify a target to run. Use: mise tasks'"
|
||||
|
||||
# ============================================================================
|
||||
# DEVELOPMENT WORKFLOW SHORTCUTS
|
||||
# ============================================================================
|
||||
|
||||
[tasks.setup]
|
||||
description = "Complete development setup"
|
||||
depends = ["install-and-build"]
|
||||
alias = "init"
|
||||
|
||||
[tasks.ci]
|
||||
description = "Run CI pipeline (lint + test sequentially)"
|
||||
depends = ["test"]
|
||||
|
||||
[tasks.ci-parallel]
|
||||
description = "Run CI pipeline (lint + test in parallel)"
|
||||
depends = ["qa-parallel"]
|
||||
alias = "ci-fast"
|
||||
306
scripts/find-missing-i18n-key.js
Normal file
@ -0,0 +1,306 @@
|
||||
/**
|
||||
* Script to find missing i18n keys in Jan components
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/find-missing-i18n-key.js [options]
|
||||
*
|
||||
* Options:
|
||||
* --locale=<locale> Only check a specific locale (e.g. --locale=id)
|
||||
* --file=<file> Only check a specific file (e.g. --file=common.json)
|
||||
* --help Show this help message
|
||||
*/
|
||||
|
||||
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
|
||||
}, {})
|
||||
|
||||
// Display help information
|
||||
if (args.help) {
|
||||
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.
|
||||
|
||||
Usage:
|
||||
node scripts/find-missing-i18n-key.js [options]
|
||||
|
||||
Options:
|
||||
--locale=<locale> Only check a specific language (e.g., --locale=id)
|
||||
--file=<file> Only check a specific file (e.g., --file=common.json)
|
||||
--help Display help information
|
||||
|
||||
Output:
|
||||
- Generate a report of missing translations
|
||||
`)
|
||||
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"),
|
||||
},
|
||||
}
|
||||
|
||||
// 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
|
||||
]
|
||||
|
||||
// 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
|
||||
})
|
||||
|
||||
// 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
|
||||
|
||||
for (const part of parts) {
|
||||
if (current === undefined || current === null) {
|
||||
return undefined
|
||||
}
|
||||
current = current[part]
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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']
|
||||
|
||||
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 = []
|
||||
|
||||
// Map namespace to actual filename
|
||||
const namespaceToFile = {
|
||||
'systemMonitor': 'system-monitor',
|
||||
'mcpServers': 'mcp-servers',
|
||||
'mcp-servers': 'mcp-servers',
|
||||
'toolApproval': 'tool-approval',
|
||||
'tool-approval': 'tool-approval'
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Recursively traverse the directory
|
||||
function findMissingI18nKeys() {
|
||||
const results = []
|
||||
|
||||
function walk(dir, baseDir, localeDirs, localesDir) {
|
||||
if (!fs.existsSync(dir)) {
|
||||
console.warn(`Warning: Directory not found: ${dir}`)
|
||||
return
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(dir)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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]
|
||||
|
||||
// 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),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
if (!localeDirs.includes(args.locale)) {
|
||||
console.error(`Error: Language '${args.locale}' not found in ${localesDir}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
const missingKeys = findMissingI18nKeys()
|
||||
|
||||
if (missingKeys.length === 0) {
|
||||
console.log("\n✅ All i18n keys are present!")
|
||||
return
|
||||
}
|
||||
|
||||
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 })
|
||||
})
|
||||
|
||||
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")
|
||||
|
||||
// Exit code 1 indicates missing keys
|
||||
process.exit(1)
|
||||
} catch (error) {
|
||||
console.error("Error:", error.message)
|
||||
console.error(error.stack)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
253
scripts/find-missing-translations.js
Normal file
@ -0,0 +1,253 @@
|
||||
/**
|
||||
* Script to find missing translations in locale files for Jan
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/find-missing-translations.js [options]
|
||||
*
|
||||
* Options:
|
||||
* --locale=<locale> Only check a specific locale (e.g. --locale=id)
|
||||
* --file=<file> Only check a specific file (e.g. --file=common.json)
|
||||
* --help Show this help message
|
||||
*/
|
||||
|
||||
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
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
// Show help if requested
|
||||
if (args.help) {
|
||||
console.log(`
|
||||
Find Missing Translations for Jan
|
||||
|
||||
A utility script to identify missing translations across locale files.
|
||||
Compares non-English locale files to the English ones to find any missing keys.
|
||||
|
||||
Usage:
|
||||
node scripts/find-missing-translations.js [options]
|
||||
|
||||
Options:
|
||||
--locale=<locale> Only check a specific locale (e.g. --locale=id)
|
||||
--file=<file> Only check a specific file (e.g. --file=common.json)
|
||||
--help Show this help message
|
||||
|
||||
Output:
|
||||
- Generates a report of missing translations for the web-app
|
||||
`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// Path to the locales directory
|
||||
const LOCALES_DIR = path.join(__dirname, "../web-app/src/locales")
|
||||
|
||||
// Recursively find all keys in an object
|
||||
function findKeys(obj, parentKey = "") {
|
||||
let keys = []
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
return keys
|
||||
}
|
||||
|
||||
// Get value at a dotted path in an object
|
||||
function getValueAtPath(obj, path) {
|
||||
const parts = path.split(".")
|
||||
let current = obj
|
||||
|
||||
for (const part of parts) {
|
||||
if (current === undefined || current === null) {
|
||||
return undefined
|
||||
}
|
||||
current = current[part]
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
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("."))
|
||||
|
||||
// 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
|
||||
|
||||
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(", ")}`
|
||||
)
|
||||
|
||||
// Results object to store missing translations
|
||||
const missingTranslations = {}
|
||||
|
||||
// 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)
|
||||
|
||||
// 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
|
||||
|
||||
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)
|
||||
|
||||
// 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)
|
||||
|
||||
if (localeValue === undefined) {
|
||||
missingKeys.push({
|
||||
key,
|
||||
englishValue,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (missingKeys.length > 0) {
|
||||
missingTranslations[locale][name] = missingKeys
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return outputResults(missingTranslations)
|
||||
}
|
||||
|
||||
// Function to output results
|
||||
function outputResults(missingTranslations) {
|
||||
let hasMissingTranslations = false
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
hasMissingTranslations = true
|
||||
console.log(`📝 ${locale}:`)
|
||||
|
||||
for (const [fileName, missingItems] of Object.entries(files)) {
|
||||
if (missingItems.file) {
|
||||
console.log(` - ${fileName}: ${missingItems.file}`)
|
||||
continue
|
||||
}
|
||||
|
||||
console.log(` - ${fileName}: ${missingItems.length} missing translations`)
|
||||
|
||||
for (const { key, englishValue } of missingItems) {
|
||||
console.log(` ${key}: "${englishValue}"`)
|
||||
}
|
||||
}
|
||||
|
||||
console.log("")
|
||||
}
|
||||
|
||||
return hasMissingTranslations
|
||||
}
|
||||
|
||||
// Main function to find missing translations
|
||||
function findMissingTranslations() {
|
||||
try {
|
||||
console.log("Starting translation check for Jan web-app...")
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// Run the main function
|
||||
findMissingTranslations()
|
||||
@ -28,7 +28,7 @@
|
||||
"@tabler/icons-react": "^3.33.0",
|
||||
"@tailwindcss/vite": "^4.1.4",
|
||||
"@tanstack/react-router": "^1.116.0",
|
||||
"@tanstack/react-router-devtools": "^1.116.0",
|
||||
"@tanstack/react-router-devtools": "^1.121.34",
|
||||
"@tauri-apps/api": "^2.5.0",
|
||||
"@tauri-apps/plugin-deep-link": "~2",
|
||||
"@tauri-apps/plugin-dialog": "^2.2.1",
|
||||
|
||||
@ -3,6 +3,7 @@ import * as DialogPrimitive from '@radix-ui/react-dialog'
|
||||
import { XIcon } from 'lucide-react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
@ -58,6 +59,7 @@ function DialogContent({
|
||||
'aria-describedby': ariaDescribedBy,
|
||||
...props
|
||||
}: DialogContentProps) {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
@ -74,7 +76,7 @@ function DialogContent({
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close className="data-[state=open]:text-main-view-fg/50 absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-0 focus:outline-0 disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 cursor-pointer">
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
<span className="sr-only">{t('close')}</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
|
||||
@ -3,6 +3,7 @@ import * as SheetPrimitive from '@radix-ui/react-dialog'
|
||||
import { XIcon } from 'lucide-react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
|
||||
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
||||
@ -50,6 +51,7 @@ function SheetContent({
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||
side?: 'top' | 'right' | 'bottom' | 'left'
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
@ -72,7 +74,7 @@ function SheetContent({
|
||||
{children}
|
||||
<SheetPrimitive.Close className="absolute top-4 text-main-view-fg right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-0 disabled:pointer-events-none">
|
||||
<XIcon className="size-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
<span className="sr-only">{t('close')}</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { useLocalApiServer } from '@/hooks/useLocalApiServer'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { Eye, EyeOff } from 'lucide-react'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
|
||||
interface ApiKeyInputProps {
|
||||
showError?: boolean
|
||||
@ -16,23 +17,24 @@ export function ApiKeyInput({
|
||||
const [inputValue, setInputValue] = useState(apiKey.toString())
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const { t } = useTranslation()
|
||||
|
||||
const validateApiKey = (value: string) => {
|
||||
const validateApiKey = useCallback((value: string) => {
|
||||
if (!value || value.trim().length === 0) {
|
||||
setError('API Key is required')
|
||||
setError(t('common:apiKeyRequired'))
|
||||
onValidationChange?.(false)
|
||||
return false
|
||||
}
|
||||
setError('')
|
||||
onValidationChange?.(true)
|
||||
return true
|
||||
}
|
||||
}, [onValidationChange, t])
|
||||
|
||||
useEffect(() => {
|
||||
if (showError) {
|
||||
validateApiKey(inputValue)
|
||||
}
|
||||
}, [showError, inputValue])
|
||||
}, [showError, inputValue, validateApiKey])
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value
|
||||
@ -67,7 +69,7 @@ export function ApiKeyInput({
|
||||
? 'border-1 border-destructive focus:border-destructive focus:ring-destructive'
|
||||
: ''
|
||||
}`}
|
||||
placeholder="Enter API Key"
|
||||
placeholder={t('common:enterApiKey')}
|
||||
/>
|
||||
<div className="absolute right-2 top-1/2 transform -translate-y-1/2 flex items-center gap-1">
|
||||
<button
|
||||
|
||||
@ -23,7 +23,7 @@ import {
|
||||
IconPlayerStopFilled,
|
||||
IconX,
|
||||
} from '@tabler/icons-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
import { useGeneralSetting } from '@/hooks/useGeneralSetting'
|
||||
import { useModelProvider } from '@/hooks/useModelProvider'
|
||||
|
||||
@ -379,7 +379,7 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
|
||||
// When Shift+Enter is pressed, a new line is added (default behavior)
|
||||
}
|
||||
}}
|
||||
placeholder={t('common.placeholder.chatInput')}
|
||||
placeholder={t('common:placeholder.chatInput')}
|
||||
autoFocus
|
||||
spellCheck={spellCheckChatInput}
|
||||
data-gramm={spellCheckChatInput}
|
||||
@ -437,7 +437,7 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Vision</p>
|
||||
<p>{t('vision')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@ -454,7 +454,7 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Embeddings</p>
|
||||
<p>{t('embeddings')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@ -510,7 +510,7 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Tools</p>
|
||||
<p>{t('tools')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@ -544,7 +544,7 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Reasoning</p>
|
||||
<p>{t('reasoning')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
@ -2,9 +2,11 @@ import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { useAppearance } from '@/hooks/useAppearance'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { IconCircleCheckFilled } from '@tabler/icons-react'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
|
||||
export function ChatWidthSwitcher() {
|
||||
const { chatWidth, setChatWidth } = useAppearance()
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
@ -16,7 +18,7 @@ export function ChatWidthSwitcher() {
|
||||
onClick={() => setChatWidth('compact')}
|
||||
>
|
||||
<div className="flex items-center justify-between px-4 py-2 bg-main-view-fg/10">
|
||||
<span className="font-medium text-xs font-sans">Compact Width</span>
|
||||
<span className="font-medium text-xs font-sans">{t('common:compactWidth')}</span>
|
||||
{chatWidth === 'compact' && (
|
||||
<IconCircleCheckFilled className="size-4 text-accent" />
|
||||
)}
|
||||
@ -27,7 +29,7 @@ export function ChatWidthSwitcher() {
|
||||
<Skeleton className="h-2 w-full rounded-full" />
|
||||
<Skeleton className="h-2 w-full rounded-full" />
|
||||
<div className="bg-main-view-fg/10 h-8 px-4 w-full flex-shrink-0 border-none resize-none outline-0 rounded-2xl flex items-center">
|
||||
<span className="text-main-view-fg/50">Ask me anything...</span>
|
||||
<span className="text-main-view-fg/50">{t('common:placeholder.chatInput')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -40,7 +42,7 @@ export function ChatWidthSwitcher() {
|
||||
onClick={() => setChatWidth('full')}
|
||||
>
|
||||
<div className="flex items-center justify-between px-4 py-2 bg-main-view-fg/10">
|
||||
<span className="font-medium text-xs font-sans">Full Width</span>
|
||||
<span className="font-medium text-xs font-sans">{t('common:fullWidth')}</span>
|
||||
{chatWidth === 'full' && (
|
||||
<IconCircleCheckFilled className="size-4 text-accent" />
|
||||
)}
|
||||
@ -51,7 +53,7 @@ export function ChatWidthSwitcher() {
|
||||
<Skeleton className="h-2 w-full rounded-full" />
|
||||
<Skeleton className="h-2 w-full rounded-full" />
|
||||
<div className="bg-main-view-fg/10 h-8 px-4 w-full flex-shrink-0 border-none resize-none outline-0 rounded-2xl flex items-center">
|
||||
<span className="text-main-view-fg/50">Ask me anything...</span>
|
||||
<span className="text-main-view-fg/50">{t('common:placeholder.chatInput')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { RenderMarkdown } from './RenderMarkdown'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
|
||||
const EXAMPLE_CODE = `\`\`\`typescript
|
||||
// Example code for preview
|
||||
@ -12,10 +13,11 @@ console.log(message); // Outputs: Hello, Jan!
|
||||
\`\`\``
|
||||
|
||||
export function CodeBlockExample() {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className="w-full overflow-hidden border border-main-view-fg/10 rounded-md my-2">
|
||||
<div className="flex items-center justify-between px-4 py-2 bg-main-view-fg/10">
|
||||
<span className="font-medium text-xs font-sans">Preview</span>
|
||||
<span className="font-medium text-xs font-sans">{t('preview')}</span>
|
||||
</div>
|
||||
<div className="overflow-auto p-2">
|
||||
<RenderMarkdown content={EXAMPLE_CODE} />
|
||||
|
||||
@ -7,9 +7,11 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
|
||||
export function ColorPickerAppBgColor() {
|
||||
const { appBgColor, setAppBgColor } = useAppearance()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const predefineAppBgColor: RgbaColor[] = [
|
||||
{
|
||||
@ -80,7 +82,7 @@ export function ColorPickerAppBgColor() {
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
title="Pick Color Window Background"
|
||||
title={t('common:pickColorWindowBackground')}
|
||||
className="size-6 cursor-pointer flex items-center justify-center rounded-sm hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out data-[state=open]:bg-main-view-fg/10"
|
||||
>
|
||||
<IconColorPicker size={18} className="text-main-view-fg/50" />
|
||||
|
||||
@ -14,8 +14,10 @@ import { DownloadEvent, DownloadState, events, AppEvent } from '@janhq/core'
|
||||
import { IconDownload, IconX } from '@tabler/icons-react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
|
||||
export function DownloadManagement() {
|
||||
const { t } = useTranslation()
|
||||
const { setProviders } = useModelProvider()
|
||||
const { open: isLeftPanelOpen } = useLeftPanel()
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false)
|
||||
@ -67,20 +69,20 @@ export function DownloadManagement() {
|
||||
isDownloading: false,
|
||||
downloadProgress: 1,
|
||||
}))
|
||||
toast.success('App Update Downloaded', {
|
||||
description: 'The app update has been downloaded successfully.',
|
||||
toast.success(t('common:toast.appUpdateDownloaded.title'), {
|
||||
description: t('common:toast.appUpdateDownloaded.description'),
|
||||
})
|
||||
}, [])
|
||||
}, [t])
|
||||
|
||||
const onAppUpdateDownloadError = useCallback(() => {
|
||||
setAppUpdateState((prev) => ({
|
||||
...prev,
|
||||
isDownloading: false,
|
||||
}))
|
||||
toast.error('App Update Download Failed', {
|
||||
description: 'Failed to download the app update. Please try again.',
|
||||
toast.error(t('common:toast.appUpdateDownloadFailed.title'), {
|
||||
description: t('common:toast.appUpdateDownloadFailed.description'),
|
||||
})
|
||||
}, [])
|
||||
}, [t])
|
||||
|
||||
const downloadProcesses = useMemo(() => {
|
||||
// Get downloads with progress data
|
||||
@ -178,12 +180,12 @@ export function DownloadManagement() {
|
||||
removeDownload(state.modelId)
|
||||
removeLocalDownloadingModel(state.modelId)
|
||||
getProviders().then(setProviders)
|
||||
toast.success('Download Complete', {
|
||||
toast.success(t('common:toast.downloadComplete.title'), {
|
||||
id: 'download-complete',
|
||||
description: `The model ${state.modelId} has been downloaded`,
|
||||
description: t('common:toast.downloadComplete.description', { modelId: state.modelId }),
|
||||
})
|
||||
},
|
||||
[removeDownload, removeLocalDownloadingModel, setProviders]
|
||||
[removeDownload, removeLocalDownloadingModel, setProviders, t]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@ -238,7 +240,7 @@ export function DownloadManagement() {
|
||||
<div className="bg-primary font-bold size-5 rounded-full absolute -top-2 -right-1 flex items-center justify-center text-primary-fg">
|
||||
{downloadCount}
|
||||
</div>
|
||||
<p className="text-left-panel-fg/80 font-medium">Downloads</p>
|
||||
<p className="text-left-panel-fg/80 font-medium">{t('downloads')}</p>
|
||||
<div className="mt-2 flex items-center justify-between space-x-2">
|
||||
<Progress value={overallProgress * 100} />
|
||||
<span className="text-xs font-medium text-left-panel-fg/80 shrink-0">
|
||||
@ -270,7 +272,7 @@ export function DownloadManagement() {
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<div className="p-2 py-1.5 bg-main-view-fg/5 border-b border-main-view-fg/6">
|
||||
<p className="text-xs text-main-view-fg/70">Downloading</p>
|
||||
<p className="text-xs text-main-view-fg/70">{t('downloading')}</p>
|
||||
</div>
|
||||
<div className="p-2 max-h-[300px] overflow-y-auto space-y-2">
|
||||
{appUpdateState.isDownloading && (
|
||||
@ -307,10 +309,9 @@ export function DownloadManagement() {
|
||||
title="Cancel download"
|
||||
onClick={() => {
|
||||
abortDownload(download.name).then(() => {
|
||||
toast.info('Download Cancelled', {
|
||||
toast.info(t('common:toast.downloadCancelled.title'), {
|
||||
id: 'cancel-download',
|
||||
description:
|
||||
'The download process was cancelled',
|
||||
description: t('common:toast.downloadCancelled.description'),
|
||||
})
|
||||
if (downloadProcesses.length === 0) {
|
||||
setIsPopoverOpen(false)
|
||||
|
||||
@ -16,6 +16,7 @@ import { ModelSetting } from '@/containers/ModelSetting'
|
||||
import ProvidersAvatar from '@/containers/ProvidersAvatar'
|
||||
import { Fzf } from 'fzf'
|
||||
import { localStorageKey } from '@/constants/localStorage'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
|
||||
type DropdownModelProviderProps = {
|
||||
model?: ThreadModel
|
||||
@ -67,6 +68,7 @@ const DropdownModelProvider = ({
|
||||
const [displayModel, setDisplayModel] = useState<string>('')
|
||||
const { updateCurrentThreadModel } = useThreads()
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation()
|
||||
|
||||
// Search state
|
||||
const [open, setOpen] = useState(false)
|
||||
@ -117,9 +119,9 @@ const DropdownModelProvider = ({
|
||||
if (selectedProvider && selectedModel) {
|
||||
setDisplayModel(selectedModel.id)
|
||||
} else {
|
||||
setDisplayModel('Select a model')
|
||||
setDisplayModel(t('common:selectAModel'))
|
||||
}
|
||||
}, [selectedProvider, selectedModel])
|
||||
}, [selectedProvider, selectedModel, t])
|
||||
|
||||
// Reset search value when dropdown closes
|
||||
const onOpenChange = useCallback((open: boolean) => {
|
||||
@ -306,7 +308,7 @@ const DropdownModelProvider = ({
|
||||
ref={searchInputRef}
|
||||
value={searchValue}
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
placeholder="Search models..."
|
||||
placeholder={t('common:searchModels')}
|
||||
className="text-sm font-normal outline-0"
|
||||
/>
|
||||
{searchValue.length > 0 && (
|
||||
@ -324,7 +326,7 @@ const DropdownModelProvider = ({
|
||||
<div className="max-h-[320px] overflow-y-auto">
|
||||
{Object.keys(groupedItems).length === 0 && searchValue ? (
|
||||
<div className="py-3 px-4 text-sm text-main-view-fg/60">
|
||||
No models found for "{searchValue}"
|
||||
{t('common:noModelsFoundFor', { searchValue })}
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-1">
|
||||
|
||||
@ -14,6 +14,7 @@ import { useToolAvailable } from '@/hooks/useToolAvailable'
|
||||
|
||||
import React from 'react'
|
||||
import { useAppState } from '@/hooks/useAppState'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
|
||||
interface DropdownToolsAvailableProps {
|
||||
children: (isOpen: boolean, toolsCount: number) => React.ReactNode
|
||||
@ -28,6 +29,7 @@ export default function DropdownToolsAvailable({
|
||||
}: DropdownToolsAvailableProps) {
|
||||
const { tools } = useAppState()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
setIsOpen(open)
|
||||
@ -96,7 +98,7 @@ export default function DropdownToolsAvailable({
|
||||
<DropdownMenu onOpenChange={handleOpenChange}>
|
||||
<DropdownMenuTrigger asChild>{renderTrigger()}</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="max-w-64">
|
||||
<DropdownMenuItem disabled>No tools available</DropdownMenuItem>
|
||||
<DropdownMenuItem disabled>{t('common:noToolsAvailable')}</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
|
||||
@ -6,20 +6,22 @@ import {
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { fontSizeOptions, useAppearance } from '@/hooks/useAppearance'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
|
||||
export function FontSizeSwitcher() {
|
||||
const { fontSize, setFontSize } = useAppearance()
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<span
|
||||
title="Adjust Font Size"
|
||||
title={t('common:adjustFontSize')}
|
||||
className="flex cursor-pointer items-center gap-1 px-2 py-1 rounded-sm bg-main-view-fg/15 text-sm outline-none text-main-view-fg font-medium"
|
||||
>
|
||||
{fontSizeOptions.find(
|
||||
(item: { value: string; label: string }) => item.value === fontSize
|
||||
)?.label || 'Medium'}
|
||||
)?.label || t('common:medium')}
|
||||
</span>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-24">
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useGeneralSetting } from '@/hooks/useGeneralSetting'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAppTranslation } from '@/i18n'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@ -12,10 +12,12 @@ const LANGUAGES = [
|
||||
{ value: 'en', label: 'English' },
|
||||
{ value: 'id', label: 'Bahasa' },
|
||||
{ value: 'vn', label: 'Tiếng Việt' },
|
||||
{ value: 'zh-CN', label: '简体中文' },
|
||||
{ value: 'zh-TW', label: '繁體中文' },
|
||||
]
|
||||
|
||||
export default function LanguageSwitcher() {
|
||||
const { i18n } = useTranslation()
|
||||
const { i18n, t } = useAppTranslation()
|
||||
const { setCurrentLanguage, currentLanguage } = useGeneralSetting()
|
||||
|
||||
const changeLanguage = (lng: string) => {
|
||||
@ -27,13 +29,13 @@ export default function LanguageSwitcher() {
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<span
|
||||
title="Change Language"
|
||||
title={t('common:changeLanguage')}
|
||||
className="flex cursor-pointer items-center gap-1 px-2 py-1 rounded-sm bg-main-view-fg/15 text-sm outline-none text-main-view-fg font-medium"
|
||||
>
|
||||
{LANGUAGES.find(
|
||||
(lang: { value: string; label: string }) =>
|
||||
lang.value === currentLanguage
|
||||
)?.label || 'English'}
|
||||
)?.label || t('common:english')}
|
||||
</span>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-24">
|
||||
|
||||
@ -25,7 +25,7 @@ import {
|
||||
|
||||
import { useThreads } from '@/hooks/useThreads'
|
||||
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
import { useMemo, useState } from 'react'
|
||||
import {
|
||||
Dialog,
|
||||
@ -43,22 +43,22 @@ import { DownloadManagement } from '@/containers/DownloadManegement'
|
||||
|
||||
const mainMenus = [
|
||||
{
|
||||
title: 'common.newChat',
|
||||
title: 'common:newChat',
|
||||
icon: IconCirclePlusFilled,
|
||||
route: route.home,
|
||||
},
|
||||
{
|
||||
title: 'Assistants',
|
||||
title: 'common:assistants',
|
||||
icon: IconClipboardSmileFilled,
|
||||
route: route.assistant,
|
||||
},
|
||||
{
|
||||
title: 'common.hub',
|
||||
title: 'common:hub',
|
||||
icon: IconAppsFilled,
|
||||
route: route.hub,
|
||||
},
|
||||
{
|
||||
title: 'common.settings',
|
||||
title: 'common:settings',
|
||||
icon: IconSettingsFilled,
|
||||
route: route.settings.general,
|
||||
},
|
||||
@ -114,7 +114,7 @@ const LeftPanel = () => {
|
||||
<IconSearch className="absolute size-4 top-1/2 left-2 -translate-y-1/2 text-left-panel-fg/50" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t('common.search')}
|
||||
placeholder={t('common:search')}
|
||||
className="w-full pl-7 pr-8 py-1 bg-left-panel-fg/10 rounded-sm text-left-panel-fg focus:outline-none focus:ring-1 focus:ring-left-panel-fg/10"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
@ -138,7 +138,7 @@ const LeftPanel = () => {
|
||||
<IconSearch className="absolute size-4 top-1/2 left-2 -translate-y-1/2 text-left-panel-fg/50" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t('common.search')}
|
||||
placeholder={t('common:search')}
|
||||
className="w-full pl-7 pr-8 py-1 bg-left-panel-fg/10 rounded-sm text-left-panel-fg focus:outline-none focus:ring-1 focus:ring-left-panel-fg/10"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
@ -159,7 +159,7 @@ const LeftPanel = () => {
|
||||
<>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="block text-xs text-left-panel-fg/50 px-1 font-semibold sticky top-0">
|
||||
{t('common.favorites')}
|
||||
{t('common:favorites')}
|
||||
</span>
|
||||
<div className="relative">
|
||||
<DropdownMenu>
|
||||
@ -175,15 +175,14 @@ const LeftPanel = () => {
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
unstarAllThreads()
|
||||
toast.success('All Threads Unfavorited', {
|
||||
toast.success(t('common:toast.allThreadsUnfavorited.title'), {
|
||||
id: 'unfav-all-threads',
|
||||
description:
|
||||
'All threads have been removed from your favorites.',
|
||||
description: t('common:toast.allThreadsUnfavorited.description'),
|
||||
})
|
||||
}}
|
||||
>
|
||||
<IconStar size={16} />
|
||||
<span>{t('common.unstarAll')}</span>
|
||||
<span>{t('common:unstarAll')}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@ -206,7 +205,7 @@ const LeftPanel = () => {
|
||||
{unFavoritedThreads.length > 0 && (
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="block text-xs text-left-panel-fg/50 px-1 font-semibold">
|
||||
{t('common.recents')}
|
||||
{t('common:recents')}
|
||||
</span>
|
||||
<div className="relative">
|
||||
<Dialog>
|
||||
@ -231,15 +230,18 @@ const LeftPanel = () => {
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
<span>{t('common.deleteAll')}</span>
|
||||
<span>{t('common:deleteAll')}</span>
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete All Threads</DialogTitle>
|
||||
<DialogTitle>
|
||||
{t('common:dialogs.deleteAllThreads.title')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
All threads will be deleted. This action cannot
|
||||
be undone.
|
||||
{t(
|
||||
'common:dialogs.deleteAllThreads.description'
|
||||
)}
|
||||
</DialogDescription>
|
||||
<DialogFooter className="mt-2">
|
||||
<DialogClose asChild>
|
||||
@ -248,7 +250,7 @@ const LeftPanel = () => {
|
||||
size="sm"
|
||||
className="hover:no-underline"
|
||||
>
|
||||
Cancel
|
||||
{t('common:cancel')}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
@ -256,17 +258,16 @@ const LeftPanel = () => {
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
deleteAllThreads()
|
||||
toast.success('Delete All Threads', {
|
||||
toast.success(t('common:toast.deleteAllThreads.title'), {
|
||||
id: 'delete-all-thread',
|
||||
description:
|
||||
'All threads have been permanently deleted.',
|
||||
description: t('common:toast.deleteAllThreads.description'),
|
||||
})
|
||||
setTimeout(() => {
|
||||
navigate({ to: route.home })
|
||||
}, 0)
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
{t('common:deleteAll')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogHeader>
|
||||
@ -282,11 +283,12 @@ const LeftPanel = () => {
|
||||
<div className="px-1 mt-2">
|
||||
<div className="flex items-center gap-1 text-left-panel-fg/80">
|
||||
<IconSearch size={18} />
|
||||
<h6 className="font-medium text-base">No results found</h6>
|
||||
<h6 className="font-medium text-base">
|
||||
{t('common:noResultsFound')}
|
||||
</h6>
|
||||
</div>
|
||||
<p className="text-left-panel-fg/60 mt-1 text-xs leading-relaxed">
|
||||
We couldn't find any chats matching your search. Try a
|
||||
different keyword.
|
||||
{t('common:noResultsFoundDesc')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@ -296,10 +298,12 @@ const LeftPanel = () => {
|
||||
<div className="px-1 mt-2">
|
||||
<div className="flex items-center gap-1 text-left-panel-fg/80">
|
||||
<IconMessageFilled size={18} />
|
||||
<h6 className="font-medium text-base">No threads yet</h6>
|
||||
<h6 className="font-medium text-base">
|
||||
{t('common:noThreadsYet')}
|
||||
</h6>
|
||||
</div>
|
||||
<p className="text-left-panel-fg/60 mt-1 text-xs leading-relaxed">
|
||||
Start a new conversation to see your thread history here.
|
||||
{t('common:noThreadsYetDesc')}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -14,6 +14,7 @@ import { useModelProvider } from '@/hooks/useModelProvider'
|
||||
import { updateModel, stopModel } from '@/services/models'
|
||||
import { ModelSettingParams } from '@janhq/core'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
|
||||
type ModelSettingProps = {
|
||||
provider: ProviderObject
|
||||
@ -27,6 +28,7 @@ export function ModelSetting({
|
||||
smallIcon,
|
||||
}: ModelSettingProps) {
|
||||
const { updateProvider } = useModelProvider()
|
||||
const { t } = useTranslation()
|
||||
|
||||
// Create a debounced version of stopModel that waits 500ms after the last call
|
||||
const debouncedStopModel = debounce((modelId: string) => {
|
||||
@ -104,9 +106,9 @@ export function ModelSetting({
|
||||
</SheetTrigger>
|
||||
<SheetContent className="h-[calc(100%-8px)] top-1 right-1 rounded-e-md overflow-y-auto">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Model Settings - {model.id}</SheetTitle>
|
||||
<SheetTitle>{t('common:modelSettings.title', { modelId: model.id })}</SheetTitle>
|
||||
<SheetDescription>
|
||||
Configure model settings to optimize performance and behavior.
|
||||
{t('common:modelSettings.description')}
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="px-4 space-y-6">
|
||||
|
||||
@ -19,6 +19,7 @@ import { openAIProviderSettings } from '@/mock/data'
|
||||
import ProvidersAvatar from '@/containers/ProvidersAvatar'
|
||||
import cloneDeep from 'lodash/cloneDeep'
|
||||
import { toast } from 'sonner'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
|
||||
const ProvidersMenu = ({
|
||||
stepSetupRemoteProvider,
|
||||
@ -29,11 +30,11 @@ const ProvidersMenu = ({
|
||||
const navigate = useNavigate()
|
||||
const matches = useMatches()
|
||||
const [name, setName] = useState('')
|
||||
const { t } = useTranslation()
|
||||
|
||||
const createProvider = useCallback(() => {
|
||||
if (providers.some((e) => e.provider === name)) {
|
||||
toast.error(
|
||||
`Provider with name "${name}" already exists. Please choose a different name.`
|
||||
)
|
||||
toast.error(t('providerAlreadyExists', { name }))
|
||||
return
|
||||
}
|
||||
const newProvider = {
|
||||
@ -53,14 +54,14 @@ const ProvidersMenu = ({
|
||||
},
|
||||
})
|
||||
}, 0)
|
||||
}, [providers, name, addProvider, navigate])
|
||||
}, [providers, name, addProvider, t, navigate])
|
||||
|
||||
return (
|
||||
<div className="w-44 py-2 border-r border-main-view-fg/5 pb-10 overflow-y-auto">
|
||||
<Link to={route.settings.general}>
|
||||
<div className="flex items-center gap-0.5 ml-3 mb-4 mt-1">
|
||||
<IconArrowLeft size={16} className="text-main-view-fg/70" />
|
||||
<span className="text-main-view-fg/80">Back</span>
|
||||
<span className="text-main-view-fg/80">{t('common:back')}</span>
|
||||
</div>
|
||||
</Link>
|
||||
<div className="first-step-setup-remote-provider">
|
||||
@ -108,17 +109,17 @@ const ProvidersMenu = ({
|
||||
<DialogTrigger asChild>
|
||||
<div className="flex cursor-pointer px-4 my-1.5 items-center gap-1.5 text-main-view-fg/80">
|
||||
<IconCirclePlus size={18} />
|
||||
<span>Add Provider</span>
|
||||
<span>{t('provider:addProvider')}</span>
|
||||
</div>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add OpenAI Provider</DialogTitle>
|
||||
<DialogTitle>{t('provider:addOpenAIProvider')}</DialogTitle>
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="mt-2"
|
||||
placeholder="Enter a name for your provider"
|
||||
placeholder={t('provider:enterNameForProvider')}
|
||||
onKeyDown={(e) => {
|
||||
// Prevent key from being captured by parent components
|
||||
e.stopPropagation()
|
||||
@ -131,12 +132,12 @@ const ProvidersMenu = ({
|
||||
size="sm"
|
||||
className="hover:no-underline"
|
||||
>
|
||||
Cancel
|
||||
{t('common:cancel')}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<DialogClose asChild>
|
||||
<Button disabled={!name} onClick={createProvider}>
|
||||
Create
|
||||
{t('common:create')}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
|
||||
@ -15,6 +15,7 @@ import { useCodeblock } from '@/hooks/useCodeblock'
|
||||
import 'katex/dist/katex.min.css'
|
||||
import { IconCopy, IconCopyCheck } from '@tabler/icons-react'
|
||||
import rehypeRaw from 'rehype-raw'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
|
||||
interface MarkdownProps {
|
||||
content: string
|
||||
@ -33,6 +34,7 @@ function RenderMarkdownComponent({
|
||||
components,
|
||||
isWrapping,
|
||||
}: MarkdownProps) {
|
||||
const { t } = useTranslation()
|
||||
const { codeBlockStyle, showLineNumbers } = useCodeblock()
|
||||
|
||||
// State for tracking which code block has been copied
|
||||
@ -91,12 +93,12 @@ function RenderMarkdownComponent({
|
||||
{copiedId === codeId ? (
|
||||
<>
|
||||
<IconCopyCheck size={16} className="text-primary" />
|
||||
<span>Copied!</span>
|
||||
<span>{t('copied')}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IconCopy size={16} />
|
||||
<span>Copy</span>
|
||||
<span>{t('copy')}</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Link, useMatches } from '@tanstack/react-router'
|
||||
import { route } from '@/constants/routes'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
import { useModelProvider } from '@/hooks/useModelProvider'
|
||||
import { useGeneralSetting } from '@/hooks/useGeneralSetting'
|
||||
|
||||
@ -19,44 +19,44 @@ const SettingsMenu = () => {
|
||||
|
||||
const menuSettings = [
|
||||
{
|
||||
title: 'common.general',
|
||||
title: 'common:general',
|
||||
route: route.settings.general,
|
||||
},
|
||||
{
|
||||
title: 'common.appearance',
|
||||
title: 'common:appearance',
|
||||
route: route.settings.appearance,
|
||||
},
|
||||
{
|
||||
title: 'common.privacy',
|
||||
title: 'common:privacy',
|
||||
route: route.settings.privacy,
|
||||
},
|
||||
{
|
||||
title: 'common.keyboardShortcuts',
|
||||
title: 'common:keyboardShortcuts',
|
||||
route: route.settings.shortcuts,
|
||||
},
|
||||
{
|
||||
title: 'Hardware',
|
||||
title: 'common:hardware',
|
||||
route: route.settings.hardware,
|
||||
},
|
||||
// Only show MCP Servers when experimental features are enabled
|
||||
...(experimentalFeatures
|
||||
? [
|
||||
{
|
||||
title: 'MCP Servers',
|
||||
title: 'common:mcp-servers',
|
||||
route: route.settings.mcp_servers,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: 'Local API Server',
|
||||
title: 'common:local_api_server',
|
||||
route: route.settings.local_api_server,
|
||||
},
|
||||
{
|
||||
title: 'HTTPS Proxy',
|
||||
title: 'common:https_proxy',
|
||||
route: route.settings.https_proxy,
|
||||
},
|
||||
{
|
||||
title: 'Extensions',
|
||||
title: 'common:extensions',
|
||||
route: route.settings.extensions,
|
||||
},
|
||||
]
|
||||
@ -84,7 +84,7 @@ const SettingsMenu = () => {
|
||||
{/* Model Providers Link with default parameter */}
|
||||
{isActive ? (
|
||||
<div className="block px-2 mt-1 gap-1.5 py-1 w-full rounded bg-main-view-fg/5 cursor-pointer">
|
||||
<span>{t('common.modelProviders')}</span>
|
||||
<span>{t('common:modelProviders')}</span>
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
@ -94,7 +94,7 @@ const SettingsMenu = () => {
|
||||
className="block px-2 gap-1.5 cursor-pointer hover:bg-main-view-fg/5 py-1 w-full rounded"
|
||||
>
|
||||
<span className="text-main-view-fg/80">
|
||||
{t('common.modelProviders')}
|
||||
{t('common:modelProviders')}
|
||||
</span>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
@ -4,8 +4,10 @@ import { Link } from '@tanstack/react-router'
|
||||
import { route } from '@/constants/routes'
|
||||
import HeaderPage from './HeaderPage'
|
||||
import { isProd } from '@/lib/version'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
|
||||
function SetupScreen() {
|
||||
const { t } = useTranslation()
|
||||
const { providers } = useModelProvider()
|
||||
const firstItemRemoteProvider =
|
||||
providers.length > 0 ? providers[1].provider : 'openai'
|
||||
@ -17,11 +19,10 @@ function SetupScreen() {
|
||||
<div className="w-4/6 mx-auto">
|
||||
<div className="mb-8 text-left">
|
||||
<h1 className="font-editorialnew text-main-view-fg text-4xl">
|
||||
Welcome to Jan
|
||||
{t('setup:welcome')}
|
||||
</h1>
|
||||
<p className="text-main-view-fg/70 text-lg mt-2">
|
||||
To get started, you'll need to either download a local AI model or
|
||||
connect to a cloud model using an API key
|
||||
{t('setup:description')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-4 flex-col">
|
||||
@ -35,7 +36,7 @@ function SetupScreen() {
|
||||
>
|
||||
<div>
|
||||
<h1 className="text-main-view-fg font-medium text-base">
|
||||
Set up local model
|
||||
{t('setup:localModel')}
|
||||
</h1>
|
||||
</div>
|
||||
</Link>
|
||||
@ -53,7 +54,7 @@ function SetupScreen() {
|
||||
}}
|
||||
>
|
||||
<h1 className="text-main-view-fg font-medium text-base">
|
||||
Set up remote provider
|
||||
{t('setup:remoteProvider')}
|
||||
</h1>
|
||||
</Link>
|
||||
}
|
||||
|
||||
@ -6,12 +6,15 @@ import {
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { useTheme } from '@/hooks/useTheme'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
|
||||
export function ThemeSwitcher() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const themeOptions = [
|
||||
{ value: 'dark', label: 'Dark' },
|
||||
{ value: 'light', label: 'Light' },
|
||||
{ value: 'auto', label: 'System' },
|
||||
{ value: 'dark', label: t('common:dark') },
|
||||
{ value: 'light', label: t('common:light') },
|
||||
{ value: 'auto', label: t('common:system') },
|
||||
]
|
||||
|
||||
const { setTheme, activeTheme } = useTheme()
|
||||
@ -20,11 +23,11 @@ export function ThemeSwitcher() {
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<span
|
||||
title="Edit Theme"
|
||||
title={t('common:editTheme')}
|
||||
className="flex cursor-pointer items-center gap-1 px-2 py-1 rounded-sm bg-main-view-fg/15 text-sm outline-none text-main-view-fg font-medium"
|
||||
>
|
||||
{themeOptions.find((item) => item.value === activeTheme)?.label ||
|
||||
'Auto'}
|
||||
t('common:auto')}
|
||||
</span>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-24">
|
||||
|
||||
@ -2,6 +2,7 @@ import { ChevronDown, ChevronUp, Loader } from 'lucide-react'
|
||||
import { create } from 'zustand'
|
||||
import { RenderMarkdown } from './RenderMarkdown'
|
||||
import { useAppState } from '@/hooks/useAppState'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
|
||||
interface Props {
|
||||
text: string
|
||||
@ -28,6 +29,7 @@ const useThinkingStore = create<ThinkingBlockState>((set) => ({
|
||||
const ThinkingBlock = ({ id, text }: Props) => {
|
||||
const { thinkingState, toggleState } = useThinkingStore()
|
||||
const { streamingContent } = useAppState()
|
||||
const { t } = useTranslation()
|
||||
const loading = !text.includes('</think>') && streamingContent
|
||||
const isExpanded = thinkingState[id] ?? false
|
||||
const handleClick = () => toggleState(id)
|
||||
@ -51,7 +53,7 @@ const ThinkingBlock = ({ id, text }: Props) => {
|
||||
<ChevronDown className="size-4 text-main-view-fg/60" />
|
||||
)}
|
||||
<span className="font-medium">
|
||||
{loading ? 'Thinking...' : 'Thought'}
|
||||
{loading ? t('common:thinking') : t('common:thought')}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -39,9 +39,11 @@ import TokenSpeedIndicator from '@/containers/TokenSpeedIndicator'
|
||||
|
||||
import CodeEditor from '@uiw/react-textarea-code-editor'
|
||||
import '@uiw/react-textarea-code-editor/dist.css'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
|
||||
const CopyButton = ({ text }: { text: string }) => {
|
||||
const [copied, setCopied] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(text)
|
||||
@ -57,7 +59,7 @@ const CopyButton = ({ text }: { text: string }) => {
|
||||
{copied ? (
|
||||
<>
|
||||
<IconCopyCheck size={16} className="text-accent" />
|
||||
<span className="opacity-100">Copied!</span>
|
||||
<span className="opacity-100">{t('copied')}</span>
|
||||
</>
|
||||
) : (
|
||||
<Tooltip>
|
||||
@ -65,7 +67,7 @@ const CopyButton = ({ text }: { text: string }) => {
|
||||
<IconCopy size={16} />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Copy</p>
|
||||
<p>{t('copy')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
@ -86,6 +88,7 @@ export const ThreadContent = memo(
|
||||
}
|
||||
) => {
|
||||
const [message, setMessage] = useState(item.content?.[0]?.text?.value || '')
|
||||
const { t } = useTranslation()
|
||||
|
||||
// Use useMemo to stabilize the components prop
|
||||
const linkComponents = useMemo(
|
||||
@ -215,13 +218,13 @@ export const ThreadContent = memo(
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Edit</p>
|
||||
<p>{t('edit')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Message</DialogTitle>
|
||||
<DialogTitle>{t('common:dialogs.editMessage.title')}</DialogTitle>
|
||||
<Textarea
|
||||
value={message}
|
||||
onChange={(e) => {
|
||||
@ -248,10 +251,9 @@ export const ThreadContent = memo(
|
||||
disabled={!message}
|
||||
onClick={() => {
|
||||
editMessage(item.id)
|
||||
toast.success('Edit Message', {
|
||||
toast.success(t('common:toast.editMessage.title'), {
|
||||
id: 'edit-message',
|
||||
description:
|
||||
'Message edited successfully. Please wait for the model to respond.',
|
||||
description: t('common:toast.editMessage.description'),
|
||||
})
|
||||
}}
|
||||
>
|
||||
@ -274,7 +276,7 @@ export const ThreadContent = memo(
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Delete</p>
|
||||
<p>{t('delete')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@ -371,7 +373,7 @@ export const ThreadContent = memo(
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Delete</p>
|
||||
<p>{t('delete')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Dialog>
|
||||
@ -383,13 +385,13 @@ export const ThreadContent = memo(
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Metadata</p>
|
||||
<p>{t('metadata')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Message Metadata</DialogTitle>
|
||||
<DialogTitle>{t('common:dialogs.messageMetadata.title')}</DialogTitle>
|
||||
<div className="space-y-2">
|
||||
<div className="border border-main-view-fg/10 rounded-md overflow-hidden">
|
||||
<CodeEditor
|
||||
@ -424,7 +426,7 @@ export const ThreadContent = memo(
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Regenerate</p>
|
||||
<p>{t('regenerate')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@ -30,7 +30,7 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
import { DialogClose, DialogFooter, DialogHeader } from '@/components/ui/dialog'
|
||||
import {
|
||||
Dialog,
|
||||
@ -85,7 +85,7 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
|
||||
return (thread.title || '').replace(/<span[^>]*>|<\/span>/g, '')
|
||||
}, [thread.title])
|
||||
|
||||
const [title, setTitle] = useState(plainTitleForRename || 'New Thread')
|
||||
const [title, setTitle] = useState(plainTitleForRename || t('common:newThread'))
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -101,7 +101,7 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
|
||||
)}
|
||||
>
|
||||
<div className="py-1 pr-2 truncate">
|
||||
<span>{thread.title || 'New Thread'}</span>
|
||||
<span>{thread.title || t('common:newThread')}</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<DropdownMenu
|
||||
@ -127,7 +127,7 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
|
||||
}}
|
||||
>
|
||||
<IconStarFilled />
|
||||
<span>{t('common.unstar')}</span>
|
||||
<span>{t('common:unstar')}</span>
|
||||
</DropdownMenuItem>
|
||||
) : (
|
||||
<DropdownMenuItem
|
||||
@ -137,26 +137,26 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
|
||||
}}
|
||||
>
|
||||
<IconStar />
|
||||
<span>{t('common.star')}</span>
|
||||
<span>{t('common:star')}</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<Dialog
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setOpenDropdown(false)
|
||||
setTitle(plainTitleForRename || 'New Thread')
|
||||
setTitle(plainTitleForRename || t('common:newThread'))
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
<IconEdit />
|
||||
<span>{t('common.rename')}</span>
|
||||
<span>{t('common:rename')}</span>
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Thread Title</DialogTitle>
|
||||
<DialogTitle>{t('common:threadTitle')}</DialogTitle>
|
||||
<Input
|
||||
value={title}
|
||||
onChange={(e) => {
|
||||
@ -175,7 +175,7 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
|
||||
size="sm"
|
||||
className="hover:no-underline"
|
||||
>
|
||||
Cancel
|
||||
{t('common:cancel')}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
@ -183,14 +183,13 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
|
||||
onClick={() => {
|
||||
renameThread(thread.id, title)
|
||||
setOpenDropdown(false)
|
||||
toast.success('Rename Thread', {
|
||||
toast.success(t('common:toast.renameThread.title'), {
|
||||
id: 'rename-thread',
|
||||
description:
|
||||
"Thread title has been renamed to '" + title + "'",
|
||||
description: t('common:toast.renameThread.description', { title }),
|
||||
})
|
||||
}}
|
||||
>
|
||||
Rename
|
||||
{t('common:rename')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogHeader>
|
||||
@ -206,15 +205,14 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
|
||||
<DialogTrigger asChild>
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
<IconTrash />
|
||||
<span>{t('common.delete')}</span>
|
||||
<span>{t('common:delete')}</span>
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete Thread</DialogTitle>
|
||||
<DialogTitle>{t('common:deleteThread')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to delete this thread? This action
|
||||
cannot be undone.
|
||||
{t('common:dialogs.deleteThread.description')}
|
||||
</DialogDescription>
|
||||
<DialogFooter className="mt-2 flex items-center">
|
||||
<DialogClose asChild>
|
||||
@ -223,7 +221,7 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
|
||||
size="sm"
|
||||
className="hover:no-underline"
|
||||
>
|
||||
Cancel
|
||||
{t('common:cancel')}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
@ -231,17 +229,16 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
|
||||
onClick={() => {
|
||||
deleteThread(thread.id)
|
||||
setOpenDropdown(false)
|
||||
toast.success('Delete Thread', {
|
||||
toast.success(t('common:toast.deleteThread.title'), {
|
||||
id: 'delete-thread',
|
||||
description:
|
||||
'This thread has been permanently deleted.',
|
||||
description: t('common:toast.deleteThread.description'),
|
||||
})
|
||||
setTimeout(() => {
|
||||
navigate({ to: route.home })
|
||||
}, 0)
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
{t('common:delete')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogHeader>
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
|
||||
interface Props {
|
||||
result: string
|
||||
@ -127,6 +128,7 @@ const ContentItemRenderer = ({
|
||||
|
||||
const ToolCallBlock = ({ id, name, result, loading, args }: Props) => {
|
||||
const { collapseState, setCollapseState } = useToolCallBlockStore()
|
||||
const { t } = useTranslation()
|
||||
const isExpanded = collapseState[id] ?? (loading ? true : false)
|
||||
const [modalImage, setModalImage] = useState<{
|
||||
url: string
|
||||
@ -187,7 +189,7 @@ const ToolCallBlock = ({ id, name, result, loading, args }: Props) => {
|
||||
loading ? 'text-main-view-fg/40' : 'text-accent'
|
||||
)}
|
||||
>
|
||||
{loading ? 'Calling tool' : 'Completed'}{' '}
|
||||
{loading ? t('common:callingTool') : t('common:completed')}{' '}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
@ -248,7 +250,7 @@ const ToolCallBlock = ({ id, name, result, loading, args }: Props) => {
|
||||
>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] p-0">
|
||||
<DialogHeader className="p-6 pb-2">
|
||||
<DialogTitle>{modalImage?.alt || 'Image'}</DialogTitle>
|
||||
<DialogTitle>{modalImage?.alt || t('common:image')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex justify-center items-center p-6 pt-2">
|
||||
{modalImage && (
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { useLocalApiServer } from '@/hooks/useLocalApiServer'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
|
||||
export function TrustedHostsInput() {
|
||||
const { trustedHosts, setTrustedHosts } = useLocalApiServer()
|
||||
const [inputValue, setInputValue] = useState(trustedHosts.join(', '))
|
||||
const { t } = useTranslation()
|
||||
|
||||
// Update input value when trustedHosts changes externally
|
||||
useEffect(() => {
|
||||
@ -37,7 +39,7 @@ export function TrustedHostsInput() {
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
className="w-full h-8 text-sm"
|
||||
placeholder="Enter trusted hosts"
|
||||
placeholder={t('common:enterTrustedHosts')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -2,8 +2,10 @@ import { Button } from '@/components/ui/button'
|
||||
import { useAnalytic } from '@/hooks/useAnalytic'
|
||||
import { IconFileTextShield } from '@tabler/icons-react'
|
||||
import posthog from 'posthog-js'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
|
||||
export function PromptAnalytic() {
|
||||
const { t } = useTranslation()
|
||||
const { setProductAnalyticPrompt, setProductAnalytic } = useAnalytic()
|
||||
|
||||
const handleProductAnalytics = (isAllowed: boolean) => {
|
||||
@ -23,7 +25,7 @@ export function PromptAnalytic() {
|
||||
<div className="flex items-center gap-2">
|
||||
<IconFileTextShield className="text-accent" />
|
||||
<h2 className="font-medium text-main-view-fg/80">
|
||||
Help Us Improve Jan
|
||||
{t('helpUsImproveJan')}
|
||||
</h2>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-main-view-fg/70">
|
||||
@ -41,9 +43,9 @@ export function PromptAnalytic() {
|
||||
className="text-main-view-fg/70"
|
||||
onClick={() => handleProductAnalytics(false)}
|
||||
>
|
||||
Deny
|
||||
{t('deny')}
|
||||
</Button>
|
||||
<Button onClick={() => handleProductAnalytics(true)}>Allow</Button>
|
||||
<Button onClick={() => handleProductAnalytics(true)}>{t('allow')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -28,7 +28,7 @@ import {
|
||||
import { useTheme } from '@/hooks/useTheme'
|
||||
import { teamEmoji } from '@/utils/teamEmoji'
|
||||
import { AvatarEmoji } from '@/containers/AvatarEmoji'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
import { cn, isDev } from '@/lib/utils'
|
||||
|
||||
interface AddEditAssistantProps {
|
||||
@ -222,13 +222,17 @@ export default function AddEditAssistant({
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingKey ? 'Edit Assistant' : 'Add Assistant'}
|
||||
{editingKey
|
||||
? t('assistants:editAssistant')
|
||||
: t('assistants:addAssistant')}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative">
|
||||
<label className="text-sm mb-2 inline-block">Emoji</label>
|
||||
<label className="text-sm mb-2 inline-block">
|
||||
{t('assistants:emoji')}
|
||||
</label>
|
||||
<div
|
||||
className="border rounded-sm p-1 w-9 h-9 flex items-center justify-center border-main-view-fg/10 cursor-pointer"
|
||||
onClick={() => setShowEmojiPicker(!showEmojiPicker)}
|
||||
@ -267,12 +271,12 @@ export default function AddEditAssistant({
|
||||
|
||||
<div className="space-y-2 w-full">
|
||||
<label className="text-sm mb-2 inline-block">
|
||||
{t(`common.name`)}
|
||||
{t(`common:name`)}
|
||||
</label>
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Enter name"
|
||||
placeholder={t('assistants:enterName')}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
@ -280,22 +284,24 @@ export default function AddEditAssistant({
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm mb-2 inline-block">
|
||||
Description (optional)
|
||||
{t('assistants:description')}
|
||||
</label>
|
||||
<Textarea
|
||||
value={description || ''}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Enter description"
|
||||
placeholder={t('assistants:enterDescription')}
|
||||
className="resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm mb-2 inline-block">Instructions</label>
|
||||
<label className="text-sm mb-2 inline-block">
|
||||
{t('assistants:instructions')}
|
||||
</label>
|
||||
<Textarea
|
||||
value={instructions}
|
||||
onChange={(e) => setInstructions(e.target.value)}
|
||||
placeholder="Enter instructions"
|
||||
placeholder={t('assistants:enterInstructions')}
|
||||
className="resize-none"
|
||||
rows={4}
|
||||
/>
|
||||
@ -303,7 +309,9 @@ export default function AddEditAssistant({
|
||||
|
||||
<div className="space-y-2 my-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm">Predefined Parameters</label>
|
||||
<label className="text-sm">
|
||||
{t('assistants:predefinedParameters')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{Object.entries(paramsSettings).map(([key, setting]) => (
|
||||
@ -360,7 +368,7 @@ export default function AddEditAssistant({
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm">Parameters</label>
|
||||
<label className="text-sm">{t('assistants:parameters')}</label>
|
||||
<div
|
||||
className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out"
|
||||
onClick={handleAddParameter}
|
||||
@ -376,7 +384,7 @@ export default function AddEditAssistant({
|
||||
onChange={(e) =>
|
||||
handleParameterChange(index, e.target.value, 'key')
|
||||
}
|
||||
placeholder="Key"
|
||||
placeholder={t('assistants:key')}
|
||||
className="w-24"
|
||||
/>
|
||||
|
||||
@ -402,28 +410,28 @@ export default function AddEditAssistant({
|
||||
handleParameterChange(index, 'string', 'type')
|
||||
}
|
||||
>
|
||||
String
|
||||
{t('assistants:stringValue')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
handleParameterChange(index, 'number', 'type')
|
||||
}
|
||||
>
|
||||
Number
|
||||
{t('assistants:numberValue')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
handleParameterChange(index, 'boolean', 'type')
|
||||
}
|
||||
>
|
||||
Boolean
|
||||
{t('assistants:booleanValue')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
handleParameterChange(index, 'json', 'type')
|
||||
}
|
||||
>
|
||||
JSON
|
||||
{t('assistants:jsonValue')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@ -433,7 +441,11 @@ export default function AddEditAssistant({
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
value={paramsValues[index] ? 'True' : 'False'}
|
||||
value={
|
||||
paramsValues[index]
|
||||
? t('assistants:trueValue')
|
||||
: t('assistants:falseValue')
|
||||
}
|
||||
readOnly
|
||||
/>
|
||||
<IconChevronDown
|
||||
@ -448,14 +460,14 @@ export default function AddEditAssistant({
|
||||
handleParameterChange(index, true, 'value')
|
||||
}
|
||||
>
|
||||
True
|
||||
{t('assistants:trueValue')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
handleParameterChange(index, false, 'value')
|
||||
}
|
||||
>
|
||||
False
|
||||
{t('assistants:falseValue')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@ -469,7 +481,7 @@ export default function AddEditAssistant({
|
||||
onChange={(e) =>
|
||||
handleParameterChange(index, e.target.value, 'value')
|
||||
}
|
||||
placeholder="JSON Value"
|
||||
placeholder={t('assistants:jsonValuePlaceholder')}
|
||||
className="flex-1"
|
||||
/>
|
||||
) : (
|
||||
@ -479,7 +491,7 @@ export default function AddEditAssistant({
|
||||
handleParameterChange(index, e.target.value, 'value')
|
||||
}
|
||||
type={paramsTypes[index] === 'number' ? 'number' : 'text'}
|
||||
placeholder="Value"
|
||||
placeholder={t('assistants:value')}
|
||||
className="flex-1"
|
||||
/>
|
||||
)}
|
||||
@ -496,7 +508,7 @@ export default function AddEditAssistant({
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button onClick={handleSave}>Save</Button>
|
||||
<Button onClick={handleSave}>{t('assistants:save')}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@ -10,6 +10,7 @@ import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { IconPlus, IconTrash, IconGripVertical } from '@tabler/icons-react'
|
||||
import { MCPServerConfig } from '@/hooks/useMCPServers'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
@ -107,6 +108,7 @@ export default function AddEditMCPServer({
|
||||
initialData,
|
||||
onSave,
|
||||
}: AddEditMCPServerProps) {
|
||||
const { t } = useTranslation()
|
||||
const [serverName, setServerName] = useState('')
|
||||
const [command, setCommand] = useState('')
|
||||
const [args, setArgs] = useState<string[]>([''])
|
||||
@ -230,32 +232,38 @@ export default function AddEditMCPServer({
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingKey ? 'Edit MCP Server' : 'Add MCP Server'}
|
||||
{editingKey
|
||||
? t('mcp-servers:editServer')
|
||||
: t('mcp-servers:addServer')}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm mb-2 inline-block">Server Name</label>
|
||||
<label className="text-sm mb-2 inline-block">
|
||||
{t('mcp-servers:serverName')}
|
||||
</label>
|
||||
<Input
|
||||
value={serverName}
|
||||
onChange={(e) => setServerName(e.target.value)}
|
||||
placeholder="Enter server name"
|
||||
placeholder={t('mcp-servers:enterServerName')}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm mb-2 inline-block">Command</label>
|
||||
<label className="text-sm mb-2 inline-block">
|
||||
{t('mcp-servers:command')}
|
||||
</label>
|
||||
<Input
|
||||
value={command}
|
||||
onChange={(e) => setCommand(e.target.value)}
|
||||
placeholder="Enter command (uvx or npx)"
|
||||
placeholder={t('mcp-servers:enterCommand')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm">Arguments</label>
|
||||
<label className="text-sm">{t('mcp-servers:arguments')}</label>
|
||||
<div
|
||||
className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out"
|
||||
onClick={handleAddArg}
|
||||
@ -288,7 +296,9 @@ export default function AddEditMCPServer({
|
||||
onChange={(value) => handleArgChange(index, value)}
|
||||
onRemove={() => handleRemoveArg(index)}
|
||||
canRemove={args.length > 1}
|
||||
placeholder={`Argument ${index + 1}`}
|
||||
placeholder={t('mcp-servers:argument', {
|
||||
index: index + 1,
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
@ -297,7 +307,7 @@ export default function AddEditMCPServer({
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm">Environment Variables</label>
|
||||
<label className="text-sm">{t('mcp-servers:envVars')}</label>
|
||||
<div
|
||||
className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out"
|
||||
onClick={handleAddEnv}
|
||||
@ -311,13 +321,13 @@ export default function AddEditMCPServer({
|
||||
<Input
|
||||
value={key}
|
||||
onChange={(e) => handleEnvKeyChange(index, e.target.value)}
|
||||
placeholder="Key"
|
||||
placeholder={t('mcp-servers:key')}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Input
|
||||
value={envValues[index] || ''}
|
||||
onChange={(e) => handleEnvValueChange(index, e.target.value)}
|
||||
placeholder="Value"
|
||||
placeholder={t('mcp-servers:value')}
|
||||
className="flex-1"
|
||||
/>
|
||||
{envKeys.length > 1 && (
|
||||
@ -334,7 +344,7 @@ export default function AddEditMCPServer({
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button onClick={handleSave}>Save</Button>
|
||||
<Button onClick={handleSave}>{t('mcp-servers:save')}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@ -13,6 +13,7 @@ import { useModelProvider } from '@/hooks/useModelProvider'
|
||||
import { IconPlus } from '@tabler/icons-react'
|
||||
import { useState } from 'react'
|
||||
import { getProviderTitle } from '@/lib/utils'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
|
||||
type DialogAddModelProps = {
|
||||
provider: ModelProvider
|
||||
@ -20,6 +21,7 @@ type DialogAddModelProps = {
|
||||
}
|
||||
|
||||
export const DialogAddModel = ({ provider, trigger }: DialogAddModelProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { updateProvider } = useModelProvider()
|
||||
const [modelId, setModelId] = useState<string>('')
|
||||
const [open, setOpen] = useState(false)
|
||||
@ -62,10 +64,11 @@ export const DialogAddModel = ({ provider, trigger }: DialogAddModelProps) => {
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add New Model</DialogTitle>
|
||||
<DialogTitle>{t('providers:addModel.title')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Add a new model to the {getProviderTitle(provider.provider)}
|
||||
provider.
|
||||
{t('providers:addModel.description', {
|
||||
provider: getProviderTitle(provider.provider),
|
||||
})}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@ -75,13 +78,14 @@ export const DialogAddModel = ({ provider, trigger }: DialogAddModelProps) => {
|
||||
htmlFor="model-id"
|
||||
className="text-sm font-medium inline-block"
|
||||
>
|
||||
Model ID <span className="text-destructive">*</span>
|
||||
{t('providers:addModel.modelId')}{' '}
|
||||
<span className="text-destructive">*</span>
|
||||
</label>
|
||||
<Input
|
||||
id="model-id"
|
||||
value={modelId}
|
||||
onChange={(e) => setModelId(e.target.value)}
|
||||
placeholder="Enter model ID"
|
||||
placeholder={t('providers:addModel.enterModelId')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@ -96,7 +100,9 @@ export const DialogAddModel = ({ provider, trigger }: DialogAddModelProps) => {
|
||||
className="flex items-center gap-1 hover:underline text-primary"
|
||||
>
|
||||
<span>
|
||||
See model list from {getProviderTitle(provider.provider)}
|
||||
{t('providers:addModel.exploreModels', {
|
||||
provider: getProviderTitle(provider.provider),
|
||||
})}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
@ -108,7 +114,7 @@ export const DialogAddModel = ({ provider, trigger }: DialogAddModelProps) => {
|
||||
onClick={handleSubmit}
|
||||
disabled={!modelId.trim()}
|
||||
>
|
||||
Add Model
|
||||
{t('providers:addModel.addModel')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@ -8,8 +8,10 @@ import { useReleaseNotes } from '@/hooks/useReleaseNotes'
|
||||
import { RenderMarkdown } from '../RenderMarkdown'
|
||||
import { cn, isDev } from '@/lib/utils'
|
||||
import { isNightly, isBeta } from '@/lib/version'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
|
||||
const DialogAppUpdater = () => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
updateState,
|
||||
downloadAndInstallUpdate,
|
||||
@ -67,10 +69,12 @@ const DialogAppUpdater = () => {
|
||||
/>
|
||||
<div>
|
||||
<div className="text-base font-medium">
|
||||
New Version: Jan {updateState.updateInfo?.version}
|
||||
{t('updater:newVersion', {
|
||||
version: updateState.updateInfo?.version,
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-1 text-main-view-fg/70 font-normal mb-2">
|
||||
There's a new app update available to download.
|
||||
{t('updater:updateAvailable')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -80,9 +84,7 @@ const DialogAppUpdater = () => {
|
||||
<div className="max-h-[500px] p-4 w-[400px] overflow-y-scroll text-sm font-normal leading-relaxed">
|
||||
{isNightly && !isBeta ? (
|
||||
<p className="text-sm font-normal">
|
||||
You are using a nightly build. This version is built from
|
||||
the latest development branch and may not have release
|
||||
notes.
|
||||
{t('updater:nightlyBuild')}
|
||||
</p>
|
||||
) : (
|
||||
<RenderMarkdown
|
||||
@ -111,7 +113,9 @@ const DialogAppUpdater = () => {
|
||||
className="px-0 text-main-view-fg/70"
|
||||
onClick={() => setShowReleaseNotes(!showReleaseNotes)}
|
||||
>
|
||||
{showReleaseNotes ? 'Hide' : 'Show'} release notes
|
||||
{showReleaseNotes
|
||||
? t('updater:hideReleaseNotes')
|
||||
: t('updater:showReleaseNotes')}
|
||||
</Button>
|
||||
<div className="flex gap-x-5">
|
||||
<Button
|
||||
@ -119,15 +123,15 @@ const DialogAppUpdater = () => {
|
||||
className="px-0 text-main-view-fg/70 remind-me-later"
|
||||
onClick={() => setRemindMeLater(true)}
|
||||
>
|
||||
Remind me later
|
||||
{t('updater:remindMeLater')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleUpdate}
|
||||
disabled={updateState.isDownloading}
|
||||
>
|
||||
{updateState.isDownloading
|
||||
? 'Downloading...'
|
||||
: 'Update Now'}
|
||||
? t('updater:downloading')
|
||||
: t('updater:updateNow')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
} from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { IconFolder } from '@tabler/icons-react'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
|
||||
interface ChangeDataFolderLocationProps {
|
||||
children: React.ReactNode
|
||||
@ -28,6 +29,7 @@ export default function ChangeDataFolderLocation({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: ChangeDataFolderLocationProps) {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
@ -35,18 +37,17 @@ export default function ChangeDataFolderLocation({
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<IconFolder size={20} />
|
||||
Change Data Folder Location
|
||||
{t('settings:dialogs.changeDataFolder.title')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to change the data folder location? This will
|
||||
move all your data to the new location and restart the application.
|
||||
{t('settings:dialogs.changeDataFolder.description')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-main-view-fg/80 mb-2">
|
||||
Current Location:
|
||||
{t('settings:dialogs.changeDataFolder.currentLocation')}
|
||||
</h4>
|
||||
<div className="bg-main-view-fg/5 border border-main-view-fg/10 rounded">
|
||||
<code className="text-xs text-main-view-fg/70 break-all">
|
||||
@ -57,7 +58,7 @@ export default function ChangeDataFolderLocation({
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-main-view-fg/80 mb-2">
|
||||
New Location:
|
||||
{t('settings:dialogs.changeDataFolder.newLocation')}
|
||||
</h4>
|
||||
<div className="bg-accent/10 border border-accent/20 rounded">
|
||||
<code className="text-xs text-accent break-all">{newPath}</code>
|
||||
@ -68,11 +69,13 @@ export default function ChangeDataFolderLocation({
|
||||
<DialogFooter className="flex items-center gap-2">
|
||||
<DialogClose asChild>
|
||||
<Button variant="link" size="sm">
|
||||
Cancel
|
||||
{t('settings:dialogs.changeDataFolder.cancel')}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<DialogClose asChild>
|
||||
<Button onClick={onConfirm}>Change Location</Button>
|
||||
<Button onClick={onConfirm}>
|
||||
{t('settings:dialogs.changeDataFolder.changeLocation')}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
DialogDescription,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
|
||||
interface DeleteMCPServerConfirmProps {
|
||||
open: boolean
|
||||
@ -21,15 +22,15 @@ export default function DeleteMCPServerConfirm({
|
||||
serverName,
|
||||
onConfirm,
|
||||
}: DeleteMCPServerConfirmProps) {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete MCP Server</DialogTitle>
|
||||
<DialogTitle>{t('mcp-servers:deleteServer.title')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to delete the MCP server{' '}
|
||||
{t('mcp-servers:deleteServer.description', { serverName })}
|
||||
<span className="font-medium text-main-view-fg">{serverName}</span>?
|
||||
This action cannot be undone.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
@ -40,7 +41,7 @@ export default function DeleteMCPServerConfirm({
|
||||
onOpenChange(false)
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
{t('mcp-servers:deleteServer.delete')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@ -17,6 +17,7 @@ import { IconTrash } from '@tabler/icons-react'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
|
||||
type DialogDeleteModelProps = {
|
||||
provider: ModelProvider
|
||||
@ -27,6 +28,7 @@ export const DialogDeleteModel = ({
|
||||
provider,
|
||||
modelId,
|
||||
}: DialogDeleteModelProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [selectedModelId, setSelectedModelId] = useState<string>('')
|
||||
const { setProviders, deleteModel: deleteModelCache } = useModelProvider()
|
||||
|
||||
@ -43,10 +45,15 @@ export const DialogDeleteModel = ({
|
||||
}))
|
||||
setProviders(filteredProviders)
|
||||
})
|
||||
toast.success('Delete Model', {
|
||||
id: `delete-model-${selectedModel?.id}`,
|
||||
description: `Model ${selectedModel?.id} has been permanently deleted.`,
|
||||
})
|
||||
toast.success(
|
||||
t('providers:deleteModel.title', { modelId: selectedModel?.id }),
|
||||
{
|
||||
id: `delete-model-${selectedModel?.id}`,
|
||||
description: t('providers:deleteModel.success', {
|
||||
modelId: selectedModel?.id,
|
||||
}),
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@ -78,22 +85,23 @@ export const DialogDeleteModel = ({
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete Model: {selectedModel.id}</DialogTitle>
|
||||
<DialogTitle>
|
||||
{t('providers:deleteModel.title', { modelId: selectedModel.id })}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to delete this model? This action cannot be
|
||||
undone.
|
||||
{t('providers:deleteModel.description')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogFooter className="mt-2">
|
||||
<DialogClose asChild>
|
||||
<Button variant="link" size="sm" className="hover:no-underline">
|
||||
Cancel
|
||||
{t('providers:deleteModel.cancel')}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<DialogClose asChild>
|
||||
<Button variant="destructive" size="sm" onClick={removeModel}>
|
||||
Delete
|
||||
{t('providers:deleteModel.delete')}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
|
||||
@ -18,11 +18,13 @@ import { useModelProvider } from '@/hooks/useModelProvider'
|
||||
import { useRouter } from '@tanstack/react-router'
|
||||
import { route } from '@/constants/routes'
|
||||
import { normalizeProvider } from '@/lib/models'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
|
||||
type Props = {
|
||||
provider?: ProviderObject
|
||||
}
|
||||
const DeleteProvider = ({ provider }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { deleteProvider, providers } = useModelProvider()
|
||||
const router = useRouter()
|
||||
if (
|
||||
@ -34,9 +36,11 @@ const DeleteProvider = ({ provider }: Props) => {
|
||||
|
||||
const removeProvider = async () => {
|
||||
deleteProvider(provider.provider)
|
||||
toast.success('Delete Provider', {
|
||||
toast.success(t('providers:deleteProvider.title'), {
|
||||
id: `delete-provider-${provider.provider}`,
|
||||
description: `Provider ${provider.provider} has been permanently deleted.`,
|
||||
description: t('providers:deleteProvider.success', {
|
||||
provider: provider.provider,
|
||||
}),
|
||||
})
|
||||
setTimeout(() => {
|
||||
router.navigate({
|
||||
@ -50,28 +54,31 @@ const DeleteProvider = ({ provider }: Props) => {
|
||||
|
||||
return (
|
||||
<CardItem
|
||||
title="Delete Provider"
|
||||
description="Delete this provider and all its models. This action cannot be undone."
|
||||
title={t('providers:deleteProvider.title')}
|
||||
description={t('providers:deleteProvider.description')}
|
||||
actions={
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="destructive" size="sm">
|
||||
Delete
|
||||
{t('providers:deleteProvider.delete')}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete Provider: {provider.provider}</DialogTitle>
|
||||
<DialogTitle>
|
||||
{t('providers:deleteProvider.confirmTitle', {
|
||||
provider: provider.provider,
|
||||
})}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to delete this provider? This action
|
||||
cannot be undone.
|
||||
{t('providers:deleteProvider.confirmDescription')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogFooter className="mt-2">
|
||||
<DialogClose asChild>
|
||||
<Button variant="link" size="sm" className="hover:no-underline">
|
||||
Cancel
|
||||
{t('providers:deleteProvider.cancel')}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<DialogClose asChild>
|
||||
@ -80,7 +87,7 @@ const DeleteProvider = ({ provider }: Props) => {
|
||||
size="sm"
|
||||
onClick={removeProvider}
|
||||
>
|
||||
Delete
|
||||
{t('providers:deleteProvider.delete')}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
|
||||
@ -10,6 +10,7 @@ import { Button } from '@/components/ui/button'
|
||||
import { MCPServerConfig } from '@/hooks/useMCPServers'
|
||||
import CodeEditor from '@uiw/react-textarea-code-editor'
|
||||
import '@uiw/react-textarea-code-editor/dist.css'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
|
||||
interface EditJsonMCPserverProps {
|
||||
open: boolean
|
||||
@ -26,6 +27,7 @@ export default function EditJsonMCPserver({
|
||||
initialData,
|
||||
onSave,
|
||||
}: EditJsonMCPserverProps) {
|
||||
const { t } = useTranslation()
|
||||
const [jsonContent, setJsonContent] = useState('')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
@ -36,10 +38,10 @@ export default function EditJsonMCPserver({
|
||||
setJsonContent(JSON.stringify(initialData, null, 2))
|
||||
setError(null)
|
||||
} catch {
|
||||
setError('Failed to parse initial data')
|
||||
setError(t('mcp-servers:editJson.errorParse'))
|
||||
}
|
||||
}
|
||||
}, [open, initialData])
|
||||
}, [open, initialData, t])
|
||||
|
||||
const handlePaste = (e: React.ClipboardEvent) => {
|
||||
const pastedText = e.clipboardData.getData('text')
|
||||
@ -51,7 +53,7 @@ export default function EditJsonMCPserver({
|
||||
setError(null)
|
||||
} catch (error) {
|
||||
e.preventDefault()
|
||||
setError('Invalid JSON format in pasted content')
|
||||
setError(t('mcp-servers:editJson.errorPaste'))
|
||||
console.error('Paste error:', error)
|
||||
}
|
||||
}
|
||||
@ -63,7 +65,7 @@ export default function EditJsonMCPserver({
|
||||
onOpenChange(false)
|
||||
setError(null)
|
||||
} catch {
|
||||
setError('Invalid JSON format')
|
||||
setError(t('mcp-servers:editJson.errorFormat'))
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,8 +75,8 @@ export default function EditJsonMCPserver({
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{serverName
|
||||
? `Edit JSON for MCP Server: ${serverName}`
|
||||
: 'Edit All MCP Servers JSON'}
|
||||
? t('mcp-servers:editJson.title', { serverName })
|
||||
: t('mcp-servers:editJson.titleAll')}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-2">
|
||||
@ -82,7 +84,7 @@ export default function EditJsonMCPserver({
|
||||
<CodeEditor
|
||||
value={jsonContent}
|
||||
language="json"
|
||||
placeholder="Enter JSON configuration"
|
||||
placeholder={t('mcp-servers:editJson.placeholder')}
|
||||
onChange={(e) => setJsonContent(e.target.value)}
|
||||
onPaste={handlePaste}
|
||||
style={{
|
||||
@ -96,7 +98,7 @@ export default function EditJsonMCPserver({
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button onClick={handleSave}>Save</Button>
|
||||
<Button onClick={handleSave}>{t('mcp-servers:editJson.save')}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@ -22,6 +22,7 @@ import {
|
||||
IconCodeCircle2,
|
||||
} from '@tabler/icons-react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
|
||||
// No need to define our own interface, we'll use the existing Model type
|
||||
type DialogEditModelProps = {
|
||||
@ -33,6 +34,7 @@ export const DialogEditModel = ({
|
||||
provider,
|
||||
modelId,
|
||||
}: DialogEditModelProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { updateProvider } = useModelProvider()
|
||||
const [selectedModelId, setSelectedModelId] = useState<string>('')
|
||||
const [capabilities, setCapabilities] = useState<Record<string, boolean>>({
|
||||
@ -140,20 +142,24 @@ export const DialogEditModel = ({
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="line-clamp-1" title={selectedModel.id}>
|
||||
Edit Model: {selectedModel.id}
|
||||
{t('providers:editModel.title', { modelId: selectedModel.id })}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Configure model capabilities by toggling the options below.
|
||||
{t('providers:editModel.description')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="py-1">
|
||||
<h3 className="text-sm font-medium mb-3">Capabilities</h3>
|
||||
<h3 className="text-sm font-medium mb-3">
|
||||
{t('providers:editModel.capabilities')}
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<IconTool className="size-4 text-main-view-fg/70" />
|
||||
<span className="text-sm">Tools</span>
|
||||
<span className="text-sm">
|
||||
{t('providers:editModel.tools')}
|
||||
</span>
|
||||
</div>
|
||||
<Switch
|
||||
id="tools-capability"
|
||||
@ -167,7 +173,9 @@ export const DialogEditModel = ({
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<IconEye className="size-4 text-main-view-fg/70" />
|
||||
<span className="text-sm">Vision</span>
|
||||
<span className="text-sm">
|
||||
{t('providers:editModel.vision')}
|
||||
</span>
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
@ -180,14 +188,18 @@ export const DialogEditModel = ({
|
||||
}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Not available yet</TooltipContent>
|
||||
<TooltipContent>
|
||||
{t('providers:editModel.notAvailable')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<IconCodeCircle2 className="size-4 text-main-view-fg/70" />
|
||||
<span className="text-sm">Embeddings</span>
|
||||
<span className="text-sm">
|
||||
{t('providers:editModel.embeddings')}
|
||||
</span>
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
@ -200,7 +212,9 @@ export const DialogEditModel = ({
|
||||
}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Not available yet</TooltipContent>
|
||||
<TooltipContent>
|
||||
{t('providers:editModel.notAvailable')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
@ -221,7 +235,7 @@ export const DialogEditModel = ({
|
||||
{/* <div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<IconAtom className="size-4 text-main-view-fg/70" />
|
||||
<span className="text-sm">Reasoning</span>
|
||||
<span className="text-sm">{t('reasoning')}</span>
|
||||
</div>
|
||||
<Switch
|
||||
id="reasoning-capability"
|
||||
|
||||
@ -9,8 +9,11 @@ import {
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useToolApproval } from '@/hooks/useToolApproval'
|
||||
import { AlertTriangle } from 'lucide-react'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
import { Trans } from 'react-i18next'
|
||||
|
||||
export default function ToolApproval() {
|
||||
const { t } = useTranslation()
|
||||
const { isModalOpen, modalProps, setModalOpen } = useToolApproval()
|
||||
|
||||
if (!modalProps) {
|
||||
@ -47,9 +50,13 @@ export default function ToolApproval() {
|
||||
<AlertTriangle className="size-4" />
|
||||
</div>
|
||||
<div>
|
||||
<DialogTitle>Tool Call Request</DialogTitle>
|
||||
<DialogTitle>{t('tools:toolApproval.title')}</DialogTitle>
|
||||
<DialogDescription className="mt-1 text-main-view-fg/70">
|
||||
The assistant wants to use the tool: <strong>{toolName}</strong>
|
||||
<Trans
|
||||
i18nKey="tools:toolApproval.description"
|
||||
values={{ toolName }}
|
||||
components={{ strong: <strong className="font-semibold" /> }}
|
||||
/>
|
||||
</DialogDescription>
|
||||
</div>
|
||||
</div>
|
||||
@ -57,26 +64,30 @@ export default function ToolApproval() {
|
||||
|
||||
<div className="bg-main-view-fg/8 p-2 border border-main-view-fg/5 rounded-lg">
|
||||
<p className="text-sm text-main-view-fg/70 leading-relaxed">
|
||||
<strong>Security Notice:</strong> Malicious tools or conversation
|
||||
content could potentially trick the assistant into attempting
|
||||
harmful actions. Review each tool call carefully before approving.
|
||||
{t('tools:toolApproval.securityNotice')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex flex-col gap-2 sm:flex-row sm:justify-end">
|
||||
<Button variant="link" onClick={handleDeny} className="w-full">
|
||||
Deny
|
||||
</Button>
|
||||
<DialogFooter className="flex flex-col gap-2 sm:flex-row sm:justify-between">
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={handleAllowOnce}
|
||||
className="border border-main-view-fg/20"
|
||||
onClick={handleDeny}
|
||||
className="flex-1 text-right sm:flex-none"
|
||||
>
|
||||
Allow Once
|
||||
</Button>
|
||||
<Button variant="default" onClick={handleAllow}>
|
||||
Always Allow
|
||||
{t('tools:toolApproval.deny')}
|
||||
</Button>
|
||||
<div className="flex flex-col sm:flex-row sm:gap-2 sm:items-center">
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={handleAllowOnce}
|
||||
className="border border-main-view-fg/20"
|
||||
>
|
||||
{t('tools:toolApproval.allowOnce')}
|
||||
</Button>
|
||||
<Button variant="default" onClick={handleAllow}>
|
||||
{t('tools:toolApproval.alwaysAllow')}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@ -1,47 +1,7 @@
|
||||
import i18n from 'i18next'
|
||||
import { initReactI18next } from 'react-i18next'
|
||||
// Re-export our custom i18n implementation
|
||||
export { default } from '@/i18n/setup'
|
||||
|
||||
import enCommon from '@/locales/en/common.json'
|
||||
import idCommon from '@/locales/id/common.json'
|
||||
import vnCommon from '@/locales/vn/common.json'
|
||||
import enChat from '@/locales/en/chat.json'
|
||||
import idChat from '@/locales/id/chat.json'
|
||||
import vnChat from '@/locales/vn/chat.json'
|
||||
import enSettings from '@/locales/en/settings.json'
|
||||
import idSettings from '@/locales/id/settings.json'
|
||||
import vnSettings from '@/locales/vn/settings.json'
|
||||
|
||||
import { localStorageKey } from '@/constants/localStorage'
|
||||
|
||||
const stored = localStorage.getItem(localStorageKey.settingGeneral)
|
||||
const parsed = stored ? JSON.parse(stored) : {}
|
||||
const defaultLang = parsed?.state?.currentLanguage
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
resources: {
|
||||
en: {
|
||||
chat: enChat,
|
||||
common: enCommon,
|
||||
settings: enSettings,
|
||||
},
|
||||
id: {
|
||||
chat: idChat,
|
||||
common: idCommon,
|
||||
settings: idSettings,
|
||||
},
|
||||
vn: {
|
||||
chat: vnChat,
|
||||
common: vnCommon,
|
||||
settings: vnSettings,
|
||||
},
|
||||
},
|
||||
lng: defaultLang,
|
||||
fallbackLng: 'en',
|
||||
ns: ['chat', 'common', 'settings'],
|
||||
defaultNS: 'common',
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
})
|
||||
|
||||
export default i18n
|
||||
// Re-export compatibility functions for existing code
|
||||
export { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
export { useAppTranslation } from '@/i18n/hooks'
|
||||
export { TranslationProvider } from '@/i18n/TranslationContext'
|
||||
46
web-app/src/i18n/TranslationContext.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import React, { ReactNode, useEffect, useCallback } from "react"
|
||||
import i18next, { loadTranslations } from "./setup"
|
||||
import { useGeneralSetting } from "@/hooks/useGeneralSetting"
|
||||
import { TranslationContext } from "./context"
|
||||
|
||||
// Translation provider component
|
||||
export const TranslationProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||
// Get the current language from general settings
|
||||
const { currentLanguage } = useGeneralSetting()
|
||||
|
||||
// Load translations once when the component mounts
|
||||
useEffect(() => {
|
||||
try {
|
||||
loadTranslations()
|
||||
} catch (error) {
|
||||
console.error("Failed to load translations:", error)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Update language when currentLanguage changes
|
||||
useEffect(() => {
|
||||
if (currentLanguage) {
|
||||
i18next.changeLanguage(currentLanguage)
|
||||
}
|
||||
}, [currentLanguage])
|
||||
|
||||
// Memoize the translation function to prevent unnecessary re-renders
|
||||
const translate = useCallback(
|
||||
(key: string, options?: Record<string, unknown>) => {
|
||||
return i18next.t(key, options)
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
return (
|
||||
<TranslationContext.Provider
|
||||
value={{
|
||||
t: translate,
|
||||
i18n: i18next,
|
||||
}}>
|
||||
{children}
|
||||
</TranslationContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default TranslationProvider
|
||||
11
web-app/src/i18n/context.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { createContext } from "react"
|
||||
import i18next from "./setup"
|
||||
|
||||
// Create context for translations
|
||||
export const TranslationContext = createContext<{
|
||||
t: (key: string, options?: Record<string, unknown>) => string
|
||||
i18n: typeof i18next
|
||||
}>({
|
||||
t: (key: string) => key,
|
||||
i18n: i18next,
|
||||
})
|
||||
5
web-app/src/i18n/hooks.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { useContext } from "react"
|
||||
import { TranslationContext } from "./context"
|
||||
|
||||
// Custom hook for easy translations
|
||||
export const useAppTranslation = () => useContext(TranslationContext)
|
||||
8
web-app/src/i18n/index.ts
Normal file
@ -0,0 +1,8 @@
|
||||
// Export the main i18n setup
|
||||
export { default as i18n, loadTranslations } from './setup'
|
||||
|
||||
// Export the React context and hook
|
||||
export { TranslationProvider } from './TranslationContext'
|
||||
|
||||
// Export types
|
||||
export type { I18nInstance, TranslationResources } from './setup'
|
||||
34
web-app/src/i18n/react-i18next-compat.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { useAppTranslation } from './hooks'
|
||||
|
||||
// Compatibility layer for react-i18next
|
||||
// This allows existing code to work without changes
|
||||
|
||||
/**
|
||||
* Hook that mimics react-i18next's useTranslation hook
|
||||
* @param namespace - Optional namespace (not used in our implementation as we handle it in the key)
|
||||
* @returns Object with t function and i18n instance
|
||||
*/
|
||||
export const useTranslation = (namespace?: string) => {
|
||||
const { t, i18n: i18nInstance } = useAppTranslation()
|
||||
|
||||
// If namespace is provided, we can prefix keys with it
|
||||
const namespacedT = namespace
|
||||
? (key: string, options?: Record<string, unknown>) => {
|
||||
// If key already has namespace, use as-is, otherwise prefix with namespace
|
||||
const finalKey = key.includes(':') ? key : `${namespace}:${key}`
|
||||
return t(finalKey, options)
|
||||
}
|
||||
: t
|
||||
|
||||
return {
|
||||
t: namespacedT,
|
||||
i18n: i18nInstance,
|
||||
}
|
||||
}
|
||||
|
||||
// Export the i18n instance for direct usage
|
||||
export { default as i18n } from './setup'
|
||||
|
||||
// Re-export other utilities
|
||||
export { TranslationProvider } from './TranslationContext'
|
||||
export { useAppTranslation } from './hooks'
|
||||
156
web-app/src/i18n/setup.ts
Normal file
@ -0,0 +1,156 @@
|
||||
import { localStorageKey } from '@/constants/localStorage'
|
||||
|
||||
// Types for our i18n implementation
|
||||
export interface TranslationResources {
|
||||
[language: string]: {
|
||||
[namespace: string]: {
|
||||
[key: string]: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface I18nInstance {
|
||||
language: string
|
||||
fallbackLng: string
|
||||
resources: TranslationResources
|
||||
namespaces: string[]
|
||||
defaultNS: string
|
||||
changeLanguage: (lng: string) => void
|
||||
t: (key: string, options?: Record<string, unknown>) => string
|
||||
}
|
||||
|
||||
// Global i18n instance
|
||||
let i18nInstance: I18nInstance
|
||||
|
||||
// Dynamically load locale files
|
||||
const localeFiles = import.meta.glob('../locales/**/*.json', { eager: true })
|
||||
|
||||
const resources: TranslationResources = {}
|
||||
const namespaces: string[] = []
|
||||
|
||||
// Process all locale files
|
||||
Object.entries(localeFiles).forEach(([path, module]) => {
|
||||
// Example path: '../locales/en/common.json' -> language: 'en', namespace: 'common'
|
||||
const match = path.match(/\.\.\/locales\/([^/]+)\/([^/]+)\.json/)
|
||||
|
||||
if (match) {
|
||||
const [, language, namespace] = match
|
||||
|
||||
// Initialize language object if it doesn't exist
|
||||
if (!resources[language]) {
|
||||
resources[language] = {}
|
||||
}
|
||||
|
||||
// Add namespace to list if it's not already there
|
||||
if (!namespaces.includes(namespace)) {
|
||||
namespaces.push(namespace)
|
||||
}
|
||||
|
||||
// Add namespace resources to language
|
||||
resources[language][namespace] = (module as { default: { [key: string]: string } }).default || (module as { [key: string]: string })
|
||||
}
|
||||
})
|
||||
|
||||
// Get stored language preference
|
||||
const getStoredLanguage = (): string => {
|
||||
try {
|
||||
const stored = localStorage.getItem(localStorageKey.settingGeneral)
|
||||
const parsed = stored ? JSON.parse(stored) : {}
|
||||
return parsed?.state?.currentLanguage || 'en'
|
||||
} catch {
|
||||
return 'en'
|
||||
}
|
||||
}
|
||||
|
||||
// Translation function
|
||||
const translate = (key: string, options: Record<string, unknown> = {}): string => {
|
||||
const { language, fallbackLng, resources: res, defaultNS } = i18nInstance
|
||||
|
||||
// Parse key to extract namespace and actual key
|
||||
let namespace = defaultNS
|
||||
let translationKey = key
|
||||
|
||||
if (key.includes(':')) {
|
||||
const parts = key.split(':')
|
||||
namespace = parts[0]
|
||||
translationKey = parts[1]
|
||||
}
|
||||
|
||||
// Helper function to get nested value from object using dot notation
|
||||
const getNestedValue = (obj: Record<string, unknown>, path: string): string | undefined => {
|
||||
return path.split('.').reduce((current, key) => {
|
||||
return current && typeof current === 'object' && current !== null && key in current
|
||||
? (current as Record<string, unknown>)[key]
|
||||
: undefined
|
||||
}, obj as unknown) as string | undefined
|
||||
}
|
||||
|
||||
// Try to get translation from current language
|
||||
let translation = getNestedValue(res[language]?.[namespace], translationKey)
|
||||
|
||||
// Fallback to fallback language if not found
|
||||
if (translation === undefined && language !== fallbackLng) {
|
||||
translation = getNestedValue(res[fallbackLng]?.[namespace], translationKey)
|
||||
}
|
||||
|
||||
// If still not found, return the key itself
|
||||
if (translation === undefined) {
|
||||
console.warn(`Translation missing for key: ${key}`)
|
||||
return key
|
||||
}
|
||||
|
||||
// Handle interpolation
|
||||
if (typeof translation === 'string' && options) {
|
||||
return translation.replace(/\{\{(\w+)\}\}/g, (match, variable) => {
|
||||
return options[variable] !== undefined ? String(options[variable]) : match
|
||||
})
|
||||
}
|
||||
|
||||
return String(translation)
|
||||
}
|
||||
|
||||
// Change language function
|
||||
const changeLanguage = (lng: string): void => {
|
||||
if (i18nInstance && resources[lng]) {
|
||||
i18nInstance.language = lng
|
||||
|
||||
// Update localStorage
|
||||
try {
|
||||
const stored = localStorage.getItem(localStorageKey.settingGeneral)
|
||||
const parsed = stored ? JSON.parse(stored) : { state: {} }
|
||||
parsed.state.currentLanguage = lng
|
||||
localStorage.setItem(localStorageKey.settingGeneral, JSON.stringify(parsed))
|
||||
} catch (error) {
|
||||
console.error('Failed to save language preference:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize i18n instance
|
||||
const initI18n = (): I18nInstance => {
|
||||
const currentLanguage = getStoredLanguage()
|
||||
|
||||
i18nInstance = {
|
||||
language: currentLanguage,
|
||||
fallbackLng: 'en',
|
||||
resources,
|
||||
namespaces,
|
||||
defaultNS: 'common',
|
||||
changeLanguage,
|
||||
t: translate,
|
||||
}
|
||||
|
||||
return i18nInstance
|
||||
}
|
||||
|
||||
// Load translations function (for compatibility with reference implementation)
|
||||
export const loadTranslations = (): void => {
|
||||
// Translations are already loaded via import.meta.glob
|
||||
// This function exists for compatibility but doesn't need to do anything
|
||||
console.log('Translations loaded:', Object.keys(resources))
|
||||
}
|
||||
|
||||
// Initialize and export the i18n instance
|
||||
const i18n = initI18n()
|
||||
|
||||
export default i18n
|
||||
32
web-app/src/locales/en/assistants.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"title": "Assistants",
|
||||
"editAssistant": "Edit Assistant",
|
||||
"deleteAssistant": "Delete Assistant",
|
||||
"deleteConfirmation": "Delete Assistant",
|
||||
"deleteConfirmationDesc": "Are you sure you want to delete this assistant? This action cannot be undone.",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete",
|
||||
"addAssistant": "Add Assistant",
|
||||
"emoji": "Emoji",
|
||||
"name": "Name",
|
||||
"enterName": "Enter name",
|
||||
"description": "Description (optional)",
|
||||
"enterDescription": "Enter description",
|
||||
"instructions": "Instructions",
|
||||
"enterInstructions": "Enter instructions",
|
||||
"predefinedParameters": "Predefined Parameters",
|
||||
"parameters": "Parameters",
|
||||
"key": "Key",
|
||||
"value": "Value",
|
||||
"stringValue": "String",
|
||||
"numberValue": "Number",
|
||||
"booleanValue": "Boolean",
|
||||
"jsonValue": "JSON",
|
||||
"trueValue": "True",
|
||||
"falseValue": "False",
|
||||
"jsonValuePlaceholder": "JSON Value",
|
||||
"save": "Save",
|
||||
"createNew": "Create New Assistant",
|
||||
"personality": "Personality",
|
||||
"capabilities": "Capabilities"
|
||||
}
|
||||
@ -1,9 +1,10 @@
|
||||
{
|
||||
"chat": {
|
||||
"welcome": "Hi, how are you?",
|
||||
"description": "How can I help you today?",
|
||||
"status": {
|
||||
"empty": "No Chats Found"
|
||||
}
|
||||
}
|
||||
}
|
||||
"welcome": "Hi, how are you?",
|
||||
"description": "How can I help you today?",
|
||||
"status": {
|
||||
"empty": "No Chats Found"
|
||||
},
|
||||
"sendMessage": "Send Message",
|
||||
"newConversation": "New Conversation",
|
||||
"clearHistory": "Clear History"
|
||||
}
|
||||
@ -1,32 +1,267 @@
|
||||
{
|
||||
"common": {
|
||||
"general": "General",
|
||||
"settings": "Settings",
|
||||
"modelProviders": "Model Providers",
|
||||
"appearance": "Appearance",
|
||||
"privacy": "Privacy",
|
||||
"keyboardShortcuts": "Shortcuts",
|
||||
"newChat": "New Chat",
|
||||
"favorites": "Favorites",
|
||||
"recents": "Recents",
|
||||
"hub": "Hub",
|
||||
"helpSupport": "Help & Support",
|
||||
"unstarAll": "Unstar All",
|
||||
"unstar": "Unstar",
|
||||
"deleteAll": "Delete All",
|
||||
"star": "Star",
|
||||
"rename": "Rename",
|
||||
"delete": "Delete",
|
||||
"dataFolder": "Data Folder",
|
||||
"others": "Other",
|
||||
"language": "Language",
|
||||
"reset": "Reset",
|
||||
"search": "Search",
|
||||
"name": "Name",
|
||||
"assistants": "Assistants",
|
||||
"hardware": "Hardware",
|
||||
"mcp-servers": "Mcp Servers",
|
||||
"local_api_server": "Local API Server",
|
||||
"https_proxy": "HTTPS Proxy",
|
||||
"extensions": "Extensions",
|
||||
"general": "General",
|
||||
"settings": "Settings",
|
||||
"modelProviders": "Model Providers",
|
||||
"appearance": "Appearance",
|
||||
"privacy": "Privacy",
|
||||
"keyboardShortcuts": "Shortcuts",
|
||||
"newChat": "New Chat",
|
||||
"favorites": "Favorites",
|
||||
"recents": "Recents",
|
||||
"hub": "Hub",
|
||||
"helpSupport": "Help & Support",
|
||||
"helpUsImproveJan": "Help Us Improve Jan",
|
||||
"unstarAll": "Unstar All",
|
||||
"unstar": "Unstar",
|
||||
"deleteAll": "Delete All",
|
||||
"star": "Star",
|
||||
"rename": "Rename",
|
||||
"delete": "Delete",
|
||||
"copied": "Copied!",
|
||||
"dataFolder": "Data Folder",
|
||||
"others": "Other",
|
||||
"language": "Language",
|
||||
"reset": "Reset",
|
||||
"search": "Search",
|
||||
"name": "Name",
|
||||
"cancel": "Cancel",
|
||||
"create": "Create",
|
||||
"save": "Save",
|
||||
"edit": "Edit",
|
||||
"copy": "Copy",
|
||||
"back": "Back",
|
||||
"close": "Close",
|
||||
"next": "Next",
|
||||
"finish": "Finish",
|
||||
"skip": "Skip",
|
||||
"allow": "Allow",
|
||||
"deny": "Deny",
|
||||
"start": "Start",
|
||||
"stop": "Stop",
|
||||
"preview": "Preview",
|
||||
"compactWidth": "Compact Width",
|
||||
"fullWidth": "Full Width",
|
||||
"dark": "Dark",
|
||||
"light": "Light",
|
||||
"system": "System",
|
||||
"auto": "Auto",
|
||||
"english": "English",
|
||||
"medium": "Medium",
|
||||
"newThread": "New Thread",
|
||||
"noResultsFound": "No results found",
|
||||
"noThreadsYet": "No threads yet",
|
||||
"noThreadsYetDesc": "Start a new conversation to see your thread history here.",
|
||||
"downloads": "Downloads",
|
||||
"downloading": "Downloading",
|
||||
"cancelDownload": "Cancel download",
|
||||
"downloadCancelled": "Download Cancelled",
|
||||
"downloadComplete": "Download Complete",
|
||||
"thinking": "Thinking...",
|
||||
"thought": "Thought",
|
||||
"callingTool": "Calling tool",
|
||||
"completed": "Completed",
|
||||
"image": "Image",
|
||||
"vision": "Vision",
|
||||
"embeddings": "Embeddings",
|
||||
"tools": "Tools",
|
||||
"webSearch": "Web Search",
|
||||
"reasoning": "Reasoning",
|
||||
"selectAModel": "Select a model",
|
||||
"noToolsAvailable": "No tools available",
|
||||
"noModelsFoundFor": "No models found for \"{{searchValue}}\"",
|
||||
"customAvatar": "Custom avatar",
|
||||
"editAssistant": "Edit Assistant",
|
||||
"jan": "Jan",
|
||||
"metadata": "Metadata",
|
||||
"regenerate": "Regenerate",
|
||||
"threadImage": "Thread image",
|
||||
"editMessage": "Edit Message",
|
||||
"deleteMessage": "Delete Message",
|
||||
"deleteThread": "Delete Thread",
|
||||
"renameThread": "Rename Thread",
|
||||
"threadTitle": "Thread Title",
|
||||
"deleteAllThreads": "Delete All Threads",
|
||||
"allThreadsUnfavorited": "All Threads Unfavorited",
|
||||
"deleteAllThreadsConfirm": "Are you sure you want to delete all threads? This action cannot be undone.",
|
||||
"addProvider": "Add Provider",
|
||||
"addOpenAIProvider": "Add OpenAI Provider",
|
||||
"enterNameForProvider": "Enter a name for your provider",
|
||||
"providerAlreadyExists": "Provider with name \"{{name}}\" already exists. Please choose a different name.",
|
||||
"adjustFontSize": "Adjust Font Size",
|
||||
"changeLanguage": "Change Language",
|
||||
"editTheme": "Edit Theme",
|
||||
"editCodeBlockStyle": "Edit Code Block Style",
|
||||
"editServerHost": "Edit Server Host",
|
||||
"pickColorWindowBackground": "Pick Color Window Background",
|
||||
"pickColorAppMainView": "Pick Color App Main View",
|
||||
"pickColorAppPrimary": "Pick Color App Primary",
|
||||
"pickColorAppAccent": "Pick Color App Accent",
|
||||
"pickColorAppDestructive": "Pick Color App Destructive",
|
||||
"apiKeyRequired": "API Key is required",
|
||||
"enterTrustedHosts": "Enter trusted hosts",
|
||||
"placeholder": {
|
||||
"chatInput": "Ask me anything..."
|
||||
},
|
||||
"confirm": "Confirm",
|
||||
"loading": "Loading...",
|
||||
"error": "Error",
|
||||
"success": "Success",
|
||||
"warning": "Warning",
|
||||
"noResultsFoundDesc": "We couldn't find any chats matching your search. Try a different keyword.",
|
||||
"searchModels": "Search models...",
|
||||
"searchStyles": "Search styles...",
|
||||
"createAssistant": "Create Assistant",
|
||||
"enterApiKey": "Enter API Key",
|
||||
"scrollToBottom": "Scroll to bottom",
|
||||
"addModel": {
|
||||
"title": "Add Model",
|
||||
"modelId": "Model ID",
|
||||
"enterModelId": "Enter Model ID",
|
||||
"addModel": "Add Model",
|
||||
"description": "Add a new model to the provider",
|
||||
"exploreModels": "See model list from provider"
|
||||
},
|
||||
"mcpServers": {
|
||||
"editServer": "Edit Server",
|
||||
"addServer": "Add Server",
|
||||
"serverName": "Server Name",
|
||||
"enterServerName": "Enter server name",
|
||||
"command": "Command",
|
||||
"enterCommand": "Enter command",
|
||||
"arguments": "Arguments",
|
||||
"argument": "Argument {{index}}",
|
||||
"envVars": "Environment Variables",
|
||||
"key": "Key",
|
||||
"value": "Value",
|
||||
"save": "Save"
|
||||
},
|
||||
"deleteServer": {
|
||||
"title": "Delete Server",
|
||||
"delete": "Delete"
|
||||
},
|
||||
"editJson": {
|
||||
"errorParse": "Failed to parse JSON",
|
||||
"errorPaste": "Failed to paste JSON",
|
||||
"errorFormat": "Invalid JSON format",
|
||||
"titleAll": "Edit All Servers Configuration",
|
||||
"placeholder": "Enter JSON configuration...",
|
||||
"save": "Save"
|
||||
},
|
||||
"editModel": {
|
||||
"title": "Edit Model: {{modelId}}",
|
||||
"description": "Configure model capabilities by toggling the options below.",
|
||||
"capabilities": "Capabilities",
|
||||
"tools": "Tools",
|
||||
"vision": "Vision",
|
||||
"embeddings": "Embeddings",
|
||||
"notAvailable": "Not available yet"
|
||||
},
|
||||
"outOfContextError": {
|
||||
"truncateInput": "Truncate Input",
|
||||
"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?",
|
||||
"increaseContextSize": "Increase Context Size"
|
||||
},
|
||||
"toolApproval": {
|
||||
"title": "Tool Permission Request",
|
||||
"description": "The assistant wants to use <strong>{{toolName}}</strong>",
|
||||
"securityNotice": "Only allow tools you trust. Tools can access your system and data.",
|
||||
"deny": "Deny",
|
||||
"allowOnce": "Allow Once",
|
||||
"alwaysAllow": "Always Allow"
|
||||
},
|
||||
"deleteModel": {
|
||||
"title": "Delete Model: {{modelId}}",
|
||||
"description": "Are you sure you want to delete this model? This action cannot be undone.",
|
||||
"success": "Model {{modelId}} has been permanently deleted.",
|
||||
"cancel": "Cancel",
|
||||
|
||||
"placeholder": {
|
||||
"chatInput": "Ask me anything..."
|
||||
"delete": "Delete"
|
||||
},
|
||||
"deleteProvider": {
|
||||
"title": "Delete Provider",
|
||||
"description": "Delete this provider and all its models. This action cannot be undone.",
|
||||
"success": "Provider {{provider}} has been permanently deleted.",
|
||||
"confirmTitle": "Delete Provider: {{provider}}",
|
||||
"confirmDescription": "Are you sure you want to delete this provider? This action cannot be undone.",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete"
|
||||
},
|
||||
"modelSettings": {
|
||||
"title": "Model Settings - {{modelId}}",
|
||||
"description": "Configure model settings to optimize performance and behavior."
|
||||
},
|
||||
"dialogs": {
|
||||
"changeDataFolder": {
|
||||
"title": "Change Data Folder Location",
|
||||
"description": "Are you sure you want to change the data folder location? This will move all your data to the new location and restart the application.",
|
||||
"currentLocation": "Current Location:",
|
||||
"newLocation": "New Location:",
|
||||
"cancel": "Cancel",
|
||||
"changeLocation": "Change Location"
|
||||
},
|
||||
"deleteAllThreads": {
|
||||
"title": "Delete All Threads",
|
||||
"description": "All threads will be deleted. This action cannot be undone."
|
||||
},
|
||||
"deleteThread": {
|
||||
"description": "Are you sure you want to delete this thread? This action cannot be undone."
|
||||
},
|
||||
"editMessage": {
|
||||
"title": "Edit Message"
|
||||
},
|
||||
"messageMetadata": {
|
||||
"title": "Message Metadata"
|
||||
}
|
||||
},
|
||||
"toast": {
|
||||
"allThreadsUnfavorited": {
|
||||
"title": "All Threads Unfavorited",
|
||||
"description": "All threads have been removed from your favorites."
|
||||
},
|
||||
"deleteAllThreads": {
|
||||
"title": "Delete All Threads",
|
||||
"description": "All threads have been permanently deleted."
|
||||
},
|
||||
"renameThread": {
|
||||
"title": "Rename Thread",
|
||||
"description": "Thread title has been renamed to '{{title}}'"
|
||||
},
|
||||
"deleteThread": {
|
||||
"title": "Delete Thread",
|
||||
"description": "This thread has been permanently deleted."
|
||||
},
|
||||
"editMessage": {
|
||||
"title": "Edit Message",
|
||||
"description": "Message edited successfully. Please wait for the model to respond."
|
||||
},
|
||||
"appUpdateDownloaded": {
|
||||
"title": "App Update Downloaded",
|
||||
"description": "The app update has been downloaded successfully."
|
||||
},
|
||||
"appUpdateDownloadFailed": {
|
||||
"title": "App Update Download Failed",
|
||||
"description": "Failed to download the app update. Please try again."
|
||||
},
|
||||
"downloadComplete": {
|
||||
"title": "Download Complete",
|
||||
"description": "The model {{modelId}} has been downloaded"
|
||||
},
|
||||
"downloadCancelled": {
|
||||
"title": "Download Cancelled",
|
||||
"description": "The download process was cancelled"
|
||||
}
|
||||
},
|
||||
"cortexFailureDialog": {
|
||||
"title": "Local AI Engine Issue",
|
||||
"description": "The local AI engine (Cortex) failed to start after multiple attempts. This might prevent some features from working correctly.",
|
||||
"contactSupport": "Contact Support",
|
||||
"restartJan": "Restart Jan"
|
||||
}
|
||||
}
|
||||
|
||||
31
web-app/src/locales/en/hub.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"sortNewest": "Newest",
|
||||
"sortMostDownloaded": "Most downloaded",
|
||||
"use": "Use",
|
||||
"download": "Download",
|
||||
"downloaded": "Downloaded",
|
||||
"loadingModels": "Loading models...",
|
||||
"noModels": "No models found",
|
||||
"by": "By",
|
||||
"downloads": "Downloads",
|
||||
"variants": "Variants",
|
||||
"showVariants": "Show variants",
|
||||
"useModel": "Use this model",
|
||||
"downloadModel": "Download model",
|
||||
"searchPlaceholder": "Search for models on Hugging Face...",
|
||||
"editTheme": "Edit Theme",
|
||||
"joyride": {
|
||||
"recommendedModelTitle": "Recommended Model",
|
||||
"recommendedModelContent": "Browse and download powerful AI models from various providers, all in one place. We suggest starting with Jan-Nano - a model optimized for function calling, tool integration, and research capabilities. It's ideal for building interactive AI agents.",
|
||||
"downloadInProgressTitle": "Download in Progress",
|
||||
"downloadInProgressContent": "Your model is now downloading. Track progress here - once finished, it will be ready to use.",
|
||||
"downloadModelTitle": "Download Model",
|
||||
"downloadModelContent": "Click the Download button to begin downloading the model.",
|
||||
"back": "Back",
|
||||
"close": "Close",
|
||||
"lastWithDownload": "Download",
|
||||
"last": "Finish",
|
||||
"next": "Next",
|
||||
"skip": "Skip"
|
||||
}
|
||||
}
|
||||
3
web-app/src/locales/en/logs.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"noLogs": "No logs available"
|
||||
}
|
||||
43
web-app/src/locales/en/mcp-servers.json
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"editServer": "Edit MCP Server",
|
||||
"addServer": "Add MCP Server",
|
||||
"serverName": "Server Name",
|
||||
"enterServerName": "Enter server name",
|
||||
"command": "Command",
|
||||
"enterCommand": "Enter command (uvx or npx)",
|
||||
"arguments": "Arguments",
|
||||
"argument": "Argument {{index}}",
|
||||
"envVars": "Environment Variables",
|
||||
"key": "Key",
|
||||
"value": "Value",
|
||||
"save": "Save",
|
||||
"status": "Status",
|
||||
"connected": "Connected",
|
||||
"disconnected": "Disconnected",
|
||||
"deleteServer": {
|
||||
"title": "Delete MCP Server",
|
||||
"description": "Are you sure you want to delete the MCP server {{serverName}}? This action cannot be undone.",
|
||||
"delete": "Delete"
|
||||
},
|
||||
"editJson": {
|
||||
"title": "Edit JSON for MCP Server: {{serverName}}",
|
||||
"titleAll": "Edit All MCP Servers JSON",
|
||||
"placeholder": "Enter JSON configuration",
|
||||
"errorParse": "Failed to parse initial data",
|
||||
"errorPaste": "Invalid JSON format in pasted content",
|
||||
"errorFormat": "Invalid JSON format",
|
||||
"save": "Save"
|
||||
},
|
||||
"checkParams": "Please check the parameters according to the tutorial.",
|
||||
"title": "MCP Servers",
|
||||
"experimental": "Experimental",
|
||||
"editAllJson": "Edit All Servers JSON",
|
||||
"findMore": "Find more MCP servers at",
|
||||
"allowPermissions": "Allow All MCP Tool Permissions",
|
||||
"allowPermissionsDesc": "When enabled, all MCP tool calls will be automatically approved without showing permission dialogs.",
|
||||
"noServers": "No MCP servers found",
|
||||
"args": "Args",
|
||||
"env": "Env",
|
||||
"serverStatusActive": "Server {{serverKey}} activated successfully",
|
||||
"serverStatusInactive": "Server {{serverKey}} deactivated successfully"
|
||||
}
|
||||
5
web-app/src/locales/en/provider.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"addProvider": "Add Provider",
|
||||
"addOpenAIProvider": "Add OpenAI Provider",
|
||||
"enterNameForProvider": "Enter name for provider"
|
||||
}
|
||||
68
web-app/src/locales/en/providers.json
Normal file
@ -0,0 +1,68 @@
|
||||
{
|
||||
"joyride": {
|
||||
"chooseProviderTitle": "Choose a Provider",
|
||||
"chooseProviderContent": "Pick the provider you want to use, make sure you have access to an API key for it.",
|
||||
"getApiKeyTitle": "Get Your API Key",
|
||||
"getApiKeyContent": "Log into the provider's dashboard to find or generate your API key.",
|
||||
"insertApiKeyTitle": "Insert Your API Key",
|
||||
"insertApiKeyContent": "Paste your API key here to connect and activate the provider.",
|
||||
"back": "Back",
|
||||
"close": "Close",
|
||||
"last": "Finish",
|
||||
"next": "Next",
|
||||
"skip": "Skip"
|
||||
},
|
||||
"refreshModelsError": "Provider must have base URL and API key configured to fetch models.",
|
||||
"refreshModelsSuccess": "Added {{count}} new model(s) from {{provider}}.",
|
||||
"noNewModels": "No new models found. All available models are already added.",
|
||||
"refreshModelsFailed": "Failed to fetch models from {{provider}}. Please check your API key and base URL.",
|
||||
"models": "Models",
|
||||
"refreshing": "Refreshing...",
|
||||
"refresh": "Refresh",
|
||||
"import": "Import",
|
||||
"importModelSuccess": "Model {{provider}} has been imported successfully.",
|
||||
"importModelError": "Failed to import model:",
|
||||
"stop": "Stop",
|
||||
"start": "Start",
|
||||
"noModelFound": "No model found",
|
||||
"noModelFoundDesc": "Available models will be listed here. If you don't have any models yet, visit the Hub to download.",
|
||||
"configuration": "Configuration",
|
||||
"apiEndpoint": "API Endpoint",
|
||||
"testConnection": "Test Connection",
|
||||
"addModel": {
|
||||
"title": "Add New Model",
|
||||
"description": "Add a new model to the {{provider}} provider.",
|
||||
"modelId": "Model ID",
|
||||
"enterModelId": "Enter model ID",
|
||||
"exploreModels": "See model list from {{provider}}",
|
||||
"addModel": "Add Model"
|
||||
},
|
||||
"deleteModel": {
|
||||
"title": "Delete Model: {{modelId}}",
|
||||
"description": "Are you sure you want to delete this model? This action cannot be undone.",
|
||||
"success": "Model {{modelId}} has been permanently deleted.",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete"
|
||||
},
|
||||
"deleteProvider": {
|
||||
"title": "Delete Provider",
|
||||
"description": "Delete this provider and all its models. This action cannot be undone.",
|
||||
"success": "Provider {{provider}} has been permanently deleted.",
|
||||
"confirmTitle": "Delete Provider: {{provider}}",
|
||||
"confirmDescription": "Are you sure you want to delete this provider? This action cannot be undone.",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete"
|
||||
},
|
||||
"editModel": {
|
||||
"title": "Edit Model: {{modelId}}",
|
||||
"description": "Configure model capabilities by toggling the options below.",
|
||||
"capabilities": "Capabilities",
|
||||
"tools": "Tools",
|
||||
"vision": "Vision",
|
||||
"embeddings": "Embeddings",
|
||||
"notAvailable": "Not available yet"
|
||||
},
|
||||
"addProvider": "Add Provider",
|
||||
"addOpenAIProvider": "Add OpenAI Provider",
|
||||
"enterNameForProvider": "Enter name for provider"
|
||||
}
|
||||
@ -1,19 +1,248 @@
|
||||
{
|
||||
"settings": {
|
||||
"general": {
|
||||
"autoDownload": "Automatic download new updates"
|
||||
},
|
||||
"dataFolder": {
|
||||
"appData": "App Data",
|
||||
"appDataDesc": "Default location for messages and other user data.",
|
||||
"appLogs": "App Logs",
|
||||
"appLogsDesc": "Default location App Logs."
|
||||
},
|
||||
"others": {
|
||||
"spellCheck": "Spell Check",
|
||||
"spellCheckDesc": "Enable spell check for your threads.",
|
||||
"resetFactory": "Reset To Factory Settings",
|
||||
"resetFactoryDesc": "Restore application to its initial state, erasing all models and chat history. This action is irreversible and recommended only if the application is corrupted."
|
||||
"autoDownload": "Automatic download new updates",
|
||||
"checkForUpdates": "Check for Updates",
|
||||
"checkForUpdatesDesc": "Check if a newer version of Jan is available.",
|
||||
"checkingForUpdates": "Checking for updates...",
|
||||
"noUpdateAvailable": "You're running the latest version",
|
||||
"devVersion": "Development version detected",
|
||||
"updateError": "Failed to check for updates",
|
||||
"changeLocation": "Change Location",
|
||||
"copied": "Copied",
|
||||
"copyPath": "Copy Path",
|
||||
"openLogs": "Open Logs",
|
||||
"revealLogs": "Reveal Logs",
|
||||
"showInFinder": "Show in Finder",
|
||||
"showInFileExplorer": "Show in File Explorer",
|
||||
"openContainingFolder": "Open Containing Folder",
|
||||
"failedToRelocateDataFolder": "Failed to relocate data folder",
|
||||
"failedToRelocateDataFolderDesc": "Failed to relocate data folder. Please try again.",
|
||||
"factoryResetTitle": "Reset to Factory Settings",
|
||||
"factoryResetDesc": "This will reset all app settings to their defaults. This can't be undone. We only recommend this if the app is corrupted.",
|
||||
"cancel": "Cancel",
|
||||
"reset": "Reset",
|
||||
"resources": "Resources",
|
||||
"documentation": "Documentation",
|
||||
"documentationDesc": "Learn how to use Jan and explore its features.",
|
||||
"viewDocs": "View Docs",
|
||||
"releaseNotes": "Release Notes",
|
||||
"releaseNotesDesc": "See what's new in the latest version of Jan.",
|
||||
"viewReleases": "View Releases",
|
||||
"community": "Community",
|
||||
"github": "GitHub",
|
||||
"githubDesc": "Contribute to Jan's development.",
|
||||
"discord": "Discord",
|
||||
"discordDesc": "Join our community for support and discussions.",
|
||||
"support": "Support",
|
||||
"reportAnIssue": "Report an Issue",
|
||||
"reportAnIssueDesc": "Found a bug? Help us out by filing an issue on GitHub.",
|
||||
"reportIssue": "Report Issue",
|
||||
"credits": "Credits",
|
||||
"creditsDesc1": "Jan is built with ❤️ by the Menlo Team.",
|
||||
"creditsDesc2": "Special thanks to our open-source dependencies—especially llama.cpp and Tauri—and to our amazing AI community.",
|
||||
"appVersion": "App Version",
|
||||
"dataFolder": {
|
||||
"appData": "App Data",
|
||||
"appDataDesc": "Default location for messages and other user data.",
|
||||
"appLogs": "App Logs",
|
||||
"appLogsDesc": "View detailed logs of the App."
|
||||
},
|
||||
"others": {
|
||||
"spellCheck": "Spell Check",
|
||||
"spellCheckDesc": "Enable spell check for your threads.",
|
||||
"resetFactory": "Reset To Factory Settings",
|
||||
"resetFactoryDesc": "Restore application to its initial state, erasing all models and chat history. This action is irreversible and recommended only if the application is corrupted."
|
||||
},
|
||||
"shortcuts": {
|
||||
"application": "Application",
|
||||
"newChat": "New Chat",
|
||||
"newChatDesc": "Create a new chat.",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"toggleSidebarDesc": "Show or hide the sidebar.",
|
||||
"zoomIn": "Zoom In",
|
||||
"zoomInDesc": "Increase the zoom level.",
|
||||
"zoomOut": "Zoom Out",
|
||||
"zoomOutDesc": "Decrease the zoom level.",
|
||||
"chat": "Chat",
|
||||
"sendMessage": "Send Message",
|
||||
"sendMessageDesc": "Send the current message.",
|
||||
"enter": "Enter",
|
||||
"newLine": "New Line",
|
||||
"newLineDesc": "Insert a new line.",
|
||||
"shiftEnter": "Shift + Enter",
|
||||
"navigation": "Navigation",
|
||||
"goToSettings": "Go to Settings",
|
||||
"goToSettingsDesc": "Open settings."
|
||||
},
|
||||
"appearance": {
|
||||
"title": "Appearance",
|
||||
"theme": "Theme",
|
||||
"themeDesc": "Match the OS theme.",
|
||||
"fontSize": "Font Size",
|
||||
"fontSizeDesc": "Adjust the app's font size.",
|
||||
"windowBackground": "Window Background",
|
||||
"windowBackgroundDesc": "Set the app window's background color.",
|
||||
"appMainView": "App Main View",
|
||||
"appMainViewDesc": "Set the main content area's background color.",
|
||||
"primary": "Primary",
|
||||
"primaryDesc": "Set the primary color for UI components.",
|
||||
"accent": "Accent",
|
||||
"accentDesc": "Set the accent color for UI highlights.",
|
||||
"destructive": "Destructive",
|
||||
"destructiveDesc": "Set the color for destructive actions.",
|
||||
"resetToDefault": "Reset to Default",
|
||||
"resetToDefaultDesc": "Reset all appearance settings to default.",
|
||||
"resetAppearanceSuccess": "Appearance reset successfully",
|
||||
"resetAppearanceSuccessDesc": "All appearance settings have been restored to default.",
|
||||
"chatWidth": "Chat Width",
|
||||
"chatWidthDesc": "Customize the width of the chat view.",
|
||||
"codeBlockTitle": "Code Block",
|
||||
"codeBlockDesc": "Choose a syntax highlighting style.",
|
||||
"showLineNumbers": "Show Line Numbers",
|
||||
"showLineNumbersDesc": "Display line numbers in code blocks.",
|
||||
"resetCodeBlockStyle": "Reset Code Block Style",
|
||||
"resetCodeBlockStyleDesc": "Reset code block style to default.",
|
||||
"resetCodeBlockSuccess": "Code block style reset successfully",
|
||||
"resetCodeBlockSuccessDesc": "Code block style has been restored to default."
|
||||
},
|
||||
"hardware": {
|
||||
"os": "Operating System",
|
||||
"name": "Name",
|
||||
"version": "Version",
|
||||
"cpu": "CPU",
|
||||
"model": "Model",
|
||||
"architecture": "Architecture",
|
||||
"cores": "Cores",
|
||||
"instructions": "Instructions",
|
||||
"usage": "Usage",
|
||||
"memory": "Memory",
|
||||
"totalRam": "Total RAM",
|
||||
"availableRam": "Available RAM",
|
||||
"vulkan": "Vulkan",
|
||||
"enableVulkan": "Enable Vulkan",
|
||||
"enableVulkanDesc": "Use Vulkan API for GPU acceleration. Do not enable Vulkan if you have an NVIDIA GPU as it may cause compatibility issues.",
|
||||
"gpus": "GPUs",
|
||||
"noGpus": "No GPUs detected",
|
||||
"vram": "VRAM",
|
||||
"freeOf": "free of",
|
||||
"driverVersion": "Driver Version",
|
||||
"computeCapability": "Compute Capability",
|
||||
"systemMonitor": "System Monitor"
|
||||
},
|
||||
"httpsProxy": {
|
||||
"proxy": "Proxy",
|
||||
"proxyUrl": "Proxy URL",
|
||||
"proxyUrlDesc": "The URL and port of your proxy server.",
|
||||
"proxyUrlPlaceholder": "http://proxy.example.com:8080",
|
||||
"authentication": "Authentication",
|
||||
"authenticationDesc": "Credentials for the proxy server, if required.",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"noProxy": "No Proxy",
|
||||
"noProxyDesc": "A comma-separated list of hosts to bypass the proxy.",
|
||||
"noProxyPlaceholder": "localhost,127.0.0.1,.local",
|
||||
"sslVerification": "SSL Verification",
|
||||
"ignoreSsl": "Ignore SSL Certificates",
|
||||
"ignoreSslDesc": "Allow self-signed or unverified certificates. This may be required for some proxies but reduces security. Only enable if you trust your proxy.",
|
||||
"proxySsl": "Proxy SSL",
|
||||
"proxySslDesc": "Validate the SSL certificate when connecting to the proxy.",
|
||||
"proxyHostSsl": "Proxy Host SSL",
|
||||
"proxyHostSslDesc": "Validate the SSL certificate of the proxy's host.",
|
||||
"peerSsl": "Peer SSL",
|
||||
"peerSslDesc": "Validate the SSL certificates of peer connections.",
|
||||
"hostSsl": "Host SSL",
|
||||
"hostSslDesc": "Validate the SSL certificates of destination hosts."
|
||||
},
|
||||
"localApiServer": {
|
||||
"title": "Local API Server",
|
||||
"description": "Run an OpenAI-compatible server locally.",
|
||||
"startServer": "Start Server",
|
||||
"stopServer": "Stop Server",
|
||||
"serverLogs": "Server Logs",
|
||||
"serverLogsDesc": "View detailed logs of the local API server.",
|
||||
"openLogs": "Open Logs",
|
||||
"serverConfiguration": "Server Configuration",
|
||||
"serverHost": "Server Host",
|
||||
"serverHostDesc": "Network address for the server.",
|
||||
"serverPort": "Server Port",
|
||||
"serverPortDesc": "Port number for the API server.",
|
||||
"apiPrefix": "API Prefix",
|
||||
"apiPrefixDesc": "Path prefix for API endpoints.",
|
||||
"apiKey": "API Key",
|
||||
"apiKeyDesc": "Authenticate requests with an API key.",
|
||||
"trustedHosts": "Trusted Hosts",
|
||||
"trustedHostsDesc": "Hosts allowed to access the server, separated by commas.",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"cors": "Cross-Origin Resource Sharing (CORS)",
|
||||
"corsDesc": "Allow cross-origin requests to the API server.",
|
||||
"verboseLogs": "Verbose Server Logs",
|
||||
"verboseLogsDesc": "Enable detailed server logs for debugging."
|
||||
},
|
||||
"privacy": {
|
||||
"analytics": "Analytics",
|
||||
"helpUsImprove": "Help us improve",
|
||||
"helpUsImproveDesc": "To help us improve Jan, you can share anonymous data like feature usage and user counts. We never collect your chats or personal information.",
|
||||
"privacyPolicy": "You have full control over your data. Learn more in our Privacy Policy.",
|
||||
"analyticsDesc": "To improve Jan, we need to understand how it's used—but only with your help. You can change this setting anytime.",
|
||||
"privacyPromises": "Your choice here won't change our core privacy promises:",
|
||||
"promise1": "Your conversations stay private and on your device",
|
||||
"promise2": "We never collect your personal information or chat content",
|
||||
"promise3": "All data sharing is anonymous and aggregated",
|
||||
"promise4": "You can opt out anytime without losing functionality",
|
||||
"promise5": "We're transparent about what we collect and why"
|
||||
},
|
||||
"general": {
|
||||
"showInFinder": "Show in Finder",
|
||||
"showInFileExplorer": "Show in File Explorer",
|
||||
"openContainingFolder": "Open Containing Folder",
|
||||
"failedToRelocateDataFolder": "Failed to relocate data folder",
|
||||
"failedToRelocateDataFolderDesc": "Failed to relocate data folder. Please try again.",
|
||||
"devVersion": "Development version detected",
|
||||
"noUpdateAvailable": "You're running the latest version",
|
||||
"updateError": "Failed to check for updates",
|
||||
"appVersion": "App Version",
|
||||
"checkForUpdates": "Check for Updates",
|
||||
"checkForUpdatesDesc": "Check if a newer version of Jan is available.",
|
||||
"checkingForUpdates": "Checking for updates...",
|
||||
"copied": "Copied",
|
||||
"copyPath": "Copy Path",
|
||||
"changeLocation": "Change Location",
|
||||
"openLogs": "Open Logs",
|
||||
"revealLogs": "Reveal Logs",
|
||||
"factoryResetTitle": "Reset to Factory Settings",
|
||||
"factoryResetDesc": "This will reset all app settings to their defaults. This can't be undone. We only recommend this if the app is corrupted.",
|
||||
"cancel": "Cancel",
|
||||
"reset": "Reset",
|
||||
"resources": "Resources",
|
||||
"documentation": "Documentation",
|
||||
"documentationDesc": "Learn how to use Jan and explore its features.",
|
||||
"viewDocs": "View Docs",
|
||||
"releaseNotes": "Release Notes",
|
||||
"releaseNotesDesc": "See what's new in the latest version of Jan.",
|
||||
"viewReleases": "View Releases",
|
||||
"community": "Community",
|
||||
"github": "GitHub",
|
||||
"githubDesc": "Contribute to Jan's development.",
|
||||
"discord": "Discord",
|
||||
"discordDesc": "Join our community for support and discussions.",
|
||||
"support": "Support",
|
||||
"reportAnIssue": "Report an Issue",
|
||||
"reportAnIssueDesc": "Found a bug? Help us out by filing an issue on GitHub.",
|
||||
"reportIssue": "Report Issue",
|
||||
"credits": "Credits",
|
||||
"creditsDesc1": "Jan is built with ❤️ by the Menlo Team.",
|
||||
"creditsDesc2": "Special thanks to our open-source dependencies—especially llama.cpp and Tauri—and to our amazing AI community."
|
||||
},
|
||||
"extensions": {
|
||||
"title": "Extensions"
|
||||
},
|
||||
"dialogs": {
|
||||
"changeDataFolder": {
|
||||
"title": "Change Data Folder Location",
|
||||
"description": "Are you sure you want to change the data folder location? This will move all your data to the new location and restart the application.",
|
||||
"currentLocation": "Current Location:",
|
||||
"newLocation": "New Location:",
|
||||
"cancel": "Cancel",
|
||||
"changeLocation": "Change Location"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
6
web-app/src/locales/en/setup.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"welcome": "Welcome to Jan",
|
||||
"description": "To get started, you'll need to either download a local AI model or connect to a cloud model using an API key",
|
||||
"localModel": "Set up local model",
|
||||
"remoteProvider": "Set up remote provider"
|
||||
}
|
||||
28
web-app/src/locales/en/system-monitor.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"title": "System Monitor",
|
||||
"cpuUsage": "CPU Usage",
|
||||
"model": "Model",
|
||||
"cores": "Cores",
|
||||
"architecture": "Architecture",
|
||||
"currentUsage": "Current Usage",
|
||||
"memoryUsage": "Memory Usage",
|
||||
"totalRam": "Total RAM",
|
||||
"availableRam": "Available RAM",
|
||||
"usedRam": "Used RAM",
|
||||
"runningModels": "Running Models",
|
||||
"noRunningModels": "No models are currently running",
|
||||
"provider": "Provider",
|
||||
"uptime": "Uptime",
|
||||
"actions": "Actions",
|
||||
"stop": "Stop",
|
||||
"activeGpus": "Active GPUs",
|
||||
"noGpus": "No GPUs detected",
|
||||
"noActiveGpus": "No active GPUs. All GPUs are currently disabled.",
|
||||
"vramUsage": "VRAM Usage",
|
||||
"driverVersion": "Driver Version:",
|
||||
"computeCapability": "Compute Capability:",
|
||||
"active": "Active",
|
||||
"performance": "Performance",
|
||||
"resources": "Resources",
|
||||
"refresh": "Refresh"
|
||||
}
|
||||
11
web-app/src/locales/en/tool-approval.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"title": "Tool Call Request",
|
||||
"description": "The assistant wants to use the tool: <strong>{{toolName}}</strong>",
|
||||
"securityNotice": "<strong>Security Notice:</strong> Malicious tools or conversation content could potentially trick the assistant into attempting harmful actions. Review each tool call carefully before approving.",
|
||||
"deny": "Deny",
|
||||
"allowOnce": "Allow Once",
|
||||
"alwaysAllow": "Always Allow",
|
||||
"permissions": "Permissions",
|
||||
"approve": "Approve",
|
||||
"reject": "Reject"
|
||||
}
|
||||
10
web-app/src/locales/en/tools.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"toolApproval": {
|
||||
"title": "Tool Approval Required",
|
||||
"description": "The assistant wants to use <strong>{{toolName}}</strong>",
|
||||
"securityNotice": "This tool wants to perform an action. Please review and approve.",
|
||||
"deny": "Deny",
|
||||
"allowOnce": "Allow Once",
|
||||
"alwaysAllow": "Always Allow"
|
||||
}
|
||||
}
|
||||