feat: revamp thread screen (#802)
* Make thread screen as default screen * Blank state when user have not any model * Cleanup topbar thread screen * Improve style right panel * Add instructions right panel * Styling thread list history * Resolve conflict * Default title new thread * Fix trigger panel sidebar * Make default right panel false when no activethread * Fix CI test * chore: assistant instruction with system prompt * Fix title and blank state explore the hub * Claenup style thread screen and add buble message for assitant * Remove unused import * Styling more menus on thread list and right panel, and make max height textarea 400 pixel * Finished revamp ui thread * Finished system monitor UI * Style box running models * Make animate right panel more smooth * Add status arround textarea for starting model info * Temporary disable hide left panel * chore: system resource monitoring update * copy nits * chore: typo * Reverse icon chevron accordion * Move my models into setting page --------- Co-authored-by: Louis <louis@jan.ai> Co-authored-by: 0xSage <n@pragmatic.vc>
This commit is contained in:
parent
e5a440fc8f
commit
424b00338e
@ -143,6 +143,7 @@ export type ThreadAssistantInfo = {
|
|||||||
assistant_id: string;
|
assistant_id: string;
|
||||||
assistant_name: string;
|
assistant_name: string;
|
||||||
model: ModelInfo;
|
model: ModelInfo;
|
||||||
|
instructions?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -288,13 +289,13 @@ export type Assistant = {
|
|||||||
/** Represents the name of the object. */
|
/** Represents the name of the object. */
|
||||||
name: string;
|
name: string;
|
||||||
/** Represents the description of the object. */
|
/** Represents the description of the object. */
|
||||||
description: string;
|
description?: string;
|
||||||
/** Represents the model of the object. */
|
/** Represents the model of the object. */
|
||||||
model: string;
|
model: string;
|
||||||
/** Represents the instructions for the object. */
|
/** Represents the instructions for the object. */
|
||||||
instructions: string;
|
instructions?: string;
|
||||||
/** Represents the tools associated with the object. */
|
/** Represents the tools associated with the object. */
|
||||||
tools: any;
|
tools?: any;
|
||||||
/** Represents the file identifiers associated with the object. */
|
/** Represents the file identifiers associated with the object. */
|
||||||
file_ids: string[];
|
file_ids: string[];
|
||||||
/** Represents the metadata of the object. */
|
/** Represents the metadata of the object. */
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
import { BrowserWindow } from "electron";
|
import { BrowserWindow } from 'electron'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages the current window instance.
|
* Manages the current window instance.
|
||||||
*/
|
*/
|
||||||
export class WindowManager {
|
export class WindowManager {
|
||||||
public static instance: WindowManager = new WindowManager();
|
public static instance: WindowManager = new WindowManager()
|
||||||
public currentWindow?: BrowserWindow;
|
public currentWindow?: BrowserWindow
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
if (WindowManager.instance) {
|
if (WindowManager.instance) {
|
||||||
return WindowManager.instance;
|
return WindowManager.instance
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -21,17 +21,17 @@ export class WindowManager {
|
|||||||
createWindow(options?: Electron.BrowserWindowConstructorOptions | undefined) {
|
createWindow(options?: Electron.BrowserWindowConstructorOptions | undefined) {
|
||||||
this.currentWindow = new BrowserWindow({
|
this.currentWindow = new BrowserWindow({
|
||||||
width: 1200,
|
width: 1200,
|
||||||
minWidth: 800,
|
minWidth: 1200,
|
||||||
height: 800,
|
height: 800,
|
||||||
show: false,
|
show: false,
|
||||||
trafficLightPosition: {
|
trafficLightPosition: {
|
||||||
x: 10,
|
x: 10,
|
||||||
y: 15,
|
y: 15,
|
||||||
},
|
},
|
||||||
titleBarStyle: "hidden",
|
titleBarStyle: 'hidden',
|
||||||
vibrancy: "sidebar",
|
vibrancy: 'sidebar',
|
||||||
...options,
|
...options,
|
||||||
});
|
})
|
||||||
return this.currentWindow;
|
return this.currentWindow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,41 +1,41 @@
|
|||||||
import { _electron as electron } from "playwright";
|
import { _electron as electron } from 'playwright'
|
||||||
import { ElectronApplication, Page, expect, test } from "@playwright/test";
|
import { ElectronApplication, Page, expect, test } from '@playwright/test'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
findLatestBuild,
|
findLatestBuild,
|
||||||
parseElectronApp,
|
parseElectronApp,
|
||||||
stubDialog,
|
stubDialog,
|
||||||
} from "electron-playwright-helpers";
|
} from 'electron-playwright-helpers'
|
||||||
|
|
||||||
let electronApp: ElectronApplication;
|
let electronApp: ElectronApplication
|
||||||
let page: Page;
|
let page: Page
|
||||||
|
|
||||||
test.beforeAll(async () => {
|
test.beforeAll(async () => {
|
||||||
process.env.CI = "e2e";
|
process.env.CI = 'e2e'
|
||||||
|
|
||||||
const latestBuild = findLatestBuild("dist");
|
const latestBuild = findLatestBuild('dist')
|
||||||
expect(latestBuild).toBeTruthy();
|
expect(latestBuild).toBeTruthy()
|
||||||
|
|
||||||
// parse the packaged Electron app and find paths and other info
|
// parse the packaged Electron app and find paths and other info
|
||||||
const appInfo = parseElectronApp(latestBuild);
|
const appInfo = parseElectronApp(latestBuild)
|
||||||
expect(appInfo).toBeTruthy();
|
expect(appInfo).toBeTruthy()
|
||||||
|
|
||||||
electronApp = await electron.launch({
|
electronApp = await electron.launch({
|
||||||
args: [appInfo.main], // main file from package.json
|
args: [appInfo.main], // main file from package.json
|
||||||
executablePath: appInfo.executable, // path to the Electron executable
|
executablePath: appInfo.executable, // path to the Electron executable
|
||||||
});
|
})
|
||||||
await stubDialog(electronApp, "showMessageBox", { response: 1 });
|
await stubDialog(electronApp, 'showMessageBox', { response: 1 })
|
||||||
|
|
||||||
page = await electronApp.firstWindow();
|
page = await electronApp.firstWindow()
|
||||||
});
|
})
|
||||||
|
|
||||||
test.afterAll(async () => {
|
test.afterAll(async () => {
|
||||||
await electronApp.close();
|
await electronApp.close()
|
||||||
await page.close();
|
await page.close()
|
||||||
});
|
})
|
||||||
|
|
||||||
test("explores models", async () => {
|
test('explores models', async () => {
|
||||||
await page.getByTestId("Explore Models").first().click();
|
await page.getByTestId('Hub').first().click()
|
||||||
await page.getByTestId("testid-explore-models").isVisible();
|
await page.getByTestId('testid-explore-models').isVisible()
|
||||||
// More test cases here...
|
// More test cases here...
|
||||||
});
|
})
|
||||||
|
|||||||
@ -1,55 +1,55 @@
|
|||||||
import { _electron as electron } from "playwright";
|
import { _electron as electron } from 'playwright'
|
||||||
import { ElectronApplication, Page, expect, test } from "@playwright/test";
|
import { ElectronApplication, Page, expect, test } from '@playwright/test'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
findLatestBuild,
|
findLatestBuild,
|
||||||
parseElectronApp,
|
parseElectronApp,
|
||||||
stubDialog,
|
stubDialog,
|
||||||
} from "electron-playwright-helpers";
|
} from 'electron-playwright-helpers'
|
||||||
|
|
||||||
let electronApp: ElectronApplication;
|
let electronApp: ElectronApplication
|
||||||
let page: Page;
|
let page: Page
|
||||||
|
|
||||||
test.beforeAll(async () => {
|
test.beforeAll(async () => {
|
||||||
process.env.CI = "e2e";
|
process.env.CI = 'e2e'
|
||||||
|
|
||||||
const latestBuild = findLatestBuild("dist");
|
const latestBuild = findLatestBuild('dist')
|
||||||
expect(latestBuild).toBeTruthy();
|
expect(latestBuild).toBeTruthy()
|
||||||
|
|
||||||
// parse the packaged Electron app and find paths and other info
|
// parse the packaged Electron app and find paths and other info
|
||||||
const appInfo = parseElectronApp(latestBuild);
|
const appInfo = parseElectronApp(latestBuild)
|
||||||
expect(appInfo).toBeTruthy();
|
expect(appInfo).toBeTruthy()
|
||||||
expect(appInfo.asar).toBe(true);
|
expect(appInfo.asar).toBe(true)
|
||||||
expect(appInfo.executable).toBeTruthy();
|
expect(appInfo.executable).toBeTruthy()
|
||||||
expect(appInfo.main).toBeTruthy();
|
expect(appInfo.main).toBeTruthy()
|
||||||
expect(appInfo.name).toBe("jan");
|
expect(appInfo.name).toBe('jan')
|
||||||
expect(appInfo.packageJson).toBeTruthy();
|
expect(appInfo.packageJson).toBeTruthy()
|
||||||
expect(appInfo.packageJson.name).toBe("jan");
|
expect(appInfo.packageJson.name).toBe('jan')
|
||||||
expect(appInfo.platform).toBeTruthy();
|
expect(appInfo.platform).toBeTruthy()
|
||||||
expect(appInfo.platform).toBe(process.platform);
|
expect(appInfo.platform).toBe(process.platform)
|
||||||
expect(appInfo.resourcesDir).toBeTruthy();
|
expect(appInfo.resourcesDir).toBeTruthy()
|
||||||
|
|
||||||
electronApp = await electron.launch({
|
electronApp = await electron.launch({
|
||||||
args: [appInfo.main], // main file from package.json
|
args: [appInfo.main], // main file from package.json
|
||||||
executablePath: appInfo.executable, // path to the Electron executable
|
executablePath: appInfo.executable, // path to the Electron executable
|
||||||
});
|
})
|
||||||
await stubDialog(electronApp, "showMessageBox", { response: 1 });
|
await stubDialog(electronApp, 'showMessageBox', { response: 1 })
|
||||||
|
|
||||||
page = await electronApp.firstWindow();
|
page = await electronApp.firstWindow()
|
||||||
});
|
})
|
||||||
|
|
||||||
test.afterAll(async () => {
|
test.afterAll(async () => {
|
||||||
await electronApp.close();
|
await electronApp.close()
|
||||||
await page.close();
|
await page.close()
|
||||||
});
|
})
|
||||||
|
|
||||||
test("renders the home page", async () => {
|
test('renders the home page', async () => {
|
||||||
expect(page).toBeDefined();
|
expect(page).toBeDefined()
|
||||||
|
|
||||||
// Welcome text is available
|
// Welcome text is available
|
||||||
const welcomeText = await page
|
const welcomeText = await page
|
||||||
.getByTestId("testid-welcome-title")
|
.getByTestId('testid-welcome-title')
|
||||||
.first()
|
.first()
|
||||||
.isVisible();
|
.isVisible()
|
||||||
expect(welcomeText).toBe(false);
|
expect(welcomeText).toBe(false)
|
||||||
});
|
})
|
||||||
|
|||||||
@ -1,41 +0,0 @@
|
|||||||
import { _electron as electron } from "playwright";
|
|
||||||
import { ElectronApplication, Page, expect, test } from "@playwright/test";
|
|
||||||
|
|
||||||
import {
|
|
||||||
findLatestBuild,
|
|
||||||
parseElectronApp,
|
|
||||||
stubDialog,
|
|
||||||
} from "electron-playwright-helpers";
|
|
||||||
|
|
||||||
let electronApp: ElectronApplication;
|
|
||||||
let page: Page;
|
|
||||||
|
|
||||||
test.beforeAll(async () => {
|
|
||||||
process.env.CI = "e2e";
|
|
||||||
|
|
||||||
const latestBuild = findLatestBuild("dist");
|
|
||||||
expect(latestBuild).toBeTruthy();
|
|
||||||
|
|
||||||
// parse the packaged Electron app and find paths and other info
|
|
||||||
const appInfo = parseElectronApp(latestBuild);
|
|
||||||
expect(appInfo).toBeTruthy();
|
|
||||||
|
|
||||||
electronApp = await electron.launch({
|
|
||||||
args: [appInfo.main], // main file from package.json
|
|
||||||
executablePath: appInfo.executable, // path to the Electron executable
|
|
||||||
});
|
|
||||||
await stubDialog(electronApp, "showMessageBox", { response: 1 });
|
|
||||||
|
|
||||||
page = await electronApp.firstWindow();
|
|
||||||
});
|
|
||||||
|
|
||||||
test.afterAll(async () => {
|
|
||||||
await electronApp.close();
|
|
||||||
await page.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("shows my models", async () => {
|
|
||||||
await page.getByTestId("My Models").first().click();
|
|
||||||
await page.getByTestId("testid-my-models").isVisible();
|
|
||||||
// More test cases here...
|
|
||||||
});
|
|
||||||
@ -1,43 +1,43 @@
|
|||||||
import { _electron as electron } from "playwright";
|
import { _electron as electron } from 'playwright'
|
||||||
import { ElectronApplication, Page, expect, test } from "@playwright/test";
|
import { ElectronApplication, Page, expect, test } from '@playwright/test'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
findLatestBuild,
|
findLatestBuild,
|
||||||
parseElectronApp,
|
parseElectronApp,
|
||||||
stubDialog,
|
stubDialog,
|
||||||
} from "electron-playwright-helpers";
|
} from 'electron-playwright-helpers'
|
||||||
|
|
||||||
let electronApp: ElectronApplication;
|
let electronApp: ElectronApplication
|
||||||
let page: Page;
|
let page: Page
|
||||||
|
|
||||||
test.beforeAll(async () => {
|
test.beforeAll(async () => {
|
||||||
process.env.CI = "e2e";
|
process.env.CI = 'e2e'
|
||||||
|
|
||||||
const latestBuild = findLatestBuild("dist");
|
const latestBuild = findLatestBuild('dist')
|
||||||
expect(latestBuild).toBeTruthy();
|
expect(latestBuild).toBeTruthy()
|
||||||
|
|
||||||
// parse the packaged Electron app and find paths and other info
|
// parse the packaged Electron app and find paths and other info
|
||||||
const appInfo = parseElectronApp(latestBuild);
|
const appInfo = parseElectronApp(latestBuild)
|
||||||
expect(appInfo).toBeTruthy();
|
expect(appInfo).toBeTruthy()
|
||||||
|
|
||||||
electronApp = await electron.launch({
|
electronApp = await electron.launch({
|
||||||
args: [appInfo.main], // main file from package.json
|
args: [appInfo.main], // main file from package.json
|
||||||
executablePath: appInfo.executable, // path to the Electron executable
|
executablePath: appInfo.executable, // path to the Electron executable
|
||||||
});
|
})
|
||||||
await stubDialog(electronApp, "showMessageBox", { response: 1 });
|
await stubDialog(electronApp, 'showMessageBox', { response: 1 })
|
||||||
|
|
||||||
page = await electronApp.firstWindow();
|
page = await electronApp.firstWindow()
|
||||||
});
|
})
|
||||||
|
|
||||||
test.afterAll(async () => {
|
test.afterAll(async () => {
|
||||||
await electronApp.close();
|
await electronApp.close()
|
||||||
await page.close();
|
await page.close()
|
||||||
});
|
})
|
||||||
|
|
||||||
test("renders left navigation panel", async () => {
|
test('renders left navigation panel', async () => {
|
||||||
// Chat section should be there
|
// Chat section should be there
|
||||||
const chatSection = await page.getByTestId("Chat").first().isVisible();
|
const chatSection = await page.getByTestId('Chat').first().isVisible()
|
||||||
expect(chatSection).toBe(false);
|
expect(chatSection).toBe(false)
|
||||||
|
|
||||||
// Home actions
|
// Home actions
|
||||||
/* Disable unstable feature tests
|
/* Disable unstable feature tests
|
||||||
@ -45,7 +45,10 @@ test("renders left navigation panel", async () => {
|
|||||||
** Enable back when it is whitelisted
|
** Enable back when it is whitelisted
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const myModelsBtn = await page.getByTestId("My Models").first().isEnabled();
|
const systemMonitorBtn = await page
|
||||||
const settingsBtn = await page.getByTestId("Settings").first().isEnabled();
|
.getByTestId('System Monitor')
|
||||||
expect([myModelsBtn, settingsBtn].filter((e) => !e).length).toBe(0);
|
.first()
|
||||||
});
|
.isEnabled()
|
||||||
|
const settingsBtn = await page.getByTestId('Settings').first().isEnabled()
|
||||||
|
expect([systemMonitorBtn, settingsBtn].filter((e) => !e).length).toBe(0)
|
||||||
|
})
|
||||||
|
|||||||
@ -1,40 +1,40 @@
|
|||||||
import { _electron as electron } from "playwright";
|
import { _electron as electron } from 'playwright'
|
||||||
import { ElectronApplication, Page, expect, test } from "@playwright/test";
|
import { ElectronApplication, Page, expect, test } from '@playwright/test'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
findLatestBuild,
|
findLatestBuild,
|
||||||
parseElectronApp,
|
parseElectronApp,
|
||||||
stubDialog,
|
stubDialog,
|
||||||
} from "electron-playwright-helpers";
|
} from 'electron-playwright-helpers'
|
||||||
|
|
||||||
let electronApp: ElectronApplication;
|
let electronApp: ElectronApplication
|
||||||
let page: Page;
|
let page: Page
|
||||||
|
|
||||||
test.beforeAll(async () => {
|
test.beforeAll(async () => {
|
||||||
process.env.CI = "e2e";
|
process.env.CI = 'e2e'
|
||||||
|
|
||||||
const latestBuild = findLatestBuild("dist");
|
const latestBuild = findLatestBuild('dist')
|
||||||
expect(latestBuild).toBeTruthy();
|
expect(latestBuild).toBeTruthy()
|
||||||
|
|
||||||
// parse the packaged Electron app and find paths and other info
|
// parse the packaged Electron app and find paths and other info
|
||||||
const appInfo = parseElectronApp(latestBuild);
|
const appInfo = parseElectronApp(latestBuild)
|
||||||
expect(appInfo).toBeTruthy();
|
expect(appInfo).toBeTruthy()
|
||||||
|
|
||||||
electronApp = await electron.launch({
|
electronApp = await electron.launch({
|
||||||
args: [appInfo.main], // main file from package.json
|
args: [appInfo.main], // main file from package.json
|
||||||
executablePath: appInfo.executable, // path to the Electron executable
|
executablePath: appInfo.executable, // path to the Electron executable
|
||||||
});
|
})
|
||||||
await stubDialog(electronApp, "showMessageBox", { response: 1 });
|
await stubDialog(electronApp, 'showMessageBox', { response: 1 })
|
||||||
|
|
||||||
page = await electronApp.firstWindow();
|
page = await electronApp.firstWindow()
|
||||||
});
|
})
|
||||||
|
|
||||||
test.afterAll(async () => {
|
test.afterAll(async () => {
|
||||||
await electronApp.close();
|
await electronApp.close()
|
||||||
await page.close();
|
await page.close()
|
||||||
});
|
})
|
||||||
|
|
||||||
test("shows settings", async () => {
|
test('shows settings', async () => {
|
||||||
await page.getByTestId("Settings").first().click();
|
await page.getByTestId('Settings').first().click()
|
||||||
await page.getByTestId("testid-setting-description").isVisible();
|
await page.getByTestId('testid-setting-description').isVisible()
|
||||||
});
|
})
|
||||||
|
|||||||
41
electron/tests/system-monitor.e2e.spec.ts
Normal file
41
electron/tests/system-monitor.e2e.spec.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { _electron as electron } from 'playwright'
|
||||||
|
import { ElectronApplication, Page, expect, test } from '@playwright/test'
|
||||||
|
|
||||||
|
import {
|
||||||
|
findLatestBuild,
|
||||||
|
parseElectronApp,
|
||||||
|
stubDialog,
|
||||||
|
} from 'electron-playwright-helpers'
|
||||||
|
|
||||||
|
let electronApp: ElectronApplication
|
||||||
|
let page: Page
|
||||||
|
|
||||||
|
test.beforeAll(async () => {
|
||||||
|
process.env.CI = 'e2e'
|
||||||
|
|
||||||
|
const latestBuild = findLatestBuild('dist')
|
||||||
|
expect(latestBuild).toBeTruthy()
|
||||||
|
|
||||||
|
// parse the packaged Electron app and find paths and other info
|
||||||
|
const appInfo = parseElectronApp(latestBuild)
|
||||||
|
expect(appInfo).toBeTruthy()
|
||||||
|
|
||||||
|
electronApp = await electron.launch({
|
||||||
|
args: [appInfo.main], // main file from package.json
|
||||||
|
executablePath: appInfo.executable, // path to the Electron executable
|
||||||
|
})
|
||||||
|
await stubDialog(electronApp, 'showMessageBox', { response: 1 })
|
||||||
|
|
||||||
|
page = await electronApp.firstWindow()
|
||||||
|
})
|
||||||
|
|
||||||
|
test.afterAll(async () => {
|
||||||
|
await electronApp.close()
|
||||||
|
await page.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('shows system monitor', async () => {
|
||||||
|
await page.getByTestId('System Monitor').first().click()
|
||||||
|
await page.getByTestId('testid-system-monitor').isVisible()
|
||||||
|
// More test cases here...
|
||||||
|
})
|
||||||
@ -89,12 +89,12 @@ export default class JanAssistantExtension implements AssistantExtension {
|
|||||||
private async createJanAssistant(): Promise<void> {
|
private async createJanAssistant(): Promise<void> {
|
||||||
const janAssistant: Assistant = {
|
const janAssistant: Assistant = {
|
||||||
avatar: "",
|
avatar: "",
|
||||||
thread_location: undefined, // TODO: make this property ?
|
thread_location: undefined,
|
||||||
id: "jan",
|
id: "jan",
|
||||||
object: "assistant", // TODO: maybe we can set default value for this?
|
object: "assistant", // TODO: maybe we can set default value for this?
|
||||||
created_at: Date.now(),
|
created_at: Date.now(),
|
||||||
name: "Jan Assistant",
|
name: "Jan",
|
||||||
description: "Just Jan Assistant",
|
description: "A default assistant that can use all downloaded models",
|
||||||
model: "*",
|
model: "*",
|
||||||
instructions: "Your name is Jan.",
|
instructions: "Your name is Jan.",
|
||||||
tools: undefined,
|
tools: undefined,
|
||||||
|
|||||||
@ -20,9 +20,11 @@
|
|||||||
"@radix-ui/react-avatar": "^1.0.4",
|
"@radix-ui/react-avatar": "^1.0.4",
|
||||||
"@radix-ui/react-context": "^1.0.1",
|
"@radix-ui/react-context": "^1.0.1",
|
||||||
"@radix-ui/react-dialog": "^1.0.5",
|
"@radix-ui/react-dialog": "^1.0.5",
|
||||||
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@radix-ui/react-label": "^2.0.2",
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
"@radix-ui/react-progress": "^1.0.3",
|
"@radix-ui/react-progress": "^1.0.3",
|
||||||
"@radix-ui/react-scroll-area": "^1.0.5",
|
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||||
|
"@radix-ui/react-select": "^2.0.0",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
"@radix-ui/react-switch": "^1.0.3",
|
"@radix-ui/react-switch": "^1.0.3",
|
||||||
"@radix-ui/react-toast": "^1.1.5",
|
"@radix-ui/react-toast": "^1.1.5",
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&-success {
|
&-success {
|
||||||
@apply border-transparent bg-green-500 text-green-900 hover:bg-green-500/80;
|
@apply border-transparent bg-green-100 text-green-600;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-secondary {
|
&-secondary {
|
||||||
|
|||||||
@ -25,7 +25,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&-list-item {
|
&-list-item {
|
||||||
@apply text-foreground aria-selected:bg-primary relative flex cursor-pointer select-none items-center rounded-md px-2 py-2 text-sm outline-none;
|
@apply text-foreground aria-selected:bg-secondary relative flex cursor-pointer select-none items-center rounded-md px-2 py-2 text-sm outline-none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-empty {
|
&-empty {
|
||||||
|
|||||||
@ -10,3 +10,4 @@ export * from './tooltip'
|
|||||||
export * from './modal'
|
export * from './modal'
|
||||||
export * from './command'
|
export * from './command'
|
||||||
export * from './textarea'
|
export * from './textarea'
|
||||||
|
export * from './select'
|
||||||
|
|||||||
@ -9,7 +9,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
|||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
className={twMerge('input test', className)}
|
className={twMerge('input', className)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -14,6 +14,7 @@
|
|||||||
@import './modal/styles.scss';
|
@import './modal/styles.scss';
|
||||||
@import './command/styles.scss';
|
@import './command/styles.scss';
|
||||||
@import './textarea/styles.scss';
|
@import './textarea/styles.scss';
|
||||||
|
@import './select/styles.scss';
|
||||||
|
|
||||||
.animate-spin {
|
.animate-spin {
|
||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
@ -104,7 +105,3 @@
|
|||||||
--secondary-foreground: 210 20% 98%;
|
--secondary-foreground: 210 20% 98%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:is(p) {
|
|
||||||
@apply text-muted-foreground;
|
|
||||||
}
|
|
||||||
|
|||||||
139
uikit/src/select/index.tsx
Normal file
139
uikit/src/select/index.tsx
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import * as React from 'react'
|
||||||
|
import {
|
||||||
|
CaretSortIcon,
|
||||||
|
// CheckIcon,
|
||||||
|
ChevronDownIcon,
|
||||||
|
ChevronUpIcon,
|
||||||
|
} from '@radix-ui/react-icons'
|
||||||
|
|
||||||
|
import * as SelectPrimitive from '@radix-ui/react-select'
|
||||||
|
|
||||||
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
|
const Select = SelectPrimitive.Root
|
||||||
|
|
||||||
|
const SelectGroup = SelectPrimitive.Group
|
||||||
|
|
||||||
|
const SelectValue = SelectPrimitive.Value
|
||||||
|
|
||||||
|
const SelectTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={twMerge('select', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<SelectPrimitive.Icon asChild>
|
||||||
|
<CaretSortIcon className="select-caret" />
|
||||||
|
</SelectPrimitive.Icon>
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
|
))
|
||||||
|
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||||
|
|
||||||
|
const SelectScrollUpButton = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.ScrollUpButton
|
||||||
|
ref={ref}
|
||||||
|
className={twMerge('select-scroll-up-button', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronUpIcon />
|
||||||
|
</SelectPrimitive.ScrollUpButton>
|
||||||
|
))
|
||||||
|
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||||
|
|
||||||
|
const SelectScrollDownButton = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.ScrollDownButton
|
||||||
|
ref={ref}
|
||||||
|
className={twMerge('select-scroll-down-button', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronDownIcon />
|
||||||
|
</SelectPrimitive.ScrollDownButton>
|
||||||
|
))
|
||||||
|
SelectScrollDownButton.displayName =
|
||||||
|
SelectPrimitive.ScrollDownButton.displayName
|
||||||
|
|
||||||
|
const SelectContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||||
|
>(({ className, children, position = 'popper', ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Portal>
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={twMerge(
|
||||||
|
'select-content',
|
||||||
|
position === 'popper' &&
|
||||||
|
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
position={position}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SelectScrollUpButton />
|
||||||
|
<SelectPrimitive.Viewport
|
||||||
|
className={twMerge(
|
||||||
|
'select-trigger-viewport',
|
||||||
|
position === 'popper' && 'w-full'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SelectPrimitive.Viewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPrimitive.Portal>
|
||||||
|
))
|
||||||
|
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const SelectLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Label
|
||||||
|
ref={ref}
|
||||||
|
className={twMerge('select-label', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||||
|
|
||||||
|
const SelectItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={twMerge('select-item', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{/* <span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<SelectPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ItemIndicator>
|
||||||
|
</span> */}
|
||||||
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
|
</SelectPrimitive.Item>
|
||||||
|
))
|
||||||
|
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
Select,
|
||||||
|
SelectGroup,
|
||||||
|
SelectValue,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectContent,
|
||||||
|
SelectLabel,
|
||||||
|
SelectItem,
|
||||||
|
SelectScrollUpButton,
|
||||||
|
SelectScrollDownButton,
|
||||||
|
}
|
||||||
31
uikit/src/select/styles.scss
Normal file
31
uikit/src/select/styles.scss
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
.select {
|
||||||
|
@apply ring-offset-background 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 disabled:opacity-50 [&>span]:line-clamp-1;
|
||||||
|
|
||||||
|
&-caret {
|
||||||
|
@apply h-4 w-4 opacity-50;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-scroll-up-button {
|
||||||
|
@apply flex cursor-default items-center justify-center py-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-scroll-down-button {
|
||||||
|
@apply flex cursor-default items-center justify-center py-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-label {
|
||||||
|
@apply px-2 py-1.5 text-sm font-semibold;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-item {
|
||||||
|
@apply hover:bg-secondary relative my-1 block w-full cursor-pointer select-none items-center rounded-sm px-4 py-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-trigger-viewport {
|
||||||
|
@apply w-full py-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-content {
|
||||||
|
@apply bg-background border-border relative z-50 mt-1 block max-h-96 w-full min-w-[8rem] overflow-hidden rounded-md border shadow-md;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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-background/50 font-sans text-sm antialiased">
|
<body className="bg-white font-sans text-sm antialiased dark:bg-background/50">
|
||||||
<div className="title-bar" />
|
<div className="title-bar" />
|
||||||
<Providers>{children}</Providers>
|
<Providers>{children}</Providers>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -8,29 +8,25 @@ import { useMainViewState } from '@/hooks/useMainViewState'
|
|||||||
|
|
||||||
import ChatScreen from '@/screens/Chat'
|
import ChatScreen from '@/screens/Chat'
|
||||||
import ExploreModelsScreen from '@/screens/ExploreModels'
|
import ExploreModelsScreen from '@/screens/ExploreModels'
|
||||||
import MyModelsScreen from '@/screens/MyModels'
|
|
||||||
import SettingsScreen from '@/screens/Settings'
|
import SettingsScreen from '@/screens/Settings'
|
||||||
import WelcomeScreen from '@/screens/Welcome'
|
import SystemMonitorScreen from '@/screens/SystemMonitor'
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const { mainViewState } = useMainViewState()
|
const { mainViewState } = useMainViewState()
|
||||||
|
|
||||||
let children = null
|
let children = null
|
||||||
switch (mainViewState) {
|
switch (mainViewState) {
|
||||||
case MainViewState.Welcome:
|
case MainViewState.Hub:
|
||||||
children = <WelcomeScreen />
|
|
||||||
break
|
|
||||||
|
|
||||||
case MainViewState.ExploreModels:
|
|
||||||
children = <ExploreModelsScreen />
|
children = <ExploreModelsScreen />
|
||||||
break
|
break
|
||||||
|
|
||||||
case MainViewState.MyModels:
|
case MainViewState.Settings:
|
||||||
children = <MyModelsScreen />
|
children = <SettingsScreen />
|
||||||
break
|
break
|
||||||
|
|
||||||
case MainViewState.Setting:
|
case MainViewState.SystemMonitor:
|
||||||
children = <SettingsScreen />
|
children = <SystemMonitorScreen />
|
||||||
break
|
break
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
export enum MainViewState {
|
export enum MainViewState {
|
||||||
Welcome,
|
Hub,
|
||||||
ExploreModels,
|
|
||||||
MyModels,
|
MyModels,
|
||||||
Setting,
|
Settings,
|
||||||
Chat,
|
Thread,
|
||||||
|
SystemMonitor,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
import { ReactNode, useState } from 'react'
|
import { ReactNode, useState, useRef } from 'react'
|
||||||
import { Fragment } from 'react'
|
|
||||||
|
|
||||||
import { Menu, Transition } from '@headlessui/react'
|
|
||||||
import {
|
import {
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
EllipsisVerticalIcon,
|
MoreVerticalIcon,
|
||||||
} from '@heroicons/react/20/solid'
|
FolderOpenIcon,
|
||||||
|
Code2Icon,
|
||||||
|
} from 'lucide-react'
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
|
import { useClickOutside } from '@/hooks/useClickOutside'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
title: string
|
title: string
|
||||||
@ -21,65 +23,75 @@ export default function CardSidebar({
|
|||||||
onViewJsonClick,
|
onViewJsonClick,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [show, setShow] = useState(true)
|
const [show, setShow] = useState(true)
|
||||||
|
const [more, setMore] = useState(false)
|
||||||
|
const [menu, setMenu] = useState<HTMLDivElement | null>(null)
|
||||||
|
const [toggle, setToggle] = useState<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
|
useClickOutside(() => setMore(false), null, [menu, toggle])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col">
|
<div
|
||||||
<div className="flex items-center rounded-lg border border-border">
|
className={twMerge(
|
||||||
|
'flex w-full flex-col rounded-lg border border-border',
|
||||||
|
show && 'border border-border'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
'relative flex items-center rounded-t-md bg-zinc-200 dark:bg-zinc-600/10',
|
||||||
|
show && 'border-b border-border'
|
||||||
|
)}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShow(!show)}
|
onClick={() => setShow(!show)}
|
||||||
className="flex w-full flex-1 items-center py-2"
|
className="flex w-full flex-1 items-center space-x-2 px-3 py-2"
|
||||||
>
|
>
|
||||||
<ChevronDownIcon
|
<ChevronDownIcon
|
||||||
className={`h-5 w-5 flex-none text-gray-400 ${
|
className={twMerge(
|
||||||
show && 'rotate-180'
|
'h-5 w-5 flex-none rotate-180 text-gray-400',
|
||||||
}`}
|
show && 'rotate-0'
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<span className="text-xs uppercase">{title}</span>
|
<span className="font-bold">{title}</span>
|
||||||
</button>
|
</button>
|
||||||
<Menu as="div" className="relative flex-none">
|
<div
|
||||||
<Menu.Button className="-m-2.5 block p-2.5 text-gray-500 hover:text-gray-900">
|
ref={setToggle}
|
||||||
<span className="sr-only">Open options</span>
|
className="cursor-pointer bg-zinc-200 p-2 dark:bg-zinc-600/10"
|
||||||
<EllipsisVerticalIcon className="h-5 w-5" aria-hidden="true" />
|
onClick={() => setMore(!more)}
|
||||||
</Menu.Button>
|
|
||||||
<Transition
|
|
||||||
as={Fragment}
|
|
||||||
enter="transition ease-out duration-100"
|
|
||||||
enterFrom="transform opacity-0 scale-95"
|
|
||||||
enterTo="transform opacity-100 scale-100"
|
|
||||||
leave="transition ease-in duration-75"
|
|
||||||
leaveFrom="transform opacity-100 scale-100"
|
|
||||||
leaveTo="transform opacity-0 scale-95"
|
|
||||||
>
|
>
|
||||||
<Menu.Items className="absolute right-0 z-10 mt-2 w-32 origin-top-right rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none">
|
<MoreVerticalIcon className="h-5 w-5" />
|
||||||
<Menu.Item>
|
</div>
|
||||||
{({ active }) => (
|
{more && (
|
||||||
<a
|
<div
|
||||||
onClick={() => onRevealInFinderClick(title)}
|
className="absolute right-0 top-8 z-20 w-52 overflow-hidden rounded-lg border border-border bg-background shadow-lg"
|
||||||
className={twMerge(
|
ref={setMenu}
|
||||||
active ? 'bg-gray-50' : '',
|
|
||||||
'block cursor-pointer px-3 py-1 text-xs leading-6 text-gray-900'
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
Reveal in finder
|
<div
|
||||||
</a>
|
className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-secondary"
|
||||||
)}
|
onClick={() => {
|
||||||
</Menu.Item>
|
onRevealInFinderClick(title)
|
||||||
<Menu.Item>
|
setMore(false)
|
||||||
{({ active }) => (
|
}}
|
||||||
<a
|
|
||||||
onClick={() => onViewJsonClick(title)}
|
|
||||||
className={twMerge(
|
|
||||||
active ? 'bg-gray-50' : '',
|
|
||||||
'block cursor-pointer px-3 py-1 text-xs leading-6 text-gray-900'
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
View a JSON
|
<FolderOpenIcon size={16} className="text-muted-foreground" />
|
||||||
</a>
|
<span className="text-bold text-black dark:text-muted-foreground">
|
||||||
|
Reveal in Finder
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-secondary"
|
||||||
|
onClick={() => {
|
||||||
|
onViewJsonClick(title)
|
||||||
|
setMore(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Code2Icon size={16} className="text-muted-foreground" />
|
||||||
|
<span className="text-bold text-black dark:text-muted-foreground">
|
||||||
|
View as JSON
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</Menu.Item>
|
|
||||||
</Menu.Items>
|
|
||||||
</Transition>
|
|
||||||
</Menu>
|
|
||||||
</div>
|
</div>
|
||||||
{show && <div className="flex flex-col gap-2 p-2">{children}</div>}
|
{show && <div className="flex flex-col gap-2 p-2">{children}</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,104 +1,114 @@
|
|||||||
import { Fragment, useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
import { Listbox, Transition } from '@headlessui/react'
|
|
||||||
import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid'
|
|
||||||
|
|
||||||
import { Model } from '@janhq/core'
|
import { Model } from '@janhq/core'
|
||||||
import { atom, useSetAtom } from 'jotai'
|
import {
|
||||||
|
Button,
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@janhq/uikit'
|
||||||
|
|
||||||
|
import { atom, useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
|
import { MonitorIcon } from 'lucide-react'
|
||||||
|
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
|
import { MainViewState } from '@/constants/screens'
|
||||||
|
|
||||||
import { getDownloadedModels } from '@/hooks/useGetDownloadedModels'
|
import { getDownloadedModels } from '@/hooks/useGetDownloadedModels'
|
||||||
|
|
||||||
|
import { useMainViewState } from '@/hooks/useMainViewState'
|
||||||
|
|
||||||
|
import { toGigabytes } from '@/utils/converter'
|
||||||
|
|
||||||
|
import { activeThreadAtom } from '@/helpers/atoms/Conversation.atom'
|
||||||
|
|
||||||
export const selectedModelAtom = atom<Model | undefined>(undefined)
|
export const selectedModelAtom = atom<Model | undefined>(undefined)
|
||||||
|
|
||||||
export default function DropdownListSidebar() {
|
export default function DropdownListSidebar() {
|
||||||
const [downloadedModels, setDownloadedModels] = useState<Model[]>([])
|
const [downloadedModels, setDownloadedModels] = useState<Model[]>([])
|
||||||
const [selected, setSelected] = useState<Model | undefined>()
|
|
||||||
const setSelectedModel = useSetAtom(selectedModelAtom)
|
const setSelectedModel = useSetAtom(selectedModelAtom)
|
||||||
|
const activeThread = useAtomValue(activeThreadAtom)
|
||||||
|
const [selected, setSelected] = useState<Model | undefined>()
|
||||||
|
const { setMainViewState } = useMainViewState()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getDownloadedModels().then((downloadedModels) => {
|
getDownloadedModels().then((downloadedModels) => {
|
||||||
setDownloadedModels(downloadedModels)
|
setDownloadedModels(downloadedModels)
|
||||||
|
|
||||||
if (downloadedModels.length > 0) {
|
if (downloadedModels.length > 0) {
|
||||||
setSelected(downloadedModels[0])
|
setSelected(
|
||||||
setSelectedModel(downloadedModels[0])
|
downloadedModels.filter(
|
||||||
|
(x) => x.id === activeThread?.assistants[0].model.id
|
||||||
|
)[0] || downloadedModels[0]
|
||||||
|
)
|
||||||
|
setSelectedModel(
|
||||||
|
downloadedModels.filter(
|
||||||
|
(x) => x.id === activeThread?.assistants[0].model.id
|
||||||
|
)[0] || downloadedModels[0]
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [])
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [activeThread])
|
||||||
if (!selected) return null
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Listbox
|
<Select
|
||||||
value={selected}
|
value={selected?.id}
|
||||||
onChange={(model) => {
|
onValueChange={(value) => {
|
||||||
setSelected(model)
|
setSelected(downloadedModels.filter((x) => x.id === value)[0])
|
||||||
setSelectedModel(model)
|
setSelectedModel(downloadedModels.filter((x) => x.id === value)[0])
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{({ open }) => (
|
<SelectTrigger className="w-full">
|
||||||
<>
|
<SelectValue placeholder="Choose model to start">
|
||||||
<div className="relative mt-2">
|
{downloadedModels.filter((x) => x.id === selected?.id)[0]?.name}
|
||||||
<Listbox.Button className="relative w-full cursor-default rounded-md bg-white py-1.5 pl-3 pr-10 text-left text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-600 sm:text-sm sm:leading-6">
|
</SelectValue>
|
||||||
<span className="block truncate">{selected.name}</span>
|
</SelectTrigger>
|
||||||
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
<SelectContent className="right-5 block w-full min-w-[300px] pr-0">
|
||||||
<ChevronUpDownIcon
|
<div className="flex w-full items-center space-x-2 px-4 py-2">
|
||||||
className="h-5 w-5 text-gray-400"
|
<MonitorIcon size={20} className="text-muted-foreground" />
|
||||||
aria-hidden="true"
|
<span>Local</span>
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</Listbox.Button>
|
|
||||||
|
|
||||||
<Transition
|
|
||||||
show={open}
|
|
||||||
as={Fragment}
|
|
||||||
leave="transition ease-in duration-100"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<Listbox.Options className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
|
|
||||||
{downloadedModels.map((model) => (
|
|
||||||
<Listbox.Option
|
|
||||||
key={model.id}
|
|
||||||
className={({ active }) =>
|
|
||||||
twMerge(
|
|
||||||
active ? 'bg-indigo-600 text-white' : 'text-gray-900',
|
|
||||||
'relative cursor-default select-none py-2 pl-3 pr-9'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
value={model}
|
|
||||||
>
|
|
||||||
{({ selected, active }) => (
|
|
||||||
<>
|
|
||||||
<span
|
|
||||||
className={twMerge(
|
|
||||||
selected ? 'font-semibold' : 'font-normal',
|
|
||||||
'block truncate'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{model.name}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{selected ? (
|
|
||||||
<span
|
|
||||||
className={twMerge(
|
|
||||||
active ? 'text-white' : 'text-indigo-600',
|
|
||||||
'absolute inset-y-0 right-0 flex items-center pr-4'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<CheckIcon className="h-5 w-5" aria-hidden="true" />
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Listbox.Option>
|
|
||||||
))}
|
|
||||||
</Listbox.Options>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
<div className="border-b border-border" />
|
||||||
|
{downloadedModels.length === 0 ? (
|
||||||
|
<div className="px-4 py-2">
|
||||||
|
<p>{`Oops, you don't have a model yet.`}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<SelectGroup>
|
||||||
|
{downloadedModels.map((x, i) => {
|
||||||
|
return (
|
||||||
|
<SelectItem
|
||||||
|
key={i}
|
||||||
|
value={x.id}
|
||||||
|
className={twMerge(x.id === selected?.id && 'bg-secondary')}
|
||||||
|
>
|
||||||
|
<div className="flex w-full justify-between">
|
||||||
|
<span className="line-clamp-1 block">{x.name}</span>
|
||||||
|
<span className="font-bold text-muted-foreground">
|
||||||
|
{toGigabytes(x.metadata.size)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</SelectGroup>
|
||||||
)}
|
)}
|
||||||
</Listbox>
|
<div className="border-b border-border" />
|
||||||
|
<div className="w-full px-4 py-2">
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
className="bg-blue-100 font-bold text-blue-600 hover:bg-blue-100 hover:text-blue-600"
|
||||||
|
onClick={() => setMainViewState(MainViewState.Hub)}
|
||||||
|
>
|
||||||
|
Explore The Hub
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,18 +12,14 @@ import {
|
|||||||
ModalTrigger,
|
ModalTrigger,
|
||||||
} from '@janhq/uikit'
|
} from '@janhq/uikit'
|
||||||
|
|
||||||
import { useAtomValue } from 'jotai'
|
|
||||||
|
|
||||||
import { useDownloadState } from '@/hooks/useDownloadState'
|
import { useDownloadState } from '@/hooks/useDownloadState'
|
||||||
|
|
||||||
import { formatDownloadPercentage } from '@/utils/converter'
|
import { formatDownloadPercentage } from '@/utils/converter'
|
||||||
|
|
||||||
import { extensionManager } from '@/extension'
|
import { extensionManager } from '@/extension'
|
||||||
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
|
||||||
|
|
||||||
export default function DownloadingState() {
|
export default function DownloadingState() {
|
||||||
const { downloadStates } = useDownloadState()
|
const { downloadStates } = useDownloadState()
|
||||||
const models = useAtomValue(downloadingModelsAtom)
|
|
||||||
|
|
||||||
const totalCurrentProgress = downloadStates
|
const totalCurrentProgress = downloadStates
|
||||||
.map((a) => a.size.transferred + a.size.transferred)
|
.map((a) => a.size.transferred + a.size.transferred)
|
||||||
|
|||||||
@ -30,7 +30,7 @@ const BottomBar = () => {
|
|||||||
const { downloadStates } = useDownloadState()
|
const { downloadStates } = useDownloadState()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed bottom-0 left-16 z-20 flex h-12 w-[calc(100%-64px)] items-center justify-between border-t border-border bg-background/50 px-3">
|
<div className="fixed bottom-0 left-16 z-20 flex h-12 w-[calc(100%-64px)] items-center justify-between border-t border-border bg-background/80 px-3">
|
||||||
<div className="flex flex-shrink-0 items-center gap-x-2">
|
<div className="flex flex-shrink-0 items-center gap-x-2">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
{progress && progress > 0 ? (
|
{progress && progress > 0 ? (
|
||||||
@ -49,7 +49,7 @@ const BottomBar = () => {
|
|||||||
name="Active model:"
|
name="Active model:"
|
||||||
value={
|
value={
|
||||||
activeModel?.id || (
|
activeModel?.id || (
|
||||||
<Badge themes="outline">
|
<Badge themes="outline" className="pl-1">
|
||||||
<ShortCut menu="E" />
|
<ShortCut menu="E" />
|
||||||
to show your model
|
to show your model
|
||||||
</Badge>
|
</Badge>
|
||||||
@ -63,7 +63,7 @@ const BottomBar = () => {
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
themes="outline"
|
themes="outline"
|
||||||
onClick={() => setMainViewState(MainViewState.ExploreModels)}
|
onClick={() => setMainViewState(MainViewState.Hub)}
|
||||||
>
|
>
|
||||||
Download your first model
|
Download your first model
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
import { useContext } from 'react'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
@ -11,9 +9,8 @@ import { motion as m } from 'framer-motion'
|
|||||||
import {
|
import {
|
||||||
MessageCircleIcon,
|
MessageCircleIcon,
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
DatabaseIcon,
|
MonitorIcon,
|
||||||
CpuIcon,
|
LayoutGridIcon,
|
||||||
BookOpenIcon,
|
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
@ -34,36 +31,51 @@ export default function RibbonNav() {
|
|||||||
|
|
||||||
const primaryMenus = [
|
const primaryMenus = [
|
||||||
{
|
{
|
||||||
name: 'Getting Started',
|
name: 'Thread',
|
||||||
icon: <BookOpenIcon size={20} className="flex-shrink-0" />,
|
icon: (
|
||||||
state: MainViewState.Welcome,
|
<MessageCircleIcon
|
||||||
|
size={20}
|
||||||
|
className="flex-shrink-0 text-muted-foreground"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
state: MainViewState.Thread,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Chat',
|
name: 'Hub',
|
||||||
icon: <MessageCircleIcon size={20} className="flex-shrink-0" />,
|
icon: (
|
||||||
state: MainViewState.Chat,
|
<LayoutGridIcon
|
||||||
|
size={20}
|
||||||
|
className="flex-shrink-0 text-muted-foreground"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
state: MainViewState.Hub,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const secondaryMenus = [
|
const secondaryMenus = [
|
||||||
{
|
{
|
||||||
name: 'Explore Models',
|
name: 'System Monitor',
|
||||||
icon: <CpuIcon size={20} className="flex-shrink-0" />,
|
icon: (
|
||||||
state: MainViewState.ExploreModels,
|
<MonitorIcon
|
||||||
},
|
size={20}
|
||||||
{
|
className="flex-shrink-0 text-muted-foreground"
|
||||||
name: 'My Models',
|
/>
|
||||||
icon: <DatabaseIcon size={20} className="flex-shrink-0" />,
|
),
|
||||||
state: MainViewState.MyModels,
|
state: MainViewState.SystemMonitor,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Settings',
|
name: 'Settings',
|
||||||
icon: <SettingsIcon size={20} className="flex-shrink-0" />,
|
icon: (
|
||||||
state: MainViewState.Setting,
|
<SettingsIcon
|
||||||
|
size={20}
|
||||||
|
className="flex-shrink-0 text-muted-foreground"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
state: MainViewState.Settings,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
return (
|
return (
|
||||||
<div className="relative top-12 flex h-[calc(100%-48px)] w-16 flex-shrink-0 flex-col border-r border-border py-4">
|
<div className="relative top-12 flex h-[calc(100%-48px)] w-16 flex-shrink-0 flex-col border-r border-border bg-background py-4">
|
||||||
<div className="mt-2 flex h-full w-full flex-col items-center justify-between">
|
<div className="mt-2 flex h-full w-full flex-col items-center justify-between">
|
||||||
<div className="flex h-full w-full flex-col items-center justify-between">
|
<div className="flex h-full w-full flex-col items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
@ -90,7 +102,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-primary/50"
|
className="absolute inset-0 left-0 h-full w-full rounded-md bg-gray-200 dark:bg-secondary"
|
||||||
layoutId="active-state-primary"
|
layoutId="active-state-primary"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -126,7 +138,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-primary/50"
|
className="absolute inset-0 left-0 h-full w-full rounded-md bg-gray-200 dark:bg-secondary"
|
||||||
layoutId="active-state-secondary"
|
layoutId="active-state-secondary"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -85,12 +85,12 @@ export default function CommandListDownloadedModel() {
|
|||||||
<CommandGroup heading="Find another model">
|
<CommandGroup heading="Find another model">
|
||||||
<CommandItem
|
<CommandItem
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
setMainViewState(MainViewState.ExploreModels)
|
setMainViewState(MainViewState.Hub)
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CpuIcon size={16} className="mr-3 text-muted-foreground" />
|
<CpuIcon size={16} className="mr-3 text-muted-foreground" />
|
||||||
<span>Explore Models</span>
|
<span>Explore The Hub</span>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
</CommandList>
|
</CommandList>
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { Fragment, useState, useEffect } from 'react'
|
import { Fragment, useState, useEffect } from 'react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
|
||||||
CommandModal,
|
CommandModal,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
CommandGroup,
|
CommandGroup,
|
||||||
@ -11,14 +10,7 @@ import {
|
|||||||
CommandList,
|
CommandList,
|
||||||
} from '@janhq/uikit'
|
} from '@janhq/uikit'
|
||||||
|
|
||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
import { MessageCircleIcon, SettingsIcon, LayoutGridIcon } from 'lucide-react'
|
||||||
import {
|
|
||||||
MessageCircleIcon,
|
|
||||||
SettingsIcon,
|
|
||||||
DatabaseIcon,
|
|
||||||
CpuIcon,
|
|
||||||
BookOpenIcon,
|
|
||||||
} from 'lucide-react'
|
|
||||||
|
|
||||||
import ShortCut from '@/containers/Shortcut'
|
import ShortCut from '@/containers/Shortcut'
|
||||||
|
|
||||||
@ -26,43 +18,27 @@ import { MainViewState } from '@/constants/screens'
|
|||||||
|
|
||||||
import { useMainViewState } from '@/hooks/useMainViewState'
|
import { useMainViewState } from '@/hooks/useMainViewState'
|
||||||
|
|
||||||
import { showRightSideBarAtom } from '@/screens/Chat/Sidebar'
|
|
||||||
|
|
||||||
import { activeThreadAtom } from '@/helpers/atoms/Conversation.atom'
|
|
||||||
|
|
||||||
export default function CommandSearch() {
|
export default function CommandSearch() {
|
||||||
const { setMainViewState } = useMainViewState()
|
const { setMainViewState } = useMainViewState()
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const setShowRightSideBar = useSetAtom(showRightSideBarAtom)
|
|
||||||
const activeThread = useAtomValue(activeThreadAtom)
|
|
||||||
|
|
||||||
const menus = [
|
const menus = [
|
||||||
{
|
|
||||||
name: 'Getting Started',
|
|
||||||
icon: <BookOpenIcon size={16} className="mr-3 text-muted-foreground" />,
|
|
||||||
state: MainViewState.Welcome,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'Chat',
|
name: 'Chat',
|
||||||
icon: (
|
icon: (
|
||||||
<MessageCircleIcon size={16} className="mr-3 text-muted-foreground" />
|
<MessageCircleIcon size={16} className="mr-3 text-muted-foreground" />
|
||||||
),
|
),
|
||||||
state: MainViewState.Chat,
|
state: MainViewState.Thread,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Explore Models',
|
name: 'Hub',
|
||||||
icon: <CpuIcon size={16} className="mr-3 text-muted-foreground" />,
|
icon: <LayoutGridIcon size={16} className="mr-3 text-muted-foreground" />,
|
||||||
state: MainViewState.ExploreModels,
|
state: MainViewState.Hub,
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'My Models',
|
|
||||||
icon: <DatabaseIcon size={16} className="mr-3 text-muted-foreground" />,
|
|
||||||
state: MainViewState.MyModels,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Settings',
|
name: 'Settings',
|
||||||
icon: <SettingsIcon size={16} className="mr-3 text-muted-foreground" />,
|
icon: <SettingsIcon size={16} className="mr-3 text-muted-foreground" />,
|
||||||
state: MainViewState.Setting,
|
state: MainViewState.Settings,
|
||||||
shortcut: <ShortCut menu="," />,
|
shortcut: <ShortCut menu="," />,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@ -75,7 +51,7 @@ export default function CommandSearch() {
|
|||||||
}
|
}
|
||||||
if (e.key === ',' && (e.metaKey || e.ctrlKey)) {
|
if (e.key === ',' && (e.metaKey || e.ctrlKey)) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setMainViewState(MainViewState.Setting)
|
setMainViewState(MainViewState.Settings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
document.addEventListener('keydown', down)
|
document.addEventListener('keydown', down)
|
||||||
@ -85,7 +61,8 @@ export default function CommandSearch() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className="relative">
|
{/* Temporary disable view search input until we have proper UI placement, but we keep function cmd + K for showing list page */}
|
||||||
|
{/* <div className="relative">
|
||||||
<Button
|
<Button
|
||||||
themes="outline"
|
themes="outline"
|
||||||
className="unset-drag h-8 w-[300px] justify-start text-left text-xs font-normal text-muted-foreground focus:ring-0"
|
className="unset-drag h-8 w-[300px] justify-start text-left text-xs font-normal text-muted-foreground focus:ring-0"
|
||||||
@ -96,8 +73,7 @@ export default function CommandSearch() {
|
|||||||
<div className="absolute right-2 top-1/2 -translate-y-1/2">
|
<div className="absolute right-2 top-1/2 -translate-y-1/2">
|
||||||
<ShortCut menu="K" />
|
<ShortCut menu="K" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
<CommandModal open={open} onOpenChange={setOpen}>
|
<CommandModal open={open} onOpenChange={setOpen}>
|
||||||
<CommandInput placeholder="Type a command or search..." />
|
<CommandInput placeholder="Type a command or search..." />
|
||||||
<CommandList>
|
<CommandList>
|
||||||
@ -124,15 +100,6 @@ export default function CommandSearch() {
|
|||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</CommandModal>
|
</CommandModal>
|
||||||
{activeThread && (
|
|
||||||
<Button
|
|
||||||
themes="outline"
|
|
||||||
className="unset-drag justify-start text-left text-xs font-normal text-muted-foreground focus:ring-0"
|
|
||||||
onClick={() => setShowRightSideBar((show) => !show)}
|
|
||||||
>
|
|
||||||
Toggle right
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,21 +1,86 @@
|
|||||||
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
import { PanelLeftIcon, PenSquareIcon, PanelRightIcon } from 'lucide-react'
|
||||||
|
|
||||||
import CommandListDownloadedModel from '@/containers/Layout/TopBar/CommandListDownloadedModel'
|
import CommandListDownloadedModel from '@/containers/Layout/TopBar/CommandListDownloadedModel'
|
||||||
import CommandSearch from '@/containers/Layout/TopBar/CommandSearch'
|
import CommandSearch from '@/containers/Layout/TopBar/CommandSearch'
|
||||||
|
|
||||||
|
import { MainViewState } from '@/constants/screens'
|
||||||
|
|
||||||
|
import { useCreateNewThread } from '@/hooks/useCreateNewThread'
|
||||||
|
import useGetAssistants from '@/hooks/useGetAssistants'
|
||||||
import { useMainViewState } from '@/hooks/useMainViewState'
|
import { useMainViewState } from '@/hooks/useMainViewState'
|
||||||
|
|
||||||
|
import { showRightSideBarAtom } from '@/screens/Chat/Sidebar'
|
||||||
|
|
||||||
|
import { activeThreadAtom } from '@/helpers/atoms/Conversation.atom'
|
||||||
|
|
||||||
const TopBar = () => {
|
const TopBar = () => {
|
||||||
const { viewStateName } = useMainViewState()
|
const activeThread = useAtomValue(activeThreadAtom)
|
||||||
|
const { mainViewState } = useMainViewState()
|
||||||
|
const { requestCreateNewThread } = useCreateNewThread()
|
||||||
|
const { assistants } = useGetAssistants()
|
||||||
|
const setShowRightSideBar = useSetAtom(showRightSideBarAtom)
|
||||||
|
|
||||||
|
const titleScreen = (viewStateName: MainViewState) => {
|
||||||
|
switch (viewStateName) {
|
||||||
|
case MainViewState.Thread:
|
||||||
|
return activeThread ? activeThread?.title : 'New Thread'
|
||||||
|
|
||||||
|
default:
|
||||||
|
return MainViewState[viewStateName]?.replace(/([A-Z])/g, ' $1').trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCreateConversationClick = async () => {
|
||||||
|
if (assistants.length === 0) {
|
||||||
|
alert('No assistant available')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
requestCreateNewThread(assistants[0])
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed left-0 top-0 z-50 flex h-12 w-full border-b border-border bg-background/50">
|
<div className="fixed left-0 top-0 z-50 flex h-12 w-full border-b border-border bg-background/80 backdrop-blur-md">
|
||||||
|
{mainViewState === MainViewState.Thread && (
|
||||||
|
<div className="absolute left-16 h-full w-60 border-r border-border" />
|
||||||
|
)}
|
||||||
<div className="relative left-16 flex w-[calc(100%-64px)] items-center justify-between space-x-4 pl-6 pr-2">
|
<div className="relative left-16 flex w-[calc(100%-64px)] items-center justify-between space-x-4 pl-6 pr-2">
|
||||||
|
{mainViewState === MainViewState.Thread ? (
|
||||||
|
<div className="unset-drag flex space-x-8">
|
||||||
|
<div className="flex w-52 justify-between">
|
||||||
|
<div className="cursor-pointer">
|
||||||
|
<PanelLeftIcon
|
||||||
|
size={20}
|
||||||
|
className="invisible text-muted-foreground"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="cursor-pointer pr-2"
|
||||||
|
onClick={onCreateConversationClick}
|
||||||
|
>
|
||||||
|
<PenSquareIcon size={20} className="text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-bold">
|
||||||
|
{titleScreen(mainViewState)}
|
||||||
|
</span>
|
||||||
|
{activeThread && (
|
||||||
|
<div
|
||||||
|
className="unset-drag absolute right-4 cursor-pointer"
|
||||||
|
onClick={() => setShowRightSideBar((show) => !show)}
|
||||||
|
>
|
||||||
|
<PanelRightIcon size={20} className="text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium">
|
<span className="text-sm font-bold">
|
||||||
{viewStateName.replace(/([A-Z])/g, ' $1').trim()}
|
{titleScreen(mainViewState)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<CommandSearch />
|
<CommandSearch />
|
||||||
{/* Command without trigger interface */}
|
|
||||||
<CommandListDownloadedModel />
|
<CommandListDownloadedModel />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -35,7 +35,6 @@ export default function ModalCancelDownload({ model, isFromList }: Props) {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[model.id]
|
[model.id]
|
||||||
)
|
)
|
||||||
const models = useAtomValue(downloadingModelsAtom)
|
|
||||||
const downloadState = useAtomValue(downloadAtom)
|
const downloadState = useAtomValue(downloadAtom)
|
||||||
const cancelText = `Cancel ${formatDownloadPercentage(downloadState.percent)}`
|
const cancelText = `Cancel ${formatDownloadPercentage(downloadState.percent)}`
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,7 @@ export default function ShortCut(props: { menu: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="inline-flex items-center justify-center rounded-md bg-secondary px-1 py-0.5 text-xs font-bold text-muted-foreground">
|
<div className="inline-flex items-center justify-center rounded-full bg-secondary px-1 py-0.5 text-xs font-bold text-muted-foreground">
|
||||||
<p>{getSymbol(os) + ' + ' + menu}</p>
|
<p>{getSymbol(os) + ' + ' + menu}</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -16,7 +16,7 @@ export function toaster(props: Props) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'pointer-events-auto relative flex min-w-[200px] max-w-[350px] gap-x-4 rounded-lg border border-border bg-background px-4 py-3',
|
'unset-drag relative flex min-w-[200px] max-w-[350px] gap-x-4 rounded-lg border border-border bg-background px-4 py-3',
|
||||||
t.visible ? 'animate-enter' : 'animate-leave',
|
t.visible ? 'animate-enter' : 'animate-leave',
|
||||||
type === 'success' && 'bg-primary text-primary-foreground'
|
type === 'success' && 'bg-primary text-primary-foreground'
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -88,7 +88,7 @@ export const cleanConversationMessages = atom(null, (get, set, id: string) => {
|
|||||||
const newData: Record<string, ThreadMessage[]> = {
|
const newData: Record<string, ThreadMessage[]> = {
|
||||||
...get(chatMessages),
|
...get(chatMessages),
|
||||||
}
|
}
|
||||||
newData[id] = newData[id].filter((e) => e.role === ChatCompletionRole.System)
|
newData[id] = newData[id]?.filter((e) => e.role === ChatCompletionRole.System)
|
||||||
set(chatMessages, newData)
|
set(chatMessages, newData)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
import { atom } from 'jotai'
|
import { atom } from 'jotai'
|
||||||
|
|
||||||
export const totalRamAtom = atom<number>(0)
|
export const totalRamAtom = atom<number>(0)
|
||||||
|
export const usedRamAtom = atom<number>(0)
|
||||||
|
|
||||||
|
export const cpuUsageAtom = atom<number>(0)
|
||||||
|
|||||||
42
web/hooks/useClickOutside.ts
Normal file
42
web/hooks/useClickOutside.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
const DEFAULT_EVENTS = ['mousedown', 'touchstart']
|
||||||
|
|
||||||
|
export function useClickOutside<T extends HTMLElement = any>(
|
||||||
|
handler: () => void,
|
||||||
|
events?: string[] | null,
|
||||||
|
nodes?: (HTMLElement | null)[]
|
||||||
|
) {
|
||||||
|
const ref = useRef<T>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const listener = (event: any) => {
|
||||||
|
const { target } = event ?? {}
|
||||||
|
if (Array.isArray(nodes)) {
|
||||||
|
const shouldIgnore =
|
||||||
|
target?.hasAttribute('data-ignore-outside-clicks') ||
|
||||||
|
(!document.body.contains(target) && target.tagName !== 'HTML')
|
||||||
|
const shouldTrigger = nodes.every(
|
||||||
|
(node) => !!node && !event.composedPath().includes(node)
|
||||||
|
)
|
||||||
|
shouldTrigger && !shouldIgnore && handler()
|
||||||
|
} else if (ref.current && !ref.current.contains(target)) {
|
||||||
|
handler()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
;(events || DEFAULT_EVENTS).forEach((fn) =>
|
||||||
|
document.addEventListener(fn, listener)
|
||||||
|
)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
;(events || DEFAULT_EVENTS).forEach((fn) =>
|
||||||
|
document.removeEventListener(fn, listener)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [ref, handler, nodes])
|
||||||
|
|
||||||
|
return ref
|
||||||
|
}
|
||||||
@ -40,7 +40,6 @@ export const useCreateNewThread = () => {
|
|||||||
const setActiveThreadId = useSetAtom(setActiveThreadIdAtom)
|
const setActiveThreadId = useSetAtom(setActiveThreadIdAtom)
|
||||||
const [threadStates, setThreadStates] = useAtom(threadStatesAtom)
|
const [threadStates, setThreadStates] = useAtom(threadStatesAtom)
|
||||||
const threads = useAtomValue(threadsAtom)
|
const threads = useAtomValue(threadsAtom)
|
||||||
const activeThread = useAtomValue(activeThreadAtom)
|
|
||||||
const updateThread = useSetAtom(updateThreadAtom)
|
const updateThread = useSetAtom(updateThreadAtom)
|
||||||
|
|
||||||
const requestCreateNewThread = async (assistant: Assistant) => {
|
const requestCreateNewThread = async (assistant: Assistant) => {
|
||||||
@ -69,6 +68,7 @@ export const useCreateNewThread = () => {
|
|||||||
stream: false,
|
stream: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
instructions: assistant.instructions,
|
||||||
}
|
}
|
||||||
const threadId = generateThreadId(assistant.id)
|
const threadId = generateThreadId(assistant.id)
|
||||||
const thread: Thread = {
|
const thread: Thread = {
|
||||||
@ -93,20 +93,18 @@ export const useCreateNewThread = () => {
|
|||||||
setActiveThreadId(thread.id)
|
setActiveThreadId(thread.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateThreadTitle(title: string) {
|
function updateThreadMetadata(thread: Thread) {
|
||||||
if (!activeThread) return
|
const updatedThread: Thread = {
|
||||||
const updatedConv: Thread = {
|
...thread,
|
||||||
...activeThread,
|
|
||||||
title,
|
|
||||||
}
|
}
|
||||||
updateThread(updatedConv)
|
updateThread(updatedThread)
|
||||||
extensionManager
|
extensionManager
|
||||||
.get<ConversationalExtension>(ExtensionType.Conversational)
|
.get<ConversationalExtension>(ExtensionType.Conversational)
|
||||||
?.saveThread(updatedConv)
|
?.saveThread(updatedThread)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
requestCreateNewThread,
|
requestCreateNewThread,
|
||||||
updateThreadTitle,
|
updateThreadMetadata,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,6 @@ import {
|
|||||||
} from '@/helpers/atoms/ChatMessage.atom'
|
} from '@/helpers/atoms/ChatMessage.atom'
|
||||||
import {
|
import {
|
||||||
threadsAtom,
|
threadsAtom,
|
||||||
getActiveThreadIdAtom,
|
|
||||||
setActiveThreadIdAtom,
|
setActiveThreadIdAtom,
|
||||||
} from '@/helpers/atoms/Conversation.atom'
|
} from '@/helpers/atoms/Conversation.atom'
|
||||||
|
|
||||||
@ -25,14 +24,13 @@ export default function useDeleteThread() {
|
|||||||
const { activeModel } = useActiveModel()
|
const { activeModel } = useActiveModel()
|
||||||
const [threads, setThreads] = useAtom(threadsAtom)
|
const [threads, setThreads] = useAtom(threadsAtom)
|
||||||
const setCurrentPrompt = useSetAtom(currentPromptAtom)
|
const setCurrentPrompt = useSetAtom(currentPromptAtom)
|
||||||
const activeThreadId = useAtomValue(getActiveThreadIdAtom)
|
|
||||||
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
||||||
|
|
||||||
const setActiveConvoId = useSetAtom(setActiveThreadIdAtom)
|
const setActiveConvoId = useSetAtom(setActiveThreadIdAtom)
|
||||||
const deleteMessages = useSetAtom(deleteConversationMessage)
|
const deleteMessages = useSetAtom(deleteConversationMessage)
|
||||||
const cleanMessages = useSetAtom(cleanConversationMessages)
|
const cleanMessages = useSetAtom(cleanConversationMessages)
|
||||||
|
|
||||||
const cleanThread = async () => {
|
const cleanThread = async (activeThreadId: string) => {
|
||||||
if (activeThreadId) {
|
if (activeThreadId) {
|
||||||
const thread = threads.filter((c) => c.id === activeThreadId)[0]
|
const thread = threads.filter((c) => c.id === activeThreadId)[0]
|
||||||
cleanMessages(activeThreadId)
|
cleanMessages(activeThreadId)
|
||||||
@ -46,7 +44,7 @@ export default function useDeleteThread() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteThread = async () => {
|
const deleteThread = async (activeThreadId: string) => {
|
||||||
if (!activeThreadId) {
|
if (!activeThreadId) {
|
||||||
alert('No active thread')
|
alert('No active thread')
|
||||||
return
|
return
|
||||||
@ -60,8 +58,8 @@ export default function useDeleteThread() {
|
|||||||
deleteMessages(activeThreadId)
|
deleteMessages(activeThreadId)
|
||||||
setCurrentPrompt('')
|
setCurrentPrompt('')
|
||||||
toaster({
|
toaster({
|
||||||
title: 'Chat successfully deleted.',
|
title: 'Thread successfully deleted.',
|
||||||
description: `Chat with ${activeModel?.name} has been successfully deleted.`,
|
description: `Thread with ${activeModel?.name} has been successfully deleted.`,
|
||||||
})
|
})
|
||||||
if (availableThreads.length > 0) {
|
if (availableThreads.length > 0) {
|
||||||
setActiveConvoId(availableThreads[0].id)
|
setActiveConvoId(availableThreads[0].id)
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Model, ExtensionType, ModelExtension } from '@janhq/core'
|
import { Model, ExtensionType, ModelExtension } from '@janhq/core'
|
||||||
|
|
||||||
import { useAtom, useAtomValue } from 'jotai'
|
import { useAtom } from 'jotai'
|
||||||
|
|
||||||
import { useDownloadState } from './useDownloadState'
|
import { useDownloadState } from './useDownloadState'
|
||||||
|
|
||||||
|
|||||||
@ -6,12 +6,18 @@ import { MonitoringExtension } from '@janhq/core'
|
|||||||
import { useSetAtom } from 'jotai'
|
import { useSetAtom } from 'jotai'
|
||||||
|
|
||||||
import { extensionManager } from '@/extension/ExtensionManager'
|
import { extensionManager } from '@/extension/ExtensionManager'
|
||||||
import { totalRamAtom } from '@/helpers/atoms/SystemBar.atom'
|
import {
|
||||||
|
cpuUsageAtom,
|
||||||
|
totalRamAtom,
|
||||||
|
usedRamAtom,
|
||||||
|
} from '@/helpers/atoms/SystemBar.atom'
|
||||||
|
|
||||||
export default function useGetSystemResources() {
|
export default function useGetSystemResources() {
|
||||||
const [ram, setRam] = useState<number>(0)
|
const [ram, setRam] = useState<number>(0)
|
||||||
const [cpu, setCPU] = useState<number>(0)
|
const [cpu, setCPU] = useState<number>(0)
|
||||||
const setTotalRam = useSetAtom(totalRamAtom)
|
const setTotalRam = useSetAtom(totalRamAtom)
|
||||||
|
const setUsedRam = useSetAtom(usedRamAtom)
|
||||||
|
const setCpuUsage = useSetAtom(cpuUsageAtom)
|
||||||
|
|
||||||
const getSystemResources = async () => {
|
const getSystemResources = async () => {
|
||||||
if (
|
if (
|
||||||
@ -27,10 +33,12 @@ export default function useGetSystemResources() {
|
|||||||
|
|
||||||
const ram =
|
const ram =
|
||||||
(resourceInfor?.mem?.active ?? 0) / (resourceInfor?.mem?.total ?? 1)
|
(resourceInfor?.mem?.active ?? 0) / (resourceInfor?.mem?.total ?? 1)
|
||||||
|
if (resourceInfor?.mem?.active) setUsedRam(resourceInfor.mem.active)
|
||||||
if (resourceInfor?.mem?.total) setTotalRam(resourceInfor.mem.total)
|
if (resourceInfor?.mem?.total) setTotalRam(resourceInfor.mem.total)
|
||||||
|
|
||||||
setRam(Math.round(ram * 100))
|
setRam(Math.round(ram * 100))
|
||||||
setCPU(Math.round(currentLoadInfor?.currentLoad ?? 0))
|
setCPU(Math.round(currentLoadInfor?.currentLoad ?? 0))
|
||||||
|
setCpuUsage(Math.round(currentLoadInfor?.currentLoad ?? 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -45,6 +53,7 @@ export default function useGetSystemResources() {
|
|||||||
|
|
||||||
// clean up interval
|
// clean up interval
|
||||||
return () => clearInterval(intervalId)
|
return () => clearInterval(intervalId)
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { atom, useAtom } from 'jotai'
|
|||||||
|
|
||||||
import { MainViewState } from '@/constants/screens'
|
import { MainViewState } from '@/constants/screens'
|
||||||
|
|
||||||
const currentMainViewState = atom<MainViewState>(MainViewState.Welcome)
|
const currentMainViewState = atom<MainViewState>(MainViewState.Thread)
|
||||||
|
|
||||||
export function useMainViewState() {
|
export function useMainViewState() {
|
||||||
const [mainViewState, setMainViewState] = useAtom(currentMainViewState)
|
const [mainViewState, setMainViewState] = useAtom(currentMainViewState)
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ChatCompletionMessage,
|
ChatCompletionMessage,
|
||||||
ChatCompletionRole,
|
ChatCompletionRole,
|
||||||
@ -10,7 +12,7 @@ import {
|
|||||||
ThreadMessage,
|
ThreadMessage,
|
||||||
events,
|
events,
|
||||||
} from '@janhq/core'
|
} from '@janhq/core'
|
||||||
import { ConversationalExtension, InferenceExtension } from '@janhq/core'
|
import { ConversationalExtension } from '@janhq/core'
|
||||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
import { ulid } from 'ulid'
|
import { ulid } from 'ulid'
|
||||||
@ -44,6 +46,7 @@ export default function useSendChatMessage() {
|
|||||||
const { activeModel } = useActiveModel()
|
const { activeModel } = useActiveModel()
|
||||||
const selectedModel = useAtomValue(selectedModelAtom)
|
const selectedModel = useAtomValue(selectedModelAtom)
|
||||||
const { startModel } = useActiveModel()
|
const { startModel } = useActiveModel()
|
||||||
|
const [queuedMessage, setQueuedMessage] = useState(false)
|
||||||
|
|
||||||
const sendChatMessage = async () => {
|
const sendChatMessage = async () => {
|
||||||
if (!currentPrompt || currentPrompt.trim().length === 0) {
|
if (!currentPrompt || currentPrompt.trim().length === 0) {
|
||||||
@ -61,14 +64,15 @@ export default function useSendChatMessage() {
|
|||||||
}
|
}
|
||||||
const assistantId = activeThread.assistants[0].assistant_id ?? ''
|
const assistantId = activeThread.assistants[0].assistant_id ?? ''
|
||||||
const assistantName = activeThread.assistants[0].assistant_name ?? ''
|
const assistantName = activeThread.assistants[0].assistant_name ?? ''
|
||||||
|
const instructions = activeThread.assistants[0].instructions ?? ''
|
||||||
const updatedThread: Thread = {
|
const updatedThread: Thread = {
|
||||||
...activeThread,
|
...activeThread,
|
||||||
isFinishInit: true,
|
isFinishInit: true,
|
||||||
title: `${activeThread.assistants[0].assistant_name} with ${selectedModel.name}`,
|
|
||||||
assistants: [
|
assistants: [
|
||||||
{
|
{
|
||||||
assistant_id: assistantId,
|
assistant_id: assistantId,
|
||||||
assistant_name: assistantName,
|
assistant_name: assistantName,
|
||||||
|
instructions: instructions,
|
||||||
model: {
|
model: {
|
||||||
id: selectedModel.id,
|
id: selectedModel.id,
|
||||||
settings: selectedModel.settings,
|
settings: selectedModel.settings,
|
||||||
@ -90,7 +94,18 @@ export default function useSendChatMessage() {
|
|||||||
const prompt = currentPrompt.trim()
|
const prompt = currentPrompt.trim()
|
||||||
setCurrentPrompt('')
|
setCurrentPrompt('')
|
||||||
|
|
||||||
const messages: ChatCompletionMessage[] = currentMessages
|
const messages: ChatCompletionMessage[] = [
|
||||||
|
activeThread.assistants[0]?.instructions,
|
||||||
|
]
|
||||||
|
.map<ChatCompletionMessage>((instructions) => {
|
||||||
|
const systemMessage: ChatCompletionMessage = {
|
||||||
|
role: ChatCompletionRole.System,
|
||||||
|
content: instructions,
|
||||||
|
}
|
||||||
|
return systemMessage
|
||||||
|
})
|
||||||
|
.concat(
|
||||||
|
currentMessages
|
||||||
.map<ChatCompletionMessage>((msg) => ({
|
.map<ChatCompletionMessage>((msg) => ({
|
||||||
role: msg.role,
|
role: msg.role,
|
||||||
content: msg.content[0]?.text.value ?? '',
|
content: msg.content[0]?.text.value ?? '',
|
||||||
@ -101,7 +116,7 @@ export default function useSendChatMessage() {
|
|||||||
content: prompt,
|
content: prompt,
|
||||||
} as ChatCompletionMessage,
|
} as ChatCompletionMessage,
|
||||||
])
|
])
|
||||||
console.debug(`Sending messages: ${JSON.stringify(messages, null, 2)}`)
|
)
|
||||||
const msgId = ulid()
|
const msgId = ulid()
|
||||||
const messageRequest: MessageRequest = {
|
const messageRequest: MessageRequest = {
|
||||||
id: msgId,
|
id: msgId,
|
||||||
@ -136,17 +151,17 @@ export default function useSendChatMessage() {
|
|||||||
?.addNewMessage(threadMessage)
|
?.addNewMessage(threadMessage)
|
||||||
|
|
||||||
const modelId = selectedModel?.id ?? activeThread.assistants[0].model.id
|
const modelId = selectedModel?.id ?? activeThread.assistants[0].model.id
|
||||||
|
|
||||||
if (activeModel?.id !== modelId) {
|
if (activeModel?.id !== modelId) {
|
||||||
toaster({
|
setQueuedMessage(true)
|
||||||
title: 'Message queued.',
|
|
||||||
description: 'It will be sent once the model is done loading',
|
|
||||||
})
|
|
||||||
await startModel(modelId)
|
await startModel(modelId)
|
||||||
|
setQueuedMessage(false)
|
||||||
}
|
}
|
||||||
events.emit(EventName.OnMessageSent, messageRequest)
|
events.emit(EventName.OnMessageSent, messageRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sendChatMessage,
|
sendChatMessage,
|
||||||
|
queuedMessage,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,6 +33,7 @@
|
|||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-hook-form": "^7.47.0",
|
"react-hook-form": "^7.47.0",
|
||||||
"react-hot-toast": "^2.4.1",
|
"react-hot-toast": "^2.4.1",
|
||||||
|
"react-scroll-to-bottom": "^4.2.0",
|
||||||
"react-toastify": "^9.1.3",
|
"react-toastify": "^9.1.3",
|
||||||
"sass": "^1.69.4",
|
"sass": "^1.69.4",
|
||||||
"tailwind-merge": "^2.0.0",
|
"tailwind-merge": "^2.0.0",
|
||||||
@ -48,6 +49,7 @@
|
|||||||
"@types/node": "20.8.10",
|
"@types/node": "20.8.10",
|
||||||
"@types/react": "18.2.34",
|
"@types/react": "18.2.34",
|
||||||
"@types/react-dom": "18.2.14",
|
"@types/react-dom": "18.2.14",
|
||||||
|
"@types/react-scroll-to-bottom": "^4.2.4",
|
||||||
"@types/uuid": "^9.0.6",
|
"@types/uuid": "^9.0.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.8.0",
|
"@typescript-eslint/eslint-plugin": "^6.8.0",
|
||||||
"@typescript-eslint/parser": "^6.8.0",
|
"@typescript-eslint/parser": "^6.8.0",
|
||||||
|
|||||||
@ -1,17 +1,65 @@
|
|||||||
|
import { Fragment } from 'react'
|
||||||
|
|
||||||
|
import ScrollToBottom from 'react-scroll-to-bottom'
|
||||||
|
|
||||||
|
import { Button } from '@janhq/uikit'
|
||||||
import { useAtomValue } from 'jotai'
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
|
import LogoMark from '@/containers/Brand/Logo/Mark'
|
||||||
|
|
||||||
|
import { MainViewState } from '@/constants/screens'
|
||||||
|
|
||||||
|
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
|
||||||
|
|
||||||
|
import { useMainViewState } from '@/hooks/useMainViewState'
|
||||||
|
|
||||||
import ChatItem from '../ChatItem'
|
import ChatItem from '../ChatItem'
|
||||||
|
|
||||||
import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
|
import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
|
||||||
|
|
||||||
const ChatBody: React.FC = () => {
|
const ChatBody: React.FC = () => {
|
||||||
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
||||||
|
const { downloadedModels } = useGetDownloadedModels()
|
||||||
|
const { setMainViewState } = useMainViewState()
|
||||||
|
|
||||||
|
if (downloadedModels.length === 0)
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col overflow-y-auto">
|
<div className="mx-auto flex h-full w-3/4 flex-col items-center justify-center text-center">
|
||||||
|
<LogoMark
|
||||||
|
className="mx-auto mb-4 animate-wave"
|
||||||
|
width={56}
|
||||||
|
height={56}
|
||||||
|
/>
|
||||||
|
<h1 className="text-2xl font-bold">Welcome!</h1>
|
||||||
|
<p className="mt-1 text-base">You need to download your first model</p>
|
||||||
|
<Button
|
||||||
|
className="mt-4"
|
||||||
|
onClick={() => setMainViewState(MainViewState.Hub)}
|
||||||
|
>
|
||||||
|
Explore The Hub
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
{messages.length === 0 ? (
|
||||||
|
<div className="mx-auto flex h-full w-3/4 flex-col items-center justify-center text-center">
|
||||||
|
<LogoMark
|
||||||
|
className="mx-auto mb-4 animate-wave"
|
||||||
|
width={56}
|
||||||
|
height={56}
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-base font-medium">How can I help you?</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ScrollToBottom className="flex h-full w-full flex-col">
|
||||||
{messages.map((message) => (
|
{messages.map((message) => (
|
||||||
<ChatItem {...message} key={message.id} />
|
<ChatItem {...message} key={message.id} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</ScrollToBottom>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,10 +7,7 @@ import SimpleTextMessage from '../SimpleTextMessage'
|
|||||||
type Ref = HTMLDivElement
|
type Ref = HTMLDivElement
|
||||||
|
|
||||||
const ChatItem = forwardRef<Ref, ThreadMessage>((message, ref) => (
|
const ChatItem = forwardRef<Ref, ThreadMessage>((message, ref) => (
|
||||||
<div
|
<div ref={ref} className="relative py-4">
|
||||||
ref={ref}
|
|
||||||
className="relative py-4 first:pb-14 even:bg-secondary dark:even:bg-secondary/20"
|
|
||||||
>
|
|
||||||
<SimpleTextMessage {...message} />
|
<SimpleTextMessage {...message} />
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
|
|||||||
@ -1,7 +1,4 @@
|
|||||||
import { useMemo } from 'react'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ChatCompletionRole,
|
|
||||||
ChatCompletionMessage,
|
ChatCompletionMessage,
|
||||||
EventName,
|
EventName,
|
||||||
MessageRequest,
|
MessageRequest,
|
||||||
@ -11,8 +8,8 @@ import {
|
|||||||
events,
|
events,
|
||||||
} from '@janhq/core'
|
} from '@janhq/core'
|
||||||
import { ConversationalExtension, InferenceExtension } from '@janhq/core'
|
import { ConversationalExtension, InferenceExtension } from '@janhq/core'
|
||||||
import { atom, useAtomValue, useSetAtom } from 'jotai'
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
import { RefreshCcw, ClipboardCopy, Trash2Icon, StopCircle } from 'lucide-react'
|
import { RefreshCcw, Copy, Trash2Icon, StopCircle } from 'lucide-react'
|
||||||
|
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
@ -23,21 +20,17 @@ import {
|
|||||||
deleteMessageAtom,
|
deleteMessageAtom,
|
||||||
getCurrentChatMessagesAtom,
|
getCurrentChatMessagesAtom,
|
||||||
} from '@/helpers/atoms/ChatMessage.atom'
|
} from '@/helpers/atoms/ChatMessage.atom'
|
||||||
import {
|
import { activeThreadAtom } from '@/helpers/atoms/Conversation.atom'
|
||||||
activeThreadAtom,
|
|
||||||
threadStatesAtom,
|
|
||||||
} from '@/helpers/atoms/Conversation.atom'
|
|
||||||
|
|
||||||
const MessageToolbar = ({ message }: { message: ThreadMessage }) => {
|
const MessageToolbar = ({ message }: { message: ThreadMessage }) => {
|
||||||
const deleteMessage = useSetAtom(deleteMessageAtom)
|
const deleteMessage = useSetAtom(deleteMessageAtom)
|
||||||
const thread = useAtomValue(activeThreadAtom)
|
const thread = useAtomValue(activeThreadAtom)
|
||||||
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
||||||
const threadStateAtom = useMemo(
|
// const threadStateAtom = useMemo(
|
||||||
() => atom((get) => get(threadStatesAtom)[thread?.id ?? '']),
|
// () => atom((get) => get(threadStatesAtom)[thread?.id ?? '']),
|
||||||
[thread?.id]
|
// [thread?.id]
|
||||||
)
|
// )
|
||||||
const threadState = useAtomValue(threadStateAtom)
|
// const threadState = useAtomValue(threadStateAtom)
|
||||||
|
|
||||||
const stopInference = async () => {
|
const stopInference = async () => {
|
||||||
await extensionManager
|
await extensionManager
|
||||||
.get<InferenceExtension>(ExtensionType.Inference)
|
.get<InferenceExtension>(ExtensionType.Inference)
|
||||||
@ -51,12 +44,7 @@ const MessageToolbar = ({ message }: { message: ThreadMessage }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={twMerge('flex flex-row items-center')}>
|
||||||
className={twMerge(
|
|
||||||
'flex-row items-center',
|
|
||||||
threadState.waitingForResponse ? 'hidden' : 'flex'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex overflow-hidden rounded-md border border-border bg-background/20">
|
<div className="flex overflow-hidden rounded-md border border-border bg-background/20">
|
||||||
{message.status === MessageStatus.Pending && (
|
{message.status === MessageStatus.Pending && (
|
||||||
<div
|
<div
|
||||||
@ -97,7 +85,7 @@ const MessageToolbar = ({ message }: { message: ThreadMessage }) => {
|
|||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ClipboardCopy size={14} />
|
<Copy size={14} />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="cursor-pointer px-2 py-2 hover:bg-background/80"
|
className="cursor-pointer px-2 py-2 hover:bg-background/80"
|
||||||
|
|||||||
@ -1,30 +1,35 @@
|
|||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
|
|
||||||
import { getUserSpace, openFileExplorer } from '@janhq/core'
|
import { getUserSpace, openFileExplorer } from '@janhq/core'
|
||||||
|
|
||||||
|
import { Input, Textarea } from '@janhq/uikit'
|
||||||
|
|
||||||
import { atom, useAtomValue } from 'jotai'
|
import { atom, useAtomValue } from 'jotai'
|
||||||
|
|
||||||
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
|
import LogoMark from '@/containers/Brand/Logo/Mark'
|
||||||
import CardSidebar from '@/containers/CardSidebar'
|
import CardSidebar from '@/containers/CardSidebar'
|
||||||
import DropdownListSidebar, {
|
import DropdownListSidebar, {
|
||||||
selectedModelAtom,
|
selectedModelAtom,
|
||||||
} from '@/containers/DropdownListSidebar'
|
} from '@/containers/DropdownListSidebar'
|
||||||
import ItemCardSidebar from '@/containers/ItemCardSidebar'
|
|
||||||
|
|
||||||
import { useCreateNewThread } from '@/hooks/useCreateNewThread'
|
import { useCreateNewThread } from '@/hooks/useCreateNewThread'
|
||||||
|
|
||||||
import { activeThreadAtom } from '@/helpers/atoms/Conversation.atom'
|
import { activeThreadAtom } from '@/helpers/atoms/Conversation.atom'
|
||||||
|
|
||||||
export const showRightSideBarAtom = atom<boolean>(false)
|
export const showRightSideBarAtom = atom<boolean>(true)
|
||||||
|
|
||||||
export default function Sidebar() {
|
export default function Sidebar() {
|
||||||
const showing = useAtomValue(showRightSideBarAtom)
|
const showing = useAtomValue(showRightSideBarAtom)
|
||||||
const activeThread = useAtomValue(activeThreadAtom)
|
const activeThread = useAtomValue(activeThreadAtom)
|
||||||
const selectedModel = useAtomValue(selectedModelAtom)
|
const selectedModel = useAtomValue(selectedModelAtom)
|
||||||
const { updateThreadTitle } = useCreateNewThread()
|
const { updateThreadMetadata } = useCreateNewThread()
|
||||||
|
|
||||||
const onReviewInFinderClick = async (type: string) => {
|
const onReviewInFinderClick = async (type: string) => {
|
||||||
if (!activeThread) return
|
if (!activeThread) return
|
||||||
if (!activeThread.isFinishInit) {
|
if (!activeThread.isFinishInit) {
|
||||||
alert('Thread is not ready')
|
alert('Thread is not started yet')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,7 +61,7 @@ export default function Sidebar() {
|
|||||||
const onViewJsonClick = async (type: string) => {
|
const onViewJsonClick = async (type: string) => {
|
||||||
if (!activeThread) return
|
if (!activeThread) return
|
||||||
if (!activeThread.isFinishInit) {
|
if (!activeThread.isFinishInit) {
|
||||||
alert('Thread is not ready')
|
alert('Thread is not started yet')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,44 +92,104 @@ export default function Sidebar() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`h-full overflow-x-hidden border-l border-border duration-300 ease-linear ${
|
className={twMerge(
|
||||||
showing ? 'w-80' : 'w-0'
|
'h-full flex-shrink-0 overflow-x-hidden border-l border-border bg-background transition-all duration-100 dark:bg-background/20',
|
||||||
}`}
|
showing
|
||||||
|
? 'w-80 translate-x-0 opacity-100'
|
||||||
|
: 'w-0 translate-x-full opacity-0'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
'flex flex-col gap-4 p-4 delay-200',
|
||||||
|
showing ? 'animate-enter opacity-100' : 'opacity-0'
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-1 p-2">
|
|
||||||
<CardSidebar
|
<CardSidebar
|
||||||
title="Thread"
|
title="Thread"
|
||||||
onRevealInFinderClick={onReviewInFinderClick}
|
onRevealInFinderClick={onReviewInFinderClick}
|
||||||
onViewJsonClick={onViewJsonClick}
|
onViewJsonClick={onViewJsonClick}
|
||||||
>
|
>
|
||||||
<ItemCardSidebar
|
<div className="flex flex-col space-y-4 p-2">
|
||||||
description={activeThread?.id}
|
<div>
|
||||||
title="Thread ID"
|
<label
|
||||||
disabled
|
id="thread-title"
|
||||||
/>
|
className="mb-2 inline-block font-bold text-gray-600 dark:text-gray-300"
|
||||||
<ItemCardSidebar
|
>
|
||||||
title="Thread title"
|
Title
|
||||||
description={activeThread?.title}
|
</label>
|
||||||
onChange={(title) => updateThreadTitle(title ?? '')}
|
<Input
|
||||||
|
id="thread-title"
|
||||||
|
value={activeThread?.title}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (activeThread)
|
||||||
|
updateThreadMetadata({
|
||||||
|
...activeThread,
|
||||||
|
title: e.target.value || '',
|
||||||
|
})
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<label
|
||||||
|
id="thread-title"
|
||||||
|
className="mb-2 inline-block font-bold text-gray-600 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
Threads ID
|
||||||
|
</label>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{activeThread?.id || '-'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</CardSidebar>
|
</CardSidebar>
|
||||||
<CardSidebar
|
<CardSidebar
|
||||||
title="Assistant"
|
title="Assistant"
|
||||||
onRevealInFinderClick={onReviewInFinderClick}
|
onRevealInFinderClick={onReviewInFinderClick}
|
||||||
onViewJsonClick={onViewJsonClick}
|
onViewJsonClick={onViewJsonClick}
|
||||||
>
|
>
|
||||||
<ItemCardSidebar
|
<div className="flex flex-col space-y-4 p-2">
|
||||||
description={activeThread?.assistants[0].assistant_name ?? ''}
|
<div className="flex items-center space-x-2">
|
||||||
title="Assistant"
|
<LogoMark width={24} height={24} />
|
||||||
disabled
|
<span className="font-bold capitalize">
|
||||||
|
{activeThread?.assistants[0].assistant_name ?? '-'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
id="thread-title"
|
||||||
|
className="mb-2 inline-block font-bold text-gray-600 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
Instructions
|
||||||
|
</label>
|
||||||
|
<Textarea
|
||||||
|
id="assistant-instructions"
|
||||||
|
placeholder="Eg. You are a helpful assistant."
|
||||||
|
value={activeThread?.assistants[0].instructions ?? ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (activeThread)
|
||||||
|
updateThreadMetadata({
|
||||||
|
...activeThread,
|
||||||
|
assistants: [
|
||||||
|
{
|
||||||
|
...activeThread.assistants[0],
|
||||||
|
instructions: e.target.value || '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</CardSidebar>
|
</CardSidebar>
|
||||||
<CardSidebar
|
<CardSidebar
|
||||||
title="Model"
|
title="Model"
|
||||||
onRevealInFinderClick={onReviewInFinderClick}
|
onRevealInFinderClick={onReviewInFinderClick}
|
||||||
onViewJsonClick={onViewJsonClick}
|
onViewJsonClick={onViewJsonClick}
|
||||||
>
|
>
|
||||||
|
<div className="p-2">
|
||||||
<DropdownListSidebar />
|
<DropdownListSidebar />
|
||||||
|
</div>
|
||||||
</CardSidebar>
|
</CardSidebar>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -84,26 +84,57 @@ const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
|
|||||||
}, [props.content])
|
}, [props.content])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group relative mx-auto rounded-xl px-4 lg:w-3/4">
|
<div className="group relative mx-auto rounded-xl px-8">
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'mb-2 flex items-center justify-start gap-x-2',
|
'mb-2 flex items-center justify-start gap-x-2',
|
||||||
!isUser && 'mt-2'
|
!isUser && 'mt-2'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{!isUser && !isSystem && <LogoMark width={20} />}
|
{!isUser && !isSystem && <LogoMark width={28} />}
|
||||||
<div className="text-sm font-extrabold capitalize">{props.role}</div>
|
{isUser && (
|
||||||
<p className="text-xs font-medium">{displayDate(props.created)}</p>
|
<div className="flex h-8 w-8 items-center justify-center rounded-full border border-border">
|
||||||
|
<svg
|
||||||
|
width="12"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 12 16"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M6 0.497864C4.34315 0.497864 3 1.84101 3 3.49786C3 5.15472 4.34315 6.49786 6 6.49786C7.65685 6.49786 9 5.15472 9 3.49786C9 1.84101 7.65685 0.497864 6 0.497864ZM9.75 7.99786L2.24997 7.99787C1.00734 7.99787 0 9.00527 0 10.2479C0 11.922 0.688456 13.2633 1.81822 14.1701C2.93013 15.0625 4.42039 15.4979 6 15.4979C7.57961 15.4979 9.06987 15.0625 10.1818 14.1701C11.3115 13.2633 12 11.922 12 10.2479C12 9.00522 10.9926 7.99786 9.75 7.99786Z"
|
||||||
|
fill="#9CA3AF"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
'text-sm font-extrabold capitalize',
|
||||||
|
isUser && 'text-gray-500'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{props.role}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs font-medium text-gray-400">
|
||||||
|
{displayDate(props.created)}
|
||||||
|
</p>
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'absolute right-0 cursor-pointer transition-all',
|
'absolute right-0 cursor-pointer transition-all',
|
||||||
messages[messages.length - 1]?.id === props.id
|
messages[messages.length - 1]?.id === props.id && !isUser
|
||||||
? 'absolute -bottom-10 left-4'
|
? 'absolute -bottom-10 right-8'
|
||||||
: 'hidden group-hover:flex'
|
: 'hidden group-hover:absolute group-hover:-top-2 group-hover:right-8 group-hover:flex'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<MessageToolbar message={props} />
|
<MessageToolbar message={props} />
|
||||||
</div>
|
</div>
|
||||||
|
{messages[messages.length - 1]?.id === props.id &&
|
||||||
|
(props.status === MessageStatus.Pending || tokenSpeed > 0) && (
|
||||||
|
<p className="absolute right-8 text-xs font-medium text-foreground">
|
||||||
|
Token Speed: {Number(tokenSpeed).toFixed(2)}/s
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={twMerge('w-full')}>
|
<div className={twMerge('w-full')}>
|
||||||
@ -115,7 +146,9 @@ const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
|
|||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'message flex flex-grow flex-col gap-y-2 text-[15px] font-normal leading-relaxed',
|
'message flex flex-grow flex-col gap-y-2 text-[15px] font-normal leading-relaxed',
|
||||||
isUser && 'whitespace-pre-wrap break-words'
|
isUser
|
||||||
|
? 'whitespace-pre-wrap break-words'
|
||||||
|
: 'rounded-xl bg-secondary p-4'
|
||||||
)}
|
)}
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
dangerouslySetInnerHTML={{ __html: parsedText }}
|
dangerouslySetInnerHTML={{ __html: parsedText }}
|
||||||
@ -123,11 +156,6 @@ const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{(props.status === MessageStatus.Pending || tokenSpeed > 0) && (
|
|
||||||
<p className="mt-2 text-xs font-medium text-foreground">
|
|
||||||
Token Speed: {Number(tokenSpeed).toFixed(2)}/s
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,21 +1,28 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
import { Button } from '@janhq/uikit'
|
|
||||||
import { motion as m } from 'framer-motion'
|
import { motion as m } from 'framer-motion'
|
||||||
import { useAtomValue } from 'jotai'
|
import { useAtomValue } from 'jotai'
|
||||||
import { GalleryHorizontalEndIcon } from 'lucide-react'
|
import {
|
||||||
|
GalleryHorizontalEndIcon,
|
||||||
|
MoreVerticalIcon,
|
||||||
|
Trash2Icon,
|
||||||
|
Paintbrush,
|
||||||
|
} from 'lucide-react'
|
||||||
|
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
import { useCreateNewThread } from '@/hooks/useCreateNewThread'
|
import { useCreateNewThread } from '@/hooks/useCreateNewThread'
|
||||||
|
import useDeleteThread from '@/hooks/useDeleteConversation'
|
||||||
import useGetAllThreads from '@/hooks/useGetAllThreads'
|
import useGetAllThreads from '@/hooks/useGetAllThreads'
|
||||||
import useGetAssistants from '@/hooks/useGetAssistants'
|
|
||||||
|
|
||||||
|
import useGetAssistants from '@/hooks/useGetAssistants'
|
||||||
|
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
|
||||||
import useSetActiveThread from '@/hooks/useSetActiveThread'
|
import useSetActiveThread from '@/hooks/useSetActiveThread'
|
||||||
|
|
||||||
import { displayDate } from '@/utils/datetime'
|
import { displayDate } from '@/utils/datetime'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
activeThreadAtom,
|
||||||
threadStatesAtom,
|
threadStatesAtom,
|
||||||
threadsAtom,
|
threadsAtom,
|
||||||
} from '@/helpers/atoms/Conversation.atom'
|
} from '@/helpers/atoms/Conversation.atom'
|
||||||
@ -23,69 +30,101 @@ import {
|
|||||||
export default function ThreadList() {
|
export default function ThreadList() {
|
||||||
const threads = useAtomValue(threadsAtom)
|
const threads = useAtomValue(threadsAtom)
|
||||||
const threadStates = useAtomValue(threadStatesAtom)
|
const threadStates = useAtomValue(threadStatesAtom)
|
||||||
const { requestCreateNewThread } = useCreateNewThread()
|
|
||||||
const { assistants } = useGetAssistants()
|
|
||||||
const { getAllThreads } = useGetAllThreads()
|
const { getAllThreads } = useGetAllThreads()
|
||||||
|
const { assistants } = useGetAssistants()
|
||||||
|
const { requestCreateNewThread } = useCreateNewThread()
|
||||||
|
const activeThread = useAtomValue(activeThreadAtom)
|
||||||
|
const { deleteThread, cleanThread } = useDeleteThread()
|
||||||
|
const { downloadedModels } = useGetDownloadedModels()
|
||||||
|
|
||||||
const { activeThreadId, setActiveThread: onThreadClick } =
|
const { activeThreadId, setActiveThread: onThreadClick } =
|
||||||
useSetActiveThread()
|
useSetActiveThread()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getAllThreads()
|
getAllThreads()
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const onCreateConversationClick = async () => {
|
useEffect(() => {
|
||||||
if (assistants.length === 0) {
|
if (
|
||||||
alert('No assistant available')
|
downloadedModels.length !== 0 &&
|
||||||
return
|
threads.length === 0 &&
|
||||||
}
|
assistants.length !== 0 &&
|
||||||
|
!activeThread
|
||||||
|
) {
|
||||||
requestCreateNewThread(assistants[0])
|
requestCreateNewThread(assistants[0])
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [assistants, threads, downloadedModels, activeThread])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="px-3 py-4">
|
||||||
<div className="sticky top-0 z-20 flex flex-col border-b border-border bg-background px-4 py-3">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
themes="secondary"
|
|
||||||
onClick={onCreateConversationClick}
|
|
||||||
>
|
|
||||||
Create New Chat
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{threads.length === 0 ? (
|
{threads.length === 0 ? (
|
||||||
<div className="px-4 py-8 text-center">
|
<div className="px-4 py-8 text-center">
|
||||||
<GalleryHorizontalEndIcon
|
<GalleryHorizontalEndIcon
|
||||||
size={26}
|
size={26}
|
||||||
className="mx-auto mb-3 text-muted-foreground"
|
className="mx-auto mb-3 text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
<h2 className="font-semibold">No Chat History</h2>
|
<h2 className="font-semibold">No Thread History</h2>
|
||||||
<p className="mt-1 text-xs">Get started by creating a new chat</p>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
threads.map((thread, i) => {
|
threads.map((thread, i) => {
|
||||||
const lastMessage = threadStates[thread.id]?.lastMessage ?? ''
|
const lastMessage =
|
||||||
|
threadStates[thread.id]?.lastMessage ?? 'No new message'
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'relative flex cursor-pointer flex-col border-b border-border px-4 py-2 hover:bg-secondary/20',
|
`group/message relative mb-1 flex cursor-pointer flex-col transition-all hover:rounded-lg hover:bg-gray-100 hover:dark:bg-secondary/50`
|
||||||
activeThreadId === thread.id && 'bg-secondary-10'
|
|
||||||
)}
|
)}
|
||||||
onClick={() => onThreadClick(thread)}
|
onClick={() => onThreadClick(thread)}
|
||||||
>
|
>
|
||||||
<p className="mb-1 line-clamp-1 text-xs leading-5">
|
<div className="relative z-10 p-4 py-4">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<h2 className="line-clamp-1 font-bold">{thread.title}</h2>
|
||||||
|
<p className="mb-1 line-clamp-1 text-xs leading-5 text-muted-foreground">
|
||||||
{thread.updated &&
|
{thread.updated &&
|
||||||
displayDate(new Date(thread.updated).getTime())}
|
displayDate(new Date(thread.updated).getTime())}
|
||||||
</p>
|
</p>
|
||||||
<h2 className="line-clamp-1">{thread.title}</h2>
|
</div>
|
||||||
<p className="mt-1 line-clamp-2 text-xs">{lastMessage}</p>
|
<p className="mt-1 line-clamp-1 text-xs text-gray-700 group-hover/message:max-w-[160px] dark:text-gray-300">
|
||||||
|
{lastMessage || 'No new message'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
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`
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<MoreVerticalIcon />
|
||||||
|
<div className="invisible absolute right-0 z-20 w-40 overflow-hidden rounded-lg border border-border bg-background shadow-lg group-hover/icon:visible">
|
||||||
|
<div
|
||||||
|
className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-secondary"
|
||||||
|
onClick={() => cleanThread(thread.id)}
|
||||||
|
>
|
||||||
|
<Paintbrush size={16} className="text-muted-foreground" />
|
||||||
|
<span className="text-bold text-black dark:text-muted-foreground">
|
||||||
|
Clean thread
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-secondary"
|
||||||
|
onClick={() => deleteThread(thread.id)}
|
||||||
|
>
|
||||||
|
<Trash2Icon size={16} className="text-muted-foreground" />
|
||||||
|
<span className="text-bold text-black dark:text-muted-foreground">
|
||||||
|
Delete thread
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* {messages.length > 0 && (
|
||||||
|
)} */}
|
||||||
|
|
||||||
{activeThreadId === thread.id && (
|
{activeThreadId === thread.id && (
|
||||||
<m.div
|
<m.div
|
||||||
className="absolute right-0 top-0 h-full w-1 bg-primary/50"
|
className="absolute inset-0 left-0 h-full w-full rounded-lg bg-gray-100 p-4 dark:bg-secondary/50"
|
||||||
layoutId="active-convo"
|
layoutId="active-thread"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,20 +1,18 @@
|
|||||||
import { Fragment, useEffect, useRef, useState } from 'react'
|
import { ChangeEvent, Fragment, KeyboardEvent, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
import { Button, Badge, Textarea } from '@janhq/uikit'
|
import { Button, Textarea } from '@janhq/uikit'
|
||||||
|
|
||||||
import { useAtom, useAtomValue } from 'jotai'
|
import { useAtom, useAtomValue } from 'jotai'
|
||||||
import { Trash2Icon, Paintbrush } from 'lucide-react'
|
|
||||||
|
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
import { currentPromptAtom } from '@/containers/Providers/Jotai'
|
import LogoMark from '@/containers/Brand/Logo/Mark'
|
||||||
|
|
||||||
import ShortCut from '@/containers/Shortcut'
|
import { currentPromptAtom } from '@/containers/Providers/Jotai'
|
||||||
|
|
||||||
import { MainViewState } from '@/constants/screens'
|
import { MainViewState } from '@/constants/screens'
|
||||||
|
|
||||||
import { useActiveModel } from '@/hooks/useActiveModel'
|
import { useActiveModel } from '@/hooks/useActiveModel'
|
||||||
import useDeleteThread from '@/hooks/useDeleteConversation'
|
|
||||||
|
|
||||||
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
|
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
|
||||||
import { useMainViewState } from '@/hooks/useMainViewState'
|
import { useMainViewState } from '@/hooks/useMainViewState'
|
||||||
@ -25,39 +23,34 @@ import ChatBody from '@/screens/Chat/ChatBody'
|
|||||||
|
|
||||||
import ThreadList from '@/screens/Chat/ThreadList'
|
import ThreadList from '@/screens/Chat/ThreadList'
|
||||||
|
|
||||||
import Sidebar from './Sidebar'
|
import Sidebar, { showRightSideBarAtom } from './Sidebar'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
activeThreadAtom,
|
activeThreadAtom,
|
||||||
getActiveThreadIdAtom,
|
getActiveThreadIdAtom,
|
||||||
threadsAtom,
|
|
||||||
waitingToSendMessage,
|
waitingToSendMessage,
|
||||||
} from '@/helpers/atoms/Conversation.atom'
|
} from '@/helpers/atoms/Conversation.atom'
|
||||||
|
|
||||||
import { activeThreadStateAtom } from '@/helpers/atoms/Conversation.atom'
|
import { activeThreadStateAtom } from '@/helpers/atoms/Conversation.atom'
|
||||||
|
|
||||||
const ChatScreen = () => {
|
const ChatScreen = () => {
|
||||||
const currentConvo = useAtomValue(activeThreadAtom)
|
const activeThread = useAtomValue(activeThreadAtom)
|
||||||
const { downloadedModels } = useGetDownloadedModels()
|
const { downloadedModels } = useGetDownloadedModels()
|
||||||
const { deleteThread, cleanThread } = useDeleteThread()
|
|
||||||
const { activeModel, stateModel } = useActiveModel()
|
const { activeModel, stateModel } = useActiveModel()
|
||||||
const { setMainViewState } = useMainViewState()
|
const { setMainViewState } = useMainViewState()
|
||||||
|
|
||||||
const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom)
|
const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom)
|
||||||
const currentConvoState = useAtomValue(activeThreadStateAtom)
|
const activeThreadState = useAtomValue(activeThreadStateAtom)
|
||||||
const { sendChatMessage } = useSendChatMessage()
|
const { sendChatMessage, queuedMessage } = useSendChatMessage()
|
||||||
const isWaitingForResponse = currentConvoState?.waitingForResponse ?? false
|
const isWaitingForResponse = activeThreadState?.waitingForResponse ?? false
|
||||||
const disabled = currentPrompt.trim().length === 0 || isWaitingForResponse
|
const disabled = currentPrompt.trim().length === 0 || isWaitingForResponse
|
||||||
|
|
||||||
const activeThreadId = useAtomValue(getActiveThreadIdAtom)
|
const activeThreadId = useAtomValue(getActiveThreadIdAtom)
|
||||||
const [isWaitingToSend, setIsWaitingToSend] = useAtom(waitingToSendMessage)
|
const [isWaitingToSend, setIsWaitingToSend] = useAtom(waitingToSendMessage)
|
||||||
const conversations = useAtomValue(threadsAtom)
|
|
||||||
const isEnableChat = (currentConvo && activeModel) || conversations.length > 0
|
|
||||||
|
|
||||||
const [isModelAvailable, setIsModelAvailable] = useState(
|
const showing = useAtomValue(showRightSideBarAtom)
|
||||||
true
|
|
||||||
// downloadedModels.some((x) => x.id === currentConvo?.modelId)
|
|
||||||
)
|
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||||
const modelRef = useRef(activeModel)
|
const modelRef = useRef(activeModel)
|
||||||
|
|
||||||
@ -101,50 +94,20 @@ const ChatScreen = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full">
|
<div className="flex h-full w-full">
|
||||||
<div className="flex h-full w-64 flex-shrink-0 flex-col overflow-y-auto border-r border-border">
|
<div className="flex h-full w-60 flex-shrink-0 flex-col overflow-y-auto border-r border-border bg-background dark:bg-background/50">
|
||||||
<ThreadList />
|
<ThreadList />
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex h-full w-[calc(100%-256px)] flex-col bg-muted/10">
|
|
||||||
<div className="flex h-full w-full flex-col justify-between">
|
|
||||||
{isEnableChat && currentConvo && (
|
|
||||||
<div className="h-[53px] flex-shrink-0 border-b border-border bg-background p-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span>{currentConvo.title}</span>
|
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'flex items-center space-x-3',
|
'relative flex h-full flex-col bg-background',
|
||||||
!isModelAvailable && '-mt-1'
|
activeThread && activeThreadId && showing
|
||||||
|
? 'w-[calc(100%-560px)]'
|
||||||
|
: 'w-full'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{!isModelAvailable && (
|
<div className="flex h-full w-full flex-col justify-between">
|
||||||
<Button
|
{activeThread ? (
|
||||||
themes="secondary"
|
|
||||||
className="relative z-10"
|
|
||||||
size="sm"
|
|
||||||
onClick={() =>
|
|
||||||
setMainViewState(MainViewState.ExploreModels)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Download Model
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Paintbrush
|
|
||||||
size={16}
|
|
||||||
className="cursor-pointer text-muted-foreground"
|
|
||||||
onClick={() => cleanThread()}
|
|
||||||
/>
|
|
||||||
<Trash2Icon
|
|
||||||
size={16}
|
|
||||||
className="cursor-pointer text-muted-foreground"
|
|
||||||
onClick={() => deleteThread()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isEnableChat ? (
|
|
||||||
<div className="flex h-full w-full overflow-y-auto overflow-x-hidden">
|
<div className="flex h-full w-full overflow-y-auto overflow-x-hidden">
|
||||||
<ChatBody />
|
<ChatBody />
|
||||||
</div>
|
</div>
|
||||||
@ -152,43 +115,57 @@ const ChatScreen = () => {
|
|||||||
<div className="mx-auto mt-8 flex h-full w-3/4 flex-col items-center justify-center text-center">
|
<div className="mx-auto mt-8 flex h-full w-3/4 flex-col items-center justify-center text-center">
|
||||||
{downloadedModels.length === 0 && (
|
{downloadedModels.length === 0 && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<h1 className="text-lg font-medium">{`Oops, you don't have a Model`}</h1>
|
<LogoMark
|
||||||
<p className="mt-1">{`Let’s download your first model.`}</p>
|
className="mx-auto mb-4 animate-wave"
|
||||||
|
width={56}
|
||||||
|
height={56}
|
||||||
|
/>
|
||||||
|
<h1 className="text-2xl font-bold">Welcome!</h1>
|
||||||
|
<p className="mt-1 text-base">
|
||||||
|
You need to download your first model
|
||||||
|
</p>
|
||||||
<Button
|
<Button
|
||||||
className="mt-4"
|
className="mt-4"
|
||||||
onClick={() =>
|
onClick={() => setMainViewState(MainViewState.Hub)}
|
||||||
setMainViewState(MainViewState.ExploreModels)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Explore Models
|
Explore The Hub
|
||||||
</Button>
|
</Button>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)}
|
)}
|
||||||
{!activeModel && downloadedModels.length > 0 && (
|
|
||||||
<Fragment>
|
|
||||||
<h1 className="text-lg font-medium">{`You don’t have any actively running models`}</h1>
|
|
||||||
<p className="mt-1">{`Please start a downloaded model to use this feature.`}</p>
|
|
||||||
<Badge className="mt-4" themes="outline">
|
|
||||||
<ShortCut menu="E" />
|
|
||||||
to show your model
|
|
||||||
</Badge>
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="mx-auto flex w-full flex-shrink-0 items-center justify-center space-x-4 p-4 lg:w-3/4">
|
|
||||||
|
{stateModel.loading && (
|
||||||
|
<div className="mb-1 mt-2 py-2 text-center">
|
||||||
|
<span className="rounded-lg border border-border px-4 py-2 shadow-lg">
|
||||||
|
Starting model {stateModel.model}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{queuedMessage && (
|
||||||
|
<div className="my-2 py-2 text-center">
|
||||||
|
<span className="rounded-lg border border-border px-4 py-2 shadow-lg">
|
||||||
|
Message queued. It can be sent once the model has started
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="mx-auto flex w-full flex-shrink-0 items-end justify-center space-x-4 px-8 py-4">
|
||||||
<Textarea
|
<Textarea
|
||||||
className="min-h-10 h-10 max-h-16 resize-none pr-20"
|
className="min-h-10 h-10 max-h-[400px] resize-none pr-20"
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
onKeyDown={(e) => onKeyDown(e)}
|
onKeyDown={(e: KeyboardEvent<HTMLTextAreaElement>) =>
|
||||||
placeholder="Type your message ..."
|
onKeyDown(e)
|
||||||
disabled={stateModel.loading || !currentConvo}
|
}
|
||||||
|
placeholder="Enter your message..."
|
||||||
|
disabled={stateModel.loading || !activeThread}
|
||||||
value={currentPrompt}
|
value={currentPrompt}
|
||||||
onChange={(e) => onPromptChange(e)}
|
onChange={(e: ChangeEvent<HTMLTextAreaElement>) =>
|
||||||
|
onPromptChange(e)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
size="lg"
|
size="lg"
|
||||||
disabled={disabled || stateModel.loading || !currentConvo}
|
disabled={disabled || stateModel.loading || !activeThread}
|
||||||
themes={'primary'}
|
themes={'primary'}
|
||||||
onClick={sendChatMessage}
|
onClick={sendChatMessage}
|
||||||
>
|
>
|
||||||
@ -197,7 +174,8 @@ const ChatScreen = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Sidebar />
|
{/* Sidebar */}
|
||||||
|
{activeThreadId && activeThread && <Sidebar />}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -61,7 +61,7 @@ const ExploreModelItemHeader: React.FC<Props> = ({ model }) => {
|
|||||||
setMainViewState(MainViewState.MyModels)
|
setMainViewState(MainViewState.MyModels)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
View Downloaded Model
|
Use
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -57,7 +57,7 @@ const ModelVersionItem: React.FC<Props> = ({ model }) => {
|
|||||||
setMainViewState(MainViewState.MyModels)
|
setMainViewState(MainViewState.MyModels)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
View Downloaded Model
|
Use
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ const ExploreModelsScreen = () => {
|
|||||||
if (loading) return <Loader description="loading ..." />
|
if (loading) return <Loader description="loading ..." />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full overflow-y-auto">
|
<div className="flex h-full w-full overflow-y-auto bg-background">
|
||||||
<div className="h-full w-full p-4">
|
<div className="h-full w-full p-4">
|
||||||
<div className="h-full" data-test-id="testid-explore-models">
|
<div className="h-full" data-test-id="testid-explore-models">
|
||||||
<ScrollArea>
|
<ScrollArea>
|
||||||
|
|||||||
@ -1,77 +0,0 @@
|
|||||||
import { Button } from '@janhq/uikit'
|
|
||||||
import {
|
|
||||||
Modal,
|
|
||||||
ModalTrigger,
|
|
||||||
ModalContent,
|
|
||||||
ModalHeader,
|
|
||||||
ModalTitle,
|
|
||||||
Progress,
|
|
||||||
} from '@janhq/uikit'
|
|
||||||
import { DatabaseIcon } from 'lucide-react'
|
|
||||||
|
|
||||||
import { MainViewState } from '@/constants/screens'
|
|
||||||
|
|
||||||
import { useDownloadState } from '@/hooks/useDownloadState'
|
|
||||||
import { useMainViewState } from '@/hooks/useMainViewState'
|
|
||||||
|
|
||||||
import { formatDownloadPercentage } from '@/utils/converter'
|
|
||||||
|
|
||||||
export default function BlankStateMyModel() {
|
|
||||||
const { setMainViewState } = useMainViewState()
|
|
||||||
const { downloadStates } = useDownloadState()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-full items-center justify-center px-4">
|
|
||||||
<div className="text-center">
|
|
||||||
<DatabaseIcon size={32} className="mx-auto text-muted-foreground" />
|
|
||||||
<div className="mt-4">
|
|
||||||
<h1 className="text-xl font-bold leading-snug">{`Oops, you don't have a model yet.`}</h1>
|
|
||||||
<p className="mt-1 text-base">
|
|
||||||
{downloadStates.length > 0
|
|
||||||
? `Downloading model ... `
|
|
||||||
: `Let’s download your first model`}
|
|
||||||
</p>
|
|
||||||
{downloadStates?.length > 0 && (
|
|
||||||
<Modal>
|
|
||||||
<ModalTrigger asChild>
|
|
||||||
<Button themes="outline" className="mr-2 mt-6">
|
|
||||||
<span>Downloading {downloadStates.length} model(s)</span>
|
|
||||||
</Button>
|
|
||||||
</ModalTrigger>
|
|
||||||
<ModalContent>
|
|
||||||
<ModalHeader>
|
|
||||||
<ModalTitle>Downloading model</ModalTitle>
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
{downloadStates.map((item, i) => {
|
|
||||||
return (
|
|
||||||
<div className="pt-2" key={i}>
|
|
||||||
<Progress
|
|
||||||
className="mb-2 h-2"
|
|
||||||
value={
|
|
||||||
formatDownloadPercentage(item?.percent, {
|
|
||||||
hidePercentage: true,
|
|
||||||
}) as number
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<p>{item?.modelId}</p>
|
|
||||||
<span>{formatDownloadPercentage(item?.percent)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
className="mt-6"
|
|
||||||
onClick={() => setMainViewState(MainViewState.ExploreModels)}
|
|
||||||
>
|
|
||||||
Explore Models
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,180 +0,0 @@
|
|||||||
import { Fragment } from 'react'
|
|
||||||
|
|
||||||
import {
|
|
||||||
Badge,
|
|
||||||
Avatar,
|
|
||||||
AvatarFallback,
|
|
||||||
AvatarImage,
|
|
||||||
Button,
|
|
||||||
Modal,
|
|
||||||
ModalTrigger,
|
|
||||||
ScrollArea,
|
|
||||||
ModalClose,
|
|
||||||
ModalFooter,
|
|
||||||
ModalContent,
|
|
||||||
ModalHeader,
|
|
||||||
ModalTitle,
|
|
||||||
} from '@janhq/uikit'
|
|
||||||
|
|
||||||
import { MainViewState } from '@/constants/screens'
|
|
||||||
|
|
||||||
import { useActiveModel } from '@/hooks/useActiveModel'
|
|
||||||
import useDeleteModel from '@/hooks/useDeleteModel'
|
|
||||||
import { useDownloadState } from '@/hooks/useDownloadState'
|
|
||||||
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
|
|
||||||
|
|
||||||
import { useMainViewState } from '@/hooks/useMainViewState'
|
|
||||||
|
|
||||||
import BlankStateMyModel from '@/screens/MyModels/BlankState'
|
|
||||||
|
|
||||||
import { toGigabytes } from '@/utils/converter'
|
|
||||||
|
|
||||||
const MyModelsScreen = () => {
|
|
||||||
const { downloadedModels } = useGetDownloadedModels()
|
|
||||||
const { downloadStates } = useDownloadState()
|
|
||||||
const { setMainViewState } = useMainViewState()
|
|
||||||
const { deleteModel } = useDeleteModel()
|
|
||||||
|
|
||||||
const { activeModel, startModel, stopModel, stateModel } = useActiveModel()
|
|
||||||
|
|
||||||
if (downloadedModels.length === 0) return <BlankStateMyModel />
|
|
||||||
|
|
||||||
const onModelActionClick = (modelId: string) => {
|
|
||||||
if (activeModel && activeModel.id === modelId) {
|
|
||||||
stopModel(modelId)
|
|
||||||
} else {
|
|
||||||
startModel(modelId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-full w-full">
|
|
||||||
<ScrollArea className="h-full w-full">
|
|
||||||
<div className="p-4" data-test-id="testid-my-models">
|
|
||||||
<div className="grid grid-cols-2 gap-4 lg:grid-cols-3">
|
|
||||||
{downloadedModels.map((model, i) => {
|
|
||||||
const isActiveModel = stateModel.model === model.id
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="rounded-lg border border-border bg-background p-4"
|
|
||||||
>
|
|
||||||
<Fragment>
|
|
||||||
<div className="flex items-start gap-x-4">
|
|
||||||
<div className="inline-flex rounded-full border border-border p-1">
|
|
||||||
<Avatar className="h-8 w-8">
|
|
||||||
<AvatarImage alt={model.metadata.author} />
|
|
||||||
<AvatarFallback>
|
|
||||||
{model.metadata.author.charAt(0)}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2 className="mb-1 font-medium capitalize">
|
|
||||||
{model.metadata.author}
|
|
||||||
</h2>
|
|
||||||
<p className="line-clamp-1">{model.name}</p>
|
|
||||||
<div className="mt-2 flex items-center gap-2">
|
|
||||||
<Badge themes="secondary">v{model.version}</Badge>
|
|
||||||
<Badge themes="outline">GGUF</Badge>
|
|
||||||
<Badge themes="outline">
|
|
||||||
{toGigabytes(model.metadata.size)}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<p className="mt-2 line-clamp-2 break-all">
|
|
||||||
{model.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 flex items-center justify-center gap-x-2 gap-y-4 border-t border-border pt-4">
|
|
||||||
<Modal>
|
|
||||||
<ModalTrigger asChild>
|
|
||||||
<Button themes="ghost" block>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</ModalTrigger>
|
|
||||||
<ModalContent>
|
|
||||||
<ModalHeader>
|
|
||||||
<ModalTitle>Are you sure?</ModalTitle>
|
|
||||||
</ModalHeader>
|
|
||||||
<p className="leading-relaxed">
|
|
||||||
Delete model {model.name}, v{model.version},{' '}
|
|
||||||
{toGigabytes(model.metadata.size)}.
|
|
||||||
</p>
|
|
||||||
<ModalFooter>
|
|
||||||
<div className="flex gap-x-2">
|
|
||||||
<ModalClose asChild>
|
|
||||||
<Button themes="ghost">No</Button>
|
|
||||||
</ModalClose>
|
|
||||||
<ModalClose asChild>
|
|
||||||
<Button
|
|
||||||
themes="danger"
|
|
||||||
onClick={() =>
|
|
||||||
setTimeout(async () => {
|
|
||||||
await stopModel(model.id)
|
|
||||||
deleteModel(model)
|
|
||||||
}, 500)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Yes
|
|
||||||
</Button>
|
|
||||||
</ModalClose>
|
|
||||||
</div>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
<Button
|
|
||||||
block
|
|
||||||
themes={
|
|
||||||
isActiveModel && stateModel.state === 'stop'
|
|
||||||
? 'danger'
|
|
||||||
: 'primary'
|
|
||||||
}
|
|
||||||
className="capitalize"
|
|
||||||
loading={isActiveModel ? stateModel.loading : false}
|
|
||||||
onClick={() => onModelActionClick(model.id)}
|
|
||||||
>
|
|
||||||
{isActiveModel ? stateModel.state : 'Start'}
|
|
||||||
Model
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Fragment>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
|
|
||||||
<div className="rounded-lg border border-border bg-background p-4 hover:border-primary/60">
|
|
||||||
<div className="flex h-full flex-col justify-between">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-lg font-medium">Download more models?</h2>
|
|
||||||
<p className="mt-2 leading-relaxed">
|
|
||||||
You have <span>{downloadedModels.length}</span> model(s)
|
|
||||||
downloaded.
|
|
||||||
{downloadStates.length > 0 && (
|
|
||||||
<span>
|
|
||||||
And {downloadStates.length} downloading progress.
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 flex items-end justify-center gap-4 border-t border-border pt-4">
|
|
||||||
<Button
|
|
||||||
themes="secondary"
|
|
||||||
block
|
|
||||||
onClick={() =>
|
|
||||||
setMainViewState(MainViewState.ExploreModels)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Explore Models
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MyModelsScreen
|
|
||||||
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect, useRef, useContext } from 'react'
|
import React, { useState, useEffect, useRef, useContext } from 'react'
|
||||||
|
|
||||||
import { Switch, Button } from '@janhq/uikit'
|
import { Button } from '@janhq/uikit'
|
||||||
|
|
||||||
import Loader from '@/containers/Loader'
|
import Loader from '@/containers/Loader'
|
||||||
|
|
||||||
|
|||||||
137
web/screens/Settings/Models/Row.tsx
Normal file
137
web/screens/Settings/Models/Row.tsx
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
import { Model } from '@janhq/core'
|
||||||
|
import { Badge } from '@janhq/uikit'
|
||||||
|
|
||||||
|
import {
|
||||||
|
MoreVerticalIcon,
|
||||||
|
Trash2Icon,
|
||||||
|
PlayIcon,
|
||||||
|
StopCircleIcon,
|
||||||
|
} from 'lucide-react'
|
||||||
|
|
||||||
|
import { useActiveModel } from '@/hooks/useActiveModel'
|
||||||
|
import { useClickOutside } from '@/hooks/useClickOutside'
|
||||||
|
|
||||||
|
import useDeleteModel from '@/hooks/useDeleteModel'
|
||||||
|
|
||||||
|
import { toGigabytes } from '@/utils/converter'
|
||||||
|
|
||||||
|
type RowModelProps = {
|
||||||
|
data: Model
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RowModel(props: RowModelProps) {
|
||||||
|
const [more, setMore] = useState(false)
|
||||||
|
|
||||||
|
const [menu, setMenu] = useState<HTMLDivElement | null>(null)
|
||||||
|
const [toggle, setToggle] = useState<HTMLDivElement | null>(null)
|
||||||
|
useClickOutside(() => setMore(false), null, [menu, toggle])
|
||||||
|
|
||||||
|
const { activeModel, startModel, stopModel, stateModel } = useActiveModel()
|
||||||
|
const { deleteModel } = useDeleteModel()
|
||||||
|
|
||||||
|
const isActiveModel = stateModel.model === props.data.id
|
||||||
|
|
||||||
|
const onModelActionClick = (modelId: string) => {
|
||||||
|
if (activeModel && activeModel.id === modelId) {
|
||||||
|
stopModel(modelId)
|
||||||
|
} else {
|
||||||
|
startModel(modelId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr className="relative border-b border-border last:border-none">
|
||||||
|
<td className="px-6 py-4 font-bold">{props.data.name}</td>
|
||||||
|
<td className="px-6 py-4 font-bold">{props.data.id}</td>
|
||||||
|
<td className="px-6 py-4">
|
||||||
|
<Badge themes="secondary">
|
||||||
|
{toGigabytes(props.data.metadata.size)}
|
||||||
|
</Badge>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4">
|
||||||
|
<Badge themes="secondary">v{props.data.version}</Badge>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4">
|
||||||
|
{stateModel.loading && stateModel.model === props.data.id ? (
|
||||||
|
<Badge
|
||||||
|
className="inline-flex items-center space-x-2"
|
||||||
|
themes="secondary"
|
||||||
|
>
|
||||||
|
<span className="h-2 w-2 rounded-full bg-gray-500" />
|
||||||
|
<span className="capitalize">
|
||||||
|
{stateModel.state === 'start' ? 'Starting...' : 'Stopping...'}
|
||||||
|
</span>
|
||||||
|
</Badge>
|
||||||
|
) : activeModel && activeModel.id === props.data.id ? (
|
||||||
|
<Badge
|
||||||
|
themes="success"
|
||||||
|
className="inline-flex items-center space-x-2"
|
||||||
|
>
|
||||||
|
<span className="h-2 w-2 rounded-full bg-green-500" />
|
||||||
|
<span>Active</span>
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge
|
||||||
|
themes="secondary"
|
||||||
|
className="inline-flex items-center space-x-2"
|
||||||
|
>
|
||||||
|
<span className="h-2 w-2 rounded-full bg-gray-500" />
|
||||||
|
<span>Inactive</span>
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-center">
|
||||||
|
<div
|
||||||
|
className="cursor-pointer"
|
||||||
|
ref={setToggle}
|
||||||
|
onClick={() => {
|
||||||
|
setMore(!more)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MoreVerticalIcon className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
{more && (
|
||||||
|
<div
|
||||||
|
className="absolute right-4 top-10 z-20 w-52 overflow-hidden rounded-lg border border-border bg-background py-2 shadow-lg"
|
||||||
|
ref={setMenu}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-secondary"
|
||||||
|
onClick={() => {
|
||||||
|
onModelActionClick(props.data.id)
|
||||||
|
setMore(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{activeModel && activeModel.id === props.data.id ? (
|
||||||
|
<StopCircleIcon size={16} className="text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<PlayIcon size={16} className="text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
<span className="text-bold capitalize text-black dark:text-muted-foreground">
|
||||||
|
{isActiveModel ? stateModel.state : 'Start'}
|
||||||
|
Model
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setTimeout(async () => {
|
||||||
|
await stopModel(props.data.id)
|
||||||
|
deleteModel(props.data)
|
||||||
|
}, 500)
|
||||||
|
setMore(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2Icon size={16} className="text-muted-foreground" />
|
||||||
|
<span className="text-bold text-black dark:text-muted-foreground">
|
||||||
|
Delete Model
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
}
|
||||||
65
web/screens/Settings/Models/index.tsx
Normal file
65
web/screens/Settings/Models/index.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
import { Input } from '@janhq/uikit'
|
||||||
|
|
||||||
|
import { SearchIcon } from 'lucide-react'
|
||||||
|
|
||||||
|
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
|
||||||
|
|
||||||
|
import RowModel from './Row'
|
||||||
|
|
||||||
|
const Column = ['Name', 'Model ID', 'Size', 'Version', 'Status', '']
|
||||||
|
|
||||||
|
export default function Models() {
|
||||||
|
const { downloadedModels } = useGetDownloadedModels()
|
||||||
|
const [searchValue, setsearchValue] = useState('')
|
||||||
|
|
||||||
|
const filteredDownloadedModels = downloadedModels.filter((x) => {
|
||||||
|
return x.name.toLowerCase().includes(searchValue.toLowerCase())
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-xl border border-border shadow-sm">
|
||||||
|
<div className="px-6 py-5">
|
||||||
|
<div className="relative w-1/3">
|
||||||
|
<SearchIcon
|
||||||
|
size={20}
|
||||||
|
className="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="Search"
|
||||||
|
className="pl-8"
|
||||||
|
onChange={(e) => {
|
||||||
|
setsearchValue(e.target.value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="relative">
|
||||||
|
<table className="w-full px-8">
|
||||||
|
<thead className="w-full border-b border-border bg-secondary">
|
||||||
|
<tr>
|
||||||
|
{Column.map((col, i) => {
|
||||||
|
return (
|
||||||
|
<th
|
||||||
|
key={i}
|
||||||
|
className="px-6 py-2 text-left font-normal last:text-center"
|
||||||
|
>
|
||||||
|
{col}
|
||||||
|
</th>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{filteredDownloadedModels
|
||||||
|
? filteredDownloadedModels.map((x, i) => {
|
||||||
|
return <RowModel key={i} data={x} />
|
||||||
|
})
|
||||||
|
: null}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -12,6 +12,8 @@ import AppearanceOptions from '@/screens/Settings/Appearance'
|
|||||||
import ExtensionCatalog from '@/screens/Settings/CoreExtensions/ExtensionsCatalog'
|
import ExtensionCatalog from '@/screens/Settings/CoreExtensions/ExtensionsCatalog'
|
||||||
import PreferenceExtensions from '@/screens/Settings/CoreExtensions/PreferenceExtensions'
|
import PreferenceExtensions from '@/screens/Settings/CoreExtensions/PreferenceExtensions'
|
||||||
|
|
||||||
|
import Models from '@/screens/Settings/Models'
|
||||||
|
|
||||||
import { formatExtensionsName } from '@/utils/converter'
|
import { formatExtensionsName } from '@/utils/converter'
|
||||||
|
|
||||||
const SettingsScreen = () => {
|
const SettingsScreen = () => {
|
||||||
@ -24,7 +26,7 @@ const SettingsScreen = () => {
|
|||||||
const menu = ['Appearance']
|
const menu = ['Appearance']
|
||||||
|
|
||||||
if (typeof window !== 'undefined' && window.electronAPI) {
|
if (typeof window !== 'undefined' && window.electronAPI) {
|
||||||
menu.push('Core Extensions')
|
menu.push('Extensions')
|
||||||
}
|
}
|
||||||
menu.push('Advanced')
|
menu.push('Advanced')
|
||||||
setMenus(menu)
|
setMenus(menu)
|
||||||
@ -40,7 +42,7 @@ const SettingsScreen = () => {
|
|||||||
|
|
||||||
const handleShowOptions = (menu: string) => {
|
const handleShowOptions = (menu: string) => {
|
||||||
switch (menu) {
|
switch (menu) {
|
||||||
case 'Core Extensions':
|
case 'Extensions':
|
||||||
return <ExtensionCatalog />
|
return <ExtensionCatalog />
|
||||||
|
|
||||||
case 'Appearance':
|
case 'Appearance':
|
||||||
@ -49,6 +51,9 @@ const SettingsScreen = () => {
|
|||||||
case 'Advanced':
|
case 'Advanced':
|
||||||
return <Advanced />
|
return <Advanced />
|
||||||
|
|
||||||
|
case 'Models':
|
||||||
|
return <Models />
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<PreferenceExtensions
|
<PreferenceExtensions
|
||||||
@ -61,13 +66,13 @@ const SettingsScreen = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full">
|
<div className="flex h-full bg-background">
|
||||||
<div className="flex h-full w-64 flex-shrink-0 flex-col overflow-y-auto border-r border-border">
|
<div className="flex h-full w-64 flex-shrink-0 flex-col overflow-y-auto border-r border-border">
|
||||||
<ScrollArea className="h-full w-full">
|
<ScrollArea className="h-full w-full">
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<label className="font-bold uppercase text-muted-foreground">
|
<label className="font-bold uppercase text-muted-foreground">
|
||||||
Options
|
General
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-2 font-medium">
|
<div className="mt-2 font-medium">
|
||||||
{menus.map((menu, i) => {
|
{menus.map((menu, i) => {
|
||||||
@ -135,6 +140,38 @@ const SettingsScreen = () => {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<label className="font-bold uppercase text-muted-foreground">
|
||||||
|
Core extensions
|
||||||
|
</label>
|
||||||
|
<div className="mt-2 font-medium">
|
||||||
|
<div className="relative my-0.5 block py-1.5">
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
setActiveStaticMenu('Models')
|
||||||
|
setActivePreferenceExtension('')
|
||||||
|
}}
|
||||||
|
className="block w-full cursor-pointer"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={twMerge(
|
||||||
|
activePreferenceExtension === 'Models' &&
|
||||||
|
'relative z-10'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Models
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{activeStaticMenu === 'Models' && (
|
||||||
|
<m.div
|
||||||
|
className="absolute inset-0 -left-3 h-full w-[calc(100%+24px)] rounded-md bg-primary/50"
|
||||||
|
layoutId="active-static-core-extentions"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
117
web/screens/SystemMonitor/index.tsx
Normal file
117
web/screens/SystemMonitor/index.tsx
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import { ScrollArea, Progress, Badge, Button } from '@janhq/uikit'
|
||||||
|
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
|
import { useActiveModel } from '@/hooks/useActiveModel'
|
||||||
|
|
||||||
|
import { toGigabytes } from '@/utils/converter'
|
||||||
|
|
||||||
|
import {
|
||||||
|
cpuUsageAtom,
|
||||||
|
totalRamAtom,
|
||||||
|
usedRamAtom,
|
||||||
|
} from '@/helpers/atoms/SystemBar.atom'
|
||||||
|
|
||||||
|
const Column = ['Name', 'Model ID', 'Size', 'Version', 'Action']
|
||||||
|
|
||||||
|
export default function SystemMonitorScreen() {
|
||||||
|
const totalRam = useAtomValue(totalRamAtom)
|
||||||
|
const usedRam = useAtomValue(usedRamAtom)
|
||||||
|
const cpuUsage = useAtomValue(cpuUsageAtom)
|
||||||
|
const { activeModel, stateModel, stopModel } = useActiveModel()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full bg-background dark:bg-background/50">
|
||||||
|
<ScrollArea className="h-full w-full">
|
||||||
|
<div className="h-full p-8" data-test-id="testid-system-monitor">
|
||||||
|
<div className="grid grid-cols-2 gap-8 lg:grid-cols-3">
|
||||||
|
<div className="rounded-xl border border-border px-8 py-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h4 className="text-base font-bold uppercase">
|
||||||
|
ram ({Math.round((usedRam / totalRam) * 100)}%)
|
||||||
|
</h4>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{toGigabytes(usedRam)} GB of {toGigabytes(totalRam)} GB used
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2">
|
||||||
|
<Progress
|
||||||
|
className="mb-2 h-10 rounded-md"
|
||||||
|
value={Math.round((usedRam / totalRam) * 100)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-xl border border-border px-8 py-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h4 className="text-base font-bold uppercase">
|
||||||
|
cpu ({cpuUsage}%)
|
||||||
|
</h4>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{cpuUsage}% of 100%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2">
|
||||||
|
<Progress className="mb-2 h-10 rounded-md" value={cpuUsage} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activeModel && (
|
||||||
|
<div className="mt-8 overflow-hidden rounded-xl border border-border shadow-sm">
|
||||||
|
<div className="px-6 py-5">
|
||||||
|
<h4 className="text-base font-medium">Running Models</h4>
|
||||||
|
</div>
|
||||||
|
<div className="relative overflow-x-auto shadow-md">
|
||||||
|
<table className="w-full px-8">
|
||||||
|
<thead className="w-full border-b border-border bg-secondary">
|
||||||
|
<tr>
|
||||||
|
{Column.map((col, i) => {
|
||||||
|
return (
|
||||||
|
<th
|
||||||
|
key={i}
|
||||||
|
className="px-6 py-2 text-left font-normal last:text-center"
|
||||||
|
>
|
||||||
|
{col}
|
||||||
|
</th>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td className="px-6 py-2 font-bold">
|
||||||
|
{activeModel.name}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-2 font-bold">{activeModel.id}</td>
|
||||||
|
<td className="px-6 py-2">
|
||||||
|
<Badge themes="secondary">
|
||||||
|
{toGigabytes(activeModel.metadata.size)}
|
||||||
|
</Badge>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-2">
|
||||||
|
<Badge themes="secondary">v{activeModel.version}</Badge>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-2 text-center">
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
themes={
|
||||||
|
stateModel.state === 'stop' ? 'danger' : 'primary'
|
||||||
|
}
|
||||||
|
className="w-16"
|
||||||
|
loading={stateModel.loading}
|
||||||
|
onClick={() => stopModel(activeModel.id)}
|
||||||
|
>
|
||||||
|
Stop
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,74 +0,0 @@
|
|||||||
import { Fragment } from 'react'
|
|
||||||
|
|
||||||
import { Badge, Button } from '@janhq/uikit'
|
|
||||||
|
|
||||||
import LogoMark from '@/containers/Brand/Logo/Mark'
|
|
||||||
|
|
||||||
import ShortCut from '@/containers/Shortcut'
|
|
||||||
|
|
||||||
import { MainViewState } from '@/constants/screens'
|
|
||||||
|
|
||||||
import { useActiveModel } from '@/hooks/useActiveModel'
|
|
||||||
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
|
|
||||||
|
|
||||||
import { useMainViewState } from '@/hooks/useMainViewState'
|
|
||||||
|
|
||||||
const WelcomeScreen = () => {
|
|
||||||
const { downloadedModels } = useGetDownloadedModels()
|
|
||||||
const { activeModel } = useActiveModel()
|
|
||||||
const { setMainViewState } = useMainViewState()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-full items-center justify-center px-4">
|
|
||||||
<div className="text-center">
|
|
||||||
<LogoMark
|
|
||||||
className="mx-auto mb-4 animate-wave"
|
|
||||||
width={56}
|
|
||||||
height={56}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{downloadedModels.length === 0 && !activeModel && (
|
|
||||||
<Fragment>
|
|
||||||
<h1
|
|
||||||
data-testid="testid-welcome-title"
|
|
||||||
className="text-2xl font-bold"
|
|
||||||
>
|
|
||||||
Welcome to Jan
|
|
||||||
</h1>
|
|
||||||
<p className="mt-1">{`Let’s download your first model`}</p>
|
|
||||||
<Button
|
|
||||||
className="mt-4"
|
|
||||||
onClick={() => setMainViewState(MainViewState.ExploreModels)}
|
|
||||||
>
|
|
||||||
Explore Models
|
|
||||||
</Button>
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
{downloadedModels.length >= 1 && !activeModel && (
|
|
||||||
<Fragment>
|
|
||||||
<h1 className="mt-2 text-lg font-medium">{`You don’t have any actively running models`}</h1>
|
|
||||||
<p className="mt-1">{`Please start a downloaded model to use this feature.`}</p>
|
|
||||||
<Badge className="mt-4" themes="outline">
|
|
||||||
<ShortCut menu="E" />
|
|
||||||
to show your model
|
|
||||||
</Badge>
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
{downloadedModels.length >= 1 && activeModel && (
|
|
||||||
<Fragment>
|
|
||||||
<h1 className="mt-2 text-lg font-medium">{`Your Model is Active`}</h1>
|
|
||||||
<p className="mt-1">{`You are ready to converse.`}</p>
|
|
||||||
<Button
|
|
||||||
className="mt-4"
|
|
||||||
onClick={() => setMainViewState(MainViewState.Chat)}
|
|
||||||
>
|
|
||||||
Start a conversation
|
|
||||||
</Button>
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default WelcomeScreen
|
|
||||||
@ -1,8 +1,12 @@
|
|||||||
.message {
|
.message {
|
||||||
|
@apply text-black dark:text-gray-300;
|
||||||
ul,
|
ul,
|
||||||
ol {
|
ol {
|
||||||
list-style: auto;
|
list-style: auto;
|
||||||
padding-left: 24px;
|
padding-left: 24px;
|
||||||
}
|
}
|
||||||
@apply text-muted-foreground;
|
}
|
||||||
|
|
||||||
|
button[class*='react-scroll-to-bottom--'] {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user