diff --git a/README.md b/README.md
index d91366581..4d07ad55c 100644
--- a/README.md
+++ b/README.md
@@ -76,31 +76,31 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute
Experimental (Nightly Build)
-
+
jan.exe
-
+
Intel
-
+
M1/M2
-
+
jan.deb
-
+
jan.AppImage
diff --git a/core/src/api/index.ts b/core/src/api/index.ts
index c7dd9146e..7fb8eeb38 100644
--- a/core/src/api/index.ts
+++ b/core/src/api/index.ts
@@ -49,7 +49,7 @@ export enum DownloadEvent {
export enum LocalImportModelEvent {
onLocalImportModelUpdate = 'onLocalImportModelUpdate',
- onLocalImportModelError = 'onLocalImportModelError',
+ onLocalImportModelFailed = 'onLocalImportModelFailed',
onLocalImportModelSuccess = 'onLocalImportModelSuccess',
onLocalImportModelFinished = 'onLocalImportModelFinished',
}
diff --git a/core/src/core.ts b/core/src/core.ts
index 8831c6001..6e2442c2b 100644
--- a/core/src/core.ts
+++ b/core/src/core.ts
@@ -65,7 +65,7 @@ const joinPath: (paths: string[]) => Promise = (paths) => global.core.ap
* @param path - The path to retrieve.
* @returns {Promise} A promise that resolves with the basename.
*/
-const baseName: (paths: string[]) => Promise = (path) => global.core.api?.baseName(path)
+const baseName: (paths: string) => Promise = (path) => global.core.api?.baseName(path)
/**
* Opens an external URL in the default web browser.
diff --git a/core/src/types/model/modelImport.ts b/core/src/types/model/modelImport.ts
index 8977c42a0..7c72a691b 100644
--- a/core/src/types/model/modelImport.ts
+++ b/core/src/types/model/modelImport.ts
@@ -19,4 +19,5 @@ export type ImportingModel = {
status: ImportingModelStatus
format: string
percentage?: number
+ error?: string
}
diff --git a/docs/docs/community/community.md b/docs/docs/community/community.mdx
similarity index 59%
rename from docs/docs/community/community.md
rename to docs/docs/community/community.mdx
index 24a87daf0..d4866490e 100644
--- a/docs/docs/community/community.md
+++ b/docs/docs/community/community.mdx
@@ -29,3 +29,18 @@ keywords:
## Careers
- [Jobs](https://janai.bamboohr.com/careers)
+
+## Newsletter
+
+
diff --git a/docs/docs/wall-of-love.md b/docs/docs/wall-of-love.md
index f196c90e9..f6bfe79d8 100644
--- a/docs/docs/wall-of-love.md
+++ b/docs/docs/wall-of-love.md
@@ -1,3 +1,95 @@
---
title: Wall of Love ❤️
----
\ No newline at end of file
+---
+
+## Twitter
+
+Check out our amazing users and what they are saying about Jan!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+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
+
+
+VIDEO
+
+
+
+
+### Jan AI: Run Open Source LLM 100% Local with OpenAI endpoints
+
+
+VIDEO
+
+
+
+
+### Setup Tutorial on Jan.ai. JAN AI: Run open source LLM on local Windows PC. 100% offline LLM and AI.
+
+
+VIDEO
+
+
+
+
+### Jan.ai: Like Offline ChatGPT on Your Computer 💡
+
+
+VIDEO
+
+
+
+
+### Jan: Bring AI to your Desktop With 100% Offline AI
+
+
+VIDEO
+
+
+
+
+### AI on Your Local PC: Install JanAI (ChatGPT alternative) for Enhanced Privacy
+
+
+VIDEO
+
+
+
+
+### Install Jan to Run LLM Offline and Local First
+
+
+VIDEO
+
diff --git a/docs/src/containers/Footer/index.js b/docs/src/containers/Footer/index.js
index 7cd648149..3e62f579a 100644
--- a/docs/src/containers/Footer/index.js
+++ b/docs/src/containers/Footer/index.js
@@ -86,6 +86,10 @@ const menus = [
path: "https://janai.bamboohr.com/careers",
external: true,
},
+ {
+ menu: "Newsletter",
+ path: "/community#newsletter",
+ }
],
},
];
diff --git a/extensions/model-extension/src/index.ts b/extensions/model-extension/src/index.ts
index dd5bcdf26..fb1f26885 100644
--- a/extensions/model-extension/src/index.ts
+++ b/extensions/model-extension/src/index.ts
@@ -16,6 +16,7 @@ import {
OptionType,
ImportingModel,
LocalImportModelEvent,
+ baseName,
} from '@janhq/core'
import { extractFileName } from './helpers/path'
@@ -488,7 +489,7 @@ export default class JanModelExtension extends ModelExtension {
return
}
- const binaryFileName = extractFileName(modelBinaryPath, '')
+ const binaryFileName = await baseName(modelBinaryPath)
const model: Model = {
...defaultModel,
@@ -555,7 +556,7 @@ export default class JanModelExtension extends ModelExtension {
model: ImportingModel,
optionType: OptionType
): Promise {
- const binaryName = extractFileName(model.path, '').replace(/\s/g, '')
+ const binaryName = (await baseName(model.path)).replace(/\s/g, '')
let modelFolderName = binaryName
if (binaryName.endsWith(JanModelExtension._supportedModelFormat)) {
@@ -568,7 +569,7 @@ export default class JanModelExtension extends ModelExtension {
const modelFolderPath = await this.getModelFolderName(modelFolderName)
await fs.mkdirSync(modelFolderPath)
- const uniqueFolderName = modelFolderPath.split('/').pop()
+ const uniqueFolderName = await baseName(modelFolderPath)
const modelBinaryFile = binaryName.endsWith(
JanModelExtension._supportedModelFormat
)
@@ -637,14 +638,21 @@ export default class JanModelExtension extends ModelExtension {
for (const model of models) {
events.emit(LocalImportModelEvent.onLocalImportModelUpdate, model)
- const importedModel = await this.importModel(model, optionType)
-
- events.emit(LocalImportModelEvent.onLocalImportModelSuccess, {
- ...model,
- modelId: importedModel.id,
- })
- importedModels.push(importedModel)
+ try {
+ const importedModel = await this.importModel(model, optionType)
+ events.emit(LocalImportModelEvent.onLocalImportModelSuccess, {
+ ...model,
+ modelId: importedModel.id,
+ })
+ importedModels.push(importedModel)
+ } catch (err) {
+ events.emit(LocalImportModelEvent.onLocalImportModelFailed, {
+ ...model,
+ error: err,
+ })
+ }
}
+
events.emit(
LocalImportModelEvent.onLocalImportModelFinished,
importedModels
diff --git a/uikit/src/button/styles.scss b/uikit/src/button/styles.scss
index 003df5b4d..c97bec9e0 100644
--- a/uikit/src/button/styles.scss
+++ b/uikit/src/button/styles.scss
@@ -5,11 +5,11 @@
@apply disabled:pointer-events-none disabled:bg-zinc-100 disabled:text-zinc-400;
&-primary {
- @apply bg-primary hover:bg-primary/90 text-white;
+ @apply bg-blue-600 text-white hover:bg-blue-600/90;
}
&-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 {
@@ -17,7 +17,7 @@
}
&-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 {
@@ -66,7 +66,7 @@
[type='reset'],
[type='submit'] {
&.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;
}
&.btn-secondary {
diff --git a/uikit/src/checkbox/styles.scss b/uikit/src/checkbox/styles.scss
index 33610f837..cf35ed5ca 100644
--- a/uikit/src/checkbox/styles.scss
+++ b/uikit/src/checkbox/styles.scss
@@ -1,5 +1,5 @@
.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 {
@apply h-4 w-4;
diff --git a/uikit/src/input/styles.scss b/uikit/src/input/styles.scss
index e649f494d..51efd8e57 100644
--- a/uikit/src/input/styles.scss
+++ b/uikit/src/input/styles.scss
@@ -1,6 +1,6 @@
.input {
@apply border-border placeholder:text-muted-foreground flex h-9 w-full rounded-lg border bg-transparent px-3 py-1 transition-colors;
- @apply disabled:text-muted-foreground disabled:cursor-not-allowed disabled:bg-zinc-100 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 file:border-0 file:bg-transparent file:font-medium;
}
diff --git a/uikit/src/progress/styles.scss b/uikit/src/progress/styles.scss
index 0b7078f48..1a8483c47 100644
--- a/uikit/src/progress/styles.scss
+++ b/uikit/src/progress/styles.scss
@@ -1,7 +1,7 @@
.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 {
- @apply bg-primary h-full w-full flex-1 transition-all;
+ @apply h-full w-full flex-1 bg-blue-600 transition-all;
}
}
diff --git a/uikit/src/select/styles.scss b/uikit/src/select/styles.scss
index 90485723a..99db49766 100644
--- a/uikit/src/select/styles.scss
+++ b/uikit/src/select/styles.scss
@@ -1,6 +1,6 @@
.select {
@apply placeholder:text-muted-foreground border-border flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm disabled:cursor-not-allowed [&>span]:line-clamp-1;
- @apply disabled:text-muted-foreground disabled:cursor-not-allowed disabled:bg-zinc-100 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;
&-caret {
diff --git a/uikit/src/slider/styles.scss b/uikit/src/slider/styles.scss
index 718972efb..465392419 100644
--- a/uikit/src/slider/styles.scss
+++ b/uikit/src/slider/styles.scss
@@ -2,7 +2,7 @@
@apply relative flex w-full touch-none select-none items-center;
&-track {
- @apply relative h-1.5 w-full grow overflow-hidden rounded-full bg-gray-200 dark:bg-gray-800;
+ @apply relative h-1.5 w-full grow overflow-hidden rounded-full bg-gray-200;
[data-disabled] {
@apply cursor-not-allowed opacity-50;
}
@@ -13,6 +13,6 @@
}
&-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;
}
}
diff --git a/uikit/src/switch/styles.scss b/uikit/src/switch/styles.scss
index c8a12cdf5..57fa128ba 100644
--- a/uikit/src/switch/styles.scss
+++ b/uikit/src/switch/styles.scss
@@ -1,7 +1,7 @@
.switch {
@apply inline-flex h-[20px] w-[36px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent;
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2;
- @apply data-[state=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;
&-toggle {
diff --git a/uikit/src/tooltip/styles.scss b/uikit/src/tooltip/styles.scss
index 8ae645cee..169e081b7 100644
--- a/uikit/src/tooltip/styles.scss
+++ b/uikit/src/tooltip/styles.scss
@@ -1,6 +1,6 @@
.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 {
- @apply dark:fill-input fill-gray-950;
+ @apply fill-gray-950;
}
}
diff --git a/web/app/layout.tsx b/web/app/layout.tsx
index 6c6fc65ab..37bcdf53e 100644
--- a/web/app/layout.tsx
+++ b/web/app/layout.tsx
@@ -15,7 +15,7 @@ export const metadata: Metadata = {
export default function RootLayout({ children }: PropsWithChildren) {
return (
-
+
{children}
diff --git a/web/containers/CardSidebar/index.tsx b/web/containers/CardSidebar/index.tsx
index 132494d48..3013360e9 100644
--- a/web/containers/CardSidebar/index.tsx
+++ b/web/containers/CardSidebar/index.tsx
@@ -45,7 +45,7 @@ export default function CardSidebar({
return (
@@ -61,7 +61,7 @@ export default function CardSidebar({
if (!children) return
setShow(!show)
}}
- className="flex w-full flex-1 items-center space-x-2 rounded-lg bg-zinc-100 py-2 pr-2 dark:bg-zinc-900"
+ className="flex w-full flex-1 items-center space-x-2 rounded-lg bg-zinc-100 py-2 pr-2"
>
setMore(!more)}
>
@@ -114,7 +114,7 @@ export default function CardSidebar({
<>
{title === 'Model' ? (
-
+
{openFileTitle()}
@@ -122,7 +122,7 @@ export default function CardSidebar({
) : (
-
+
{openFileTitle()}
)}
@@ -141,7 +141,7 @@ export default function CardSidebar({
/>
<>
-
+
Edit Global Defaults for{' '}
diff --git a/web/containers/Checkbox/index.tsx b/web/containers/Checkbox/index.tsx
index a545771b6..1ced3e19d 100644
--- a/web/containers/Checkbox/index.tsx
+++ b/web/containers/Checkbox/index.tsx
@@ -34,12 +34,10 @@ const Checkbox: React.FC = ({
return (
-
- {title}
-
+
{title}
-
+
diff --git a/web/containers/DropdownListSidebar/index.tsx b/web/containers/DropdownListSidebar/index.tsx
index c05d26e51..dc5ee2605 100644
--- a/web/containers/DropdownListSidebar/index.tsx
+++ b/web/containers/DropdownListSidebar/index.tsx
@@ -203,15 +203,14 @@ const DropdownListSidebar = ({
isTabActive === 1 && '[&_.select-scroll-down-button]:hidden'
)}
>
-
-
+
+
{engineOptions.map((name, i) => {
return (
setIsTabActive(i)}
@@ -230,8 +229,7 @@ const DropdownListSidebar = ({
{name}
diff --git a/web/containers/GPUDriverPromptModal/index.tsx b/web/containers/GPUDriverPromptModal/index.tsx
index bdcf1b2f8..8d11b4efa 100644
--- a/web/containers/GPUDriverPromptModal/index.tsx
+++ b/web/containers/GPUDriverPromptModal/index.tsx
@@ -60,7 +60,7 @@ const GPUDriverPrompt: React.FC = () => {
id="default-checkbox"
type="checkbox"
onChange={onDoNotShowAgainChange}
- className="h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-2 focus:ring-blue-500 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"
/>
Don't show again
diff --git a/web/containers/Layout/BottomBar/DownloadingState/index.tsx b/web/containers/Layout/BottomBar/DownloadingState/index.tsx
index dcebacd3c..4c3d596b0 100644
--- a/web/containers/Layout/BottomBar/DownloadingState/index.tsx
+++ b/web/containers/Layout/BottomBar/DownloadingState/index.tsx
@@ -47,7 +47,7 @@ export default function DownloadingState() {
{
className="h-2 w-24"
value={transferredSize / totalSize}
/>
-
+
{progress.toFixed(2)}%
diff --git a/web/containers/Layout/BottomBar/SystemMonitor/index.tsx b/web/containers/Layout/BottomBar/SystemMonitor/index.tsx
index 90510aae7..989ae7777 100644
--- a/web/containers/Layout/BottomBar/SystemMonitor/index.tsx
+++ b/web/containers/Layout/BottomBar/SystemMonitor/index.tsx
@@ -140,7 +140,7 @@ const SystemMonitor = () => {
{gpus.length > 0 && (
{gpus.map((gpu, index) => (
-
+
{gpu.name}
diff --git a/web/containers/Layout/Ribbon/index.tsx b/web/containers/Layout/Ribbon/index.tsx
index c0bc46586..dc6191f09 100644
--- a/web/containers/Layout/Ribbon/index.tsx
+++ b/web/containers/Layout/Ribbon/index.tsx
@@ -45,7 +45,7 @@ export default function RibbonNav() {
size={20}
className={twMerge(
'flex-shrink-0 text-muted-foreground',
- serverEnabled && 'text-gray-300 dark:text-gray-700'
+ serverEnabled && 'text-gray-300'
)}
/>
),
@@ -114,7 +114,7 @@ export default function RibbonNav() {
{isActive && (
)}
@@ -166,7 +166,7 @@ export default function RibbonNav() {
{isActive && (
)}
diff --git a/web/containers/Layout/TopBar/index.tsx b/web/containers/Layout/TopBar/index.tsx
index 605d8e44d..9686a7fd9 100644
--- a/web/containers/Layout/TopBar/index.tsx
+++ b/web/containers/Layout/TopBar/index.tsx
@@ -159,7 +159,7 @@ const TopBar = () => {
size={16}
className="text-muted-foreground"
/>
-
+
{openFileTitle()}
@@ -175,7 +175,7 @@ const TopBar = () => {
className="mt-0.5 flex-shrink-0 text-muted-foreground"
/>
-
+
Edit Threads Settings
@@ -204,7 +204,7 @@ const TopBar = () => {
className="text-muted-foreground"
/>
-
+
{openFileTitle()}
diff --git a/web/containers/Loader/index.tsx b/web/containers/Loader/index.tsx
index dcf1bec65..cf6604fc8 100644
--- a/web/containers/Loader/index.tsx
+++ b/web/containers/Loader/index.tsx
@@ -7,12 +7,12 @@ export default function Loader({ description }: Props) {
{description}
diff --git a/web/containers/ModalTroubleShoot/AppLogs.tsx b/web/containers/ModalTroubleShoot/AppLogs.tsx
index d4f6bddb8..98f076599 100644
--- a/web/containers/ModalTroubleShoot/AppLogs.tsx
+++ b/web/containers/ModalTroubleShoot/AppLogs.tsx
@@ -28,7 +28,7 @@ const AppLogs = () => {
{
clipboard.copy(logs.slice(-50) ?? '')
}}
diff --git a/web/containers/ModalTroubleShoot/DeviceSpecs.tsx b/web/containers/ModalTroubleShoot/DeviceSpecs.tsx
index 5ebb610d1..282c1dc9e 100644
--- a/web/containers/ModalTroubleShoot/DeviceSpecs.tsx
+++ b/web/containers/ModalTroubleShoot/DeviceSpecs.tsx
@@ -16,7 +16,7 @@ const DeviceSpecs = () => {
{
clipboard.copy(userAgent ?? '')
}}
diff --git a/web/containers/ModalTroubleShoot/index.tsx b/web/containers/ModalTroubleShoot/index.tsx
index 2438d6333..675a1fc07 100644
--- a/web/containers/ModalTroubleShoot/index.tsx
+++ b/web/containers/ModalTroubleShoot/index.tsx
@@ -38,7 +38,7 @@ const ModalTroubleShooting: React.FC = () => {
troubleshooting guide
@@ -65,7 +65,7 @@ const ModalTroubleShooting: React.FC = () => {
Discord
@@ -77,8 +77,8 @@ const ModalTroubleShooting: React.FC = () => {
{/* TODO @faisal replace this once we have better tabs component UI */}
-
-
+
+
{logOption.map((name, i) => {
return (
{
{name}
{isTabActive === i && (
)}
diff --git a/web/containers/ModelConfigInput/index.tsx b/web/containers/ModelConfigInput/index.tsx
index d573a0bf9..f99d3eb39 100644
--- a/web/containers/ModelConfigInput/index.tsx
+++ b/web/containers/ModelConfigInput/index.tsx
@@ -30,12 +30,10 @@ const ModelConfigInput: React.FC = ({
return (
-
- {title}
-
+
{title}
-
+
diff --git a/web/containers/OpenAiKeyInput/index.tsx b/web/containers/OpenAiKeyInput/index.tsx
index 7ef97cf38..89e3c85ef 100644
--- a/web/containers/OpenAiKeyInput/index.tsx
+++ b/web/containers/OpenAiKeyInput/index.tsx
@@ -33,7 +33,7 @@ const OpenAiKeyInput: React.FC = () => {
API Key
diff --git a/web/containers/Providers/ModelImportListener.tsx b/web/containers/Providers/ModelImportListener.tsx
index 60347ba40..f1ca2a768 100644
--- a/web/containers/Providers/ModelImportListener.tsx
+++ b/web/containers/Providers/ModelImportListener.tsx
@@ -12,6 +12,7 @@ import { useSetAtom } from 'jotai'
import { snackbar } from '../Toast'
import {
+ setImportingModelErrorAtom,
setImportingModelSuccessAtom,
updateImportingModelProgressAtom,
} from '@/helpers/atoms/Model.atom'
@@ -21,6 +22,7 @@ const ModelImportListener = ({ children }: PropsWithChildren) => {
updateImportingModelProgressAtom
)
const setImportingModelSuccess = useSetAtom(setImportingModelSuccessAtom)
+ const setImportingModelFailed = useSetAtom(setImportingModelErrorAtom)
const onImportModelUpdate = useCallback(
async (state: ImportingModel) => {
@@ -30,6 +32,14 @@ const ModelImportListener = ({ children }: PropsWithChildren) => {
[updateImportingModelProgress]
)
+ const onImportModelFailed = useCallback(
+ async (state: ImportingModel) => {
+ if (!state.importId) return
+ setImportingModelFailed(state.importId, state.error ?? '')
+ },
+ [setImportingModelFailed]
+ )
+
const onImportModelSuccess = useCallback(
(state: ImportingModel) => {
if (!state.modelId) return
@@ -62,6 +72,10 @@ const ModelImportListener = ({ children }: PropsWithChildren) => {
LocalImportModelEvent.onLocalImportModelFinished,
onImportModelFinished
)
+ events.on(
+ LocalImportModelEvent.onLocalImportModelFailed,
+ onImportModelFailed
+ )
return () => {
console.debug('ModelImportListener: unregistering event listeners...')
@@ -77,8 +91,17 @@ const ModelImportListener = ({ children }: PropsWithChildren) => {
LocalImportModelEvent.onLocalImportModelFinished,
onImportModelFinished
)
+ events.off(
+ LocalImportModelEvent.onLocalImportModelFailed,
+ onImportModelFailed
+ )
}
- }, [onImportModelUpdate, onImportModelSuccess, onImportModelFinished])
+ }, [
+ onImportModelUpdate,
+ onImportModelSuccess,
+ onImportModelFinished,
+ onImportModelFailed,
+ ])
return
{children}
}
diff --git a/web/containers/Providers/Theme.tsx b/web/containers/Providers/Theme.tsx
index bc37c72cc..71b8df434 100644
--- a/web/containers/Providers/Theme.tsx
+++ b/web/containers/Providers/Theme.tsx
@@ -6,17 +6,9 @@ import { ThemeProvider } from 'next-themes'
import { motion as m } from 'framer-motion'
-import { useBodyClass } from '@/hooks/useBodyClass'
-
-import { useUserConfigs } from '@/hooks/useUserConfigs'
-
export default function ThemeWrapper({ children }: PropsWithChildren) {
- const [config] = useUserConfigs()
-
- useBodyClass(config.primaryColor || 'primary-yellow')
-
return (
-
+
{
{
clipboard.copy(logs.slice(-100) ?? '')
}}
diff --git a/web/containers/SliderRightPanel/index.tsx b/web/containers/SliderRightPanel/index.tsx
index 7c017e70f..fa1fdb4f6 100644
--- a/web/containers/SliderRightPanel/index.tsx
+++ b/web/containers/SliderRightPanel/index.tsx
@@ -42,12 +42,10 @@ const SliderRightPanel: React.FC = ({
return (
-
- {title}
-
+
{title}
-
+
diff --git a/web/containers/Toast/index.tsx b/web/containers/Toast/index.tsx
index eae340fee..a077127dd 100644
--- a/web/containers/Toast/index.tsx
+++ b/web/containers/Toast/index.tsx
@@ -108,11 +108,11 @@ export function toaster(props: Props) {
return (
-
+
{renderIcon(type)}
{title}
@@ -120,7 +120,7 @@ export function toaster(props: Props) {
toast.dismiss(t.id)}
/>
@@ -138,16 +138,16 @@ export function snackbar(props: Props) {
return (
-
+
{renderIcon(type)}
{description}
toast.dismiss(t.id)}
/>
diff --git a/web/helpers/atoms/Model.atom.ts b/web/helpers/atoms/Model.atom.ts
index 7a6aa6440..da6dc5918 100644
--- a/web/helpers/atoms/Model.atom.ts
+++ b/web/helpers/atoms/Model.atom.ts
@@ -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(
null,
(get, set, importId: string, modelId: string) => {
diff --git a/web/hooks/useDropModelBinaries.ts b/web/hooks/useDropModelBinaries.ts
new file mode 100644
index 000000000..c08e1dc73
--- /dev/null
+++ b/web/hooks/useDropModelBinaries.ts
@@ -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 }
+}
diff --git a/web/hooks/useUserConfigs.ts b/web/hooks/useUserConfigs.ts
deleted file mode 100644
index 250d20a80..000000000
--- a/web/hooks/useUserConfigs.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { useAtom } from 'jotai'
-import { atomWithStorage } from 'jotai/utils'
-
-export const userConfigs = atomWithStorage
('config', {
- gettingStartedShow: true,
- primaryColor: 'primary-blue',
-})
-
-export function useUserConfigs() {
- return useAtom(userConfigs)
-}
diff --git a/web/screens/Chat/CleanThreadModal/index.tsx b/web/screens/Chat/CleanThreadModal/index.tsx
index 6ef505e6f..7f95850d3 100644
--- a/web/screens/Chat/CleanThreadModal/index.tsx
+++ b/web/screens/Chat/CleanThreadModal/index.tsx
@@ -34,9 +34,7 @@ const CleanThreadModal: React.FC = ({ threadId }) => {
e.stopPropagation()}>
-
- Clean thread
-
+
Clean thread
diff --git a/web/screens/Chat/DeleteThreadModal/index.tsx b/web/screens/Chat/DeleteThreadModal/index.tsx
index edbdb09b4..6c47ce5bf 100644
--- a/web/screens/Chat/DeleteThreadModal/index.tsx
+++ b/web/screens/Chat/DeleteThreadModal/index.tsx
@@ -33,10 +33,8 @@ const DeleteThreadModal: React.FC = ({ threadId }) => {
e.stopPropagation()}>
-
-
- Delete thread
-
+
+ Delete thread
diff --git a/web/screens/Chat/ErrorMessage/index.tsx b/web/screens/Chat/ErrorMessage/index.tsx
index c9041e23a..0ac4757a7 100644
--- a/web/screens/Chat/ErrorMessage/index.tsx
+++ b/web/screens/Chat/ErrorMessage/index.tsx
@@ -54,7 +54,7 @@ const ErrorMessage = ({ message }: { message: ThreadMessage }) => {
Port 3928 is currently unavailable. Check for conflicting apps,
or access
setModalTroubleShooting(true)}
>
troubleshooting assistance
@@ -72,7 +72,7 @@ const ErrorMessage = ({ message }: { message: ThreadMessage }) => {
Jan’s in beta. Access
setModalTroubleShooting(true)}
>
troubleshooting assistance
diff --git a/web/screens/Chat/Sidebar/index.tsx b/web/screens/Chat/Sidebar/index.tsx
index 7187c84b3..4f7e1bd50 100644
--- a/web/screens/Chat/Sidebar/index.tsx
+++ b/web/screens/Chat/Sidebar/index.tsx
@@ -71,7 +71,7 @@ const Sidebar: React.FC = () => {
return (
{
Title
@@ -106,7 +106,7 @@ const Sidebar: React.FC = () => {
Threads ID
@@ -127,7 +127,7 @@ const Sidebar: React.FC = () => {
Instructions
@@ -203,14 +203,14 @@ const Sidebar: React.FC = () => {
Retrieval
@@ -269,7 +269,7 @@ const Sidebar: React.FC = () => {
Embedding Model
@@ -277,7 +277,7 @@ const Sidebar: React.FC = () => {
@@ -309,7 +309,7 @@ const Sidebar: React.FC = () => {
Vector Database
@@ -317,7 +317,7 @@ const Sidebar: React.FC = () => {
diff --git a/web/screens/Chat/SimpleTextMessage/index.tsx b/web/screens/Chat/SimpleTextMessage/index.tsx
index c3bdc8661..e5e2364b4 100644
--- a/web/screens/Chat/SimpleTextMessage/index.tsx
+++ b/web/screens/Chat/SimpleTextMessage/index.tsx
@@ -18,7 +18,7 @@ import hljs from 'highlight.js'
import { useAtomValue } from 'jotai'
import { FolderOpenIcon } from 'lucide-react'
-import { Marked, Renderer, marked as markedDefault } from 'marked'
+import { Marked, Renderer } from 'marked'
import { markedHighlight } from 'marked-highlight'
@@ -43,19 +43,6 @@ import {
getCurrentChatMessagesAtom,
} 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 = (props) => {
let text = ''
const isUser = props.role === ChatCompletionRole.User
@@ -282,7 +269,7 @@ const SimpleTextMessage: React.FC = (props) => {
)}
- {isUser && !isMarkdownValue(text) ? (
+ {isUser ? (
<>
{editMessage === props.id ? (
diff --git a/web/screens/Chat/ThreadList/index.tsx b/web/screens/Chat/ThreadList/index.tsx
index eb372f664..0f20e5833 100644
--- a/web/screens/Chat/ThreadList/index.tsx
+++ b/web/screens/Chat/ThreadList/index.tsx
@@ -79,7 +79,7 @@ export default function ThreadList() {
{
onThreadClick(thread)
@@ -90,7 +90,7 @@ export default function ThreadList() {
{thread.updated && displayDate(thread.updated)}
{thread.title}
-
+
{threadStates[thread.id]?.lastMessage
? threadStates[thread.id]?.lastMessage
: 'No new message'}
@@ -98,7 +98,7 @@ export default function ThreadList() {
@@ -109,7 +109,7 @@ export default function ThreadList() {
{activeThreadId === thread.id && (
)}
diff --git a/web/screens/ExploreModels/HuggingFaceSearchModal/index.tsx b/web/screens/ExploreModels/HuggingFaceSearchModal/index.tsx
index a81df29fa..9c91b24fe 100644
--- a/web/screens/ExploreModels/HuggingFaceSearchModal/index.tsx
+++ b/web/screens/ExploreModels/HuggingFaceSearchModal/index.tsx
@@ -26,7 +26,7 @@ export const HuggingFaceSearchModal = () => {
{
setRepoID(e.target.value)
}}
diff --git a/web/screens/ExploreModels/index.tsx b/web/screens/ExploreModels/index.tsx
index c413e3c3b..e7fd3a9dc 100644
--- a/web/screens/ExploreModels/index.tsx
+++ b/web/screens/ExploreModels/index.tsx
@@ -13,7 +13,7 @@ import {
} from '@janhq/uikit'
import { useAtomValue, useSetAtom } from 'jotai'
-import { Plus, SearchIcon } from 'lucide-react'
+import { UploadIcon, SearchIcon } from 'lucide-react'
import { FeatureToggleContext } from '@/context/FeatureToggle'
@@ -91,17 +91,17 @@ const ExploreModelsScreen = () => {
/>
setsearchValue(e.target.value)}
/>
-
- Import Model
+
+ Import Model
{experimentalFeature && (
diff --git a/web/screens/LocalServer/index.tsx b/web/screens/LocalServer/index.tsx
index f9c2cf719..3a8668770 100644
--- a/web/screens/LocalServer/index.tsx
+++ b/web/screens/LocalServer/index.tsx
@@ -181,7 +181,7 @@ const LocalServerScreen = () => {
-
+
Server Options
@@ -231,15 +231,12 @@ const LocalServerScreen = () => {
Cross-Origin-Resource-Sharing (CORS)
-
+
@@ -266,15 +263,12 @@ const LocalServerScreen = () => {
Verbose Server Logs
-
+
@@ -315,13 +309,13 @@ const LocalServerScreen = () => {
{/* Middle Bar */}
-
+
Server Logs
openServerLog()}
>
@@ -330,7 +324,7 @@ const LocalServerScreen = () => {
clearServerLog()}
>
@@ -386,7 +380,7 @@ const LocalServerScreen = () => {
{/* Right bar */}
{
Model failed to start. Access{' '}
setModalTroubleShooting(true)}
>
troubleshooting assistance
diff --git a/web/screens/Settings/Advanced/index.tsx b/web/screens/Settings/Advanced/index.tsx
index 6320d1921..9aa204999 100644
--- a/web/screens/Settings/Advanced/index.tsx
+++ b/web/screens/Settings/Advanced/index.tsx
@@ -282,7 +282,7 @@ const Advanced = () => {
disabled={gpuList.length === 0 || !gpuEnabled}
value={selectedGpu.join()}
>
-
+
{selectedGpu.join()}
diff --git a/web/screens/Settings/Appearance/TogglePrimary.tsx b/web/screens/Settings/Appearance/TogglePrimary.tsx
deleted file mode 100644
index d75117269..000000000
--- a/web/screens/Settings/Appearance/TogglePrimary.tsx
+++ /dev/null
@@ -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 (
-
- {primaryColorOptions.map((option, i) => {
- const isActive = config.primaryColor === option.value
- return (
-
- handleChangeAccent(option.value)}
- />
- {isActive ? (
-
- ) : null}
-
- )
- })}
-
- )
-}
diff --git a/web/screens/Settings/Appearance/ToggleTheme.tsx b/web/screens/Settings/Appearance/ToggleTheme.tsx
index a183348d9..369a216fa 100644
--- a/web/screens/Settings/Appearance/ToggleTheme.tsx
+++ b/web/screens/Settings/Appearance/ToggleTheme.tsx
@@ -28,7 +28,7 @@ export default function ToggleTheme() {
{isActive ? (
) : null}
diff --git a/web/screens/Settings/Appearance/index.tsx b/web/screens/Settings/Appearance/index.tsx
index 51899ba40..9e2c551d6 100644
--- a/web/screens/Settings/Appearance/index.tsx
+++ b/web/screens/Settings/Appearance/index.tsx
@@ -1,4 +1,3 @@
-import ToggleAccent from '@/screens/Settings/Appearance/TogglePrimary'
import ToggleTheme from '@/screens/Settings/Appearance/ToggleTheme'
export default function AppearanceOptions() {
@@ -22,7 +21,6 @@ export default function AppearanceOptions() {
Choose the primary accent color used throughout the app.
-
)
diff --git a/web/screens/Settings/EditModelInfoModal/index.tsx b/web/screens/Settings/EditModelInfoModal/index.tsx
index bb87b7ed9..bc9d6521d 100644
--- a/web/screens/Settings/EditModelInfoModal/index.tsx
+++ b/web/screens/Settings/EditModelInfoModal/index.tsx
@@ -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 {
Modal,
ModalContent,
@@ -47,6 +53,7 @@ const EditModelInfoModal: React.FC = () => {
const janDataFolder = useAtomValue(janDataFolderPathAtom)
const updateImportingModel = useSetAtom(updateImportingModelAtom)
const { updateModelInfo } = useImportModel()
+ const [modelPath, setModelPath] = useState
('')
const editingModel = importingModels.find(
(model) => model.importId === editingModelId
@@ -88,13 +95,19 @@ const EditModelInfoModal: React.FC = () => {
setEditingModelId(undefined)
}
- const modelFolderPath = useMemo(() => {
- return `${janDataFolder}/models/${editingModel?.modelId}`
+ useEffect(() => {
+ const getModelPath = async () => {
+ const modelId = editingModel?.modelId
+ if (!modelId) return ''
+ const path = await joinPath([janDataFolder, 'models', modelId])
+ setModelPath(path)
+ }
+ getModelPath()
}, [janDataFolder, editingModel])
const onShowInFinderClick = useCallback(() => {
- openFileExplorer(modelFolderPath)
- }, [modelFolderPath])
+ openFileExplorer(modelPath)
+ }, [modelPath])
if (!editingModel) {
setImportModelStage('IMPORTING_MODEL')
@@ -104,7 +117,10 @@ const EditModelInfoModal: React.FC = () => {
}
return (
-
+
Edit Model Information
@@ -130,7 +146,7 @@ const EditModelInfoModal: React.FC = () => {
- {modelFolderPath}
+ {modelPath}
{openFileTitle()}
diff --git a/web/screens/Settings/ImportInProgressIcon/index.tsx b/web/screens/Settings/ImportInProgressIcon/index.tsx
index 962b52903..5f7a291c5 100644
--- a/web/screens/Settings/ImportInProgressIcon/index.tsx
+++ b/web/screens/Settings/ImportInProgressIcon/index.tsx
@@ -15,7 +15,8 @@ const ImportInProgressIcon: React.FC = ({
const [isHovered, setIsHovered] = useState(false)
const onMouseOver = () => {
- setIsHovered(true)
+ // for now we don't allow user to cancel importing
+ setIsHovered(false)
}
const onMouseOut = () => {
diff --git a/web/screens/Settings/ImportModelOptionModal/ImportModelOptionSelection.tsx b/web/screens/Settings/ImportModelOptionModal/ImportModelOptionSelection.tsx
index 5276ee195..e4f5c4cf8 100644
--- a/web/screens/Settings/ImportModelOptionModal/ImportModelOptionSelection.tsx
+++ b/web/screens/Settings/ImportModelOptionModal/ImportModelOptionSelection.tsx
@@ -16,7 +16,7 @@ const ImportModelOptionSelection: React.FC = ({
onClick={() => setSelectedOptionType(option.type)}
>
- {checked &&
}
+ {checked &&
}
diff --git a/web/screens/Settings/ImportSuccessIcon/index.tsx b/web/screens/Settings/ImportSuccessIcon/index.tsx
index ae6526d78..6bc8d0b9d 100644
--- a/web/screens/Settings/ImportSuccessIcon/index.tsx
+++ b/web/screens/Settings/ImportSuccessIcon/index.tsx
@@ -29,7 +29,7 @@ const ImportSuccessIcon: React.FC
= ({ onEditModelClick }) => {
}
const SuccessIcon: React.FC = React.memo(() => (
-
+
))
diff --git a/web/screens/Settings/ImportingModelModal/ImportingModelItem.tsx b/web/screens/Settings/ImportingModelModal/ImportingModelItem.tsx
index b7cea35c9..7b3889128 100644
--- a/web/screens/Settings/ImportingModelModal/ImportingModelItem.tsx
+++ b/web/screens/Settings/ImportingModelModal/ImportingModelItem.tsx
@@ -1,6 +1,10 @@
+import { useCallback, useMemo } from 'react'
+
import { ImportingModel } from '@janhq/core/.'
import { useSetAtom } from 'jotai'
+import { AlertCircle } from 'lucide-react'
+
import { setImportModelStageAtom } from '@/hooks/useImportModel'
import { toGibibytes } from '@/utils/converter'
@@ -16,28 +20,39 @@ type Props = {
const ImportingModelItem: React.FC
= ({ model }) => {
const setImportModelStage = useSetAtom(setImportModelStageAtom)
const setEditingModelId = useSetAtom(editingModelIdAtom)
- const sizeInGb = toGibibytes(model.size)
- const onEditModelInfoClick = () => {
+ const onEditModelInfoClick = useCallback(() => {
setEditingModelId(model.importId)
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 (
-
{model.name}
-
{sizeInGb}
+
+ {model.name}
+
+
{displayStatus}
- {model.status === 'IMPORTED' || model.status === 'FAILED' ? (
+ {model.status === 'IMPORTED' && (
- ) : (
+ )}
+ {(model.status === 'IMPORTING' || model.status === 'PREPARING') && (
)}
+ {model.status === 'FAILED' &&
}
)
}
diff --git a/web/screens/Settings/ImportingModelModal/index.tsx b/web/screens/Settings/ImportingModelModal/index.tsx
index ef4739f85..4d346f010 100644
--- a/web/screens/Settings/ImportingModelModal/index.tsx
+++ b/web/screens/Settings/ImportingModelModal/index.tsx
@@ -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 {
Button,
Modal,
@@ -31,7 +31,15 @@ const ImportingModelModal: React.FC = () => {
const setImportModelStage = useSetAtom(setImportModelStageAtom)
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(
(model) => model.status === 'IMPORTED'
diff --git a/web/screens/Settings/Models/Row.tsx b/web/screens/Settings/Models/Row.tsx
index 5ade3bad6..9ceeab92a 100644
--- a/web/screens/Settings/Models/Row.tsx
+++ b/web/screens/Settings/Models/Row.tsx
@@ -152,7 +152,7 @@ export default function RowModel(props: RowModelProps) {
) : (
)}
-
+
{isActiveModel ? stateModel.state : 'Start'}
Model
@@ -189,9 +189,7 @@ export default function RowModel(props: RowModelProps) {
}}
>
-
- Delete Model
-
+ Delete Model
)}
diff --git a/web/screens/Settings/Models/index.tsx b/web/screens/Settings/Models/index.tsx
index a77706da2..9f4ff5802 100644
--- a/web/screens/Settings/Models/index.tsx
+++ b/web/screens/Settings/Models/index.tsx
@@ -2,7 +2,6 @@ import { useCallback, useState } from 'react'
import { useDropzone } from 'react-dropzone'
-import { ImportingModel } from '@janhq/core'
import { Button, Input, ScrollArea } from '@janhq/uikit'
import { useAtomValue, useSetAtom } from 'jotai'
@@ -10,60 +9,29 @@ import { Plus, SearchIcon, UploadCloudIcon } from 'lucide-react'
import { twMerge } from 'tailwind-merge'
-import { v4 as uuidv4 } from 'uuid'
-
+import useDropModelBinaries from '@/hooks/useDropModelBinaries'
import { setImportModelStageAtom } from '@/hooks/useImportModel'
-import { getFileInfoFromFile } from '@/utils/file'
-
import RowModel from './Row'
-import {
- downloadedModelsAtom,
- importingModelsAtom,
-} from '@/helpers/atoms/Model.atom'
+import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
const Column = ['Name', 'Model ID', 'Size', 'Version', 'Status', '']
const Models: React.FC = () => {
const downloadedModels = useAtomValue(downloadedModelsAtom)
const setImportModelStage = useSetAtom(setImportModelStageAtom)
- const setImportingModels = useSetAtom(importingModelsAtom)
const [searchValue, setsearchValue] = useState('')
+ const { onDropModels } = useDropModelBinaries()
const filteredDownloadedModels = downloadedModels
.filter((x) => x.name?.toLowerCase().includes(searchValue.toLowerCase()))
.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({
noClick: true,
multiple: true,
- onDrop,
+ onDrop: onDropModels,
})
const onImportModelClick = useCallback(() => {
diff --git a/web/screens/Settings/SelectingModelModal/index.tsx b/web/screens/Settings/SelectingModelModal/index.tsx
index cfdf21392..35bd9b772 100644
--- a/web/screens/Settings/SelectingModelModal/index.tsx
+++ b/web/screens/Settings/SelectingModelModal/index.tsx
@@ -1,7 +1,7 @@
import { useCallback } from 'react'
import { useDropzone } from 'react-dropzone'
-import { ImportingModel, fs } from '@janhq/core'
+import { ImportingModel, baseName, fs } from '@janhq/core'
import { Modal, ModalContent, ModalHeader, ModalTitle } from '@janhq/uikit'
import { useAtomValue, useSetAtom } from 'jotai'
@@ -9,16 +9,15 @@ import { UploadCloudIcon } from 'lucide-react'
import { v4 as uuidv4 } from 'uuid'
+import { snackbar } from '@/containers/Toast'
+
+import useDropModelBinaries from '@/hooks/useDropModelBinaries'
import {
getImportModelStageAtom,
setImportModelStageAtom,
} from '@/hooks/useImportModel'
-import {
- FilePathWithSize,
- getFileInfoFromFile,
- getFileNameFromPath,
-} from '@/utils/file'
+import { FilePathWithSize } from '@/utils/file'
import { importingModelsAtom } from '@/helpers/atoms/Model.atom'
@@ -26,6 +25,7 @@ const SelectingModelModal: React.FC = () => {
const setImportModelStage = useSetAtom(setImportModelStageAtom)
const importModelStage = useAtomValue(getImportModelStageAtom)
const setImportingModels = useSetAtom(importingModelsAtom)
+ const { onDropModels } = useDropModelBinaries()
const onSelectFileClick = useCallback(async () => {
const filePaths = await window.core?.api?.selectModelFiles()
@@ -36,7 +36,7 @@ const SelectingModelModal: React.FC = () => {
const fileStats = await fs.fileStat(filePath, true)
if (!fileStats || fileStats.isDirectory) continue
- const fileName = getFileNameFromPath(filePath)
+ const fileName = await baseName(filePath)
sanitizedFilePaths.push({
path: filePath,
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) => {
return {
importId: uuidv4(),
modelId: undefined,
- name: name,
+ name: name.replace('.gguf', ''),
description: '',
path: path,
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
setImportingModels(importingModels)
setImportModelStage('MODEL_SELECTED')
}, [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({
noClick: true,
multiple: true,
- onDrop,
+ onDrop: onDropModels,
})
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'
return (
@@ -128,7 +116,7 @@ const SelectingModelModal: React.FC = () => {
-
+
Click to upload
diff --git a/web/screens/Settings/SettingMenu/index.tsx b/web/screens/Settings/SettingMenu/index.tsx
index fd0ea1560..9e40d1fda 100644
--- a/web/screens/Settings/SettingMenu/index.tsx
+++ b/web/screens/Settings/SettingMenu/index.tsx
@@ -15,7 +15,6 @@ const SettingMenu: React.FC = ({ activeMenu, onMenuClick }) => {
useEffect(() => {
setMenus([
'My Models',
- 'My Settings',
'Advanced Settings',
...(window.electronAPI ? ['Extensions'] : []),
])
@@ -39,7 +38,7 @@ const SettingMenu: React.FC = ({ activeMenu, onMenuClick }) => {
{isActive && (
)}
diff --git a/web/screens/Settings/index.tsx b/web/screens/Settings/index.tsx
index 1a5e4011e..ed8af3c46 100644
--- a/web/screens/Settings/index.tsx
+++ b/web/screens/Settings/index.tsx
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'
import Advanced from '@/screens/Settings/Advanced'
-import AppearanceOptions from '@/screens/Settings/Appearance'
+
import ExtensionCatalog from '@/screens/Settings/CoreExtensions'
import Models from '@/screens/Settings/Models'
@@ -14,9 +14,6 @@ const handleShowOptions = (menu: string) => {
case 'Extensions':
return
- case 'My Settings':
- return
-
case 'Advanced Settings':
return
diff --git a/web/styles/components/message.scss b/web/styles/components/message.scss
index a9bec5d48..f95061599 100644
--- a/web/styles/components/message.scss
+++ b/web/styles/components/message.scss
@@ -1,5 +1,5 @@
.message {
- @apply text-black dark:text-gray-300;
+ @apply text-black;
white-space: pre-line;
ul,
@@ -10,7 +10,7 @@
}
a {
- @apply text-blue-600 dark:text-blue-300;
+ @apply text-blue-600;
&:hover {
@apply underline;
}
diff --git a/web/types/appearance.d.ts b/web/types/appearance.d.ts
deleted file mode 100644
index 14e0c14c2..000000000
--- a/web/types/appearance.d.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-type PrimaryColor = 'primary-blue' | 'primary-green' | 'primary-purple'
-
-type UserConfig = {
- gettingStartedShow?: boolean
- primaryColor?: PrimaryColor
-}
diff --git a/web/utils/file.ts b/web/utils/file.ts
index 6a812a8e2..4a14de247 100644
--- a/web/utils/file.ts
+++ b/web/utils/file.ts
@@ -1,3 +1,5 @@
+import { baseName } from '@janhq/core'
+
export type FilePathWithSize = {
path: string
name: string
@@ -8,24 +10,17 @@ export interface FileWithPath extends File {
path?: string
}
-export const getFileNameFromPath = (filePath: string): string => {
- let fileName = filePath.split('/').pop() ?? ''
- if (fileName.split('.').length > 1) {
- fileName = fileName.split('.').slice(0, -1).join('.')
- }
-
- return fileName
-}
-
-export const getFileInfoFromFile = (
+export const getFileInfoFromFile = async (
files: FileWithPath[]
-): FilePathWithSize[] => {
+): Promise => {
const result: FilePathWithSize[] = []
for (const file of files) {
if (file.path && file.path.length > 0) {
+ const fileName = await baseName(file.path)
+
result.push({
path: file.path,
- name: getFileNameFromPath(file.path),
+ name: fileName,
size: file.size,
})
}