Merge branch 'dev' into docs-pena-team
This commit is contained in:
commit
22ad9df50b
10
README.md
10
README.md
@ -76,31 +76,31 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute
|
|||||||
<tr style="text-align:center">
|
<tr style="text-align:center">
|
||||||
<td style="text-align:center"><b>Experimental (Nightly Build)</b></td>
|
<td style="text-align:center"><b>Experimental (Nightly Build)</b></td>
|
||||||
<td style="text-align:center">
|
<td style="text-align:center">
|
||||||
<a href='https://delta.jan.ai/latest/jan-win-x64-0.4.7-289.exe'>
|
<a href='https://delta.jan.ai/latest/jan-win-x64-0.4.7-290.exe'>
|
||||||
<img src='./docs/static/img/windows.png' style="height:14px; width: 14px" />
|
<img src='./docs/static/img/windows.png' style="height:14px; width: 14px" />
|
||||||
<b>jan.exe</b>
|
<b>jan.exe</b>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align:center">
|
<td style="text-align:center">
|
||||||
<a href='https://delta.jan.ai/latest/jan-mac-x64-0.4.7-289.dmg'>
|
<a href='https://delta.jan.ai/latest/jan-mac-x64-0.4.7-290.dmg'>
|
||||||
<img src='./docs/static/img/mac.png' style="height:15px; width: 15px" />
|
<img src='./docs/static/img/mac.png' style="height:15px; width: 15px" />
|
||||||
<b>Intel</b>
|
<b>Intel</b>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align:center">
|
<td style="text-align:center">
|
||||||
<a href='https://delta.jan.ai/latest/jan-mac-arm64-0.4.7-289.dmg'>
|
<a href='https://delta.jan.ai/latest/jan-mac-arm64-0.4.7-290.dmg'>
|
||||||
<img src='./docs/static/img/mac.png' style="height:15px; width: 15px" />
|
<img src='./docs/static/img/mac.png' style="height:15px; width: 15px" />
|
||||||
<b>M1/M2</b>
|
<b>M1/M2</b>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align:center">
|
<td style="text-align:center">
|
||||||
<a href='https://delta.jan.ai/latest/jan-linux-amd64-0.4.7-289.deb'>
|
<a href='https://delta.jan.ai/latest/jan-linux-amd64-0.4.7-290.deb'>
|
||||||
<img src='./docs/static/img/linux.png' style="height:14px; width: 14px" />
|
<img src='./docs/static/img/linux.png' style="height:14px; width: 14px" />
|
||||||
<b>jan.deb</b>
|
<b>jan.deb</b>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align:center">
|
<td style="text-align:center">
|
||||||
<a href='https://delta.jan.ai/latest/jan-linux-x86_64-0.4.7-289.AppImage'>
|
<a href='https://delta.jan.ai/latest/jan-linux-x86_64-0.4.7-290.AppImage'>
|
||||||
<img src='./docs/static/img/linux.png' style="height:14px; width: 14px" />
|
<img src='./docs/static/img/linux.png' style="height:14px; width: 14px" />
|
||||||
<b>jan.AppImage</b>
|
<b>jan.AppImage</b>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@ -49,7 +49,7 @@ export enum DownloadEvent {
|
|||||||
|
|
||||||
export enum LocalImportModelEvent {
|
export enum LocalImportModelEvent {
|
||||||
onLocalImportModelUpdate = 'onLocalImportModelUpdate',
|
onLocalImportModelUpdate = 'onLocalImportModelUpdate',
|
||||||
onLocalImportModelError = 'onLocalImportModelError',
|
onLocalImportModelFailed = 'onLocalImportModelFailed',
|
||||||
onLocalImportModelSuccess = 'onLocalImportModelSuccess',
|
onLocalImportModelSuccess = 'onLocalImportModelSuccess',
|
||||||
onLocalImportModelFinished = 'onLocalImportModelFinished',
|
onLocalImportModelFinished = 'onLocalImportModelFinished',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -65,7 +65,7 @@ const joinPath: (paths: string[]) => Promise<string> = (paths) => global.core.ap
|
|||||||
* @param path - The path to retrieve.
|
* @param path - The path to retrieve.
|
||||||
* @returns {Promise<string>} A promise that resolves with the basename.
|
* @returns {Promise<string>} A promise that resolves with the basename.
|
||||||
*/
|
*/
|
||||||
const baseName: (paths: string[]) => Promise<string> = (path) => global.core.api?.baseName(path)
|
const baseName: (paths: string) => Promise<string> = (path) => global.core.api?.baseName(path)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens an external URL in the default web browser.
|
* Opens an external URL in the default web browser.
|
||||||
|
|||||||
@ -19,4 +19,5 @@ export type ImportingModel = {
|
|||||||
status: ImportingModelStatus
|
status: ImportingModelStatus
|
||||||
format: string
|
format: string
|
||||||
percentage?: number
|
percentage?: number
|
||||||
|
error?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,3 +29,18 @@ keywords:
|
|||||||
## Careers
|
## Careers
|
||||||
|
|
||||||
- [Jobs](https://janai.bamboohr.com/careers)
|
- [Jobs](https://janai.bamboohr.com/careers)
|
||||||
|
|
||||||
|
## Newsletter
|
||||||
|
|
||||||
|
<iframe
|
||||||
|
width="100%"
|
||||||
|
height="600px"
|
||||||
|
src="https://c0c7c086.sibforms.com/serve/MUIFAEWm49nC1OONIibGnlV44yxPMw6Fu1Yc8pK7nP3jp7rZ6rvrb5uOmCD8IIhrRj6-h-_AYrw-sz7JNpcUZ8LAAZoUIOjGmSvNWHwoFhxX5lb-38-fxXj933yIdGzEMBZJv4Nu2BqC2A4uThDGmjM-n_DZBV1v_mKbTcVUWVUE7VutWhRqrDr69IWI4SgbuIMACkcTiWX8ZNLw"
|
||||||
|
frameborder="0"
|
||||||
|
scrolling="auto"
|
||||||
|
allowfullscreen
|
||||||
|
style={{
|
||||||
|
margin: 'auto',
|
||||||
|
maxWidth: '100%',
|
||||||
|
}}
|
||||||
|
></iframe>
|
||||||
@ -1,3 +1,95 @@
|
|||||||
---
|
---
|
||||||
title: Wall of Love ❤️
|
title: Wall of Love ❤️
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Twitter
|
||||||
|
|
||||||
|
Check out our amazing users and what they are saying about Jan!
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">I can confirm <a href="https://t.co/Hvrfp0iaf9">https://t.co/Hvrfp0iaf9</a> is awesome 👌</p>— Cristian (@cristianmoreno) <a href="https://twitter.com/cristianmoreno/status/1757504717519749292?ref_src=twsrc%5Etfw">February 13, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8" loading="lazy"></script>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">downloaded this a few weeks ago. amazed by the speed and quality</p>— siddharth (@siddharthd01) <a href="https://twitter.com/siddharthd01/status/1757500111629025788?ref_src=twsrc%5Etfw">February 13, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8" loading="lazy"></script>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Anyone else out there running LLMs on steam deck? <a href="https://twitter.com/janframework?ref_src=twsrc%5Etfw">@janframework</a> bringing nerd dreams to life! <a href="https://t.co/7XpnBmc8MN">pic.twitter.com/7XpnBmc8MN</a></p>— crossdefault (@crossdefault) <a href="https://twitter.com/crossdefault/status/1750801065132384302?ref_src=twsrc%5Etfw">January 26, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8" loading="lazy"></script>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">If you are like me, always wanting your own ChatGPT and have sufficient coding knowledge, you would watch open sourced <a href="https://twitter.com/janframework?ref_src=twsrc%5Etfw">@janframework</a> by <a href="https://twitter.com/0xSage?ref_src=twsrc%5Etfw">@0xSage</a> like a "my-own-ai" hawk<br></br>Still under development, the architecture is really futuristic. The desktop app for Windows, Mac, Linux are… <a href="https://t.co/0HrNquhBsL">pic.twitter.com/0HrNquhBsL</a></p>— Umesh = EG = Educated Guess - NGI doing AI (@trading_indian) <a href="https://twitter.com/trading_indian/status/1745560583548670250?ref_src=twsrc%5Etfw">January 11, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8" loading="lazy"></script>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">came across <a href="https://twitter.com/janframework?ref_src=twsrc%5Etfw">@janframework</a> yesterday and it's my fav native Apple Silicon LLM app yet. Love that I can switch to GPT 4 API and offline LLM models seamlessly. Looks promising! <a href="https://t.co/gyOX9gHbKQ">https://t.co/gyOX9gHbKQ</a></p>— Keith Hawkins (@kph_practice) <a href="https://twitter.com/kph_practice/status/1744729548074459310?ref_src=twsrc%5Etfw">January 9, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8" loading="lazy"></script>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">i just ran some ai models locally on my laptop using @janhq_ and can't believe how easy and cool it is. so, now i can have the same experience as with ChatGPT, but offline and without any data concerns</p>— Sergey Kaplich (@sergey_kaplich) <a href="https://twitter.com/sergey_kaplich/status/1742993414986068423?ref_src=twsrc%5Etfw">January 4, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8" loading="lazy"></script>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<blockquote class="twitter-tweet"><p lang="en" dir="ltr"><a href="https://t.co/scBqJ3kIzj">https://t.co/scBqJ3kIzj</a> Great way to try open source all models, like Mixtral8x7b offline. Love to see</p>— Chubby♨️ (@kimmonismus) <a href="https://twitter.com/kimmonismus/status/1742843063938994469?ref_src=twsrc%5Etfw">January 4, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8" loading="lazy"></script>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Please share your love for Jan on Twitter and tag us [@janframework](https://twitter.com/janframework)! We would love to hear from you!
|
||||||
|
|
||||||
|
## YouTube
|
||||||
|
|
||||||
|
Watch these amazing videos to see how Jan is being used and loved by the community!
|
||||||
|
|
||||||
|
### Run Any Chatbot FREE Locally on Your Computer
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<iframe width="100%" height="600" src="https://www.youtube.com/embed/zkafOIyQM8s" title="Run Any Chatbot FREE Locally on Your Computer" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br></br>
|
||||||
|
|
||||||
|
### Jan AI: Run Open Source LLM 100% Local with OpenAI endpoints
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<iframe width="100%" height="705" src="https://www.youtube.com/embed/9ta2S425Zu8" title="Jan AI: Run Open Source LLM 100% Local with OpenAI endpoints" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br></br>
|
||||||
|
|
||||||
|
### Setup Tutorial on Jan.ai. JAN AI: Run open source LLM on local Windows PC. 100% offline LLM and AI.
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<iframe width="100%" height="705" src="https://www.youtube.com/embed/ZCiEQVOjH5U" title="Setup Tutorial on Jan.ai. JAN AI: Run open source LLM on local Windows PC. 100% offline LLM and AI." frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br></br>
|
||||||
|
|
||||||
|
### Jan.ai: Like Offline ChatGPT on Your Computer 💡
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<iframe width="100%" height="600" src="https://www.youtube.com/embed/ES021_sY6WQ" title="Jan.ai: Like Offline ChatGPT on Your Computer 💡" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br></br>
|
||||||
|
|
||||||
|
### Jan: Bring AI to your Desktop With 100% Offline AI
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<iframe width="100%" height="600" src="https://www.youtube.com/embed/QpMQgJL4AZA" title="Jan: Bring AI to your Desktop With 100% Offline AI" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br></br>
|
||||||
|
|
||||||
|
### AI on Your Local PC: Install JanAI (ChatGPT alternative) for Enhanced Privacy
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<iframe width="100%" height="600" src="https://www.youtube.com/embed/CbJGxNmdWws" title="AI on Your Local PC: Install JanAI (ChatGPT alternative) for Enhanced Privacy" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br></br>
|
||||||
|
|
||||||
|
### Install Jan to Run LLM Offline and Local First
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<iframe width="100%" height="600" src="https://www.youtube.com/embed/7JpzE-_cKo4" title="Install Jan to Run LLM Offline and Local First" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||||
|
</div>
|
||||||
|
|||||||
@ -86,6 +86,10 @@ const menus = [
|
|||||||
path: "https://janai.bamboohr.com/careers",
|
path: "https://janai.bamboohr.com/careers",
|
||||||
external: true,
|
external: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
menu: "Newsletter",
|
||||||
|
path: "/community#newsletter",
|
||||||
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import {
|
|||||||
OptionType,
|
OptionType,
|
||||||
ImportingModel,
|
ImportingModel,
|
||||||
LocalImportModelEvent,
|
LocalImportModelEvent,
|
||||||
|
baseName,
|
||||||
} from '@janhq/core'
|
} from '@janhq/core'
|
||||||
|
|
||||||
import { extractFileName } from './helpers/path'
|
import { extractFileName } from './helpers/path'
|
||||||
@ -488,7 +489,7 @@ export default class JanModelExtension extends ModelExtension {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const binaryFileName = extractFileName(modelBinaryPath, '')
|
const binaryFileName = await baseName(modelBinaryPath)
|
||||||
|
|
||||||
const model: Model = {
|
const model: Model = {
|
||||||
...defaultModel,
|
...defaultModel,
|
||||||
@ -555,7 +556,7 @@ export default class JanModelExtension extends ModelExtension {
|
|||||||
model: ImportingModel,
|
model: ImportingModel,
|
||||||
optionType: OptionType
|
optionType: OptionType
|
||||||
): Promise<Model> {
|
): Promise<Model> {
|
||||||
const binaryName = extractFileName(model.path, '').replace(/\s/g, '')
|
const binaryName = (await baseName(model.path)).replace(/\s/g, '')
|
||||||
|
|
||||||
let modelFolderName = binaryName
|
let modelFolderName = binaryName
|
||||||
if (binaryName.endsWith(JanModelExtension._supportedModelFormat)) {
|
if (binaryName.endsWith(JanModelExtension._supportedModelFormat)) {
|
||||||
@ -568,7 +569,7 @@ export default class JanModelExtension extends ModelExtension {
|
|||||||
const modelFolderPath = await this.getModelFolderName(modelFolderName)
|
const modelFolderPath = await this.getModelFolderName(modelFolderName)
|
||||||
await fs.mkdirSync(modelFolderPath)
|
await fs.mkdirSync(modelFolderPath)
|
||||||
|
|
||||||
const uniqueFolderName = modelFolderPath.split('/').pop()
|
const uniqueFolderName = await baseName(modelFolderPath)
|
||||||
const modelBinaryFile = binaryName.endsWith(
|
const modelBinaryFile = binaryName.endsWith(
|
||||||
JanModelExtension._supportedModelFormat
|
JanModelExtension._supportedModelFormat
|
||||||
)
|
)
|
||||||
@ -637,14 +638,21 @@ export default class JanModelExtension extends ModelExtension {
|
|||||||
|
|
||||||
for (const model of models) {
|
for (const model of models) {
|
||||||
events.emit(LocalImportModelEvent.onLocalImportModelUpdate, model)
|
events.emit(LocalImportModelEvent.onLocalImportModelUpdate, model)
|
||||||
const importedModel = await this.importModel(model, optionType)
|
try {
|
||||||
|
const importedModel = await this.importModel(model, optionType)
|
||||||
events.emit(LocalImportModelEvent.onLocalImportModelSuccess, {
|
events.emit(LocalImportModelEvent.onLocalImportModelSuccess, {
|
||||||
...model,
|
...model,
|
||||||
modelId: importedModel.id,
|
modelId: importedModel.id,
|
||||||
})
|
})
|
||||||
importedModels.push(importedModel)
|
importedModels.push(importedModel)
|
||||||
|
} catch (err) {
|
||||||
|
events.emit(LocalImportModelEvent.onLocalImportModelFailed, {
|
||||||
|
...model,
|
||||||
|
error: err,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
events.emit(
|
events.emit(
|
||||||
LocalImportModelEvent.onLocalImportModelFinished,
|
LocalImportModelEvent.onLocalImportModelFinished,
|
||||||
importedModels
|
importedModels
|
||||||
|
|||||||
@ -5,11 +5,11 @@
|
|||||||
@apply disabled:pointer-events-none disabled:bg-zinc-100 disabled:text-zinc-400;
|
@apply disabled:pointer-events-none disabled:bg-zinc-100 disabled:text-zinc-400;
|
||||||
|
|
||||||
&-primary {
|
&-primary {
|
||||||
@apply bg-primary hover:bg-primary/90 text-white;
|
@apply bg-blue-600 text-white hover:bg-blue-600/90;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-secondary-blue {
|
&-secondary-blue {
|
||||||
@apply bg-blue-200 text-blue-600 hover:bg-blue-300/50 dark:hover:bg-blue-200/80;
|
@apply bg-blue-200 text-blue-600 hover:bg-blue-300/50;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-danger {
|
&-danger {
|
||||||
@ -17,7 +17,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&-secondary-danger {
|
&-secondary-danger {
|
||||||
@apply bg-red-200 text-red-600 hover:bg-red-300/50 dark:hover:bg-red-200/80;
|
@apply bg-red-200 text-red-600 hover:bg-red-300/50;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-outline {
|
&-outline {
|
||||||
@ -66,7 +66,7 @@
|
|||||||
[type='reset'],
|
[type='reset'],
|
||||||
[type='submit'] {
|
[type='submit'] {
|
||||||
&.btn-primary {
|
&.btn-primary {
|
||||||
@apply bg-primary hover:bg-primary/90;
|
@apply bg-blue-600 hover:bg-blue-600/90;
|
||||||
@apply disabled:pointer-events-none disabled:bg-zinc-100 disabled:text-zinc-400;
|
@apply disabled:pointer-events-none disabled:bg-zinc-100 disabled:text-zinc-400;
|
||||||
}
|
}
|
||||||
&.btn-secondary {
|
&.btn-secondary {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
.checkbox {
|
.checkbox {
|
||||||
@apply border-border data-[state=checked]:bg-primary h-5 w-5 flex-shrink-0 rounded-md border data-[state=checked]:text-white;
|
@apply border-border h-5 w-5 flex-shrink-0 rounded-md border data-[state=checked]:bg-blue-600 data-[state=checked]:text-white;
|
||||||
|
|
||||||
&--icon {
|
&--icon {
|
||||||
@apply h-4 w-4;
|
@apply h-4 w-4;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
.input {
|
.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 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 disabled:dark:bg-zinc-800 disabled:dark:text-zinc-600;
|
@apply disabled:text-muted-foreground disabled:cursor-not-allowed disabled:bg-zinc-100;
|
||||||
@apply focus-within:outline-none focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1;
|
@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;
|
@apply file:border-0 file:bg-transparent file:font-medium;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
.progress {
|
.progress {
|
||||||
@apply bg-secondary relative h-4 w-full overflow-hidden rounded-full;
|
@apply relative h-4 w-full overflow-hidden rounded-full bg-gray-100;
|
||||||
|
|
||||||
&-indicator {
|
&-indicator {
|
||||||
@apply bg-primary h-full w-full flex-1 transition-all;
|
@apply h-full w-full flex-1 bg-blue-600 transition-all;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
.select {
|
.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 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 disabled:dark:bg-zinc-800 disabled:dark:text-zinc-600;
|
@apply disabled:text-muted-foreground disabled:cursor-not-allowed disabled:bg-zinc-100;
|
||||||
@apply focus-within:outline-none focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1;
|
@apply focus-within:outline-none focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1;
|
||||||
|
|
||||||
&-caret {
|
&-caret {
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
@apply relative flex w-full touch-none select-none items-center;
|
@apply relative flex w-full touch-none select-none items-center;
|
||||||
|
|
||||||
&-track {
|
&-track {
|
||||||
@apply relative h-1.5 w-full grow overflow-hidden rounded-full bg-gray-200 dark:bg-gray-800;
|
@apply relative h-1.5 w-full grow overflow-hidden rounded-full bg-gray-200;
|
||||||
[data-disabled] {
|
[data-disabled] {
|
||||||
@apply cursor-not-allowed opacity-50;
|
@apply cursor-not-allowed opacity-50;
|
||||||
}
|
}
|
||||||
@ -13,6 +13,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&-thumb {
|
&-thumb {
|
||||||
@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;
|
@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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
.switch {
|
.switch {
|
||||||
@apply inline-flex h-[20px] w-[36px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent;
|
@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 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2;
|
||||||
@apply data-[state=checked]:bg-primary data-[state=unchecked]:bg-input;
|
@apply data-[state=unchecked]:bg-input data-[state=checked]:bg-blue-600;
|
||||||
@apply disabled:cursor-not-allowed disabled:opacity-50;
|
@apply disabled:cursor-not-allowed disabled:opacity-50;
|
||||||
|
|
||||||
&-toggle {
|
&-toggle {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
.tooltip {
|
.tooltip {
|
||||||
@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;
|
@apply z-50 overflow-hidden rounded-md bg-gray-950 px-2 py-1.5 text-xs font-medium text-gray-200 shadow-md;
|
||||||
&-arrow {
|
&-arrow {
|
||||||
@apply dark:fill-input fill-gray-950;
|
@apply fill-gray-950;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export const metadata: Metadata = {
|
|||||||
export default function RootLayout({ children }: PropsWithChildren) {
|
export default function RootLayout({ children }: PropsWithChildren) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en" suppressHydrationWarning>
|
||||||
<body className="bg-white font-sans text-sm antialiased dark:bg-background">
|
<body className="bg-white font-sans text-sm antialiased">
|
||||||
<div className="title-bar" />
|
<div className="title-bar" />
|
||||||
<Providers>{children}</Providers>
|
<Providers>{children}</Providers>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -45,7 +45,7 @@ export default function CardSidebar({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'flex w-full flex-col border-t border-border bg-zinc-100 dark:bg-zinc-900',
|
'flex w-full flex-col border-t border-border bg-zinc-100',
|
||||||
asChild ? 'rounded-lg border' : 'border-t'
|
asChild ? 'rounded-lg border' : 'border-t'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -61,7 +61,7 @@ export default function CardSidebar({
|
|||||||
if (!children) return
|
if (!children) return
|
||||||
setShow(!show)
|
setShow(!show)
|
||||||
}}
|
}}
|
||||||
className="flex w-full flex-1 items-center space-x-2 rounded-lg bg-zinc-100 py-2 pr-2 dark:bg-zinc-900"
|
className="flex w-full flex-1 items-center space-x-2 rounded-lg bg-zinc-100 py-2 pr-2"
|
||||||
>
|
>
|
||||||
<ChevronDownIcon
|
<ChevronDownIcon
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
@ -79,7 +79,7 @@ export default function CardSidebar({
|
|||||||
{!hideMoreVerticalAction && (
|
{!hideMoreVerticalAction && (
|
||||||
<div
|
<div
|
||||||
ref={setToggle}
|
ref={setToggle}
|
||||||
className="cursor-pointer rounded-lg bg-zinc-100 p-2 px-3 dark:bg-zinc-900"
|
className="cursor-pointer rounded-lg bg-zinc-100 p-2 px-3"
|
||||||
onClick={() => setMore(!more)}
|
onClick={() => setMore(!more)}
|
||||||
>
|
>
|
||||||
<MoreVerticalIcon className="h-5 w-5" />
|
<MoreVerticalIcon className="h-5 w-5" />
|
||||||
@ -114,7 +114,7 @@ export default function CardSidebar({
|
|||||||
<>
|
<>
|
||||||
{title === 'Model' ? (
|
{title === 'Model' ? (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="font-medium text-black dark:text-muted-foreground">
|
<span className="font-medium text-black">
|
||||||
{openFileTitle()}
|
{openFileTitle()}
|
||||||
</span>
|
</span>
|
||||||
<span className="mt-1 text-muted-foreground">
|
<span className="mt-1 text-muted-foreground">
|
||||||
@ -122,7 +122,7 @@ export default function CardSidebar({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-bold text-black dark:text-muted-foreground">
|
<span className="text-bold text-black">
|
||||||
{openFileTitle()}
|
{openFileTitle()}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@ -141,7 +141,7 @@ export default function CardSidebar({
|
|||||||
/>
|
/>
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="line-clamp-1 font-medium text-black dark:text-muted-foreground">
|
<span className="line-clamp-1 font-medium text-black">
|
||||||
Edit Global Defaults for{' '}
|
Edit Global Defaults for{' '}
|
||||||
<span
|
<span
|
||||||
className="font-bold"
|
className="font-bold"
|
||||||
@ -175,7 +175,7 @@ export default function CardSidebar({
|
|||||||
{show && (
|
{show && (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'flex flex-col gap-2 bg-white px-2 dark:bg-background',
|
'flex flex-col gap-2 bg-white px-2',
|
||||||
asChild && 'rounded-b-lg'
|
asChild && 'rounded-b-lg'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -34,12 +34,10 @@ const Checkbox: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<div className="mb-1 flex items-center gap-x-2">
|
<div className="mb-1 flex items-center gap-x-2">
|
||||||
<p className="text-sm font-semibold text-zinc-500 dark:text-gray-300">
|
<p className="text-sm font-semibold text-zinc-500">{title}</p>
|
||||||
{title}
|
|
||||||
</p>
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<InfoIcon size={16} className="flex-shrink-0 dark:text-gray-500" />
|
<InfoIcon size={16} className="flex-shrink-0" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPortal>
|
<TooltipPortal>
|
||||||
<TooltipContent side="top" className="max-w-[240px]">
|
<TooltipContent side="top" className="max-w-[240px]">
|
||||||
|
|||||||
@ -203,15 +203,14 @@ const DropdownListSidebar = ({
|
|||||||
isTabActive === 1 && '[&_.select-scroll-down-button]:hidden'
|
isTabActive === 1 && '[&_.select-scroll-down-button]:hidden'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="relative px-2 py-2 dark:bg-secondary/50">
|
<div className="relative px-2 py-2">
|
||||||
<ul className="inline-flex w-full space-x-2 rounded-lg bg-zinc-100 px-1 dark:bg-secondary">
|
<ul className="inline-flex w-full space-x-2 rounded-lg bg-zinc-100 px-1">
|
||||||
{engineOptions.map((name, i) => {
|
{engineOptions.map((name, i) => {
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'relative my-1 flex w-full cursor-pointer items-center justify-center space-x-2 px-2 py-2',
|
'relative my-1 flex w-full cursor-pointer items-center justify-center space-x-2 px-2 py-2',
|
||||||
isTabActive === i &&
|
isTabActive === i && 'rounded-md bg-background'
|
||||||
'rounded-md bg-background dark:bg-white'
|
|
||||||
)}
|
)}
|
||||||
key={i}
|
key={i}
|
||||||
onClick={() => setIsTabActive(i)}
|
onClick={() => setIsTabActive(i)}
|
||||||
@ -230,8 +229,7 @@ const DropdownListSidebar = ({
|
|||||||
<span
|
<span
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'relative z-50 font-medium text-muted-foreground',
|
'relative z-50 font-medium text-muted-foreground',
|
||||||
isTabActive === i &&
|
isTabActive === i && 'font-bold text-foreground'
|
||||||
'font-bold text-foreground dark:text-black'
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
|
|||||||
@ -60,7 +60,7 @@ const GPUDriverPrompt: React.FC = () => {
|
|||||||
id="default-checkbox"
|
id="default-checkbox"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
onChange={onDoNotShowAgainChange}
|
onChange={onDoNotShowAgainChange}
|
||||||
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"
|
className="h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-2 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
<span>Don't show again</span>
|
<span>Don't show again</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -47,7 +47,7 @@ export default function DownloadingState() {
|
|||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
<span
|
<span
|
||||||
className="absolute left-0 h-full rounded-md rounded-l-md bg-primary/20"
|
className="absolute left-0 h-full rounded-md rounded-l-md bg-blue-500/20"
|
||||||
style={{
|
style={{
|
||||||
width: `${totalPercentage}%`,
|
width: `${totalPercentage}%`,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -48,7 +48,7 @@ const ImportingModelState: React.FC = () => {
|
|||||||
className="h-2 w-24"
|
className="h-2 w-24"
|
||||||
value={transferredSize / totalSize}
|
value={transferredSize / totalSize}
|
||||||
/>
|
/>
|
||||||
<span className="text-xs font-bold text-primary">
|
<span className="text-xs font-bold text-blue-600">
|
||||||
{progress.toFixed(2)}%
|
{progress.toFixed(2)}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -140,7 +140,7 @@ const SystemMonitor = () => {
|
|||||||
{gpus.length > 0 && (
|
{gpus.length > 0 && (
|
||||||
<div className="mb-4 border-b border-border pb-4 last:border-none">
|
<div className="mb-4 border-b border-border pb-4 last:border-none">
|
||||||
{gpus.map((gpu, index) => (
|
{gpus.map((gpu, index) => (
|
||||||
<div key={index} className="mt-4 flex flex-col gap-2">
|
<div key={index} className="mt-4 flex flex-col gap-x-2">
|
||||||
<div className="flex w-full items-start justify-between">
|
<div className="flex w-full items-start justify-between">
|
||||||
<span className="line-clamp-1 w-1/2 font-bold">
|
<span className="line-clamp-1 w-1/2 font-bold">
|
||||||
{gpu.name}
|
{gpu.name}
|
||||||
|
|||||||
@ -45,7 +45,7 @@ export default function RibbonNav() {
|
|||||||
size={20}
|
size={20}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'flex-shrink-0 text-muted-foreground',
|
'flex-shrink-0 text-muted-foreground',
|
||||||
serverEnabled && 'text-gray-300 dark:text-gray-700'
|
serverEnabled && 'text-gray-300'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@ -114,7 +114,7 @@ export default function RibbonNav() {
|
|||||||
</div>
|
</div>
|
||||||
{isActive && (
|
{isActive && (
|
||||||
<m.div
|
<m.div
|
||||||
className="absolute inset-0 left-0 h-full w-full rounded-md bg-gray-200 dark:bg-secondary"
|
className="absolute inset-0 left-0 h-full w-full rounded-md bg-gray-200"
|
||||||
layoutId="active-state-primary"
|
layoutId="active-state-primary"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -166,7 +166,7 @@ export default function RibbonNav() {
|
|||||||
</div>
|
</div>
|
||||||
{isActive && (
|
{isActive && (
|
||||||
<m.div
|
<m.div
|
||||||
className="absolute inset-0 left-0 h-full w-full rounded-md bg-gray-200 dark:bg-secondary"
|
className="absolute inset-0 left-0 h-full w-full rounded-md bg-gray-200"
|
||||||
layoutId="active-state-secondary"
|
layoutId="active-state-secondary"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -159,7 +159,7 @@ const TopBar = () => {
|
|||||||
size={16}
|
size={16}
|
||||||
className="text-muted-foreground"
|
className="text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
<span className="font-medium text-black dark:text-muted-foreground">
|
<span className="font-medium text-black ">
|
||||||
{openFileTitle()}
|
{openFileTitle()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -175,7 +175,7 @@ const TopBar = () => {
|
|||||||
className="mt-0.5 flex-shrink-0 text-muted-foreground"
|
className="mt-0.5 flex-shrink-0 text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="font-medium text-black dark:text-muted-foreground">
|
<span className="font-medium text-black ">
|
||||||
Edit Threads Settings
|
Edit Threads Settings
|
||||||
</span>
|
</span>
|
||||||
<span className="mt-1 text-muted-foreground">
|
<span className="mt-1 text-muted-foreground">
|
||||||
@ -204,7 +204,7 @@ const TopBar = () => {
|
|||||||
className="text-muted-foreground"
|
className="text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="font-medium text-black dark:text-muted-foreground">
|
<span className="font-medium text-black ">
|
||||||
{openFileTitle()}
|
{openFileTitle()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -7,12 +7,12 @@ export default function Loader({ description }: Props) {
|
|||||||
<div className="space-y-16">
|
<div className="space-y-16">
|
||||||
<div className="loader">
|
<div className="loader">
|
||||||
<div className="loader-inner">
|
<div className="loader-inner">
|
||||||
<label className="h-2 w-2 rounded-full bg-primary" />
|
<label className="h-2 w-2 rounded-full bg-blue-500" />
|
||||||
<label className="h-2 w-2 rounded-full bg-primary" />
|
<label className="h-2 w-2 rounded-full bg-blue-500" />
|
||||||
<label className="h-2 w-2 rounded-full bg-primary" />
|
<label className="h-2 w-2 rounded-full bg-blue-500" />
|
||||||
<label className="h-2 w-2 rounded-full bg-primary" />
|
<label className="h-2 w-2 rounded-full bg-blue-500" />
|
||||||
<label className="h-2 w-2 rounded-full bg-primary" />
|
<label className="h-2 w-2 rounded-full bg-blue-500" />
|
||||||
<label className="h-2 w-2 rounded-full bg-primary" />
|
<label className="h-2 w-2 rounded-full bg-blue-500" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="font-medium text-muted-foreground">{description}</p>
|
<p className="font-medium text-muted-foreground">{description}</p>
|
||||||
|
|||||||
@ -28,7 +28,7 @@ const AppLogs = () => {
|
|||||||
<div className="absolute -top-11 right-2">
|
<div className="absolute -top-11 right-2">
|
||||||
<Button
|
<Button
|
||||||
themes="outline"
|
themes="outline"
|
||||||
className="bg-white dark:bg-secondary/50"
|
className="bg-white"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
clipboard.copy(logs.slice(-50) ?? '')
|
clipboard.copy(logs.slice(-50) ?? '')
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -16,7 +16,7 @@ const DeviceSpecs = () => {
|
|||||||
<div className="absolute -top-11 right-2">
|
<div className="absolute -top-11 right-2">
|
||||||
<Button
|
<Button
|
||||||
themes="outline"
|
themes="outline"
|
||||||
className="bg-white dark:bg-secondary/50"
|
className="bg-white"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
clipboard.copy(userAgent ?? '')
|
clipboard.copy(userAgent ?? '')
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -38,7 +38,7 @@ const ModalTroubleShooting: React.FC = () => {
|
|||||||
<a
|
<a
|
||||||
href="https://jan.ai/guides/troubleshooting"
|
href="https://jan.ai/guides/troubleshooting"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-blue-600 hover:underline dark:text-blue-300"
|
className="text-blue-600 hover:underline"
|
||||||
>
|
>
|
||||||
troubleshooting guide
|
troubleshooting guide
|
||||||
</a>
|
</a>
|
||||||
@ -65,7 +65,7 @@ const ModalTroubleShooting: React.FC = () => {
|
|||||||
<a
|
<a
|
||||||
href="https://discord.gg/AsJ8krTT3N"
|
href="https://discord.gg/AsJ8krTT3N"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-blue-600 hover:underline dark:text-blue-300"
|
className="text-blue-600 hover:underline"
|
||||||
>
|
>
|
||||||
Discord
|
Discord
|
||||||
</a>
|
</a>
|
||||||
@ -77,8 +77,8 @@ const ModalTroubleShooting: React.FC = () => {
|
|||||||
|
|
||||||
<div className="flex flex-col pt-4">
|
<div className="flex flex-col pt-4">
|
||||||
{/* TODO @faisal replace this once we have better tabs component UI */}
|
{/* TODO @faisal replace this once we have better tabs component UI */}
|
||||||
<div className="relative bg-zinc-100 px-4 py-2 dark:bg-secondary/50">
|
<div className="relative bg-zinc-100 px-4 py-2">
|
||||||
<ul className="inline-flex space-x-2 rounded-lg bg-zinc-200 px-1 dark:bg-secondary">
|
<ul className="inline-flex space-x-2 rounded-lg bg-zinc-200 px-1">
|
||||||
{logOption.map((name, i) => {
|
{logOption.map((name, i) => {
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
@ -89,15 +89,14 @@ const ModalTroubleShooting: React.FC = () => {
|
|||||||
<span
|
<span
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'relative z-50 font-medium text-muted-foreground',
|
'relative z-50 font-medium text-muted-foreground',
|
||||||
isTabActive === i &&
|
isTabActive === i && 'font-bold text-foreground'
|
||||||
'font-bold text-foreground dark:text-black'
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
</span>
|
</span>
|
||||||
{isTabActive === i && (
|
{isTabActive === i && (
|
||||||
<m.div
|
<m.div
|
||||||
className="absolute left-0 top-1 h-[calc(100%-8px)] w-full rounded-md bg-background dark:bg-white"
|
className="absolute left-0 top-1 h-[calc(100%-8px)] w-full rounded-md bg-background"
|
||||||
layoutId="log-state-active"
|
layoutId="log-state-active"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -30,12 +30,10 @@ const ModelConfigInput: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="mb-2 flex items-center gap-x-2">
|
<div className="mb-2 flex items-center gap-x-2">
|
||||||
<p className="text-sm font-semibold text-zinc-500 dark:text-gray-300">
|
<p className="text-sm font-semibold text-zinc-500">{title}</p>
|
||||||
{title}
|
|
||||||
</p>
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<InfoIcon size={16} className="flex-shrink-0 dark:text-gray-500" />
|
<InfoIcon size={16} className="flex-shrink-0" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPortal>
|
<TooltipPortal>
|
||||||
<TooltipContent side="top" className="max-w-[240px]">
|
<TooltipContent side="top" className="max-w-[240px]">
|
||||||
|
|||||||
@ -33,7 +33,7 @@ const OpenAiKeyInput: React.FC = () => {
|
|||||||
<div className="my-4">
|
<div className="my-4">
|
||||||
<label
|
<label
|
||||||
id="thread-title"
|
id="thread-title"
|
||||||
className="mb-2 inline-block font-bold text-gray-600 dark:text-gray-300"
|
className="mb-2 inline-block font-bold text-gray-600"
|
||||||
>
|
>
|
||||||
API Key
|
API Key
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { useSetAtom } from 'jotai'
|
|||||||
import { snackbar } from '../Toast'
|
import { snackbar } from '../Toast'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
setImportingModelErrorAtom,
|
||||||
setImportingModelSuccessAtom,
|
setImportingModelSuccessAtom,
|
||||||
updateImportingModelProgressAtom,
|
updateImportingModelProgressAtom,
|
||||||
} from '@/helpers/atoms/Model.atom'
|
} from '@/helpers/atoms/Model.atom'
|
||||||
@ -21,6 +22,7 @@ const ModelImportListener = ({ children }: PropsWithChildren) => {
|
|||||||
updateImportingModelProgressAtom
|
updateImportingModelProgressAtom
|
||||||
)
|
)
|
||||||
const setImportingModelSuccess = useSetAtom(setImportingModelSuccessAtom)
|
const setImportingModelSuccess = useSetAtom(setImportingModelSuccessAtom)
|
||||||
|
const setImportingModelFailed = useSetAtom(setImportingModelErrorAtom)
|
||||||
|
|
||||||
const onImportModelUpdate = useCallback(
|
const onImportModelUpdate = useCallback(
|
||||||
async (state: ImportingModel) => {
|
async (state: ImportingModel) => {
|
||||||
@ -30,6 +32,14 @@ const ModelImportListener = ({ children }: PropsWithChildren) => {
|
|||||||
[updateImportingModelProgress]
|
[updateImportingModelProgress]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const onImportModelFailed = useCallback(
|
||||||
|
async (state: ImportingModel) => {
|
||||||
|
if (!state.importId) return
|
||||||
|
setImportingModelFailed(state.importId, state.error ?? '')
|
||||||
|
},
|
||||||
|
[setImportingModelFailed]
|
||||||
|
)
|
||||||
|
|
||||||
const onImportModelSuccess = useCallback(
|
const onImportModelSuccess = useCallback(
|
||||||
(state: ImportingModel) => {
|
(state: ImportingModel) => {
|
||||||
if (!state.modelId) return
|
if (!state.modelId) return
|
||||||
@ -62,6 +72,10 @@ const ModelImportListener = ({ children }: PropsWithChildren) => {
|
|||||||
LocalImportModelEvent.onLocalImportModelFinished,
|
LocalImportModelEvent.onLocalImportModelFinished,
|
||||||
onImportModelFinished
|
onImportModelFinished
|
||||||
)
|
)
|
||||||
|
events.on(
|
||||||
|
LocalImportModelEvent.onLocalImportModelFailed,
|
||||||
|
onImportModelFailed
|
||||||
|
)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
console.debug('ModelImportListener: unregistering event listeners...')
|
console.debug('ModelImportListener: unregistering event listeners...')
|
||||||
@ -77,8 +91,17 @@ const ModelImportListener = ({ children }: PropsWithChildren) => {
|
|||||||
LocalImportModelEvent.onLocalImportModelFinished,
|
LocalImportModelEvent.onLocalImportModelFinished,
|
||||||
onImportModelFinished
|
onImportModelFinished
|
||||||
)
|
)
|
||||||
|
events.off(
|
||||||
|
LocalImportModelEvent.onLocalImportModelFailed,
|
||||||
|
onImportModelFailed
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}, [onImportModelUpdate, onImportModelSuccess, onImportModelFinished])
|
}, [
|
||||||
|
onImportModelUpdate,
|
||||||
|
onImportModelSuccess,
|
||||||
|
onImportModelFinished,
|
||||||
|
onImportModelFailed,
|
||||||
|
])
|
||||||
|
|
||||||
return <Fragment>{children}</Fragment>
|
return <Fragment>{children}</Fragment>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,17 +6,9 @@ import { ThemeProvider } from 'next-themes'
|
|||||||
|
|
||||||
import { motion as m } from 'framer-motion'
|
import { motion as m } from 'framer-motion'
|
||||||
|
|
||||||
import { useBodyClass } from '@/hooks/useBodyClass'
|
|
||||||
|
|
||||||
import { useUserConfigs } from '@/hooks/useUserConfigs'
|
|
||||||
|
|
||||||
export default function ThemeWrapper({ children }: PropsWithChildren) {
|
export default function ThemeWrapper({ children }: PropsWithChildren) {
|
||||||
const [config] = useUserConfigs()
|
|
||||||
|
|
||||||
useBodyClass(config.primaryColor || 'primary-yellow')
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider attribute="class" enableSystem>
|
<ThemeProvider attribute="class" forcedTheme="light">
|
||||||
<m.div
|
<m.div
|
||||||
initial={{ opacity: 0, y: -10 }}
|
initial={{ opacity: 0, y: -10 }}
|
||||||
animate={{
|
animate={{
|
||||||
|
|||||||
@ -57,7 +57,7 @@ const ServerLogs = (props: ServerLogsProps) => {
|
|||||||
<div className="absolute -top-11 right-2">
|
<div className="absolute -top-11 right-2">
|
||||||
<Button
|
<Button
|
||||||
themes="outline"
|
themes="outline"
|
||||||
className="bg-white dark:bg-secondary/50"
|
className="bg-white"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
clipboard.copy(logs.slice(-100) ?? '')
|
clipboard.copy(logs.slice(-100) ?? '')
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -42,12 +42,10 @@ const SliderRightPanel: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="mb-3 flex items-center gap-x-2">
|
<div className="mb-3 flex items-center gap-x-2">
|
||||||
<p className="text-sm font-semibold text-zinc-500 dark:text-gray-300">
|
<p className="text-sm font-semibold text-zinc-500">{title}</p>
|
||||||
{title}
|
|
||||||
</p>
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<InfoIcon size={16} className="flex-shrink-0 dark:text-gray-500" />
|
<InfoIcon size={16} className="flex-shrink-0" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPortal>
|
<TooltipPortal>
|
||||||
<TooltipContent side="top" className="max-w-[240px]">
|
<TooltipContent side="top" className="max-w-[240px]">
|
||||||
|
|||||||
@ -108,11 +108,11 @@ export function toaster(props: Props) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'unset-drag dark:bg-zinc-white relative flex animate-enter items-center gap-x-4 rounded-lg bg-foreground px-4 py-2 text-white dark:border dark:border-border',
|
'unset-drag relative flex animate-enter items-center gap-x-4 rounded-lg bg-foreground px-4 py-2 text-white',
|
||||||
t.visible ? 'animate-enter' : 'animate-leave'
|
t.visible ? 'animate-enter' : 'animate-leave'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-x-3 dark:text-black">
|
<div className="flex items-start gap-x-3">
|
||||||
<div className="mt-1">{renderIcon(type)}</div>
|
<div className="mt-1">{renderIcon(type)}</div>
|
||||||
<div className="pr-4">
|
<div className="pr-4">
|
||||||
<h1 className="font-bold">{title}</h1>
|
<h1 className="font-bold">{title}</h1>
|
||||||
@ -120,7 +120,7 @@ export function toaster(props: Props) {
|
|||||||
</div>
|
</div>
|
||||||
<XIcon
|
<XIcon
|
||||||
size={24}
|
size={24}
|
||||||
className="absolute right-2 top-2 w-4 cursor-pointer dark:text-black"
|
className="absolute right-2 top-2 w-4 cursor-pointer"
|
||||||
onClick={() => toast.dismiss(t.id)}
|
onClick={() => toast.dismiss(t.id)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -138,16 +138,16 @@ export function snackbar(props: Props) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'unset-drag dark:bg-zinc-white relative bottom-2 flex animate-enter items-center gap-x-4 rounded-lg bg-foreground px-4 py-2 text-white dark:border dark:border-border',
|
'unset-drag relative bottom-2 flex animate-enter items-center gap-x-4 rounded-lg bg-foreground px-4 py-2 text-white',
|
||||||
t.visible ? 'animate-enter' : 'animate-leave'
|
t.visible ? 'animate-enter' : 'animate-leave'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-x-3 dark:text-black">
|
<div className="flex items-start gap-x-3">
|
||||||
<div>{renderIcon(type)}</div>
|
<div>{renderIcon(type)}</div>
|
||||||
<p className="pr-4">{description}</p>
|
<p className="pr-4">{description}</p>
|
||||||
<XIcon
|
<XIcon
|
||||||
size={24}
|
size={24}
|
||||||
className="absolute right-2 top-1/2 w-4 -translate-y-1/2 cursor-pointer dark:text-black"
|
className="absolute right-2 top-1/2 w-4 -translate-y-1/2 cursor-pointer"
|
||||||
onClick={() => toast.dismiss(t.id)}
|
onClick={() => toast.dismiss(t.id)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -67,6 +67,24 @@ export const updateImportingModelProgressAtom = atom(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const setImportingModelErrorAtom = atom(
|
||||||
|
null,
|
||||||
|
(get, set, importId: string, error: string) => {
|
||||||
|
const model = get(importingModelsAtom).find((x) => x.importId === importId)
|
||||||
|
if (!model) return
|
||||||
|
const newModel: ImportingModel = {
|
||||||
|
...model,
|
||||||
|
status: 'FAILED',
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(`Importing model ${model} failed`, error)
|
||||||
|
const newList = get(importingModelsAtom).map((m) =>
|
||||||
|
m.importId === importId ? newModel : m
|
||||||
|
)
|
||||||
|
set(importingModelsAtom, newList)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
export const setImportingModelSuccessAtom = atom(
|
export const setImportingModelSuccessAtom = atom(
|
||||||
null,
|
null,
|
||||||
(get, set, importId: string, modelId: string) => {
|
(get, set, importId: string, modelId: string) => {
|
||||||
|
|||||||
55
web/hooks/useDropModelBinaries.ts
Normal file
55
web/hooks/useDropModelBinaries.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { useCallback } from 'react'
|
||||||
|
|
||||||
|
import { ImportingModel } from '@janhq/core'
|
||||||
|
import { useSetAtom } from 'jotai'
|
||||||
|
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
|
import { snackbar } from '@/containers/Toast'
|
||||||
|
|
||||||
|
import { getFileInfoFromFile } from '@/utils/file'
|
||||||
|
|
||||||
|
import { setImportModelStageAtom } from './useImportModel'
|
||||||
|
|
||||||
|
import { importingModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||||
|
|
||||||
|
export default function useDropModelBinaries() {
|
||||||
|
const setImportingModels = useSetAtom(importingModelsAtom)
|
||||||
|
const setImportModelStage = useSetAtom(setImportModelStageAtom)
|
||||||
|
|
||||||
|
const onDropModels = useCallback(
|
||||||
|
async (acceptedFiles: File[]) => {
|
||||||
|
const files = await getFileInfoFromFile(acceptedFiles)
|
||||||
|
|
||||||
|
const unsupportedFiles = files.filter(
|
||||||
|
(file) => !file.path.endsWith('.gguf')
|
||||||
|
)
|
||||||
|
const supportedFiles = files.filter((file) => file.path.endsWith('.gguf'))
|
||||||
|
|
||||||
|
const importingModels: ImportingModel[] = supportedFiles.map((file) => ({
|
||||||
|
importId: uuidv4(),
|
||||||
|
modelId: undefined,
|
||||||
|
name: file.name.replace('.gguf', ''),
|
||||||
|
description: '',
|
||||||
|
path: file.path,
|
||||||
|
tags: [],
|
||||||
|
size: file.size,
|
||||||
|
status: 'PREPARING',
|
||||||
|
format: 'gguf',
|
||||||
|
}))
|
||||||
|
if (unsupportedFiles.length > 0) {
|
||||||
|
snackbar({
|
||||||
|
description: `File has to be a .gguf file`,
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (importingModels.length === 0) return
|
||||||
|
|
||||||
|
setImportingModels(importingModels)
|
||||||
|
setImportModelStage('MODEL_SELECTED')
|
||||||
|
},
|
||||||
|
[setImportModelStage, setImportingModels]
|
||||||
|
)
|
||||||
|
|
||||||
|
return { onDropModels }
|
||||||
|
}
|
||||||
@ -1,11 +0,0 @@
|
|||||||
import { useAtom } from 'jotai'
|
|
||||||
import { atomWithStorage } from 'jotai/utils'
|
|
||||||
|
|
||||||
export const userConfigs = atomWithStorage<UserConfig>('config', {
|
|
||||||
gettingStartedShow: true,
|
|
||||||
primaryColor: 'primary-blue',
|
|
||||||
})
|
|
||||||
|
|
||||||
export function useUserConfigs() {
|
|
||||||
return useAtom(userConfigs)
|
|
||||||
}
|
|
||||||
@ -34,9 +34,7 @@ const CleanThreadModal: React.FC<Props> = ({ threadId }) => {
|
|||||||
<ModalTrigger asChild onClick={(e) => e.stopPropagation()}>
|
<ModalTrigger asChild onClick={(e) => e.stopPropagation()}>
|
||||||
<div className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-secondary">
|
<div className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-secondary">
|
||||||
<Paintbrush size={16} className="text-muted-foreground" />
|
<Paintbrush size={16} className="text-muted-foreground" />
|
||||||
<span className="text-bold text-black dark:text-muted-foreground">
|
<span className="text-bold text-black">Clean thread</span>
|
||||||
Clean thread
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</ModalTrigger>
|
</ModalTrigger>
|
||||||
<ModalPortal />
|
<ModalPortal />
|
||||||
|
|||||||
@ -33,10 +33,8 @@ const DeleteThreadModal: React.FC<Props> = ({ threadId }) => {
|
|||||||
<Modal>
|
<Modal>
|
||||||
<ModalTrigger asChild onClick={(e) => e.stopPropagation()}>
|
<ModalTrigger asChild onClick={(e) => e.stopPropagation()}>
|
||||||
<div className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-secondary">
|
<div className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-secondary">
|
||||||
<Trash2Icon size={16} className="text-red-600 dark:text-red-300" />
|
<Trash2Icon size={16} className="text-red-600" />
|
||||||
<span className="text-bold text-red-600 dark:text-red-300">
|
<span className="text-bold text-red-600">Delete thread</span>
|
||||||
Delete thread
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</ModalTrigger>
|
</ModalTrigger>
|
||||||
<ModalPortal />
|
<ModalPortal />
|
||||||
|
|||||||
@ -54,7 +54,7 @@ const ErrorMessage = ({ message }: { message: ThreadMessage }) => {
|
|||||||
Port 3928 is currently unavailable. Check for conflicting apps,
|
Port 3928 is currently unavailable. Check for conflicting apps,
|
||||||
or access
|
or access
|
||||||
<span
|
<span
|
||||||
className="cursor-pointer text-primary dark:text-blue-400"
|
className="cursor-pointer text-blue-600"
|
||||||
onClick={() => setModalTroubleShooting(true)}
|
onClick={() => setModalTroubleShooting(true)}
|
||||||
>
|
>
|
||||||
troubleshooting assistance
|
troubleshooting assistance
|
||||||
@ -72,7 +72,7 @@ const ErrorMessage = ({ message }: { message: ThreadMessage }) => {
|
|||||||
<p>
|
<p>
|
||||||
Jan’s in beta. Access
|
Jan’s in beta. Access
|
||||||
<span
|
<span
|
||||||
className="cursor-pointer text-primary dark:text-blue-400"
|
className="cursor-pointer text-blue-600"
|
||||||
onClick={() => setModalTroubleShooting(true)}
|
onClick={() => setModalTroubleShooting(true)}
|
||||||
>
|
>
|
||||||
troubleshooting assistance
|
troubleshooting assistance
|
||||||
|
|||||||
@ -71,7 +71,7 @@ const Sidebar: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'h-full flex-shrink-0 overflow-x-hidden border-l border-border bg-background pb-6 transition-all duration-100 dark:bg-background/20',
|
'h-full flex-shrink-0 overflow-x-hidden border-l border-border bg-background pb-6 transition-all duration-100',
|
||||||
showing
|
showing
|
||||||
? 'w-80 translate-x-0 opacity-100'
|
? 'w-80 translate-x-0 opacity-100'
|
||||||
: 'w-0 translate-x-full opacity-0'
|
: 'w-0 translate-x-full opacity-0'
|
||||||
@ -87,7 +87,7 @@ const Sidebar: React.FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
id="thread-title"
|
id="thread-title"
|
||||||
className="mb-2 inline-block font-bold text-gray-600 dark:text-gray-300"
|
className="mb-2 inline-block font-bold text-gray-600"
|
||||||
>
|
>
|
||||||
Title
|
Title
|
||||||
</label>
|
</label>
|
||||||
@ -106,7 +106,7 @@ const Sidebar: React.FC = () => {
|
|||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<label
|
<label
|
||||||
id="thread-title"
|
id="thread-title"
|
||||||
className="mb-2 inline-block font-bold text-zinc-500 dark:text-gray-300"
|
className="mb-2 inline-block font-bold text-zinc-500"
|
||||||
>
|
>
|
||||||
Threads ID
|
Threads ID
|
||||||
</label>
|
</label>
|
||||||
@ -127,7 +127,7 @@ const Sidebar: React.FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
id="thread-title"
|
id="thread-title"
|
||||||
className="mb-2 inline-block font-bold text-zinc-500 dark:text-gray-300"
|
className="mb-2 inline-block font-bold text-zinc-500"
|
||||||
>
|
>
|
||||||
Instructions
|
Instructions
|
||||||
</label>
|
</label>
|
||||||
@ -203,14 +203,14 @@ const Sidebar: React.FC = () => {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<label
|
<label
|
||||||
id="retrieval"
|
id="retrieval"
|
||||||
className="inline-flex items-center font-bold text-zinc-500 dark:text-gray-300"
|
className="inline-flex items-center font-bold text-zinc-500"
|
||||||
>
|
>
|
||||||
Retrieval
|
Retrieval
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<InfoIcon
|
<InfoIcon
|
||||||
size={16}
|
size={16}
|
||||||
className="ml-2 flex-shrink-0 text-black dark:text-gray-500"
|
className="ml-2 flex-shrink-0 text-black"
|
||||||
/>
|
/>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPortal>
|
<TooltipPortal>
|
||||||
@ -269,7 +269,7 @@ const Sidebar: React.FC = () => {
|
|||||||
<div className="item-center mb-2 flex">
|
<div className="item-center mb-2 flex">
|
||||||
<label
|
<label
|
||||||
id="embedding-model"
|
id="embedding-model"
|
||||||
className="inline-flex font-bold text-zinc-500 dark:text-gray-300"
|
className="inline-flex font-bold text-zinc-500"
|
||||||
>
|
>
|
||||||
Embedding Model
|
Embedding Model
|
||||||
</label>
|
</label>
|
||||||
@ -277,7 +277,7 @@ const Sidebar: React.FC = () => {
|
|||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<InfoIcon
|
<InfoIcon
|
||||||
size={16}
|
size={16}
|
||||||
className="ml-2 flex-shrink-0 dark:text-gray-500"
|
className="ml-2 flex-shrink-0"
|
||||||
/>
|
/>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPortal>
|
<TooltipPortal>
|
||||||
@ -309,7 +309,7 @@ const Sidebar: React.FC = () => {
|
|||||||
<div className="mb-2 flex items-center">
|
<div className="mb-2 flex items-center">
|
||||||
<label
|
<label
|
||||||
id="vector-database"
|
id="vector-database"
|
||||||
className="inline-block font-bold text-zinc-500 dark:text-gray-300"
|
className="inline-block font-bold text-zinc-500"
|
||||||
>
|
>
|
||||||
Vector Database
|
Vector Database
|
||||||
</label>
|
</label>
|
||||||
@ -317,7 +317,7 @@ const Sidebar: React.FC = () => {
|
|||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<InfoIcon
|
<InfoIcon
|
||||||
size={16}
|
size={16}
|
||||||
className="ml-2 flex-shrink-0 dark:text-gray-500"
|
className="ml-2 flex-shrink-0"
|
||||||
/>
|
/>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPortal>
|
<TooltipPortal>
|
||||||
|
|||||||
@ -18,7 +18,7 @@ import hljs from 'highlight.js'
|
|||||||
|
|
||||||
import { useAtomValue } from 'jotai'
|
import { useAtomValue } from 'jotai'
|
||||||
import { FolderOpenIcon } from 'lucide-react'
|
import { FolderOpenIcon } from 'lucide-react'
|
||||||
import { Marked, Renderer, marked as markedDefault } from 'marked'
|
import { Marked, Renderer } from 'marked'
|
||||||
|
|
||||||
import { markedHighlight } from 'marked-highlight'
|
import { markedHighlight } from 'marked-highlight'
|
||||||
|
|
||||||
@ -43,19 +43,6 @@ import {
|
|||||||
getCurrentChatMessagesAtom,
|
getCurrentChatMessagesAtom,
|
||||||
} from '@/helpers/atoms/ChatMessage.atom'
|
} from '@/helpers/atoms/ChatMessage.atom'
|
||||||
|
|
||||||
function isMarkdownValue(value: string): boolean {
|
|
||||||
const tokenTypes: string[] = []
|
|
||||||
markedDefault(value, {
|
|
||||||
walkTokens: (token) => {
|
|
||||||
tokenTypes.push(token.type)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const isMarkdown = ['code', 'codespan'].some((tokenType) => {
|
|
||||||
return tokenTypes.includes(tokenType)
|
|
||||||
})
|
|
||||||
return isMarkdown
|
|
||||||
}
|
|
||||||
|
|
||||||
const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
|
const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
|
||||||
let text = ''
|
let text = ''
|
||||||
const isUser = props.role === ChatCompletionRole.User
|
const isUser = props.role === ChatCompletionRole.User
|
||||||
@ -282,7 +269,7 @@ const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isUser && !isMarkdownValue(text) ? (
|
{isUser ? (
|
||||||
<>
|
<>
|
||||||
{editMessage === props.id ? (
|
{editMessage === props.id ? (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -79,7 +79,7 @@ export default function ThreadList() {
|
|||||||
<div
|
<div
|
||||||
key={thread.id}
|
key={thread.id}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
`group/message relative mb-1 flex cursor-pointer flex-col transition-all hover:rounded-lg hover:bg-gray-100 hover:dark:bg-secondary/50`
|
`group/message relative mb-1 flex cursor-pointer flex-col transition-all hover:rounded-lg hover:bg-gray-100`
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onThreadClick(thread)
|
onThreadClick(thread)
|
||||||
@ -90,7 +90,7 @@ export default function ThreadList() {
|
|||||||
{thread.updated && displayDate(thread.updated)}
|
{thread.updated && displayDate(thread.updated)}
|
||||||
</p>
|
</p>
|
||||||
<h2 className="line-clamp-1 font-bold">{thread.title}</h2>
|
<h2 className="line-clamp-1 font-bold">{thread.title}</h2>
|
||||||
<p className="mt-1 line-clamp-1 text-xs text-gray-700 group-hover/message:max-w-[160px] dark:text-gray-300">
|
<p className="mt-1 line-clamp-1 text-xs text-gray-700 group-hover/message:max-w-[160px]">
|
||||||
{threadStates[thread.id]?.lastMessage
|
{threadStates[thread.id]?.lastMessage
|
||||||
? threadStates[thread.id]?.lastMessage
|
? threadStates[thread.id]?.lastMessage
|
||||||
: 'No new message'}
|
: 'No new message'}
|
||||||
@ -98,7 +98,7 @@ export default function ThreadList() {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
`group/icon invisible absolute bottom-2 right-2 z-20 rounded-lg p-1 text-muted-foreground hover:bg-gray-200 group-hover/message:visible hover:dark:bg-secondary`
|
`group/icon invisible absolute bottom-2 right-2 z-20 rounded-lg p-1 text-muted-foreground hover:bg-gray-200 group-hover/message:visible`
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<MoreVerticalIcon />
|
<MoreVerticalIcon />
|
||||||
@ -109,7 +109,7 @@ export default function ThreadList() {
|
|||||||
</div>
|
</div>
|
||||||
{activeThreadId === thread.id && (
|
{activeThreadId === thread.id && (
|
||||||
<m.div
|
<m.div
|
||||||
className="absolute inset-0 left-0 h-full w-full rounded-lg bg-gray-100 p-4 dark:bg-secondary/50"
|
className="absolute inset-0 left-0 h-full w-full rounded-lg bg-gray-100 p-4"
|
||||||
layoutId="active-thread"
|
layoutId="active-thread"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -26,7 +26,7 @@ export const HuggingFaceSearchModal = () => {
|
|||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
placeholder="e.g. username/repo-name"
|
placeholder="e.g. username/repo-name"
|
||||||
className="bg-white dark:bg-background"
|
className="bg-white"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setRepoID(e.target.value)
|
setRepoID(e.target.value)
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import {
|
|||||||
} from '@janhq/uikit'
|
} from '@janhq/uikit'
|
||||||
|
|
||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
import { Plus, SearchIcon } from 'lucide-react'
|
import { UploadIcon, SearchIcon } from 'lucide-react'
|
||||||
|
|
||||||
import { FeatureToggleContext } from '@/context/FeatureToggle'
|
import { FeatureToggleContext } from '@/context/FeatureToggle'
|
||||||
|
|
||||||
@ -91,17 +91,17 @@ const ExploreModelsScreen = () => {
|
|||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Search models"
|
placeholder="Search models"
|
||||||
className="bg-white pl-9 dark:bg-background"
|
className="bg-white pl-9"
|
||||||
onChange={(e) => setsearchValue(e.target.value)}
|
onChange={(e) => setsearchValue(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
themes={'primary'}
|
themes="outline"
|
||||||
className="space-x-2"
|
className="gap-2 bg-white"
|
||||||
onClick={onImportModelClick}
|
onClick={onImportModelClick}
|
||||||
>
|
>
|
||||||
<Plus className="h-3 w-3" />
|
<UploadIcon size={16} />
|
||||||
<p>Import Model</p>
|
Import Model
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{experimentalFeature && (
|
{experimentalFeature && (
|
||||||
|
|||||||
@ -181,7 +181,7 @@ const LocalServerScreen = () => {
|
|||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div className="space-y-4 p-4">
|
<div className="space-y-4 p-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-2 block text-sm font-semibold text-zinc-500 dark:text-gray-300">
|
<p className="mb-2 block text-sm font-semibold text-zinc-500 ">
|
||||||
Server Options
|
Server Options
|
||||||
</p>
|
</p>
|
||||||
<div className="flex w-full flex-shrink-0 items-center gap-x-2">
|
<div className="flex w-full flex-shrink-0 items-center gap-x-2">
|
||||||
@ -231,15 +231,12 @@ const LocalServerScreen = () => {
|
|||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
id="cors"
|
id="cors"
|
||||||
className="mb-2 inline-flex items-start gap-x-2 font-bold text-zinc-500 dark:text-gray-300"
|
className="mb-2 inline-flex items-start gap-x-2 font-bold text-zinc-500"
|
||||||
>
|
>
|
||||||
Cross-Origin-Resource-Sharing (CORS)
|
Cross-Origin-Resource-Sharing (CORS)
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<InfoIcon
|
<InfoIcon size={16} className="mt-0.5 flex-shrink-0" />
|
||||||
size={16}
|
|
||||||
className="mt-0.5 flex-shrink-0 dark:text-gray-500"
|
|
||||||
/>
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPortal>
|
<TooltipPortal>
|
||||||
<TooltipContent side="top" className="max-w-[240px]">
|
<TooltipContent side="top" className="max-w-[240px]">
|
||||||
@ -266,15 +263,12 @@ const LocalServerScreen = () => {
|
|||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
id="verbose"
|
id="verbose"
|
||||||
className="mb-2 inline-flex items-start gap-x-2 font-bold text-zinc-500 dark:text-gray-300"
|
className="mb-2 inline-flex items-start gap-x-2 font-bold text-zinc-500"
|
||||||
>
|
>
|
||||||
Verbose Server Logs
|
Verbose Server Logs
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<InfoIcon
|
<InfoIcon size={16} className="mt-0.5 flex-shrink-0" />
|
||||||
size={16}
|
|
||||||
className="mt-0.5 flex-shrink-0 dark:text-gray-500"
|
|
||||||
/>
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPortal>
|
<TooltipPortal>
|
||||||
<TooltipContent side="top" className="max-w-[240px]">
|
<TooltipContent side="top" className="max-w-[240px]">
|
||||||
@ -315,13 +309,13 @@ const LocalServerScreen = () => {
|
|||||||
|
|
||||||
{/* Middle Bar */}
|
{/* Middle Bar */}
|
||||||
<ScrollToBottom className="relative flex h-full w-full flex-col overflow-auto bg-background">
|
<ScrollToBottom className="relative flex h-full w-full flex-col overflow-auto bg-background">
|
||||||
<div className="sticky top-0 flex items-center justify-between bg-zinc-100 px-4 py-2 dark:bg-zinc-600">
|
<div className="sticky top-0 flex items-center justify-between bg-zinc-100 px-4 py-2">
|
||||||
<h2 className="font-bold">Server Logs</h2>
|
<h2 className="font-bold">Server Logs</h2>
|
||||||
<div className="space-x-2">
|
<div className="space-x-2">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
themes="outline"
|
themes="outline"
|
||||||
className="bg-white dark:bg-secondary"
|
className="bg-white"
|
||||||
onClick={() => openServerLog()}
|
onClick={() => openServerLog()}
|
||||||
>
|
>
|
||||||
<CodeIcon size={16} className="mr-2" />
|
<CodeIcon size={16} className="mr-2" />
|
||||||
@ -330,7 +324,7 @@ const LocalServerScreen = () => {
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
themes="outline"
|
themes="outline"
|
||||||
className="bg-white dark:bg-secondary"
|
className="bg-white"
|
||||||
onClick={() => clearServerLog()}
|
onClick={() => clearServerLog()}
|
||||||
>
|
>
|
||||||
<Paintbrush size={16} className="mr-2" />
|
<Paintbrush size={16} className="mr-2" />
|
||||||
@ -386,7 +380,7 @@ const LocalServerScreen = () => {
|
|||||||
{/* Right bar */}
|
{/* Right bar */}
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'h-full flex-shrink-0 overflow-x-hidden border-l border-border bg-background transition-all duration-100 dark:bg-background/20',
|
'h-full flex-shrink-0 overflow-x-hidden border-l border-border bg-background transition-all duration-100',
|
||||||
showRightSideBar
|
showRightSideBar
|
||||||
? 'w-80 translate-x-0 opacity-100'
|
? 'w-80 translate-x-0 opacity-100'
|
||||||
: 'w-0 translate-x-full opacity-0'
|
: 'w-0 translate-x-full opacity-0'
|
||||||
@ -422,7 +416,7 @@ const LocalServerScreen = () => {
|
|||||||
<span>
|
<span>
|
||||||
Model failed to start. Access{' '}
|
Model failed to start. Access{' '}
|
||||||
<span
|
<span
|
||||||
className="cursor-pointer text-primary dark:text-blue-400"
|
className="cursor-pointer text-blue-600"
|
||||||
onClick={() => setModalTroubleShooting(true)}
|
onClick={() => setModalTroubleShooting(true)}
|
||||||
>
|
>
|
||||||
troubleshooting assistance
|
troubleshooting assistance
|
||||||
|
|||||||
@ -282,7 +282,7 @@ const Advanced = () => {
|
|||||||
disabled={gpuList.length === 0 || !gpuEnabled}
|
disabled={gpuList.length === 0 || !gpuEnabled}
|
||||||
value={selectedGpu.join()}
|
value={selectedGpu.join()}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-[340px] bg-white dark:bg-gray-500">
|
<SelectTrigger className="w-[340px] bg-white">
|
||||||
<SelectValue placeholder={gpuSelectionPlaceHolder}>
|
<SelectValue placeholder={gpuSelectionPlaceHolder}>
|
||||||
<span className="line-clamp-1 w-full pr-8">
|
<span className="line-clamp-1 w-full pr-8">
|
||||||
{selectedGpu.join()}
|
{selectedGpu.join()}
|
||||||
|
|||||||
@ -1,57 +0,0 @@
|
|||||||
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 (
|
|
||||||
<div className="flex items-center">
|
|
||||||
{primaryColorOptions.map((option, i) => {
|
|
||||||
const isActive = config.primaryColor === option.value
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="relative flex h-6 w-6 items-center justify-center"
|
|
||||||
key={i}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className={twMerge('h-3.5 w-3.5 rounded-full', option.class)}
|
|
||||||
onClick={() => handleChangeAccent(option.value)}
|
|
||||||
/>
|
|
||||||
{isActive ? (
|
|
||||||
<m.div
|
|
||||||
className="absolute inset-0 h-full w-full rounded-full border border-primary/50 bg-primary/20"
|
|
||||||
layoutId="active-primary-menu"
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -28,7 +28,7 @@ export default function ToggleTheme() {
|
|||||||
</button>
|
</button>
|
||||||
{isActive ? (
|
{isActive ? (
|
||||||
<m.div
|
<m.div
|
||||||
className="absolute inset-0 h-full w-full rounded-md border border-primary/50 bg-primary/20"
|
className="absolute inset-0 h-full w-full rounded-md border border-primary/50 bg-blue-500/20"
|
||||||
layoutId="active-theme-menu"
|
layoutId="active-theme-menu"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import ToggleAccent from '@/screens/Settings/Appearance/TogglePrimary'
|
|
||||||
import ToggleTheme from '@/screens/Settings/Appearance/ToggleTheme'
|
import ToggleTheme from '@/screens/Settings/Appearance/ToggleTheme'
|
||||||
|
|
||||||
export default function AppearanceOptions() {
|
export default function AppearanceOptions() {
|
||||||
@ -22,7 +21,6 @@ export default function AppearanceOptions() {
|
|||||||
Choose the primary accent color used throughout the app.
|
Choose the primary accent color used throughout the app.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<ToggleAccent />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,6 +1,12 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
import { Model, ModelEvent, events, openFileExplorer } from '@janhq/core'
|
import {
|
||||||
|
Model,
|
||||||
|
ModelEvent,
|
||||||
|
events,
|
||||||
|
joinPath,
|
||||||
|
openFileExplorer,
|
||||||
|
} from '@janhq/core'
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
@ -47,6 +53,7 @@ const EditModelInfoModal: React.FC = () => {
|
|||||||
const janDataFolder = useAtomValue(janDataFolderPathAtom)
|
const janDataFolder = useAtomValue(janDataFolderPathAtom)
|
||||||
const updateImportingModel = useSetAtom(updateImportingModelAtom)
|
const updateImportingModel = useSetAtom(updateImportingModelAtom)
|
||||||
const { updateModelInfo } = useImportModel()
|
const { updateModelInfo } = useImportModel()
|
||||||
|
const [modelPath, setModelPath] = useState<string>('')
|
||||||
|
|
||||||
const editingModel = importingModels.find(
|
const editingModel = importingModels.find(
|
||||||
(model) => model.importId === editingModelId
|
(model) => model.importId === editingModelId
|
||||||
@ -88,13 +95,19 @@ const EditModelInfoModal: React.FC = () => {
|
|||||||
setEditingModelId(undefined)
|
setEditingModelId(undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
const modelFolderPath = useMemo(() => {
|
useEffect(() => {
|
||||||
return `${janDataFolder}/models/${editingModel?.modelId}`
|
const getModelPath = async () => {
|
||||||
|
const modelId = editingModel?.modelId
|
||||||
|
if (!modelId) return ''
|
||||||
|
const path = await joinPath([janDataFolder, 'models', modelId])
|
||||||
|
setModelPath(path)
|
||||||
|
}
|
||||||
|
getModelPath()
|
||||||
}, [janDataFolder, editingModel])
|
}, [janDataFolder, editingModel])
|
||||||
|
|
||||||
const onShowInFinderClick = useCallback(() => {
|
const onShowInFinderClick = useCallback(() => {
|
||||||
openFileExplorer(modelFolderPath)
|
openFileExplorer(modelPath)
|
||||||
}, [modelFolderPath])
|
}, [modelPath])
|
||||||
|
|
||||||
if (!editingModel) {
|
if (!editingModel) {
|
||||||
setImportModelStage('IMPORTING_MODEL')
|
setImportModelStage('IMPORTING_MODEL')
|
||||||
@ -104,7 +117,10 @@ const EditModelInfoModal: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open={importModelStage === 'EDIT_MODEL_INFO'}>
|
<Modal
|
||||||
|
open={importModelStage === 'EDIT_MODEL_INFO'}
|
||||||
|
onOpenChange={onCancelClick}
|
||||||
|
>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
<ModalTitle>Edit Model Information</ModalTitle>
|
<ModalTitle>Edit Model Information</ModalTitle>
|
||||||
@ -130,7 +146,7 @@ const EditModelInfoModal: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-1 flex flex-row items-center space-x-2">
|
<div className="mt-1 flex flex-row items-center space-x-2">
|
||||||
<span className="line-clamp-1 text-xs font-normal text-[#71717A]">
|
<span className="line-clamp-1 text-xs font-normal text-[#71717A]">
|
||||||
{modelFolderPath}
|
{modelPath}
|
||||||
</span>
|
</span>
|
||||||
<Button themes="ghost" onClick={onShowInFinderClick}>
|
<Button themes="ghost" onClick={onShowInFinderClick}>
|
||||||
{openFileTitle()}
|
{openFileTitle()}
|
||||||
|
|||||||
@ -15,7 +15,8 @@ const ImportInProgressIcon: React.FC<Props> = ({
|
|||||||
const [isHovered, setIsHovered] = useState(false)
|
const [isHovered, setIsHovered] = useState(false)
|
||||||
|
|
||||||
const onMouseOver = () => {
|
const onMouseOver = () => {
|
||||||
setIsHovered(true)
|
// for now we don't allow user to cancel importing
|
||||||
|
setIsHovered(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onMouseOut = () => {
|
const onMouseOut = () => {
|
||||||
|
|||||||
@ -16,7 +16,7 @@ const ImportModelOptionSelection: React.FC<Props> = ({
|
|||||||
onClick={() => setSelectedOptionType(option.type)}
|
onClick={() => setSelectedOptionType(option.type)}
|
||||||
>
|
>
|
||||||
<div className="flex h-5 w-5 items-center justify-center rounded-full border border-[#2563EB]">
|
<div className="flex h-5 w-5 items-center justify-center rounded-full border border-[#2563EB]">
|
||||||
{checked && <div className="h-2 w-2 rounded-full bg-primary" />}
|
{checked && <div className="h-2 w-2 rounded-full bg-blue-500" />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="ml-2 flex-1">
|
<div className="ml-2 flex-1">
|
||||||
|
|||||||
@ -29,7 +29,7 @@ const ImportSuccessIcon: React.FC<Props> = ({ onEditModelClick }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SuccessIcon: React.FC = React.memo(() => (
|
const SuccessIcon: React.FC = React.memo(() => (
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary">
|
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-blue-500">
|
||||||
<Check color="#FFF" />
|
<Check color="#FFF" />
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
|
import { useCallback, useMemo } from 'react'
|
||||||
|
|
||||||
import { ImportingModel } from '@janhq/core/.'
|
import { ImportingModel } from '@janhq/core/.'
|
||||||
import { useSetAtom } from 'jotai'
|
import { useSetAtom } from 'jotai'
|
||||||
|
|
||||||
|
import { AlertCircle } from 'lucide-react'
|
||||||
|
|
||||||
import { setImportModelStageAtom } from '@/hooks/useImportModel'
|
import { setImportModelStageAtom } from '@/hooks/useImportModel'
|
||||||
|
|
||||||
import { toGibibytes } from '@/utils/converter'
|
import { toGibibytes } from '@/utils/converter'
|
||||||
@ -16,28 +20,39 @@ type Props = {
|
|||||||
const ImportingModelItem: React.FC<Props> = ({ model }) => {
|
const ImportingModelItem: React.FC<Props> = ({ model }) => {
|
||||||
const setImportModelStage = useSetAtom(setImportModelStageAtom)
|
const setImportModelStage = useSetAtom(setImportModelStageAtom)
|
||||||
const setEditingModelId = useSetAtom(editingModelIdAtom)
|
const setEditingModelId = useSetAtom(editingModelIdAtom)
|
||||||
const sizeInGb = toGibibytes(model.size)
|
|
||||||
|
|
||||||
const onEditModelInfoClick = () => {
|
const onEditModelInfoClick = useCallback(() => {
|
||||||
setEditingModelId(model.importId)
|
setEditingModelId(model.importId)
|
||||||
setImportModelStage('EDIT_MODEL_INFO')
|
setImportModelStage('EDIT_MODEL_INFO')
|
||||||
}
|
}, [setImportModelStage, setEditingModelId, model.importId])
|
||||||
|
|
||||||
const onDeleteModelClick = () => {}
|
const onDeleteModelClick = useCallback(() => {}, [])
|
||||||
|
|
||||||
|
const displayStatus = useMemo(() => {
|
||||||
|
if (model.status === 'FAILED') {
|
||||||
|
return 'Failed'
|
||||||
|
} else {
|
||||||
|
return toGibibytes(model.size)
|
||||||
|
}
|
||||||
|
}, [model.status, model.size])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-row items-center space-x-3 rounded-lg border px-4 py-3">
|
<div className="flex w-full flex-row items-center space-x-3 rounded-lg border px-4 py-3">
|
||||||
<p className="line-clamp-1 flex-1">{model.name}</p>
|
<p className="line-clamp-1 flex-1 font-semibold text-[#09090B]">
|
||||||
<p>{sizeInGb}</p>
|
{model.name}
|
||||||
|
</p>
|
||||||
|
<p className="text-[#71717A]">{displayStatus}</p>
|
||||||
|
|
||||||
{model.status === 'IMPORTED' || model.status === 'FAILED' ? (
|
{model.status === 'IMPORTED' && (
|
||||||
<ImportSuccessIcon onEditModelClick={onEditModelInfoClick} />
|
<ImportSuccessIcon onEditModelClick={onEditModelInfoClick} />
|
||||||
) : (
|
)}
|
||||||
|
{(model.status === 'IMPORTING' || model.status === 'PREPARING') && (
|
||||||
<ImportInProgressIcon
|
<ImportInProgressIcon
|
||||||
percentage={model.percentage ?? 0}
|
percentage={model.percentage ?? 0}
|
||||||
onDeleteModelClick={onDeleteModelClick}
|
onDeleteModelClick={onDeleteModelClick}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{model.status === 'FAILED' && <AlertCircle size={24} color="#F00" />}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useMemo } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
import { openFileExplorer } from '@janhq/core'
|
import { joinPath, openFileExplorer } from '@janhq/core'
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Modal,
|
Modal,
|
||||||
@ -31,7 +31,15 @@ const ImportingModelModal: React.FC = () => {
|
|||||||
const setImportModelStage = useSetAtom(setImportModelStageAtom)
|
const setImportModelStage = useSetAtom(setImportModelStageAtom)
|
||||||
const janDataFolder = useAtomValue(janDataFolderPathAtom)
|
const janDataFolder = useAtomValue(janDataFolderPathAtom)
|
||||||
|
|
||||||
const modelFolder = useMemo(() => `${janDataFolder}/models`, [janDataFolder])
|
const [modelFolder, setModelFolder] = useState('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const getModelPath = async () => {
|
||||||
|
const modelPath = await joinPath([janDataFolder, 'models'])
|
||||||
|
setModelFolder(modelPath)
|
||||||
|
}
|
||||||
|
getModelPath()
|
||||||
|
}, [janDataFolder])
|
||||||
|
|
||||||
const finishedImportModel = importingModels.filter(
|
const finishedImportModel = importingModels.filter(
|
||||||
(model) => model.status === 'IMPORTED'
|
(model) => model.status === 'IMPORTED'
|
||||||
|
|||||||
@ -152,7 +152,7 @@ export default function RowModel(props: RowModelProps) {
|
|||||||
) : (
|
) : (
|
||||||
<PlayIcon size={16} className="text-muted-foreground" />
|
<PlayIcon size={16} className="text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
<span className="text-bold capitalize text-black dark:text-muted-foreground">
|
<span className="text-bold capitalize text-black">
|
||||||
{isActiveModel ? stateModel.state : 'Start'}
|
{isActiveModel ? stateModel.state : 'Start'}
|
||||||
Model
|
Model
|
||||||
</span>
|
</span>
|
||||||
@ -189,9 +189,7 @@ export default function RowModel(props: RowModelProps) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Trash2Icon size={16} className="text-muted-foreground" />
|
<Trash2Icon size={16} className="text-muted-foreground" />
|
||||||
<span className="text-bold text-black dark:text-muted-foreground">
|
<span className="text-bold text-black">Delete Model</span>
|
||||||
Delete Model
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import { useCallback, useState } from 'react'
|
|||||||
|
|
||||||
import { useDropzone } from 'react-dropzone'
|
import { useDropzone } from 'react-dropzone'
|
||||||
|
|
||||||
import { ImportingModel } from '@janhq/core'
|
|
||||||
import { Button, Input, ScrollArea } from '@janhq/uikit'
|
import { Button, Input, ScrollArea } from '@janhq/uikit'
|
||||||
|
|
||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
@ -10,60 +9,29 @@ import { Plus, SearchIcon, UploadCloudIcon } from 'lucide-react'
|
|||||||
|
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import useDropModelBinaries from '@/hooks/useDropModelBinaries'
|
||||||
|
|
||||||
import { setImportModelStageAtom } from '@/hooks/useImportModel'
|
import { setImportModelStageAtom } from '@/hooks/useImportModel'
|
||||||
|
|
||||||
import { getFileInfoFromFile } from '@/utils/file'
|
|
||||||
|
|
||||||
import RowModel from './Row'
|
import RowModel from './Row'
|
||||||
|
|
||||||
import {
|
import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||||
downloadedModelsAtom,
|
|
||||||
importingModelsAtom,
|
|
||||||
} from '@/helpers/atoms/Model.atom'
|
|
||||||
|
|
||||||
const Column = ['Name', 'Model ID', 'Size', 'Version', 'Status', '']
|
const Column = ['Name', 'Model ID', 'Size', 'Version', 'Status', '']
|
||||||
|
|
||||||
const Models: React.FC = () => {
|
const Models: React.FC = () => {
|
||||||
const downloadedModels = useAtomValue(downloadedModelsAtom)
|
const downloadedModels = useAtomValue(downloadedModelsAtom)
|
||||||
const setImportModelStage = useSetAtom(setImportModelStageAtom)
|
const setImportModelStage = useSetAtom(setImportModelStageAtom)
|
||||||
const setImportingModels = useSetAtom(importingModelsAtom)
|
|
||||||
const [searchValue, setsearchValue] = useState('')
|
const [searchValue, setsearchValue] = useState('')
|
||||||
|
const { onDropModels } = useDropModelBinaries()
|
||||||
|
|
||||||
const filteredDownloadedModels = downloadedModels
|
const filteredDownloadedModels = downloadedModels
|
||||||
.filter((x) => x.name?.toLowerCase().includes(searchValue.toLowerCase()))
|
.filter((x) => x.name?.toLowerCase().includes(searchValue.toLowerCase()))
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
|
||||||
const onDrop = useCallback(
|
|
||||||
(acceptedFiles: File[]) => {
|
|
||||||
const filePathWithSize = getFileInfoFromFile(acceptedFiles)
|
|
||||||
|
|
||||||
const importingModels: ImportingModel[] = filePathWithSize.map(
|
|
||||||
(file) => ({
|
|
||||||
importId: uuidv4(),
|
|
||||||
modelId: undefined,
|
|
||||||
name: file.name,
|
|
||||||
description: '',
|
|
||||||
path: file.path,
|
|
||||||
tags: [],
|
|
||||||
size: file.size,
|
|
||||||
status: 'PREPARING',
|
|
||||||
format: 'gguf',
|
|
||||||
})
|
|
||||||
)
|
|
||||||
if (importingModels.length === 0) return
|
|
||||||
|
|
||||||
setImportingModels(importingModels)
|
|
||||||
setImportModelStage('MODEL_SELECTED')
|
|
||||||
},
|
|
||||||
[setImportModelStage, setImportingModels]
|
|
||||||
)
|
|
||||||
|
|
||||||
const { getRootProps, isDragActive } = useDropzone({
|
const { getRootProps, isDragActive } = useDropzone({
|
||||||
noClick: true,
|
noClick: true,
|
||||||
multiple: true,
|
multiple: true,
|
||||||
onDrop,
|
onDrop: onDropModels,
|
||||||
})
|
})
|
||||||
|
|
||||||
const onImportModelClick = useCallback(() => {
|
const onImportModelClick = useCallback(() => {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { useDropzone } from 'react-dropzone'
|
import { useDropzone } from 'react-dropzone'
|
||||||
|
|
||||||
import { ImportingModel, fs } from '@janhq/core'
|
import { ImportingModel, baseName, fs } from '@janhq/core'
|
||||||
import { Modal, ModalContent, ModalHeader, ModalTitle } from '@janhq/uikit'
|
import { Modal, ModalContent, ModalHeader, ModalTitle } from '@janhq/uikit'
|
||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
@ -9,16 +9,15 @@ import { UploadCloudIcon } from 'lucide-react'
|
|||||||
|
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
|
import { snackbar } from '@/containers/Toast'
|
||||||
|
|
||||||
|
import useDropModelBinaries from '@/hooks/useDropModelBinaries'
|
||||||
import {
|
import {
|
||||||
getImportModelStageAtom,
|
getImportModelStageAtom,
|
||||||
setImportModelStageAtom,
|
setImportModelStageAtom,
|
||||||
} from '@/hooks/useImportModel'
|
} from '@/hooks/useImportModel'
|
||||||
|
|
||||||
import {
|
import { FilePathWithSize } from '@/utils/file'
|
||||||
FilePathWithSize,
|
|
||||||
getFileInfoFromFile,
|
|
||||||
getFileNameFromPath,
|
|
||||||
} from '@/utils/file'
|
|
||||||
|
|
||||||
import { importingModelsAtom } from '@/helpers/atoms/Model.atom'
|
import { importingModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||||
|
|
||||||
@ -26,6 +25,7 @@ const SelectingModelModal: React.FC = () => {
|
|||||||
const setImportModelStage = useSetAtom(setImportModelStageAtom)
|
const setImportModelStage = useSetAtom(setImportModelStageAtom)
|
||||||
const importModelStage = useAtomValue(getImportModelStageAtom)
|
const importModelStage = useAtomValue(getImportModelStageAtom)
|
||||||
const setImportingModels = useSetAtom(importingModelsAtom)
|
const setImportingModels = useSetAtom(importingModelsAtom)
|
||||||
|
const { onDropModels } = useDropModelBinaries()
|
||||||
|
|
||||||
const onSelectFileClick = useCallback(async () => {
|
const onSelectFileClick = useCallback(async () => {
|
||||||
const filePaths = await window.core?.api?.selectModelFiles()
|
const filePaths = await window.core?.api?.selectModelFiles()
|
||||||
@ -36,7 +36,7 @@ const SelectingModelModal: React.FC = () => {
|
|||||||
const fileStats = await fs.fileStat(filePath, true)
|
const fileStats = await fs.fileStat(filePath, true)
|
||||||
if (!fileStats || fileStats.isDirectory) continue
|
if (!fileStats || fileStats.isDirectory) continue
|
||||||
|
|
||||||
const fileName = getFileNameFromPath(filePath)
|
const fileName = await baseName(filePath)
|
||||||
sanitizedFilePaths.push({
|
sanitizedFilePaths.push({
|
||||||
path: filePath,
|
path: filePath,
|
||||||
name: fileName,
|
name: fileName,
|
||||||
@ -44,12 +44,19 @@ const SelectingModelModal: React.FC = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const importingModels: ImportingModel[] = sanitizedFilePaths.map(
|
const unsupportedFiles = sanitizedFilePaths.filter(
|
||||||
|
(file) => !file.path.endsWith('.gguf')
|
||||||
|
)
|
||||||
|
const supportedFiles = sanitizedFilePaths.filter((file) =>
|
||||||
|
file.path.endsWith('.gguf')
|
||||||
|
)
|
||||||
|
|
||||||
|
const importingModels: ImportingModel[] = supportedFiles.map(
|
||||||
({ path, name, size }: FilePathWithSize) => {
|
({ path, name, size }: FilePathWithSize) => {
|
||||||
return {
|
return {
|
||||||
importId: uuidv4(),
|
importId: uuidv4(),
|
||||||
modelId: undefined,
|
modelId: undefined,
|
||||||
name: name,
|
name: name.replace('.gguf', ''),
|
||||||
description: '',
|
description: '',
|
||||||
path: path,
|
path: path,
|
||||||
tags: [],
|
tags: [],
|
||||||
@ -59,45 +66,26 @@ const SelectingModelModal: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
if (unsupportedFiles.length > 0) {
|
||||||
|
snackbar({
|
||||||
|
description: `File has to be a .gguf file`,
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
}
|
||||||
if (importingModels.length === 0) return
|
if (importingModels.length === 0) return
|
||||||
|
|
||||||
setImportingModels(importingModels)
|
setImportingModels(importingModels)
|
||||||
setImportModelStage('MODEL_SELECTED')
|
setImportModelStage('MODEL_SELECTED')
|
||||||
}, [setImportingModels, setImportModelStage])
|
}, [setImportingModels, setImportModelStage])
|
||||||
|
|
||||||
const onDrop = useCallback(
|
|
||||||
(acceptedFiles: File[]) => {
|
|
||||||
const filePathWithSize = getFileInfoFromFile(acceptedFiles)
|
|
||||||
|
|
||||||
const importingModels: ImportingModel[] = filePathWithSize.map(
|
|
||||||
(file) => ({
|
|
||||||
importId: uuidv4(),
|
|
||||||
modelId: undefined,
|
|
||||||
name: file.name,
|
|
||||||
description: '',
|
|
||||||
path: file.path,
|
|
||||||
tags: [],
|
|
||||||
size: file.size,
|
|
||||||
status: 'PREPARING',
|
|
||||||
format: 'gguf',
|
|
||||||
})
|
|
||||||
)
|
|
||||||
if (importingModels.length === 0) return
|
|
||||||
|
|
||||||
setImportingModels(importingModels)
|
|
||||||
setImportModelStage('MODEL_SELECTED')
|
|
||||||
},
|
|
||||||
[setImportModelStage, setImportingModels]
|
|
||||||
)
|
|
||||||
|
|
||||||
const { isDragActive, getRootProps } = useDropzone({
|
const { isDragActive, getRootProps } = useDropzone({
|
||||||
noClick: true,
|
noClick: true,
|
||||||
multiple: true,
|
multiple: true,
|
||||||
onDrop,
|
onDrop: onDropModels,
|
||||||
})
|
})
|
||||||
|
|
||||||
const borderColor = isDragActive ? 'border-primary' : 'border-[#F4F4F5]'
|
const borderColor = isDragActive ? 'border-primary' : 'border-[#F4F4F5]'
|
||||||
const textColor = isDragActive ? 'text-primary' : 'text-[#71717A]'
|
const textColor = isDragActive ? 'text-blue-600' : 'text-[#71717A]'
|
||||||
const dragAndDropBgColor = isDragActive ? 'bg-[#EFF6FF]' : 'bg-white'
|
const dragAndDropBgColor = isDragActive ? 'bg-[#EFF6FF]' : 'bg-white'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -128,7 +116,7 @@ const SelectingModelModal: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<span className="text-sm font-bold text-primary">
|
<span className="text-sm font-bold text-blue-600">
|
||||||
Click to upload
|
Click to upload
|
||||||
</span>
|
</span>
|
||||||
<span className={`text-sm ${textColor} font-medium`}>
|
<span className={`text-sm ${textColor} font-medium`}>
|
||||||
|
|||||||
@ -15,7 +15,6 @@ const SettingMenu: React.FC<Props> = ({ activeMenu, onMenuClick }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMenus([
|
setMenus([
|
||||||
'My Models',
|
'My Models',
|
||||||
'My Settings',
|
|
||||||
'Advanced Settings',
|
'Advanced Settings',
|
||||||
...(window.electronAPI ? ['Extensions'] : []),
|
...(window.electronAPI ? ['Extensions'] : []),
|
||||||
])
|
])
|
||||||
@ -39,7 +38,7 @@ const SettingMenu: React.FC<Props> = ({ activeMenu, onMenuClick }) => {
|
|||||||
|
|
||||||
{isActive && (
|
{isActive && (
|
||||||
<m.div
|
<m.div
|
||||||
className="absolute inset-0 -left-3 h-full w-[calc(100%+24px)] rounded-md bg-primary/50"
|
className="absolute inset-0 -left-3 h-full w-[calc(100%+24px)] rounded-md bg-gray-200"
|
||||||
layoutId="active-static-menu"
|
layoutId="active-static-menu"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
import Advanced from '@/screens/Settings/Advanced'
|
import Advanced from '@/screens/Settings/Advanced'
|
||||||
import AppearanceOptions from '@/screens/Settings/Appearance'
|
|
||||||
import ExtensionCatalog from '@/screens/Settings/CoreExtensions'
|
import ExtensionCatalog from '@/screens/Settings/CoreExtensions'
|
||||||
|
|
||||||
import Models from '@/screens/Settings/Models'
|
import Models from '@/screens/Settings/Models'
|
||||||
@ -14,9 +14,6 @@ const handleShowOptions = (menu: string) => {
|
|||||||
case 'Extensions':
|
case 'Extensions':
|
||||||
return <ExtensionCatalog />
|
return <ExtensionCatalog />
|
||||||
|
|
||||||
case 'My Settings':
|
|
||||||
return <AppearanceOptions />
|
|
||||||
|
|
||||||
case 'Advanced Settings':
|
case 'Advanced Settings':
|
||||||
return <Advanced />
|
return <Advanced />
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
.message {
|
.message {
|
||||||
@apply text-black dark:text-gray-300;
|
@apply text-black;
|
||||||
white-space: pre-line;
|
white-space: pre-line;
|
||||||
|
|
||||||
ul,
|
ul,
|
||||||
@ -10,7 +10,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@apply text-blue-600 dark:text-blue-300;
|
@apply text-blue-600;
|
||||||
&:hover {
|
&:hover {
|
||||||
@apply underline;
|
@apply underline;
|
||||||
}
|
}
|
||||||
|
|||||||
6
web/types/appearance.d.ts
vendored
6
web/types/appearance.d.ts
vendored
@ -1,6 +0,0 @@
|
|||||||
type PrimaryColor = 'primary-blue' | 'primary-green' | 'primary-purple'
|
|
||||||
|
|
||||||
type UserConfig = {
|
|
||||||
gettingStartedShow?: boolean
|
|
||||||
primaryColor?: PrimaryColor
|
|
||||||
}
|
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { baseName } from '@janhq/core'
|
||||||
|
|
||||||
export type FilePathWithSize = {
|
export type FilePathWithSize = {
|
||||||
path: string
|
path: string
|
||||||
name: string
|
name: string
|
||||||
@ -8,24 +10,17 @@ export interface FileWithPath extends File {
|
|||||||
path?: string
|
path?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getFileNameFromPath = (filePath: string): string => {
|
export const getFileInfoFromFile = async (
|
||||||
let fileName = filePath.split('/').pop() ?? ''
|
|
||||||
if (fileName.split('.').length > 1) {
|
|
||||||
fileName = fileName.split('.').slice(0, -1).join('.')
|
|
||||||
}
|
|
||||||
|
|
||||||
return fileName
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getFileInfoFromFile = (
|
|
||||||
files: FileWithPath[]
|
files: FileWithPath[]
|
||||||
): FilePathWithSize[] => {
|
): Promise<FilePathWithSize[]> => {
|
||||||
const result: FilePathWithSize[] = []
|
const result: FilePathWithSize[] = []
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (file.path && file.path.length > 0) {
|
if (file.path && file.path.length > 0) {
|
||||||
|
const fileName = await baseName(file.path)
|
||||||
|
|
||||||
result.push({
|
result.push({
|
||||||
path: file.path,
|
path: file.path,
|
||||||
name: getFileNameFromPath(file.path),
|
name: fileName,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user