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 ( -
+{title}
++ {title} +
+
Importing model ({finishedImportModelCount}/{importingModels.length} )
-{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 = () => {