diff --git a/.gitignore b/.gitignore index 62878011e..75518bf5a 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,7 @@ extensions/inference-nitro-extension/bin/saved-* extensions/inference-nitro-extension/bin/*.tar.gz extensions/inference-nitro-extension/bin/vulkaninfoSDK.exe extensions/inference-nitro-extension/bin/vulkaninfo + + +# Turborepo +.turbo \ No newline at end of file diff --git a/docs/openapi/jan.yaml b/docs/openapi/jan.yaml index 1a37dfd84..f45db7d2d 100644 --- a/docs/openapi/jan.yaml +++ b/docs/openapi/jan.yaml @@ -1,11 +1,11 @@ +--- openapi: 3.0.0 info: title: API Reference description: > # Introduction - Jan API is compatible with the [OpenAI - API](https://platform.openai.com/docs/api-reference). + Jan API is compatible with the [OpenAI API](https://platform.openai.com/docs/api-reference). version: 0.1.8 contact: name: Jan Discord @@ -20,12 +20,12 @@ tags: description: List and describe the various models available in the API. - name: Chat description: > - Given a list of messages comprising a conversation, the model will return - a response. + Given a list of messages comprising a conversation, the model will + return a response. - name: Messages description: > - Messages capture a conversation's content. This can include the content - from LLM responses and other metadata from [chat + Messages capture a conversation's content. This can include the + content from LLM responses and other metadata from [chat completions](/specs/chats). - name: Threads - name: Assistants @@ -49,16 +49,16 @@ paths: summary: | Create chat completion description: > - Creates a model response for the given chat conversation. Equivalent - to OpenAI's create chat completion. + Creates a model response for the given chat conversation. + Equivalent to OpenAI's create chat completion. requestBody: content: application/json: schema: $ref: specs/chat.yaml#/components/schemas/ChatCompletionRequest responses: - '200': + "200": description: OK content: application/json: @@ -192,9 +192,7 @@ paths: } - response = - requests.post('http://localhost:1337/v1/chat/completions', - json=data) + response = requests.post('http://localhost:1337/v1/chat/completions', json=data) print(response.json()) /models: @@ -204,12 +202,12 @@ paths: - Models summary: List models description: > - Lists the currently available models, and provides basic information - about each one such as the owner and availability. Equivalent - to OpenAI's list model. + Lists the currently available models, and provides basic + information about each one such as the owner and availability. + Equivalent to OpenAI's list model. responses: - '200': + "200": description: OK content: application/json: @@ -228,14 +226,6 @@ paths: headers: {Accept: 'application/json'} }); const data = await response.json(); - - lang: Python - source: |- - import requests - - url = 'http://localhost:1337/v1/models' - headers = {'Accept': 'application/json'} - response = requests.get(url, headers=headers) - data = response.json() - lang: Node.js source: |- const fetch = require('node-fetch'); @@ -249,7 +239,15 @@ paths: fetch(url, options) .then(res => res.json()) .then(json => console.log(json)); - /models/download/{model_id}: + - lang: Python + source: |- + import requests + + url = 'http://localhost:1337/v1/models' + headers = {'Accept': 'application/json'} + response = requests.get(url, headers=headers) + data = response.json() + "/models/download/{model_id}": get: operationId: downloadModel tags: @@ -267,7 +265,7 @@ paths: description: | The ID of the model to use for this request. responses: - '200': + "200": description: OK content: application/json: @@ -304,20 +302,18 @@ paths: import requests - response = - requests.get('http://localhost:1337/v1/models/download/{model_id}', - headers={'accept': 'application/json'}) + response = requests.get('http://localhost:1337/v1/models/download/{model_id}', headers={'accept': 'application/json'}) data = response.json() - /models/{model_id}: + "/models/{model_id}": get: operationId: retrieveModel tags: - Models summary: Retrieve model description: > - Get a model instance, providing basic information about the model such - as the owner and permissioning. Equivalent to OpenAI's retrieve model. parameters: @@ -330,7 +326,7 @@ paths: description: | The ID of the model to use for this request. responses: - '200': + "200": description: OK content: application/json: @@ -374,9 +370,7 @@ paths: model_id = 'mistral-ins-7b-q4' - response = - requests.get(f'http://localhost:1337/v1/models/{model_id}', - headers={'accept': 'application/json'}) + response = requests.get(f'http://localhost:1337/v1/models/{model_id}', headers={'accept': 'application/json'}) print(response.json()) delete: @@ -398,7 +392,7 @@ paths: description: | The model id to delete responses: - '200': + "200": description: OK content: application/json: @@ -442,9 +436,7 @@ paths: model_id = 'mistral-ins-7b-q4' - response = - requests.delete(f'http://localhost:1337/v1/models/{model_id}', - headers={'accept': 'application/json'}) + response = requests.delete(f'http://localhost:1337/v1/models/{model_id}', headers={'accept': 'application/json'}) /threads: post: operationId: createThread @@ -462,7 +454,7 @@ paths: schema: $ref: specs/threads.yaml#/components/schemas/CreateThreadObject responses: - '200': + "200": description: Thread created successfully content: application/json: @@ -471,8 +463,8 @@ paths: x-codeSamples: - lang: cURL source: | - curl -X POST http://localhost:1337/v1/threads \ - -H "Content-Type: application/json" \ + curl -X POST http://localhost:1337/v1/threads \ + -H "Content-Type: application/json" \ -d '{ "messages": [{ "role": "user", @@ -483,6 +475,73 @@ paths: "content": "How does AI work? Explain it in simple terms." }] }' + - lang: JavaScript + source: |- + const fetch = require('node-fetch'); + + fetch('http://localhost:1337/v1/threads', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + messages: [ + { + role: 'user', + content: 'Hello, what is AI?', + file_ids: ['file-abc123'] + }, + { + role: 'user', + content: 'How does AI work? Explain it in simple terms.' + } + ] + }) + }); + - lang: Node.js + source: |- + const fetch = require('node-fetch'); + + fetch('http://localhost:1337/v1/threads', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + messages: [ + { + role: 'user', + content: 'Hello, what is AI?', + file_ids: ['file-abc123'] + }, + { + role: 'user', + content: 'How does AI work? Explain it in simple terms.' + } + ] + }) + }); + - lang: Python + source: |- + import requests + + url = 'http://localhost:1337/v1/threads' + payload = { + 'messages': [ + { + 'role': 'user', + 'content': 'Hello, what is AI?', + 'file_ids': ['file-abc123'] + }, + { + 'role': 'user', + 'content': 'How does AI work? Explain it in simple terms.' + } + ] + } + + response = requests.post(url, json=payload) + print(response.text) get: operationId: listThreads tags: @@ -491,7 +550,7 @@ paths: description: | Retrieves a list of all threads available in the system. responses: - '200': + "200": description: List of threads retrieved successfully content: application/json: @@ -516,10 +575,37 @@ paths: metadata: {} x-codeSamples: - lang: cURL - source: | - curl http://localhost:1337/v1/threads \ - -H "Content-Type: application/json" \ - /threads/{thread_id}: + source: |- + curl http://localhost:1337/v1/threads \ + -H "Content-Type: application/json" + - lang: JavaScript + source: |- + const fetch = require('node-fetch'); + + fetch('http://localhost:1337/v1/threads', { + method: 'GET', + headers: {'Content-Type': 'application/json'} + }).then(res => res.json()) + .then(json => console.log(json)); + - lang: Node.js + source: |- + const fetch = require('node-fetch'); + + fetch('http://localhost:1337/v1/threads', { + method: 'GET', + headers: {'Content-Type': 'application/json'} + }).then(res => res.json()) + .then(json => console.log(json)); + - lang: Python + source: |- + import requests + + url = 'http://localhost:1337/v1/threads' + headers = {'Content-Type': 'application/json'} + + response = requests.get(url, headers=headers) + print(response.json()) + "/threads/{thread_id}": get: operationId: getThread tags: @@ -539,7 +625,7 @@ paths: description: | The ID of the thread to retrieve. responses: - '200': + "200": description: Thread details retrieved successfully content: application/json: @@ -579,7 +665,7 @@ paths: items: $ref: specs/threads.yaml#/components/schemas/ThreadMessageObject responses: - '200': + "200": description: Thread modified successfully content: application/json: @@ -618,7 +704,7 @@ paths: description: | The ID of the thread to be deleted. responses: - '200': + "200": description: Thread deleted successfully content: application/json: @@ -639,7 +725,7 @@ paths: "https://platform.openai.com/docs/api-reference/assistants/listAssistants"> Equivalent to OpenAI's list assistants. responses: - '200': + "200": description: List of assistants retrieved successfully content: application/json: @@ -676,10 +762,36 @@ paths: metadata: {} x-codeSamples: - lang: cURL - source: | + source: |- curl http://localhost:1337/v1/assistants \ - -H "Content-Type: application/json" \ - /assistants/{assistant_id}: + -H "Content-Type: application/json" + - lang: JavaScript + source: |- + fetch('http://localhost:1337/v1/assistants', { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }) + - lang: Node.js + source: |- + const fetch = require('node-fetch'); + + fetch('http://localhost:1337/v1/assistants', { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }) + - lang: Python + source: |- + import requests + + url = 'http://localhost:1337/v1/assistants' + headers = {'Content-Type': 'application/json'} + + response = requests.get(url, headers=headers) + "/assistants/{assistant_id}": get: operationId: getAssistant tags: @@ -699,19 +811,51 @@ paths: description: | The ID of the assistant to retrieve. responses: - '200': + "200": description: null content: application/json: schema: - $ref: >- - specs/assistants.yaml#/components/schemas/RetrieveAssistantResponse + $ref: specs/assistants.yaml#/components/schemas/RetrieveAssistantResponse x-codeSamples: - lang: cURL - source: | + source: |- curl http://localhost:1337/v1/assistants/{assistant_id} \ - -H "Content-Type: application/json" \ - /threads/{thread_id}/messages: + -H "Content-Type: application/json" + - lang: JavaScript + source: |- + const fetch = require('node-fetch'); + + let assistantId = 'abc123'; + + fetch(`http://localhost:1337/v1/assistants/${assistantId}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }) + - lang: Node.js + source: |- + const fetch = require('node-fetch'); + + let assistantId = 'abc123'; + + fetch(`http://localhost:1337/v1/assistants/${assistantId}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }) + - lang: Python + source: >- + import requests + + + assistant_id = 'abc123' + + + response = requests.get(f'http://localhost:1337/v1/assistants/{assistant_id}', headers={'Content-Type': 'application/json'}) + "/threads/{thread_id}/messages": get: operationId: listMessages tags: @@ -730,7 +874,7 @@ paths: description: | The ID of the thread from which to retrieve messages. responses: - '200': + "200": description: List of messages retrieved successfully content: application/json: @@ -782,7 +926,7 @@ paths: - role - content responses: - '200': + "200": description: Message created successfully content: application/json: @@ -797,7 +941,7 @@ paths: "role": "user", "content": "How does AI work? Explain it in simple terms." }' - /threads/{thread_id}/messages/{message_id}: + "/threads/{thread_id}/messages/{message_id}": get: operationId: retrieveMessage tags: @@ -824,7 +968,7 @@ paths: description: | The ID of the message to retrieve. responses: - '200': + "200": description: OK content: application/json: @@ -833,8 +977,8 @@ paths: x-codeSamples: - lang: cURL source: > - curl - http://localhost:1337/v1/threads/{thread_id}/messages/{message_id} \ + curl http://localhost:1337/v1/threads/{thread_id}/messages/{message_id} + \ -H "Content-Type: application/json" x-webhooks: ModelObject: @@ -856,9 +1000,10 @@ x-webhooks: post: summary: The assistant object description: > - Build assistants that can call models and use tools to perform tasks. - - Equivalent to OpenAI's assistants object. + Build assistants that can call models and use tools to perform + tasks. Equivalent + to OpenAI's assistants object. operationId: AssistantObjects tags: - Assistants @@ -885,8 +1030,7 @@ x-webhooks: ThreadObject: post: summary: The thread object - description: >- - Represents a thread that contains messages. Equivalent to OpenAI's thread object. operationId: ThreadObject diff --git a/electron/handlers/native.ts b/electron/handlers/native.ts index 79fa994bf..19a473e73 100644 --- a/electron/handlers/native.ts +++ b/electron/handlers/native.ts @@ -93,12 +93,12 @@ export function handleAppIPCs() { const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, { title: 'Select model files', buttonLabel: 'Select', - properties: ['openFile', 'multiSelections'], + properties: ['openFile', 'openDirectory', 'multiSelections'], }) if (canceled) { return - } else { - return filePaths } + + return filePaths }) } diff --git a/electron/package.json b/electron/package.json index 7cdb98360..f51df233b 100644 --- a/electron/package.json +++ b/electron/package.json @@ -61,6 +61,8 @@ "test:e2e": "playwright test --workers=1", "copy:assets": "rimraf --glob \"./pre-install/*.tgz\" && cpx \"../pre-install/*.tgz\" \"./pre-install\"", "dev": "yarn copy:assets && tsc -p . && electron .", + "compile": "tsc -p .", + "start": "electron .", "build": "yarn copy:assets && run-script-os", "build:test": "yarn copy:assets && run-script-os", "build:test:darwin": "tsc -p . && electron-builder -p never -m --dir", diff --git a/package.json b/package.json index 957934fda..847e89d91 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ "build:extensions": "run-script-os", "build:test": "yarn copy:assets && yarn build:web && yarn workspace jan build:test", "build": "yarn build:web && yarn build:electron", - "build:publish": "yarn copy:assets && yarn build:web && yarn workspace jan build:publish" + "build:publish": "yarn copy:assets && yarn build:web && yarn workspace jan build:publish", + "turbo:electron": "turbo run dev --parallel --filter=!@janhq/server" }, "devDependencies": { "concurrently": "^8.2.1", diff --git a/turbo.json b/turbo.json new file mode 100644 index 000000000..0e53ece2d --- /dev/null +++ b/turbo.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://turbo.build/schema.json", + "pipeline": { + "build": { + "outputs": [".next/**", "!.next/cache/**"] + }, + "dev": { + "cache": false + }, + "web#build": { + "dependsOn": ["@janhq/core#build"] + }, + "web:dev": { + "cache": false, + "persistent": true, + "dependsOn": ["@janhq/core#build", "@janhq/uikit#build"] + }, + "electron:dev": { + "cache": false, + "persistent": true, + "dependsOn": ["@janhq/core#build", "@janhq/server#build", "jan#compile"] + }, + "electron#build": { + "dependsOn": ["web#build", "server#build", "core#build"], + "cache": false + }, + "type-check": {} + } +} diff --git a/uikit/src/button/styles.scss b/uikit/src/button/styles.scss index c97bec9e0..003df5b4d 100644 --- a/uikit/src/button/styles.scss +++ b/uikit/src/button/styles.scss @@ -5,11 +5,11 @@ @apply disabled:pointer-events-none disabled:bg-zinc-100 disabled:text-zinc-400; &-primary { - @apply bg-blue-600 text-white hover:bg-blue-600/90; + @apply bg-primary hover:bg-primary/90 text-white; } &-secondary-blue { - @apply bg-blue-200 text-blue-600 hover:bg-blue-300/50; + @apply bg-blue-200 text-blue-600 hover:bg-blue-300/50 dark:hover:bg-blue-200/80; } &-danger { @@ -17,7 +17,7 @@ } &-secondary-danger { - @apply bg-red-200 text-red-600 hover:bg-red-300/50; + @apply bg-red-200 text-red-600 hover:bg-red-300/50 dark:hover:bg-red-200/80; } &-outline { @@ -66,7 +66,7 @@ [type='reset'], [type='submit'] { &.btn-primary { - @apply bg-blue-600 hover:bg-blue-600/90; + @apply bg-primary hover:bg-primary/90; @apply disabled:pointer-events-none disabled:bg-zinc-100 disabled:text-zinc-400; } &.btn-secondary { diff --git a/uikit/src/checkbox/styles.scss b/uikit/src/checkbox/styles.scss index cf35ed5ca..33610f837 100644 --- a/uikit/src/checkbox/styles.scss +++ b/uikit/src/checkbox/styles.scss @@ -1,5 +1,5 @@ .checkbox { - @apply border-border h-5 w-5 flex-shrink-0 rounded-md border data-[state=checked]:bg-blue-600 data-[state=checked]:text-white; + @apply border-border data-[state=checked]:bg-primary h-5 w-5 flex-shrink-0 rounded-md border data-[state=checked]:text-white; &--icon { @apply h-4 w-4; diff --git a/uikit/src/input/styles.scss b/uikit/src/input/styles.scss index 51efd8e57..e649f494d 100644 --- a/uikit/src/input/styles.scss +++ b/uikit/src/input/styles.scss @@ -1,6 +1,6 @@ .input { @apply border-border placeholder:text-muted-foreground flex h-9 w-full rounded-lg border bg-transparent px-3 py-1 transition-colors; - @apply disabled:text-muted-foreground disabled:cursor-not-allowed disabled:bg-zinc-100; + @apply disabled:text-muted-foreground disabled:cursor-not-allowed disabled:bg-zinc-100 disabled:dark:bg-zinc-800 disabled:dark:text-zinc-600; @apply focus-within:outline-none focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1; @apply file:border-0 file:bg-transparent file:font-medium; } diff --git a/uikit/src/main.scss b/uikit/src/main.scss index e31b53c68..f3294e12e 100644 --- a/uikit/src/main.scss +++ b/uikit/src/main.scss @@ -42,10 +42,69 @@ --danger: 346.8 77.2% 49.8%; --danger-foreground: 355.7 100% 97.3%; - --secondary: 60 4.8% 95.9%; - --secondary-foreground: 24 9.8% 10%; - --border: 20 5.9% 90%; --input: 20 5.9% 90%; --ring: 20 14.3% 4.1%; + + .primary-blue { + --primary: 221 83% 53%; + --primary-foreground: 210 40% 98%; + + --secondary: 60 4.8% 95.9%; + --secondary-foreground: 24 9.8% 10%; + } + + .primary-green { + --primary: 142.1 76.2% 36.3%; + --primary-foreground: 355.7 100% 97.3%; + + --secondary: 240 4.8% 95.9%; + --secondary-foreground: 240 5.9% 10%; + } + + .primary-purple { + --primary: 262.1 83.3% 57.8%; + --primary-foreground: 210 20% 98%; + + --secondary: 220 14.3% 95.9%; + --secondary-foreground: 220.9 39.3% 11%; + } +} + +.dark { + --background: 20 14.3% 4.1%; + --foreground: 60 9.1% 97.8%; + + --muted: 12 6.5% 15.1%; + --muted-foreground: 24 5.4% 63.9%; + + --danger: 346.8 77.2% 49.8%; + --danger-foreground: 355.7 100% 97.3%; + + --border: 12 6.5% 15.1%; + --input: 12 6.5% 15.1%; + --ring: 35.5 91.7% 32.9%; + + .primary-blue { + --primary: 221 83% 53%; + --primary-foreground: 222.2 47.4% 11.2%; + + --secondary: 12 6.5% 15.1%; + --secondary-foreground: 60 9.1% 97.8%; + } + + .primary-green { + --primary: 142.1 70.6% 45.3%; + --primary-foreground: 144.9 80.4% 10%; + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; + } + + .primary-purple { + --primary: 263.4 70% 50.4%; + --primary-foreground: 210 20% 98%; + + --secondary: 215 27.9% 16.9%; + --secondary-foreground: 210 20% 98%; + } } diff --git a/uikit/src/progress/styles.scss b/uikit/src/progress/styles.scss index 1a8483c47..0b7078f48 100644 --- a/uikit/src/progress/styles.scss +++ b/uikit/src/progress/styles.scss @@ -1,7 +1,7 @@ .progress { - @apply relative h-4 w-full overflow-hidden rounded-full bg-gray-100; + @apply bg-secondary relative h-4 w-full overflow-hidden rounded-full; &-indicator { - @apply h-full w-full flex-1 bg-blue-600 transition-all; + @apply bg-primary h-full w-full flex-1 transition-all; } } diff --git a/uikit/src/select/styles.scss b/uikit/src/select/styles.scss index 99db49766..90485723a 100644 --- a/uikit/src/select/styles.scss +++ b/uikit/src/select/styles.scss @@ -1,6 +1,6 @@ .select { @apply placeholder:text-muted-foreground border-border flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm disabled:cursor-not-allowed [&>span]:line-clamp-1; - @apply disabled:text-muted-foreground disabled:cursor-not-allowed disabled:bg-zinc-100; + @apply disabled:text-muted-foreground disabled:cursor-not-allowed disabled:bg-zinc-100 disabled:dark:bg-zinc-800 disabled:dark:text-zinc-600; @apply focus-within:outline-none focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1; &-caret { diff --git a/uikit/src/slider/styles.scss b/uikit/src/slider/styles.scss index 465392419..718972efb 100644 --- a/uikit/src/slider/styles.scss +++ b/uikit/src/slider/styles.scss @@ -2,7 +2,7 @@ @apply relative flex w-full touch-none select-none items-center; &-track { - @apply relative h-1.5 w-full grow overflow-hidden rounded-full bg-gray-200; + @apply relative h-1.5 w-full grow overflow-hidden rounded-full bg-gray-200 dark:bg-gray-800; [data-disabled] { @apply cursor-not-allowed opacity-50; } @@ -13,6 +13,6 @@ } &-thumb { - @apply bg-background focus-visible:ring-ring block h-4 w-4 rounded-full border border-blue-600/50 shadow transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50; + @apply border-primary/50 bg-background focus-visible:ring-ring block h-4 w-4 rounded-full border shadow transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50; } } diff --git a/uikit/src/switch/styles.scss b/uikit/src/switch/styles.scss index 57fa128ba..c8a12cdf5 100644 --- a/uikit/src/switch/styles.scss +++ b/uikit/src/switch/styles.scss @@ -1,7 +1,7 @@ .switch { @apply inline-flex h-[20px] w-[36px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent; @apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2; - @apply data-[state=unchecked]:bg-input data-[state=checked]:bg-blue-600; + @apply data-[state=checked]:bg-primary data-[state=unchecked]:bg-input; @apply disabled:cursor-not-allowed disabled:opacity-50; &-toggle { diff --git a/uikit/src/tooltip/styles.scss b/uikit/src/tooltip/styles.scss index 169e081b7..8ae645cee 100644 --- a/uikit/src/tooltip/styles.scss +++ b/uikit/src/tooltip/styles.scss @@ -1,6 +1,6 @@ .tooltip { - @apply z-50 overflow-hidden rounded-md bg-gray-950 px-2 py-1.5 text-xs font-medium text-gray-200 shadow-md; + @apply dark:bg-input dark:text-foreground z-50 overflow-hidden rounded-md bg-gray-950 px-2 py-1.5 text-xs font-medium text-gray-200 shadow-md; &-arrow { - @apply fill-gray-950; + @apply dark:fill-input fill-gray-950; } } diff --git a/web/app/layout.tsx b/web/app/layout.tsx index 37bcdf53e..6c6fc65ab 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -15,7 +15,7 @@ export const metadata: Metadata = { export default function RootLayout({ children }: PropsWithChildren) { return ( - +
{children} diff --git a/web/containers/CardSidebar/index.tsx b/web/containers/CardSidebar/index.tsx index 3013360e9..132494d48 100644 --- a/web/containers/CardSidebar/index.tsx +++ b/web/containers/CardSidebar/index.tsx @@ -45,7 +45,7 @@ export default function CardSidebar({ return (
@@ -61,7 +61,7 @@ export default function CardSidebar({ if (!children) return setShow(!show) }} - className="flex w-full flex-1 items-center space-x-2 rounded-lg bg-zinc-100 py-2 pr-2" + className="flex w-full flex-1 items-center space-x-2 rounded-lg bg-zinc-100 py-2 pr-2 dark:bg-zinc-900" > setMore(!more)} > @@ -114,7 +114,7 @@ export default function CardSidebar({ <> {title === 'Model' ? (
- + {openFileTitle()} @@ -122,7 +122,7 @@ export default function CardSidebar({
) : ( - + {openFileTitle()} )} @@ -141,7 +141,7 @@ export default function CardSidebar({ /> <>
- + Edit Global Defaults for{' '} diff --git a/web/containers/Checkbox/index.tsx b/web/containers/Checkbox/index.tsx index 1ced3e19d..a545771b6 100644 --- a/web/containers/Checkbox/index.tsx +++ b/web/containers/Checkbox/index.tsx @@ -34,10 +34,12 @@ const Checkbox: React.FC = ({ return (
-

{title}

+

+ {title} +

- + diff --git a/web/containers/DropdownListSidebar/index.tsx b/web/containers/DropdownListSidebar/index.tsx index dc5ee2605..c05d26e51 100644 --- a/web/containers/DropdownListSidebar/index.tsx +++ b/web/containers/DropdownListSidebar/index.tsx @@ -203,14 +203,15 @@ const DropdownListSidebar = ({ isTabActive === 1 && '[&_.select-scroll-down-button]:hidden' )} > -
-
    +
    +
      {engineOptions.map((name, i) => { return (
    • setIsTabActive(i)} @@ -229,7 +230,8 @@ const DropdownListSidebar = ({ {name} diff --git a/web/containers/GPUDriverPromptModal/index.tsx b/web/containers/GPUDriverPromptModal/index.tsx index 8d11b4efa..bdcf1b2f8 100644 --- a/web/containers/GPUDriverPromptModal/index.tsx +++ b/web/containers/GPUDriverPromptModal/index.tsx @@ -60,7 +60,7 @@ const GPUDriverPrompt: React.FC = () => { id="default-checkbox" type="checkbox" onChange={onDoNotShowAgainChange} - className="h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-2 focus:ring-blue-500" + className="h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-blue-600" /> Don't show again
    diff --git a/web/containers/ItemCardSidebar/index.tsx b/web/containers/ItemCardSidebar/index.tsx deleted file mode 100644 index 627d7f45d..000000000 --- a/web/containers/ItemCardSidebar/index.tsx +++ /dev/null @@ -1,29 +0,0 @@ -type Props = { - title: string - description?: string - disabled?: boolean - onChange?: (text?: string) => void -} - -export default function ItemCardSidebar({ - description, - title, - disabled, - onChange, -}: Props) { - return ( -
    -
    - {title} -
    - onChange?.(e.target.value)} - /> -
    - ) -} diff --git a/web/containers/Layout/BottomBar/DownloadingState/index.tsx b/web/containers/Layout/BottomBar/DownloadingState/index.tsx index 4c3d596b0..dcebacd3c 100644 --- a/web/containers/Layout/BottomBar/DownloadingState/index.tsx +++ b/web/containers/Layout/BottomBar/DownloadingState/index.tsx @@ -47,7 +47,7 @@ export default function DownloadingState() { { className="flex cursor-pointer flex-row items-center space-x-2" onClick={onClick} > -

    +

    Importing model ({finishedImportModelCount}/{importingModels.length} )

    -
    +
    - + {progress.toFixed(2)}%
    diff --git a/web/containers/Layout/Ribbon/index.tsx b/web/containers/Layout/Ribbon/index.tsx index dc6191f09..c0bc46586 100644 --- a/web/containers/Layout/Ribbon/index.tsx +++ b/web/containers/Layout/Ribbon/index.tsx @@ -45,7 +45,7 @@ export default function RibbonNav() { size={20} className={twMerge( 'flex-shrink-0 text-muted-foreground', - serverEnabled && 'text-gray-300' + serverEnabled && 'text-gray-300 dark:text-gray-700' )} /> ), @@ -114,7 +114,7 @@ export default function RibbonNav() {
    {isActive && ( )} @@ -166,7 +166,7 @@ export default function RibbonNav() {
{isActive && ( )} diff --git a/web/containers/Layout/TopBar/index.tsx b/web/containers/Layout/TopBar/index.tsx index 9686a7fd9..605d8e44d 100644 --- a/web/containers/Layout/TopBar/index.tsx +++ b/web/containers/Layout/TopBar/index.tsx @@ -159,7 +159,7 @@ const TopBar = () => { size={16} className="text-muted-foreground" /> - + {openFileTitle()}
@@ -175,7 +175,7 @@ const TopBar = () => { className="mt-0.5 flex-shrink-0 text-muted-foreground" />
- + Edit Threads Settings @@ -204,7 +204,7 @@ const TopBar = () => { className="text-muted-foreground" />
- + {openFileTitle()}
diff --git a/web/containers/Loader/index.tsx b/web/containers/Loader/index.tsx index cf6604fc8..dcf1bec65 100644 --- a/web/containers/Loader/index.tsx +++ b/web/containers/Loader/index.tsx @@ -7,12 +7,12 @@ export default function Loader({ description }: Props) {
-

{description}

diff --git a/web/containers/ModalTroubleShoot/AppLogs.tsx b/web/containers/ModalTroubleShoot/AppLogs.tsx index 98f076599..d4f6bddb8 100644 --- a/web/containers/ModalTroubleShoot/AppLogs.tsx +++ b/web/containers/ModalTroubleShoot/AppLogs.tsx @@ -28,7 +28,7 @@ const AppLogs = () => {
{ setRepoID(e.target.value) }} diff --git a/web/screens/ExploreModels/index.tsx b/web/screens/ExploreModels/index.tsx index e7fd3a9dc..1ad0b00ae 100644 --- a/web/screens/ExploreModels/index.tsx +++ b/web/screens/ExploreModels/index.tsx @@ -1,4 +1,4 @@ -import { useCallback, useContext, useState } from 'react' +import { useCallback, useState } from 'react' import { Input, @@ -15,13 +15,12 @@ import { import { useAtomValue, useSetAtom } from 'jotai' import { UploadIcon, SearchIcon } from 'lucide-react' -import { FeatureToggleContext } from '@/context/FeatureToggle' - import { setImportModelStageAtom } from '@/hooks/useImportModel' import ExploreModelList from './ExploreModelList' import { HuggingFaceModal } from './HuggingFaceModal' +import { experimentalFeatureEnabledAtom } from '@/helpers/atoms/AppConfig.atom' import { configuredModelsAtom, downloadedModelsAtom, @@ -38,7 +37,7 @@ const ExploreModelsScreen = () => { const [showHuggingFaceModal, setShowHuggingFaceModal] = useState(false) const setImportModelStage = useSetAtom(setImportModelStageAtom) - const { experimentalFeature } = useContext(FeatureToggleContext) + const experimentalFeature = useAtomValue(experimentalFeatureEnabledAtom) const filteredModels = configuredModels.filter((x) => { if (sortSelected === 'Downloaded') { @@ -91,13 +90,13 @@ const ExploreModelsScreen = () => { /> setsearchValue(e.target.value)} />
@@ -282,7 +280,7 @@ const Advanced = () => { disabled={gpuList.length === 0 || !gpuEnabled} value={selectedGpu.join()} > - + {selectedGpu.join()} @@ -355,7 +353,7 @@ const Advanced = () => { )} {/* Vulkan for AMD GPU/ APU and Intel Arc GPU */} - {!isMac && experimentalFeature && ( + {!isMac && experimentalEnabled && (
diff --git a/web/screens/Settings/Appearance/TogglePrimary.tsx b/web/screens/Settings/Appearance/TogglePrimary.tsx new file mode 100644 index 000000000..d75117269 --- /dev/null +++ b/web/screens/Settings/Appearance/TogglePrimary.tsx @@ -0,0 +1,57 @@ +import { motion as m } from 'framer-motion' +import { twMerge } from 'tailwind-merge' + +import { useUserConfigs } from '@/hooks/useUserConfigs' + +type PrimaryColorOption = { + value: PrimaryColor + class: string +} + +const primaryColorOptions: PrimaryColorOption[] = [ + { + value: 'primary-blue', + class: 'bg-blue-500', + }, + { + value: 'primary-purple', + class: 'bg-purple-500', + }, + { + value: 'primary-green', + class: 'bg-green-500', + }, +] + +export default function TogglePrimary() { + const [config, setUserConfig] = useUserConfigs() + + const handleChangeAccent = (primaryColor: PrimaryColor) => { + setUserConfig({ ...config, primaryColor }) + } + + return ( +
+ {primaryColorOptions.map((option, i) => { + const isActive = config.primaryColor === option.value + return ( +
+
+ ) + })} +
+ ) +} diff --git a/web/screens/Settings/Appearance/ToggleTheme.tsx b/web/screens/Settings/Appearance/ToggleTheme.tsx index 369a216fa..a183348d9 100644 --- a/web/screens/Settings/Appearance/ToggleTheme.tsx +++ b/web/screens/Settings/Appearance/ToggleTheme.tsx @@ -28,7 +28,7 @@ export default function ToggleTheme() { {isActive ? ( ) : null} diff --git a/web/screens/Settings/Appearance/index.tsx b/web/screens/Settings/Appearance/index.tsx index 9e2c551d6..51899ba40 100644 --- a/web/screens/Settings/Appearance/index.tsx +++ b/web/screens/Settings/Appearance/index.tsx @@ -1,3 +1,4 @@ +import ToggleAccent from '@/screens/Settings/Appearance/TogglePrimary' import ToggleTheme from '@/screens/Settings/Appearance/ToggleTheme' export default function AppearanceOptions() { @@ -21,6 +22,7 @@ export default function AppearanceOptions() { Choose the primary accent color used throughout the app.

+
) diff --git a/web/screens/Settings/EditModelInfoModal/index.tsx b/web/screens/Settings/EditModelInfoModal/index.tsx index bc9d6521d..91dd8c5f2 100644 --- a/web/screens/Settings/EditModelInfoModal/index.tsx +++ b/web/screens/Settings/EditModelInfoModal/index.tsx @@ -116,6 +116,11 @@ const EditModelInfoModal: React.FC = () => { return null } + const onTagsChange = (e: React.ChangeEvent) => { + const tags = e.target.value.split(',') + setTags(tags) + } + return ( { Edit Model Information -
+
- +
-
+

{editingModel.name}

{toGibibytes(editingModel.size)} - - Format:{' '} - - - {editingModel.format.toUpperCase()} - +
+ + Format: + + + {editingModel.format.toUpperCase()} + +
@@ -189,7 +196,7 @@ const EditModelInfoModal: React.FC = () => {
- +
diff --git a/web/screens/Settings/ImportModelOptionModal/ImportModelOptionSelection.tsx b/web/screens/Settings/ImportModelOptionModal/ImportModelOptionSelection.tsx index e4f5c4cf8..5276ee195 100644 --- a/web/screens/Settings/ImportModelOptionModal/ImportModelOptionSelection.tsx +++ b/web/screens/Settings/ImportModelOptionModal/ImportModelOptionSelection.tsx @@ -16,7 +16,7 @@ const ImportModelOptionSelection: React.FC = ({ onClick={() => setSelectedOptionType(option.type)} >
- {checked &&
} + {checked &&
}
diff --git a/web/screens/Settings/ImportSuccessIcon/index.tsx b/web/screens/Settings/ImportSuccessIcon/index.tsx index 6bc8d0b9d..c2debb3db 100644 --- a/web/screens/Settings/ImportSuccessIcon/index.tsx +++ b/web/screens/Settings/ImportSuccessIcon/index.tsx @@ -29,8 +29,8 @@ const ImportSuccessIcon: React.FC = ({ onEditModelClick }) => { } const SuccessIcon: React.FC = React.memo(() => ( -
- +
+
)) @@ -41,10 +41,10 @@ const EditIcon: React.FC = React.memo(({ onEditModelClick }) => { return (
- +
) }) diff --git a/web/screens/Settings/ImportingModelModal/ImportingModelItem.tsx b/web/screens/Settings/ImportingModelModal/ImportingModelItem.tsx index 7b3889128..0426c3dd0 100644 --- a/web/screens/Settings/ImportingModelModal/ImportingModelItem.tsx +++ b/web/screens/Settings/ImportingModelModal/ImportingModelItem.tsx @@ -37,11 +37,11 @@ const ImportingModelItem: React.FC = ({ model }) => { }, [model.status, model.size]) return ( -
-

+

+

{model.name}

-

{displayStatus}

+

{displayStatus}

{model.status === 'IMPORTED' && ( diff --git a/web/screens/Settings/ImportingModelModal/index.tsx b/web/screens/Settings/ImportingModelModal/index.tsx index 4d346f010..f621c2fb7 100644 --- a/web/screens/Settings/ImportingModelModal/index.tsx +++ b/web/screens/Settings/ImportingModelModal/index.tsx @@ -62,7 +62,9 @@ const ImportingModelModal: React.FC = () => { Importing model ({finishedImportModel}/{importingModels.length})
- +
- + -

+

Own your model configurations, use at your own risk. Misconfigurations may result in lower quality or unexpected outputs.{' '}

diff --git a/web/screens/Settings/Models/Row.tsx b/web/screens/Settings/Models/Row.tsx index 9ceeab92a..5ade3bad6 100644 --- a/web/screens/Settings/Models/Row.tsx +++ b/web/screens/Settings/Models/Row.tsx @@ -152,7 +152,7 @@ export default function RowModel(props: RowModelProps) { ) : ( )} - + {isActiveModel ? stateModel.state : 'Start'}  Model @@ -189,7 +189,9 @@ export default function RowModel(props: RowModelProps) { }} > - Delete Model + + Delete Model +
)} diff --git a/web/screens/Settings/SelectingModelModal/index.tsx b/web/screens/Settings/SelectingModelModal/index.tsx index 35bd9b772..7579e0c3c 100644 --- a/web/screens/Settings/SelectingModelModal/index.tsx +++ b/web/screens/Settings/SelectingModelModal/index.tsx @@ -1,7 +1,7 @@ import { useCallback } from 'react' import { useDropzone } from 'react-dropzone' -import { ImportingModel, baseName, fs } from '@janhq/core' +import { ImportingModel, baseName, fs, joinPath } from '@janhq/core' import { Modal, ModalContent, ModalHeader, ModalTitle } from '@janhq/uikit' import { useAtomValue, useSetAtom } from 'jotai' @@ -34,14 +34,31 @@ const SelectingModelModal: React.FC = () => { const sanitizedFilePaths: FilePathWithSize[] = [] for (const filePath of filePaths) { const fileStats = await fs.fileStat(filePath, true) - if (!fileStats || fileStats.isDirectory) continue + if (!fileStats) continue - const fileName = await baseName(filePath) - sanitizedFilePaths.push({ - path: filePath, - name: fileName, - size: fileStats.size, - }) + if (!fileStats.isDirectory) { + const fileName = await baseName(filePath) + sanitizedFilePaths.push({ + path: filePath, + name: fileName, + size: fileStats.size, + }) + } else { + // allowing only one level of directory + const files = await fs.readdirSync(filePath) + + for (const file of files) { + const fullPath = await joinPath([filePath, file]) + const fileStats = await fs.fileStat(fullPath, true) + if (!fileStats || fileStats.isDirectory) continue + + sanitizedFilePaths.push({ + path: fullPath, + name: file, + size: fileStats.size, + }) + } + } } const unsupportedFiles = sanitizedFilePaths.filter( @@ -68,7 +85,7 @@ const SelectingModelModal: React.FC = () => { ) if (unsupportedFiles.length > 0) { snackbar({ - description: `File has to be a .gguf file`, + description: `Only files with .gguf extension can be imported.`, type: 'error', }) } @@ -84,9 +101,11 @@ const SelectingModelModal: React.FC = () => { onDrop: onDropModels, }) - const borderColor = isDragActive ? 'border-primary' : 'border-[#F4F4F5]' - const textColor = isDragActive ? 'text-blue-600' : 'text-[#71717A]' - const dragAndDropBgColor = isDragActive ? 'bg-[#EFF6FF]' : 'bg-white' + const borderColor = isDragActive ? 'border-primary' : 'border-border' + const textColor = isDragActive ? 'text-primary' : 'text-muted-foreground' + const dragAndDropBgColor = isDragActive + ? 'bg-[#EFF6FF] dark:bg-blue-50/10' + : 'bg-background' return ( { Import Model -

+

Import any model file (GGUF) or folder. Your imported model will be private to you.

@@ -116,7 +135,7 @@ const SelectingModelModal: React.FC = () => {
- + Click to upload diff --git a/web/screens/Settings/SettingMenu/index.tsx b/web/screens/Settings/SettingMenu/index.tsx index 9e40d1fda..fd0ea1560 100644 --- a/web/screens/Settings/SettingMenu/index.tsx +++ b/web/screens/Settings/SettingMenu/index.tsx @@ -15,6 +15,7 @@ const SettingMenu: React.FC = ({ activeMenu, onMenuClick }) => { useEffect(() => { setMenus([ 'My Models', + 'My Settings', 'Advanced Settings', ...(window.electronAPI ? ['Extensions'] : []), ]) @@ -38,7 +39,7 @@ const SettingMenu: React.FC = ({ activeMenu, onMenuClick }) => { {isActive && ( )} diff --git a/web/screens/Settings/index.tsx b/web/screens/Settings/index.tsx index ed8af3c46..1a5e4011e 100644 --- a/web/screens/Settings/index.tsx +++ b/web/screens/Settings/index.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react' import Advanced from '@/screens/Settings/Advanced' - +import AppearanceOptions from '@/screens/Settings/Appearance' import ExtensionCatalog from '@/screens/Settings/CoreExtensions' import Models from '@/screens/Settings/Models' @@ -14,6 +14,9 @@ const handleShowOptions = (menu: string) => { case 'Extensions': return + case 'My Settings': + return + case 'Advanced Settings': return diff --git a/web/styles/components/message.scss b/web/styles/components/message.scss index f95061599..a9bec5d48 100644 --- a/web/styles/components/message.scss +++ b/web/styles/components/message.scss @@ -1,5 +1,5 @@ .message { - @apply text-black; + @apply text-black dark:text-gray-300; white-space: pre-line; ul, @@ -10,7 +10,7 @@ } a { - @apply text-blue-600; + @apply text-blue-600 dark:text-blue-300; &:hover { @apply underline; } diff --git a/web/types/appearance.d.ts b/web/types/appearance.d.ts new file mode 100644 index 000000000..14e0c14c2 --- /dev/null +++ b/web/types/appearance.d.ts @@ -0,0 +1,6 @@ +type PrimaryColor = 'primary-blue' | 'primary-green' | 'primary-purple' + +type UserConfig = { + gettingStartedShow?: boolean + primaryColor?: PrimaryColor +}