diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 000000000..324ab3702 --- /dev/null +++ b/.github/dependabot.yaml @@ -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" diff --git a/.gitignore b/.gitignore index f702c6512..f09d958d0 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,4 @@ src-tauri/resources/bin # Helper tools .opencode OpenCode.md +archive/ \ No newline at end of file diff --git a/README.md b/README.md index 26971c578..5b3655ba8 100644 --- a/README.md +++ b/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:** diff --git a/docs/.astro/collections/docs.schema.json b/docs/.astro/collections/docs.schema.json new file mode 100644 index 000000000..623835a8d --- /dev/null +++ b/docs/.astro/collections/docs.schema.json @@ -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": "^\\; + } + interface Render { + '.md': Promise; + } + + export interface RenderedContent { + html: string; + metadata?: { + imagePaths: Array; + [key: string]: unknown; + }; + } +} + +declare module 'astro:content' { + type Flatten = T extends { [K: string]: infer U } ? U : never; + + export type CollectionKey = keyof AnyEntryMap; + export type CollectionEntry = Flatten; + + export type ContentCollectionKey = keyof ContentEntryMap; + export type DataCollectionKey = keyof DataEntryMap; + + type AllValuesOf = T extends any ? T[keyof T] : never; + type ValidContentEntrySlug = 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 | (string & {}) = string, + > = { + collection: C; + slug: E; + }; + + /** @deprecated Use `getEntry` instead. */ + export function getEntryBySlug< + C extends keyof ContentEntryMap, + E extends ValidContentEntrySlug | (string & {}), + >( + collection: C, + // Note that this has to accept a regular string too, for SSR + entrySlug: E, + ): E extends ValidContentEntrySlug + ? Promise> + : Promise | undefined>; + + /** @deprecated Use `getEntry` instead. */ + export function getDataEntryById( + collection: C, + entryId: E, + ): Promise>; + + export function getCollection>( + collection: C, + filter?: (entry: CollectionEntry) => entry is E, + ): Promise; + export function getCollection( + collection: C, + filter?: (entry: CollectionEntry) => unknown, + ): Promise[]>; + + export function getEntry< + C extends keyof ContentEntryMap, + E extends ValidContentEntrySlug | (string & {}), + >( + entry: ReferenceContentEntry, + ): E extends ValidContentEntrySlug + ? Promise> + : Promise | undefined>; + export function getEntry< + C extends keyof DataEntryMap, + E extends keyof DataEntryMap[C] | (string & {}), + >( + entry: ReferenceDataEntry, + ): E extends keyof DataEntryMap[C] + ? Promise + : Promise | undefined>; + export function getEntry< + C extends keyof ContentEntryMap, + E extends ValidContentEntrySlug | (string & {}), + >( + collection: C, + slug: E, + ): E extends ValidContentEntrySlug + ? Promise> + : Promise | 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 | undefined + : Promise + : Promise | undefined>; + + /** Resolve an array of entry references from the same collection */ + export function getEntries( + entries: ReferenceContentEntry>[], + ): Promise[]>; + export function getEntries( + entries: ReferenceDataEntry[], + ): Promise[]>; + + export function render( + entry: AnyEntryMap[C][string], + ): Promise; + + export function reference( + collection: C, + ): import('astro/zod').ZodEffects< + import('astro/zod').ZodString, + C extends keyof ContentEntryMap + ? ReferenceContentEntry> + : ReferenceDataEntry + >; + // 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( + collection: C, + ): import('astro/zod').ZodEffects; + + type ReturnTypeOrOriginal = T extends (...args: any[]) => infer R ? R : T; + type InferEntrySchema = import('astro/zod').infer< + ReturnTypeOrOriginal['schema']> + >; + + type ContentEntryMap = { + + }; + + type DataEntryMap = { + "docs": Record; + + }; + + type AnyEntryMap = ContentEntryMap & DataEntryMap; + + export type ContentConfig = typeof import("../src/content.config.mjs"); +} diff --git a/docs/.astro/data-store.json b/docs/.astro/data-store.json new file mode 100644 index 000000000..ad047fc08 --- /dev/null +++ b/docs/.astro/data-store.json @@ -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}}"] \ No newline at end of file diff --git a/docs/.astro/settings.json b/docs/.astro/settings.json new file mode 100644 index 000000000..04c1fc39c --- /dev/null +++ b/docs/.astro/settings.json @@ -0,0 +1,5 @@ +{ + "_variables": { + "lastUpdateCheck": 1750832446593 + } +} \ No newline at end of file diff --git a/docs/.astro/types.d.ts b/docs/.astro/types.d.ts new file mode 100644 index 000000000..03d7cc43f --- /dev/null +++ b/docs/.astro/types.d.ts @@ -0,0 +1,2 @@ +/// +/// \ No newline at end of file diff --git a/docs/public/assets/images/changelog/jan-v0.6.1-ui-revamp.png b/docs/public/assets/images/changelog/jan-v0.6.1-ui-revamp.png new file mode 100644 index 000000000..780880e1c Binary files /dev/null and b/docs/public/assets/images/changelog/jan-v0.6.1-ui-revamp.png differ diff --git a/docs/src/components/Changelog/index.tsx b/docs/src/components/Changelog/index.tsx index 639f8e346..30521be02 100644 --- a/docs/src/components/Changelog/index.tsx +++ b/docs/src/components/Changelog/index.tsx @@ -57,7 +57,7 @@ const Changelog = () => {

Latest release updates from the Jan team. Check out our  Roadmap diff --git a/docs/src/pages/_meta.json b/docs/src/pages/_meta.json index bd3d96dc8..ee7b176d5 100644 --- a/docs/src/pages/_meta.json +++ b/docs/src/pages/_meta.json @@ -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", diff --git a/docs/src/pages/changelog/2025-06-19-jan-ui-revamp.mdx b/docs/src/pages/changelog/2025-06-19-jan-ui-revamp.mdx new file mode 100644 index 000000000..6b5fbb87e --- /dev/null +++ b/docs/src/pages/changelog/2025-06-19-jan-ui-revamp.mdx @@ -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" + + + +## 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). \ No newline at end of file diff --git a/docs/src/pages/docs/_assets/api-server2.png b/docs/src/pages/docs/_assets/api-server2.png new file mode 100644 index 000000000..6bb0c3c0b Binary files /dev/null and b/docs/src/pages/docs/_assets/api-server2.png differ diff --git a/docs/src/pages/docs/_assets/assistant-dropdown-updated.png b/docs/src/pages/docs/_assets/assistant-dropdown-updated.png new file mode 100644 index 000000000..b6ee0b53d Binary files /dev/null and b/docs/src/pages/docs/_assets/assistant-dropdown-updated.png differ diff --git a/docs/src/pages/docs/_assets/jan-app-new.png b/docs/src/pages/docs/_assets/jan-app-new.png new file mode 100644 index 000000000..85db5d552 Binary files /dev/null and b/docs/src/pages/docs/_assets/jan-app-new.png differ diff --git a/docs/src/pages/docs/_assets/jan-nano-bench.png b/docs/src/pages/docs/_assets/jan-nano-bench.png new file mode 100644 index 000000000..ce923bc22 Binary files /dev/null and b/docs/src/pages/docs/_assets/jan-nano-bench.png differ diff --git a/docs/src/pages/docs/_assets/jan-nano-demo.gif b/docs/src/pages/docs/_assets/jan-nano-demo.gif new file mode 100644 index 000000000..a2b87619f Binary files /dev/null and b/docs/src/pages/docs/_assets/jan-nano-demo.gif differ diff --git a/docs/src/pages/docs/_assets/jan-nano-demo.mp4 b/docs/src/pages/docs/_assets/jan-nano-demo.mp4 new file mode 100644 index 000000000..efcadf999 Binary files /dev/null and b/docs/src/pages/docs/_assets/jan-nano-demo.mp4 differ diff --git a/docs/src/pages/docs/_assets/jan-nano0.png b/docs/src/pages/docs/_assets/jan-nano0.png new file mode 100644 index 000000000..f2da8b5f7 Binary files /dev/null and b/docs/src/pages/docs/_assets/jan-nano0.png differ diff --git a/docs/src/pages/docs/_assets/jan-nano1.png b/docs/src/pages/docs/_assets/jan-nano1.png new file mode 100644 index 000000000..f36427373 Binary files /dev/null and b/docs/src/pages/docs/_assets/jan-nano1.png differ diff --git a/docs/src/pages/docs/_assets/serper-mcp.png b/docs/src/pages/docs/_assets/serper-mcp.png new file mode 100644 index 000000000..8a8808d2e Binary files /dev/null and b/docs/src/pages/docs/_assets/serper-mcp.png differ diff --git a/docs/src/pages/docs/_assets/threads-context-menu-updated.png b/docs/src/pages/docs/_assets/threads-context-menu-updated.png new file mode 100644 index 000000000..e90baf2ab Binary files /dev/null and b/docs/src/pages/docs/_assets/threads-context-menu-updated.png differ diff --git a/docs/src/pages/docs/_assets/threads-favorites-and-recents-updated.png b/docs/src/pages/docs/_assets/threads-favorites-and-recents-updated.png new file mode 100644 index 000000000..5d62974e4 Binary files /dev/null and b/docs/src/pages/docs/_assets/threads-favorites-and-recents-updated.png differ diff --git a/docs/src/pages/docs/_assets/threads-new-chat-updated.png b/docs/src/pages/docs/_assets/threads-new-chat-updated.png new file mode 100644 index 000000000..6780e7df3 Binary files /dev/null and b/docs/src/pages/docs/_assets/threads-new-chat-updated.png differ diff --git a/docs/src/pages/docs/_meta.json b/docs/src/pages/docs/_meta.json index 794649990..4bcf5110b 100644 --- a/docs/src/pages/docs/_meta.json +++ b/docs/src/pages/docs/_meta.json @@ -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" } diff --git a/docs/src/pages/docs/api-server.mdx b/docs/src/pages/docs/api-server.mdx index 4f917dd66..c86ce1f9f 100644 --- a/docs/src/pages/docs/api-server.mdx +++ b/docs/src/pages/docs/api-server.mdx @@ -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. +![Local API Server](./_assets/api-server2.png) ### 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." + } + ] + }' + +``` @@ -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 diff --git a/docs/src/pages/docs/assistants.mdx b/docs/src/pages/docs/assistants.mdx index 8625252e8..814df0c8f 100644 --- a/docs/src/pages/docs/assistants.mdx +++ b/docs/src/pages/docs/assistants.mdx @@ -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: -![Assistant Dropdown](./_assets/assistant-dropdown.png) +![Assistant Dropdown](./_assets/assistant-dropdown-updated.png) - 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 diff --git a/docs/src/pages/docs/index.mdx b/docs/src/pages/docs/index.mdx index ffd93e49b..66a502335 100644 --- a/docs/src/pages/docs/index.mdx +++ b/docs/src/pages/docs/index.mdx @@ -22,10 +22,10 @@ import FAQBox from '@/components/FaqBox' # Jan -![Jan's Cover Image](./_assets/jan-app.png) +![Jan's Cover Image](./_assets/jan-app-new.png) -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: - - 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) diff --git a/docs/src/pages/docs/jan-models/jan-nano-128.mdx b/docs/src/pages/docs/jan-models/jan-nano-128.mdx new file mode 100644 index 000000000..c1a1e3d4a --- /dev/null +++ b/docs/src/pages/docs/jan-models/jan-nano-128.mdx @@ -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. + + + +**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. + + +**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: + +![Jan-Nano-128k Performance](../_assets/jan-nano-bench.png) + +**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 + + + +### 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 diff --git a/docs/src/pages/docs/jan-models/jan-nano-32.mdx b/docs/src/pages/docs/jan-models/jan-nano-32.mdx new file mode 100644 index 000000000..81573de65 --- /dev/null +++ b/docs/src/pages/docs/jan-models/jan-nano-32.mdx @@ -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 + +![Jan Nano](../_assets/jan-nano0.png) + +## 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. + + + 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**. + + + +## 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. + +![Jan Nano](../_assets/jan-nano1.png) + +**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. + +![Serper MCP](../_assets/serper-mcp.png) + +**Step 6** +Open up a new chat and ask Jan-Nano to search the web for you. + +![Jan Nano](../_assets/jan-nano-demo.gif) + +## 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) diff --git a/docs/src/pages/docs/mcp.mdx b/docs/src/pages/docs/mcp.mdx index 152aae448..b496a19ed 100644 --- a/docs/src/pages/docs/mcp.mdx +++ b/docs/src/pages/docs/mcp.mdx @@ -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
Documents, folders,
text files] + Database[📊 Your Data
Spreadsheets,
databases] + WebServices[🌐 Online Services
GitHub, Slack,
Google Drive] + Custom[🔧 Custom Tools
Special programs
you've added] + end + + subgraph "What Jan Can Do" + Read[Read & Understand
- View your files
- Check your data
- See updates] + Action[Take Actions
- Search for info
- Create content
- Run commands] + Templates[Use Templates
- Common tasks
- Saved prompts
- 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. diff --git a/docs/src/pages/docs/quickstart.mdx b/docs/src/pages/docs/quickstart.mdx index c82a6f294..f17863fa2 100644 --- a/docs/src/pages/docs/quickstart.mdx +++ b/docs/src/pages/docs/quickstart.mdx @@ -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).
@@ -104,7 +104,7 @@ these is that you can use them no matter which model you choose.
-![Add an Assistant Instruction](./_assets/add_assistant.png) +![Add an Assistant Instruction](./_assets/assistant-edit-dialog.png)
diff --git a/docs/src/pages/docs/server-examples/interpreter.mdx b/docs/src/pages/docs/server-examples/interpreter.mdx deleted file mode 100644 index 92e3f41b8..000000000 --- a/docs/src/pages/docs/server-examples/interpreter.mdx +++ /dev/null @@ -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: - - - -### 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 -``` - - - The Rust compiler is necessary for building some native extensions that Open Interpreter requires. - - -### 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!** - - \ No newline at end of file diff --git a/docs/src/pages/docs/threads.mdx b/docs/src/pages/docs/threads.mdx index d92151f76..12a21abd0 100644 --- a/docs/src/pages/docs/threads.mdx +++ b/docs/src/pages/docs/threads.mdx @@ -33,7 +33,7 @@ bottom left of Jan. 2. Select your preferred model in **Model Selector** in input field & start chatting.
-![Create New Thread](./_assets/threads-new-chat.png) +![Create New Thread](./_assets/threads-new-chat-updated.png) ## 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.
-![Favorites and Recents](./_assets/threads-favorites-and-recents.png) +![Favorites and Recents](./_assets/threads-favorites-and-recents-updated.png) ## 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
-![Context Menu](./_assets/threads-context-menu.png) +![Context Menu](./_assets/threads-context-menu-updated.png) ## Delete Thread @@ -77,7 +77,7 @@ When you want to completely remove a thread:
-![Delete Thread](./_assets/threads-context-menu.png) +![Delete Thread](./_assets/threads-context-menu-updated.png) ### Delete all threads at once diff --git a/docs/src/pages/platforms/_meta.json b/docs/src/pages/platforms/_meta.json new file mode 100644 index 000000000..bfee4c12e --- /dev/null +++ b/docs/src/pages/platforms/_meta.json @@ -0,0 +1,9 @@ +{ + "-- Switcher": { + "type": "separator", + "title": "Switcher" + }, + "index": { + "display": "hidden" + } +} diff --git a/docs/src/pages/platforms/index.mdx b/docs/src/pages/platforms/index.mdx new file mode 100644 index 000000000..a8b9053b4 --- /dev/null +++ b/docs/src/pages/platforms/index.mdx @@ -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' + +

+
+

+ 🚀 Coming Soon +

+

+ We're working on the next stage of Jan - making our local assistant more powerful and available in more platforms. +

+
+ +
+
+
🌐
+

Jan Web

+

+ Access Jan directly from your browser with our powerful web interface +

+
+ +
+
📱
+

Jan Mobile

+

+ Take Jan on the go with our native mobile applications +

+
+ +
+
+

API Platform

+

+ Integrate Jan's capabilities into your applications with our API +

+
+
+ + + **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! + + +
+

What to Expect

+
+
+ +
+ Seamless Experience: Unified interface across all platforms +
+
+
+ +
+ Privacy First: Same privacy-focused approach you trust +
+
+
+ +
+ Developer Friendly: Robust APIs and comprehensive documentation +
+
+
+
+
\ No newline at end of file diff --git a/docs/theme.config.tsx b/docs/theme.config.tsx index b09d9c492..59bc8c6c9 100644 --- a/docs/theme.config.tsx +++ b/docs/theme.config.tsx @@ -65,6 +65,54 @@ const config: DocsThemeConfig = { ), }, + sidebar: { + titleComponent: ({ type, title }) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const { asPath } = useRouter() + if (type === 'separator' && title === 'Switcher') { + return ( +
+ {[ + { 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) ? ( +
+ + {item.title} +
+ ) : ( + + + {item.title} + + ) + )} +
+ ) + } + 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.` } /> /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" diff --git a/scripts/find-missing-i18n-key.js b/scripts/find-missing-i18n-key.js new file mode 100644 index 000000000..bff64f97b --- /dev/null +++ b/scripts/find-missing-i18n-key.js @@ -0,0 +1,306 @@ +/** + * Script to find missing i18n keys in Jan components + * + * Usage: + * node scripts/find-missing-i18n-key.js [options] + * + * Options: + * --locale= Only check a specific locale (e.g. --locale=id) + * --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= Only check a specific language (e.g., --locale=id) + --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//.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() \ No newline at end of file diff --git a/scripts/find-missing-translations.js b/scripts/find-missing-translations.js new file mode 100644 index 000000000..5e307f250 --- /dev/null +++ b/scripts/find-missing-translations.js @@ -0,0 +1,253 @@ +/** + * Script to find missing translations in locale files for Jan + * + * Usage: + * node scripts/find-missing-translations.js [options] + * + * Options: + * --locale= Only check a specific locale (e.g. --locale=id) + * --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= Only check a specific locale (e.g. --locale=id) + --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//.json") + console.log("4. Run this script again to verify all translations are complete") + // Exit with error code to fail CI checks + process.exit(1) + } + } catch (error) { + console.error("Error:", error.message) + console.error(error.stack) + process.exit(1) + } +} + +// Run the main function +findMissingTranslations() \ No newline at end of file diff --git a/web-app/package.json b/web-app/package.json index 44d027623..0272d1151 100644 --- a/web-app/package.json +++ b/web-app/package.json @@ -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", diff --git a/web-app/src/components/ui/dialog.tsx b/web-app/src/components/ui/dialog.tsx index 632150029..98f4da71c 100644 --- a/web-app/src/components/ui/dialog.tsx +++ b/web-app/src/components/ui/dialog.tsx @@ -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 ( @@ -74,7 +76,7 @@ function DialogContent({ {showCloseButton && ( - Close + {t('close')} )} diff --git a/web-app/src/components/ui/sheet.tsx b/web-app/src/components/ui/sheet.tsx index bc09f428d..10ba1db5d 100644 --- a/web-app/src/components/ui/sheet.tsx +++ b/web-app/src/components/ui/sheet.tsx @@ -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) { return @@ -50,6 +51,7 @@ function SheetContent({ }: React.ComponentProps & { side?: 'top' | 'right' | 'bottom' | 'left' }) { + const { t } = useTranslation() return ( @@ -72,7 +74,7 @@ function SheetContent({ {children} - Close + {t('close')} diff --git a/web-app/src/containers/ApiKeyInput.tsx b/web-app/src/containers/ApiKeyInput.tsx index 4ea717f1f..394df5696 100644 --- a/web-app/src/containers/ApiKeyInput.tsx +++ b/web-app/src/containers/ApiKeyInput.tsx @@ -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) => { 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')} />
-

Vision

+

{t('vision')}

@@ -454,7 +454,7 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => { -

Embeddings

+

{t('embeddings')}

@@ -510,7 +510,7 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => { -

Tools

+

{t('tools')}

@@ -544,7 +544,7 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => { -

Reasoning

+

{t('reasoning')}

diff --git a/web-app/src/containers/ChatWidthSwitcher.tsx b/web-app/src/containers/ChatWidthSwitcher.tsx index ddaf4d4fe..27cc3c69d 100644 --- a/web-app/src/containers/ChatWidthSwitcher.tsx +++ b/web-app/src/containers/ChatWidthSwitcher.tsx @@ -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 (
@@ -16,7 +18,7 @@ export function ChatWidthSwitcher() { onClick={() => setChatWidth('compact')} >
- Compact Width + {t('common:compactWidth')} {chatWidth === 'compact' && ( )} @@ -27,7 +29,7 @@ export function ChatWidthSwitcher() {
- Ask me anything... + {t('common:placeholder.chatInput')}
@@ -40,7 +42,7 @@ export function ChatWidthSwitcher() { onClick={() => setChatWidth('full')} >
- Full Width + {t('common:fullWidth')} {chatWidth === 'full' && ( )} @@ -51,7 +53,7 @@ export function ChatWidthSwitcher() {
- Ask me anything... + {t('common:placeholder.chatInput')}
diff --git a/web-app/src/containers/CodeBlockExample.tsx b/web-app/src/containers/CodeBlockExample.tsx index 75b9ee3a5..47934dd44 100644 --- a/web-app/src/containers/CodeBlockExample.tsx +++ b/web-app/src/containers/CodeBlockExample.tsx @@ -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 (
- Preview + {t('preview')}
diff --git a/web-app/src/containers/ColorPickerAppBgColor.tsx b/web-app/src/containers/ColorPickerAppBgColor.tsx index 5569d2b54..eefffc272 100644 --- a/web-app/src/containers/ColorPickerAppBgColor.tsx +++ b/web-app/src/containers/ColorPickerAppBgColor.tsx @@ -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() { @@ -282,11 +283,12 @@ const LeftPanel = () => {
-
No results found
+
+ {t('common:noResultsFound')} +

- We couldn't find any chats matching your search. Try a - different keyword. + {t('common:noResultsFoundDesc')}

)} @@ -296,10 +298,12 @@ const LeftPanel = () => {
-
No threads yet
+
+ {t('common:noThreadsYet')} +

- Start a new conversation to see your thread history here. + {t('common:noThreadsYetDesc')}

diff --git a/web-app/src/containers/ModelSetting.tsx b/web-app/src/containers/ModelSetting.tsx index 776ced4d5..bc5e810e1 100644 --- a/web-app/src/containers/ModelSetting.tsx +++ b/web-app/src/containers/ModelSetting.tsx @@ -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({ - Model Settings - {model.id} + {t('common:modelSettings.title', { modelId: model.id })} - Configure model settings to optimize performance and behavior. + {t('common:modelSettings.description')}
diff --git a/web-app/src/containers/ProvidersMenu.tsx b/web-app/src/containers/ProvidersMenu.tsx index b8e97e201..7da1d2f83 100644 --- a/web-app/src/containers/ProvidersMenu.tsx +++ b/web-app/src/containers/ProvidersMenu.tsx @@ -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 (
- Back + {t('common:back')}
@@ -108,17 +109,17 @@ const ProvidersMenu = ({
- Add Provider + {t('provider:addProvider')}
- Add OpenAI Provider + {t('provider:addOpenAIProvider')} 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')} diff --git a/web-app/src/containers/RenderMarkdown.tsx b/web-app/src/containers/RenderMarkdown.tsx index af4919878..595e02352 100644 --- a/web-app/src/containers/RenderMarkdown.tsx +++ b/web-app/src/containers/RenderMarkdown.tsx @@ -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 ? ( <> - Copied! + {t('copied')} ) : ( <> - Copy + {t('copy')} )} diff --git a/web-app/src/containers/SettingsMenu.tsx b/web-app/src/containers/SettingsMenu.tsx index 5c3c0768f..5d5e3ef5b 100644 --- a/web-app/src/containers/SettingsMenu.tsx +++ b/web-app/src/containers/SettingsMenu.tsx @@ -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 ? (
- {t('common.modelProviders')} + {t('common:modelProviders')}
) : ( { className="block px-2 gap-1.5 cursor-pointer hover:bg-main-view-fg/5 py-1 w-full rounded" > - {t('common.modelProviders')} + {t('common:modelProviders')} )} diff --git a/web-app/src/containers/SetupScreen.tsx b/web-app/src/containers/SetupScreen.tsx index cf8e32c84..807568073 100644 --- a/web-app/src/containers/SetupScreen.tsx +++ b/web-app/src/containers/SetupScreen.tsx @@ -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() {

- Welcome to Jan + {t('setup:welcome')}

- 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')}

@@ -35,7 +36,7 @@ function SetupScreen() { >

- Set up local model + {t('setup:localModel')}

@@ -53,7 +54,7 @@ function SetupScreen() { }} >

- Set up remote provider + {t('setup:remoteProvider')}

} diff --git a/web-app/src/containers/ThemeSwitcher.tsx b/web-app/src/containers/ThemeSwitcher.tsx index 2f6d5bf2c..fce342a82 100644 --- a/web-app/src/containers/ThemeSwitcher.tsx +++ b/web-app/src/containers/ThemeSwitcher.tsx @@ -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() { {themeOptions.find((item) => item.value === activeTheme)?.label || - 'Auto'} + t('common:auto')} diff --git a/web-app/src/containers/ThinkingBlock.tsx b/web-app/src/containers/ThinkingBlock.tsx index c1445acf3..88ec03b52 100644 --- a/web-app/src/containers/ThinkingBlock.tsx +++ b/web-app/src/containers/ThinkingBlock.tsx @@ -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((set) => ({ const ThinkingBlock = ({ id, text }: Props) => { const { thinkingState, toggleState } = useThinkingStore() const { streamingContent } = useAppState() + const { t } = useTranslation() const loading = !text.includes('') && streamingContent const isExpanded = thinkingState[id] ?? false const handleClick = () => toggleState(id) @@ -51,7 +53,7 @@ const ThinkingBlock = ({ id, text }: Props) => { )} - {loading ? 'Thinking...' : 'Thought'} + {loading ? t('common:thinking') : t('common:thought')}
diff --git a/web-app/src/containers/ThreadContent.tsx b/web-app/src/containers/ThreadContent.tsx index 0ee166b7d..80a864c67 100644 --- a/web-app/src/containers/ThreadContent.tsx +++ b/web-app/src/containers/ThreadContent.tsx @@ -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 ? ( <> - Copied! + {t('copied')} ) : ( @@ -65,7 +67,7 @@ const CopyButton = ({ text }: { text: string }) => { -

Copy

+

{t('copy')}

)} @@ -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(
-

Edit

+

{t('edit')}

- Edit Message + {t('common:dialogs.editMessage.title')}