Merge branch 'main' into jan-182-drake
This commit is contained in:
commit
2bb370afbf
2
.gitignore
vendored
2
.gitignore
vendored
@ -12,3 +12,5 @@ dist
|
||||
build
|
||||
.DS_Store
|
||||
electron/renderer
|
||||
|
||||
*.log
|
||||
|
||||
38
electron/.eslintrc.js
Normal file
38
electron/.eslintrc.js
Normal file
@ -0,0 +1,38 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: "@typescript-eslint/parser",
|
||||
plugins: ["@typescript-eslint"],
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react/recommended",
|
||||
],
|
||||
rules: {
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"react/prop-types": "off", // In favor of strong typing - no need to dedupe
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
createClass: "createReactClass", // Regex for Component Factory to use,
|
||||
// default to "createReactClass"
|
||||
pragma: "React", // Pragma to use, default to "React"
|
||||
version: "detect", // React version. "detect" automatically picks the version you have installed.
|
||||
// You can also use `16.0`, `16.3`, etc, if you want to override the detected value.
|
||||
// default to latest and warns if missing
|
||||
// It will default to "detect" in the future
|
||||
},
|
||||
linkComponents: [
|
||||
// Components used as alternatives to <a> for linking, eg. <Link to={ url } />
|
||||
"Hyperlink",
|
||||
{ name: "Link", linkAttribute: "to" },
|
||||
],
|
||||
},
|
||||
ignorePatterns: ["renderer/*", "node_modules/*", "core/plugins"],
|
||||
};
|
||||
@ -92,7 +92,7 @@ const createConversation = (conversation: any) =>
|
||||
resolve(res);
|
||||
});
|
||||
} else {
|
||||
resolve("-");
|
||||
resolve(undefined);
|
||||
}
|
||||
});
|
||||
const createMessage = (message: any) =>
|
||||
@ -100,9 +100,24 @@ const createMessage = (message: any) =>
|
||||
if (window && window.electronAPI) {
|
||||
window.electronAPI
|
||||
.invokePluginFunc(MODULE_PATH, "storeMessage", message)
|
||||
.then((res: any) => resolve(res));
|
||||
.then((res: any) => {
|
||||
resolve(res);
|
||||
});
|
||||
} else {
|
||||
resolve("-");
|
||||
resolve(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
const updateMessage = (message: any) =>
|
||||
new Promise((resolve) => {
|
||||
if (window && window.electronAPI) {
|
||||
window.electronAPI
|
||||
.invokePluginFunc(MODULE_PATH, "updateMessage", message)
|
||||
.then((res: any) => {
|
||||
resolve(res);
|
||||
});
|
||||
} else {
|
||||
resolve(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
@ -128,6 +143,7 @@ export function init({ register }: { register: any }) {
|
||||
setupDb();
|
||||
register("getConversations", "getConv", getConversations, 1);
|
||||
register("createConversation", "insertConv", createConversation);
|
||||
register("updateMessage", "updateMessage", updateMessage);
|
||||
register("deleteConversation", "deleteConv", deleteConversation);
|
||||
register("createMessage", "insertMessage", createMessage);
|
||||
register("getConversationMessages", "getMessages", getConversationMessages);
|
||||
|
||||
@ -241,7 +241,7 @@ function getConversations() {
|
||||
);
|
||||
|
||||
db.all(
|
||||
"SELECT * FROM conversations ORDER BY created_at DESC",
|
||||
"SELECT * FROM conversations ORDER BY updated_at DESC",
|
||||
(err: any, row: any) => {
|
||||
res(row);
|
||||
}
|
||||
@ -249,7 +249,7 @@ function getConversations() {
|
||||
db.close();
|
||||
});
|
||||
}
|
||||
function storeConversation(conversation: any) {
|
||||
function storeConversation(conversation: any): Promise<number | undefined> {
|
||||
return new Promise((res) => {
|
||||
const db = new sqlite3.Database(
|
||||
path.join(app.getPath("userData"), "jan.db")
|
||||
@ -284,7 +284,7 @@ function storeConversation(conversation: any) {
|
||||
});
|
||||
}
|
||||
|
||||
function storeMessage(message: any) {
|
||||
function storeMessage(message: any): Promise<number | undefined> {
|
||||
return new Promise((res) => {
|
||||
const db = new sqlite3.Database(
|
||||
path.join(app.getPath("userData"), "jan.db")
|
||||
@ -299,7 +299,7 @@ function storeMessage(message: any) {
|
||||
message.conversation_id,
|
||||
message.user,
|
||||
message.message,
|
||||
(err: any) => {
|
||||
function (err: any) {
|
||||
if (err) {
|
||||
// Handle the insertion error here
|
||||
console.error(err.message);
|
||||
@ -318,6 +318,24 @@ function storeMessage(message: any) {
|
||||
db.close();
|
||||
});
|
||||
}
|
||||
function updateMessage(message: any): Promise<number | undefined> {
|
||||
return new Promise((res) => {
|
||||
const db = new sqlite3.Database(
|
||||
path.join(app.getPath("userData"), "jan.db")
|
||||
);
|
||||
|
||||
db.serialize(() => {
|
||||
const stmt = db.prepare(
|
||||
"UPDATE messages SET message = ?, updated_at = ? WHERE id = ?"
|
||||
);
|
||||
stmt.run(message.message, message.updated_at, message.id);
|
||||
stmt.finalize();
|
||||
res(message.id);
|
||||
});
|
||||
|
||||
db.close();
|
||||
});
|
||||
}
|
||||
|
||||
function deleteConversation(id: any) {
|
||||
return new Promise((res) => {
|
||||
@ -347,7 +365,7 @@ function getConversationMessages(conversation_id: any) {
|
||||
path.join(app.getPath("userData"), "jan.db")
|
||||
);
|
||||
|
||||
const query = `SELECT * FROM messages WHERE conversation_id = ${conversation_id} ORDER BY created_at DESC`;
|
||||
const query = `SELECT * FROM messages WHERE conversation_id = ${conversation_id} ORDER BY id DESC`;
|
||||
db.all(query, (err: Error, row: any) => {
|
||||
res(row);
|
||||
});
|
||||
@ -361,6 +379,7 @@ module.exports = {
|
||||
deleteConversation,
|
||||
storeConversation,
|
||||
storeMessage,
|
||||
updateMessage,
|
||||
getConversationMessages,
|
||||
storeModel,
|
||||
updateFinishedDownloadAt,
|
||||
|
||||
@ -1,14 +1,5 @@
|
||||
const MODULE_PATH = "inference-plugin/dist/module.js";
|
||||
|
||||
const prompt = async (prompt) =>
|
||||
new Promise(async (resolve) => {
|
||||
if (window.electronAPI) {
|
||||
window.electronAPI
|
||||
.invokePluginFunc(MODULE_PATH, "prompt", prompt)
|
||||
.then((res) => resolve(res));
|
||||
}
|
||||
});
|
||||
|
||||
const initModel = async (product) =>
|
||||
new Promise(async (resolve) => {
|
||||
if (window.electronAPI) {
|
||||
@ -18,8 +9,19 @@ const initModel = async (product) =>
|
||||
}
|
||||
});
|
||||
|
||||
const dispose = async () =>
|
||||
new Promise(async (resolve) => {
|
||||
if (window.electronAPI) {
|
||||
window.electronAPI
|
||||
.invokePluginFunc(MODULE_PATH, "killSubprocess")
|
||||
.then((res) => resolve(res));
|
||||
}
|
||||
});
|
||||
const inferenceUrl = () => "http://localhost:8080/llama/chat_completion";
|
||||
|
||||
// Register all the above functions and objects with the relevant extension points
|
||||
export function init({ register }) {
|
||||
register("initModel", "initModel", initModel);
|
||||
register("prompt", "prompt", prompt);
|
||||
register("inferenceUrl", "inferenceUrl", inferenceUrl);
|
||||
register("dispose", "dispose", dispose);
|
||||
}
|
||||
|
||||
@ -1,8 +1,18 @@
|
||||
const path = require("path");
|
||||
const { app, dialog } = require("electron");
|
||||
const _importDynamic = new Function("modulePath", "return import(modulePath)");
|
||||
const { spawn } = require("child_process");
|
||||
const fs = require("fs");
|
||||
var exec = require("child_process").exec;
|
||||
|
||||
let llamaSession = null;
|
||||
let subprocess = null;
|
||||
|
||||
process.on("exit", () => {
|
||||
// Perform cleanup tasks here
|
||||
console.log("kill subprocess on exit");
|
||||
if (subprocess) {
|
||||
subprocess.kill();
|
||||
}
|
||||
});
|
||||
|
||||
async function initModel(product) {
|
||||
// fileName fallback
|
||||
@ -18,38 +28,69 @@ async function initModel(product) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.info(`Initializing model: ${product.name}..`);
|
||||
_importDynamic("../node_modules/node-llama-cpp/dist/index.js")
|
||||
.then(({ LlamaContext, LlamaChatSession, LlamaModel }) => {
|
||||
if (subprocess) {
|
||||
console.error(
|
||||
"A subprocess is already running. Attempt to kill then reinit."
|
||||
);
|
||||
killSubprocess();
|
||||
}
|
||||
|
||||
let binaryFolder = path.join(__dirname, "nitro"); // Current directory by default
|
||||
|
||||
// Read the existing config
|
||||
const configFilePath = path.join(binaryFolder, "config", "config.json");
|
||||
let config = {};
|
||||
if (fs.existsSync(configFilePath)) {
|
||||
const rawData = fs.readFileSync(configFilePath, "utf-8");
|
||||
config = JSON.parse(rawData);
|
||||
}
|
||||
|
||||
// Update the llama_model_path
|
||||
if (!config.custom_config) {
|
||||
config.custom_config = {};
|
||||
}
|
||||
|
||||
const modelPath = path.join(app.getPath("userData"), product.fileName);
|
||||
const model = new LlamaModel({ modelPath });
|
||||
const context = new LlamaContext({ model });
|
||||
llamaSession = new LlamaChatSession({ context });
|
||||
console.info(`Init model ${product.name} successfully!`);
|
||||
})
|
||||
.catch(async (e) => {
|
||||
console.error(e);
|
||||
await dialog.showMessageBox({
|
||||
message: "Failed to import LLM module",
|
||||
|
||||
config.custom_config.llama_model_path = modelPath;
|
||||
|
||||
// Write the updated config back to the file
|
||||
fs.writeFileSync(configFilePath, JSON.stringify(config, null, 4));
|
||||
|
||||
const binaryPath =
|
||||
process.platform === "win32"
|
||||
? path.join(binaryFolder, "nitro.exe")
|
||||
: path.join(binaryFolder, "nitro");
|
||||
// Execute the binary
|
||||
|
||||
subprocess = spawn(binaryPath, [configFilePath], {cwd: binaryFolder});
|
||||
|
||||
// Handle subprocess output
|
||||
subprocess.stdout.on("data", (data) => {
|
||||
console.log(`stdout: ${data}`);
|
||||
});
|
||||
|
||||
subprocess.stderr.on("data", (data) => {
|
||||
console.error(`stderr: ${data}`);
|
||||
});
|
||||
|
||||
subprocess.on("close", (code) => {
|
||||
console.log(`child process exited with code ${code}`);
|
||||
subprocess = null;
|
||||
});
|
||||
}
|
||||
|
||||
async function prompt(prompt) {
|
||||
if (!llamaSession) {
|
||||
await dialog.showMessageBox({
|
||||
message: "Model not initialized",
|
||||
});
|
||||
|
||||
return;
|
||||
function killSubprocess() {
|
||||
if (subprocess) {
|
||||
subprocess.kill();
|
||||
subprocess = null;
|
||||
console.log("Subprocess terminated.");
|
||||
} else {
|
||||
console.error("No subprocess is currently running.");
|
||||
}
|
||||
console.log("prompt: ", prompt);
|
||||
const response = await llamaSession.prompt(prompt);
|
||||
console.log("response: ", response);
|
||||
return response;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initModel,
|
||||
prompt,
|
||||
killSubprocess,
|
||||
};
|
||||
|
||||
@ -0,0 +1 @@
|
||||
{"custom_config": {"llama_model_path":"","ctx_len":2048,"ngl":100}}
|
||||
2353
electron/core/plugins/inference-plugin/nitro/ggml-metal.metal
Normal file
2353
electron/core/plugins/inference-plugin/nitro/ggml-metal.metal
Normal file
File diff suppressed because it is too large
Load Diff
BIN
electron/core/plugins/inference-plugin/nitro/nitro
Executable file
BIN
electron/core/plugins/inference-plugin/nitro/nitro
Executable file
Binary file not shown.
BIN
electron/core/plugins/inference-plugin/nitro/nitro.exe
Normal file
BIN
electron/core/plugins/inference-plugin/nitro/nitro.exe
Normal file
Binary file not shown.
BIN
electron/core/plugins/inference-plugin/nitro/zlib.dll
Normal file
BIN
electron/core/plugins/inference-plugin/nitro/zlib.dll
Normal file
Binary file not shown.
@ -10,7 +10,7 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "webpack --config webpack.config.js",
|
||||
"build:package": "rimraf ./*.tgz && npm run build && cpx \"module.js\" \"dist\" && npm pack",
|
||||
"build:package": "rimraf ./*.tgz && npm run build && cpx \"module.js\" \"dist\" && rm -rf dist/nitro && cp -r nitro dist/nitro && npm pack",
|
||||
"build:publish": "yarn build:package && cpx *.tgz ../../pre-install"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -32,7 +32,8 @@ const ALL_MODELS = [
|
||||
id: "llama-2-13b-chat.Q4_K_M.gguf",
|
||||
slug: "llama-2-13b-chat.Q4_K_M.gguf",
|
||||
name: "Llama 2 13B Chat - GGUF",
|
||||
description: "medium, balanced quality - recommended",
|
||||
description:
|
||||
"medium, balanced quality - not recommended for RAM 16GB and below",
|
||||
avatarUrl:
|
||||
"https://aeiljuispo.cloudimg.io/v7/https://cdn-uploads.huggingface.co/production/uploads/6426d3f3a7723d62b53c259b/tvPikpAzKTKGN5wrpadOJ.jpeg?w=200&h=200&f=face",
|
||||
longDescription:
|
||||
|
||||
@ -8,35 +8,38 @@ import {
|
||||
} from "electron";
|
||||
import { readdirSync } from "fs";
|
||||
import { resolve, join, extname } from "path";
|
||||
import { unlink, createWriteStream } from "fs";
|
||||
import { rmdir, unlink, createWriteStream } from "fs";
|
||||
import isDev = require("electron-is-dev");
|
||||
import { init } from "./core/plugin-manager/pluginMgr";
|
||||
const { autoUpdater } = require("electron-updater");
|
||||
const Store = require("electron-store");
|
||||
// @ts-ignore
|
||||
import request = require("request");
|
||||
// @ts-ignore
|
||||
import progress = require("request-progress");
|
||||
|
||||
let mainWindow: BrowserWindow | undefined = undefined;
|
||||
const store = new Store();
|
||||
|
||||
autoUpdater.autoDownload = false;
|
||||
autoUpdater.autoInstallOnAppQuit = true;
|
||||
|
||||
const createMainWindow = () => {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: electronScreen.getPrimaryDisplay().workArea.width,
|
||||
height: electronScreen.getPrimaryDisplay().workArea.height,
|
||||
width: 1200,
|
||||
height: 800,
|
||||
show: false,
|
||||
backgroundColor: "white",
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
preload: join(__dirname, "preload.js"),
|
||||
webSecurity: false,
|
||||
},
|
||||
});
|
||||
|
||||
ipcMain.handle(
|
||||
"invokePluginFunc",
|
||||
async (event, modulePath, method, ...args) => {
|
||||
async (_event, modulePath, method, ...args) => {
|
||||
const module = join(app.getPath("userData"), "plugins", modulePath);
|
||||
return await import(/* webpackIgnore: true */ module)
|
||||
.then((plugin) => {
|
||||
@ -68,7 +71,10 @@ const createMainWindow = () => {
|
||||
if (isDev) mainWindow.webContents.openDevTools();
|
||||
};
|
||||
|
||||
app.whenReady().then(() => {
|
||||
app
|
||||
.whenReady()
|
||||
.then(migratePlugins)
|
||||
.then(() => {
|
||||
createMainWindow();
|
||||
setupPlugins();
|
||||
autoUpdater.checkForUpdates();
|
||||
@ -110,7 +116,9 @@ app.whenReady().then(() => {
|
||||
} else {
|
||||
result = "File deleted successfully";
|
||||
}
|
||||
console.log(`Delete file ${filePath} from ${fullPath} result: ${result}`);
|
||||
console.log(
|
||||
`Delete file ${filePath} from ${fullPath} result: ${result}`
|
||||
);
|
||||
});
|
||||
|
||||
return result;
|
||||
@ -149,7 +157,7 @@ app.whenReady().then(() => {
|
||||
createMainWindow();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/*New Update Available*/
|
||||
autoUpdater.on("update-available", async (info: any) => {
|
||||
@ -192,6 +200,25 @@ app.on("window-all-closed", () => {
|
||||
}
|
||||
});
|
||||
|
||||
function migratePlugins() {
|
||||
return new Promise((resolve) => {
|
||||
if (store.get("migrated_version") !== app.getVersion()) {
|
||||
console.log("start migration:", store.get("migrated_version"));
|
||||
const userDataPath = app.getPath("userData");
|
||||
const fullPath = join(userDataPath, "plugins");
|
||||
|
||||
rmdir(fullPath, { recursive: true }, function (err) {
|
||||
if (err) console.log(err);
|
||||
store.set("migrated_version", app.getVersion());
|
||||
console.log("migrate plugins done");
|
||||
resolve(undefined);
|
||||
});
|
||||
} else {
|
||||
resolve(undefined);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function setupPlugins() {
|
||||
init({
|
||||
// Function to check from the main process that user wants to install a plugin
|
||||
|
||||
@ -30,6 +30,7 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"",
|
||||
"dev": "tsc -p . && electron .",
|
||||
"build": "tsc -p . && electron-builder -p never -mw",
|
||||
"build:publish": "tsc -p . && electron-builder -p onTagOrDraft -mw",
|
||||
@ -37,6 +38,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"electron-is-dev": "^2.0.0",
|
||||
"electron-store": "^8.1.0",
|
||||
"electron-updater": "^6.1.4",
|
||||
"node-llama-cpp": "^2.4.1",
|
||||
"pluggable-electron": "^0.6.0",
|
||||
@ -44,9 +46,12 @@
|
||||
"request-progress": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.3",
|
||||
"@typescript-eslint/parser": "^6.7.3",
|
||||
"concurrently": "^8.2.1",
|
||||
"electron": "26.2.1",
|
||||
"electron-builder": "^24.6.4",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"wait-on": "^7.0.1"
|
||||
},
|
||||
"installConfig": {
|
||||
|
||||
1612
node_modules/.yarn-integrity
generated
vendored
1612
node_modules/.yarn-integrity
generated
vendored
File diff suppressed because it is too large
Load Diff
@ -1,29 +0,0 @@
|
||||
import { MenuAdvancedPrompt } from "../MenuAdvancedPrompt";
|
||||
import { useForm } from "react-hook-form";
|
||||
import BasicPromptButton from "../BasicPromptButton";
|
||||
import PrimaryButton from "../PrimaryButton";
|
||||
|
||||
const AdvancedPrompt: React.FC = () => {
|
||||
const { register, handleSubmit } = useForm();
|
||||
|
||||
const onSubmit = (data: any) => {};
|
||||
|
||||
return (
|
||||
<form
|
||||
className="w-[288px] h-screen flex flex-col border-r border-gray-200"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<BasicPromptButton />
|
||||
<MenuAdvancedPrompt register={register} />
|
||||
<div className="py-3 px-2 flex flex-none gap-3 items-center justify-between border-t border-gray-200">
|
||||
<PrimaryButton
|
||||
fullWidth={true}
|
||||
title="Generate"
|
||||
onClick={() => handleSubmit(onSubmit)}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdvancedPrompt;
|
||||
@ -1,18 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import TogglableHeader from "../TogglableHeader";
|
||||
|
||||
const AdvancedPromptGenerationParams = () => {
|
||||
const [expand, setExpand] = useState(true);
|
||||
return (
|
||||
<>
|
||||
<TogglableHeader
|
||||
icon={"icons/unicorn_layers-alt.svg"}
|
||||
title={"Generation Parameters"}
|
||||
expand={expand}
|
||||
onTitleClick={() => setExpand(!expand)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdvancedPromptGenerationParams;
|
||||
@ -1,31 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import { DropdownsList } from "../DropdownList";
|
||||
import TogglableHeader from "../TogglableHeader";
|
||||
import { UploadFileImage } from "../UploadFileImage";
|
||||
import { FieldValues, UseFormRegister } from "react-hook-form";
|
||||
|
||||
type Props = {
|
||||
register: UseFormRegister<FieldValues>;
|
||||
};
|
||||
|
||||
const AdvancedPromptImageUpload: React.FC<Props> = ({ register }) => {
|
||||
const [expand, setExpand] = useState(true);
|
||||
const data = ["test1", "test2", "test3", "test4"];
|
||||
|
||||
return (
|
||||
<>
|
||||
<TogglableHeader
|
||||
icon={"icons/ic_image.svg"}
|
||||
title={"Image"}
|
||||
expand={expand}
|
||||
onTitleClick={() => setExpand(!expand)}
|
||||
/>
|
||||
<div className={`${expand ? "flex" : "hidden"} flex-col gap-[5px]`}>
|
||||
<UploadFileImage register={register} />
|
||||
<DropdownsList title="Control image with ControlNet:" data={data} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdvancedPromptImageUpload;
|
||||
@ -1,29 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import { DropdownsList } from "../DropdownList";
|
||||
import TogglableHeader from "../TogglableHeader";
|
||||
|
||||
const AdvancedPromptResolution = () => {
|
||||
const [expand, setExpand] = useState(true);
|
||||
const data = ["512", "524", "536"];
|
||||
const ratioData = ["1:1", "2:2", "3:3"];
|
||||
|
||||
return (
|
||||
<>
|
||||
<TogglableHeader
|
||||
icon={"icons/unicorn_layers-alt.svg"}
|
||||
title={"Resolution"}
|
||||
expand={expand}
|
||||
onTitleClick={() => setExpand(!expand)}
|
||||
/>
|
||||
<div className={`${expand ? "flex" : "hidden"} flex-col gap-[5px]`}>
|
||||
<div className="flex gap-3 py-3">
|
||||
<DropdownsList data={data} title="Width" />
|
||||
<DropdownsList data={data} title="Height" />
|
||||
</div>
|
||||
<DropdownsList title="Select ratio" data={ratioData} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdvancedPromptResolution;
|
||||
@ -1,41 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import TogglableHeader from "../TogglableHeader";
|
||||
import { AdvancedTextArea } from "../AdvancedTextArea";
|
||||
import { FieldValues, UseFormRegister } from "react-hook-form";
|
||||
|
||||
type Props = {
|
||||
register: UseFormRegister<FieldValues>;
|
||||
};
|
||||
|
||||
const AdvancedPromptText: React.FC<Props> = ({ register }) => {
|
||||
const [expand, setExpand] = useState(true);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TogglableHeader
|
||||
icon={"icons/messicon.svg"}
|
||||
title={"Prompt"}
|
||||
expand={expand}
|
||||
onTitleClick={() => setExpand(!expand)}
|
||||
/>
|
||||
<div className={`${expand ? "flex" : "hidden"} flex-col gap-[5px]`}>
|
||||
<AdvancedTextArea
|
||||
formId="prompt"
|
||||
height={80}
|
||||
placeholder="Prompt"
|
||||
title="Prompt"
|
||||
register={register}
|
||||
/>
|
||||
<AdvancedTextArea
|
||||
formId="negativePrompt"
|
||||
height={80}
|
||||
placeholder="Describe what you don't want in your image"
|
||||
title="Negative Prompt"
|
||||
register={register}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdvancedPromptText;
|
||||
@ -1,27 +0,0 @@
|
||||
import { FieldValues, UseFormRegister } from "react-hook-form";
|
||||
|
||||
type Props = {
|
||||
formId?: string;
|
||||
height: number;
|
||||
title: string;
|
||||
placeholder: string;
|
||||
register: UseFormRegister<FieldValues>;
|
||||
};
|
||||
|
||||
export const AdvancedTextArea: React.FC<Props> = ({
|
||||
formId = "",
|
||||
height,
|
||||
placeholder,
|
||||
title,
|
||||
register,
|
||||
}) => (
|
||||
<div className="w-full flex flex-col pt-3 gap-1">
|
||||
<label className="text-sm leading-5 text-gray-800">{title}</label>
|
||||
<textarea
|
||||
style={{ height: `${height}px` }}
|
||||
className="rounded-lg py-[13px] px-5 border outline-none resize-none border-gray-300 bg-gray-50 placeholder:gray-400 text-sm font-normal"
|
||||
placeholder={placeholder}
|
||||
{...register(formId, { required: formId === "prompt" ? true : false })}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@ -1,20 +0,0 @@
|
||||
import Image from "next/image";
|
||||
|
||||
const Search: React.FC = () => {
|
||||
return (
|
||||
<div className="flex bg-gray-200 w-[343px] h-[36px] items-center px-2 gap-[6px] rounded-md">
|
||||
<Image
|
||||
src={"icons/magnifyingglass.svg"}
|
||||
width={15.63}
|
||||
height={15.78}
|
||||
alt=""
|
||||
/>
|
||||
<input
|
||||
className="bg-inherit outline-0 w-full border-0 p-0 focus:ring-0"
|
||||
placeholder="Search"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Search;
|
||||
@ -1,20 +0,0 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
imageUrl: string;
|
||||
};
|
||||
|
||||
const AiTypeCard: React.FC<Props> = ({ imageUrl, name }) => {
|
||||
return (
|
||||
<Link href={`/ai/${name}`} className='flex-1'>
|
||||
<div className="flex-1 h-full bg-[#F3F4F6] flex items-center justify-center gap-[10px] py-[13px] rounded-[8px] px-4 active:opacity-50 hover:opacity-20">
|
||||
<Image src={imageUrl} width={82} height={82} alt="" />
|
||||
<span className="font-bold">{name}</span>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default AiTypeCard;
|
||||
@ -1,15 +0,0 @@
|
||||
type Props = {
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export const ApiStep: React.FC<Props> = ({ description, title }) => {
|
||||
return (
|
||||
<div className="gap-2 flex flex-col">
|
||||
<span className="text-[#8A8A8A]">{title}</span>
|
||||
<div className="flex flex-col gap-[10px] p-[18px] bg-[#F9F9F9] overflow-y-hidden">
|
||||
<pre className="text-sm leading-5 text-black">{description}</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,20 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
currentConversationAtom,
|
||||
showingAdvancedPromptAtom,
|
||||
} from "@/_helpers/JotaiWrapper";
|
||||
import { useAtomValue, useSetAtom } from "jotai";
|
||||
import { showingAdvancedPromptAtom } from "@/_helpers/JotaiWrapper";
|
||||
import { useSetAtom } from "jotai";
|
||||
import SecondaryButton from "../SecondaryButton";
|
||||
import SendButton from "../SendButton";
|
||||
import { ProductType } from "@/_models/Product";
|
||||
|
||||
const BasicPromptAccessories: React.FC = () => {
|
||||
const setShowingAdvancedPrompt = useSetAtom(showingAdvancedPromptAtom);
|
||||
const currentConversation = useAtomValue(currentConversationAtom);
|
||||
|
||||
const shouldShowAdvancedPrompt = false;
|
||||
// currentConversation?.product.type === ProductType.ControlNet;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@ -1,39 +0,0 @@
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/outline";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
|
||||
type PropType = PropsWithChildren<
|
||||
React.DetailedHTMLProps<
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
HTMLButtonElement
|
||||
>
|
||||
>;
|
||||
|
||||
export const PrevButton: React.FC<PropType> = (props) => {
|
||||
const { children, ...restProps } = props;
|
||||
|
||||
return (
|
||||
<button
|
||||
className="embla__button embla__button--prev"
|
||||
type="button"
|
||||
{...restProps}
|
||||
>
|
||||
<ChevronLeftIcon width={20} height={20} />
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export const NextButton: React.FC<PropType> = (props) => {
|
||||
const { children, ...restProps } = props;
|
||||
|
||||
return (
|
||||
<button
|
||||
className="embla__button embla__button--next"
|
||||
type="button"
|
||||
{...restProps}
|
||||
>
|
||||
<ChevronRightIcon width={20} height={20} />
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@ -1,25 +0,0 @@
|
||||
import { useTheme } from "next-themes";
|
||||
import { SunIcon, MoonIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
export const ThemeChanger: React.FC = () => {
|
||||
const { theme, setTheme, systemTheme } = useTheme();
|
||||
const currentTheme = theme === "system" ? systemTheme : theme;
|
||||
|
||||
if (currentTheme === "dark") {
|
||||
return (
|
||||
<SunIcon
|
||||
className="h-6 w-6"
|
||||
aria-hidden="true"
|
||||
onClick={() => setTheme("light")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MoonIcon
|
||||
className="h-6 w-6"
|
||||
aria-hidden="true"
|
||||
onClick={() => setTheme("dark")}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -5,14 +5,24 @@ import ChatItem from "../ChatItem";
|
||||
import { ChatMessage } from "@/_models/ChatMessage";
|
||||
import useChatMessages from "@/_hooks/useChatMessages";
|
||||
import {
|
||||
currentChatMessagesAtom,
|
||||
chatMessages,
|
||||
getActiveConvoIdAtom,
|
||||
showingTyping,
|
||||
} from "@/_helpers/JotaiWrapper";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { selectAtom } from "jotai/utils";
|
||||
import LoadingIndicator from "../LoadingIndicator";
|
||||
|
||||
const ChatBody: React.FC = () => {
|
||||
const messages = useAtomValue(currentChatMessagesAtom);
|
||||
const activeConversationId = useAtomValue(getActiveConvoIdAtom) ?? "";
|
||||
const messageList = useAtomValue(
|
||||
selectAtom(
|
||||
chatMessages,
|
||||
useCallback((v) => v[activeConversationId], [activeConversationId])
|
||||
)
|
||||
);
|
||||
const [content, setContent] = useState<React.JSX.Element[]>([]);
|
||||
|
||||
const isTyping = useAtomValue(showingTyping);
|
||||
const [offset, setOffset] = useState(0);
|
||||
const { loading, hasMore } = useChatMessages(offset);
|
||||
@ -35,13 +45,18 @@ const ChatBody: React.FC = () => {
|
||||
[loading, hasMore]
|
||||
);
|
||||
|
||||
const content = messages.map((message, index) => {
|
||||
if (messages.length === index + 1) {
|
||||
React.useEffect(() => {
|
||||
const list = messageList?.map((message, index) => {
|
||||
if (messageList?.length === index + 1) {
|
||||
return (
|
||||
// @ts-ignore
|
||||
return <ChatItem ref={lastPostRef} message={message} key={message.id} />;
|
||||
<ChatItem ref={lastPostRef} message={message} key={message.id} />
|
||||
);
|
||||
}
|
||||
return <ChatItem message={message} key={message.id} />;
|
||||
});
|
||||
setContent(list);
|
||||
}, [messageList, lastPostRef]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col-reverse flex-1 py-4 overflow-y-auto scroll">
|
||||
|
||||
@ -3,7 +3,7 @@ import SimpleImageMessage from "../SimpleImageMessage";
|
||||
import SimpleTextMessage from "../SimpleTextMessage";
|
||||
import { ChatMessage, MessageType } from "@/_models/ChatMessage";
|
||||
import StreamTextMessage from "../StreamTextMessage";
|
||||
import { useAtom } from "jotai";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { currentStreamingMessageAtom } from "@/_helpers/JotaiWrapper";
|
||||
|
||||
export default function renderChatMessage({
|
||||
@ -13,11 +13,11 @@ export default function renderChatMessage({
|
||||
senderName,
|
||||
createdAt,
|
||||
imageUrls,
|
||||
htmlText,
|
||||
text,
|
||||
status,
|
||||
}: ChatMessage): React.ReactNode {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const [message, _] = useAtom(currentStreamingMessageAtom);
|
||||
const message = useAtomValue(currentStreamingMessageAtom);
|
||||
switch (messageType) {
|
||||
case MessageType.ImageWithText:
|
||||
return (
|
||||
@ -48,7 +48,7 @@ export default function renderChatMessage({
|
||||
avatarUrl={senderAvatarUrl}
|
||||
senderName={senderName}
|
||||
createdAt={createdAt}
|
||||
text={text}
|
||||
text={htmlText && htmlText.trim().length > 0 ? htmlText : text}
|
||||
/>
|
||||
) : (
|
||||
<StreamTextMessage
|
||||
@ -57,7 +57,7 @@ export default function renderChatMessage({
|
||||
avatarUrl={senderAvatarUrl}
|
||||
senderName={senderName}
|
||||
createdAt={createdAt}
|
||||
text={text}
|
||||
text={htmlText && htmlText.trim().length > 0 ? htmlText : text}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
|
||||
@ -1,37 +0,0 @@
|
||||
import {
|
||||
getActiveConvoIdAtom,
|
||||
setActiveConvoIdAtom,
|
||||
} from "@/_helpers/JotaiWrapper";
|
||||
import { useAtomValue, useSetAtom } from "jotai";
|
||||
import Image from "next/image";
|
||||
|
||||
type Props = {
|
||||
imageUrl: string;
|
||||
conversationId: string;
|
||||
};
|
||||
|
||||
const CompactHistoryItem: React.FC<Props> = ({ imageUrl, conversationId }) => {
|
||||
const activeConvoId = useAtomValue(getActiveConvoIdAtom);
|
||||
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom);
|
||||
|
||||
const isSelected = activeConvoId === conversationId;
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => setActiveConvoId(conversationId)}
|
||||
className={`${
|
||||
isSelected ? "bg-gray-100" : "bg-transparent"
|
||||
} w-14 h-14 rounded-lg`}
|
||||
>
|
||||
<Image
|
||||
className="rounded-full mx-auto"
|
||||
src={imageUrl}
|
||||
width={36}
|
||||
height={36}
|
||||
alt=""
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default CompactHistoryItem;
|
||||
@ -1,21 +0,0 @@
|
||||
import { useAtomValue } from "jotai";
|
||||
import CompactHistoryItem from "../CompactHistoryItem";
|
||||
import { userConversationsAtom } from "@/_helpers/JotaiWrapper";
|
||||
|
||||
const CompactHistoryList: React.FC = () => {
|
||||
const conversations = useAtomValue(userConversationsAtom);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 gap-1 mt-3">
|
||||
{conversations.map(({ id, image }) => (
|
||||
<CompactHistoryItem
|
||||
key={id}
|
||||
conversationId={id ?? ""}
|
||||
imageUrl={image ?? ""}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CompactHistoryList;
|
||||
@ -1,11 +0,0 @@
|
||||
import CompactHistoryList from "../CompactHistoryList";
|
||||
import CompactLogo from "../CompactLogo";
|
||||
|
||||
const CompactSideBar: React.FC = () => (
|
||||
<div className="h-screen w-16 border-r border-gray-300 flex flex-col items-center pt-3 gap-3">
|
||||
<CompactLogo />
|
||||
<CompactHistoryList />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default CompactSideBar;
|
||||
@ -1,33 +0,0 @@
|
||||
import { ApiStep } from "../ApiStep";
|
||||
|
||||
const DescriptionPane: React.FC = () => {
|
||||
const data = [
|
||||
{
|
||||
title: "Install the Node.js client:",
|
||||
description: "npm install replicate",
|
||||
},
|
||||
{
|
||||
title:
|
||||
"Next, copy your API token and authenticate by setting it as an environment variable:",
|
||||
description:
|
||||
"export REPLICATE_API_TOKEN=r8_*************************************",
|
||||
},
|
||||
{
|
||||
title: "lorem ipsum dolor asimet",
|
||||
description: "come codes here",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-[full]">
|
||||
<h2 className="text-[20px] tracking-[-0.4px] leading-[25px]">
|
||||
Run the model
|
||||
</h2>
|
||||
{data.map((item, index) => (
|
||||
<ApiStep key={index} {...item} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DescriptionPane;
|
||||
@ -1,29 +0,0 @@
|
||||
import useCreateConversation from "@/_hooks/useCreateConversation";
|
||||
import { Product } from "@/_models/Product";
|
||||
|
||||
type Props = {
|
||||
product: Product;
|
||||
};
|
||||
|
||||
const GenerateImageCard: React.FC<Props> = ({ product }) => {
|
||||
const { name, avatarUrl } = product;
|
||||
const { requestCreateConvo } = useCreateConversation();
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => requestCreateConvo(product)}
|
||||
className="relative active:opacity-50 text-left"
|
||||
>
|
||||
<img
|
||||
src={avatarUrl}
|
||||
alt=""
|
||||
className="w-full h-full rounded-[8px] bg-gray-200 group-hover:opacity-75 object-cover object-center"
|
||||
/>
|
||||
<div className="absolute bottom-0 rounded-br-[8px] rounded-bl-[8px] bg-[rgba(0,0,0,0.5)] w-full p-3">
|
||||
<span className="text-white font-semibold">{name}</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default GenerateImageCard;
|
||||
@ -1,27 +0,0 @@
|
||||
import { Product } from "@/_models/Product";
|
||||
import GenerateImageCard from "../GenerateImageCard";
|
||||
import { PhotoIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
type Props = {
|
||||
products: Product[];
|
||||
};
|
||||
|
||||
const GenerateImageList: React.FC<Props> = ({ products }) => (
|
||||
<>
|
||||
{products.length === 0 ? null : (
|
||||
<div className="flex items-center gap-3 mt-8 mb-2">
|
||||
<PhotoIcon width={24} height={24} className="ml-6" />
|
||||
<span className="font-semibold text-gray-900 dark:text-white">
|
||||
Generate Images
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-2 mx-6 mb-6 grid grid-cols-2 gap-6 sm:gap-x-6 md:grid-cols-4 md:gap-8">
|
||||
{products.map((item) => (
|
||||
<GenerateImageCard key={item.name} product={item} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
export default GenerateImageList;
|
||||
@ -1,42 +0,0 @@
|
||||
import useCreateConversation from "@/_hooks/useCreateConversation";
|
||||
import { executeSerial } from "@/_services/pluginService";
|
||||
import Image from "next/image";
|
||||
import React from "react";
|
||||
import { DataService } from "../../../shared/coreService";
|
||||
|
||||
const HistoryEmpty: React.FC = () => {
|
||||
const { requestCreateConvo } = useCreateConversation();
|
||||
const startChat = async () => {
|
||||
const downloadedModels = await executeSerial(
|
||||
DataService.GET_FINISHED_DOWNLOAD_MODELS
|
||||
);
|
||||
if (!downloadedModels || downloadedModels?.length === 0) {
|
||||
alert(
|
||||
"Seems like there is no model downloaded yet. Please download a model first."
|
||||
);
|
||||
} else {
|
||||
requestCreateConvo(downloadedModels[0]);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="mt-5 flex flex-col w-full h-full items-center justify-center gap-4">
|
||||
<Image
|
||||
src={"icons/chats-circle-light.svg"}
|
||||
width={50}
|
||||
height={50}
|
||||
alt=""
|
||||
/>
|
||||
<p className="text-sm leading-5 text-center text-[#9CA3AF]">
|
||||
Its empty here
|
||||
</p>
|
||||
<button
|
||||
onClick={startChat}
|
||||
className="bg-[#1F2A37] py-[10px] px-5 gap-2 rounded-[8px] text-[14px] font-medium leading-[21px] text-white"
|
||||
>
|
||||
Let's chat
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(HistoryEmpty);
|
||||
@ -1,12 +0,0 @@
|
||||
import { displayDate } from "@/_utils/datetime";
|
||||
import React from "react";
|
||||
|
||||
type Props = {
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
const HistoryItemDate: React.FC<Props> = ({ timestamp }) => {
|
||||
return <p className="text-gray-400 text-xs">{displayDate(timestamp)}</p>;
|
||||
};
|
||||
|
||||
export default React.memo(HistoryItemDate);
|
||||
@ -1,20 +0,0 @@
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
const JanWelcomeTitle: React.FC<Props> = ({ title, description }) => (
|
||||
<div className="flex items-center flex-col gap-3">
|
||||
<h2 className="text-[22px] leading-7 font-bold">{title}</h2>
|
||||
<span className="flex items-center text-xs leading-[18px]">
|
||||
Operated by
|
||||
<Image src={"icons/ico_logo.svg"} width={42} height={22} alt="" />
|
||||
</span>
|
||||
<span className="text-sm text-center font-normal">{description}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default React.memo(JanWelcomeTitle);
|
||||
@ -1,21 +0,0 @@
|
||||
import AdvancedPromptText from "../AdvancedPromptText";
|
||||
import AdvancedPromptImageUpload from "../AdvancedPromptImageUpload";
|
||||
import AdvancedPromptResolution from "../AdvancedPromptResolution";
|
||||
import AdvancedPromptGenerationParams from "../AdvancedPromptGenerationParams";
|
||||
import { FieldValues, UseFormRegister } from "react-hook-form";
|
||||
|
||||
type Props = {
|
||||
register: UseFormRegister<FieldValues>;
|
||||
};
|
||||
|
||||
export const MenuAdvancedPrompt: React.FC<Props> = ({ register }) => (
|
||||
<div className="flex flex-col flex-1 p-3 gap-[10px] overflow-x-hidden scroll">
|
||||
<AdvancedPromptText register={register} />
|
||||
<hr className="my-5" />
|
||||
<AdvancedPromptImageUpload register={register} />
|
||||
<hr className="my-5" />
|
||||
<AdvancedPromptResolution />
|
||||
<hr className="my-5" />
|
||||
<AdvancedPromptGenerationParams />
|
||||
</div>
|
||||
);
|
||||
@ -1,69 +0,0 @@
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
const MobileDownload = () => {
|
||||
return (
|
||||
<div className="flex items-center flex-col box-border rounded-lg border border-gray-200 p-4 bg-[#F9FAFB] mb-3">
|
||||
{/** Jan logo */}
|
||||
<Image
|
||||
src="icons/janai_logo.svg"
|
||||
alt={""}
|
||||
width={32}
|
||||
height={32}
|
||||
style={{ objectFit: "contain" }}
|
||||
/>
|
||||
<b>Jan Mobile</b>
|
||||
{/** Messages */}
|
||||
<p className="font-light text-[12px] text-center">
|
||||
Stay up to date and move work forward with Jan on iOS & Android.
|
||||
Download the app today.
|
||||
</p>
|
||||
{/** Buttons */}
|
||||
<div className="flex w-full mt-4 justify-between">
|
||||
<a
|
||||
href={process.env.NEXT_PUBLIC_DOWNLOAD_APP_IOS || ""}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-[48%]"
|
||||
>
|
||||
<div className="flex box-border h-11 rounded-md bg-gray-300 p-2 items-center hover:bg-gray-200 focus:bg-gray-600">
|
||||
<Image
|
||||
src="icons/social_icon_apple.svg"
|
||||
alt={""}
|
||||
width={26}
|
||||
height={26}
|
||||
style={{ objectFit: "contain" }}
|
||||
/>
|
||||
<div className="ml-1">
|
||||
<p className="text-[8px]">Download on the</p>
|
||||
<p className="text-[10px] font-bold">AppStore</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href={process.env.NEXT_PUBLIC_DOWNLOAD_APP_ANDROID || ""}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-[48%]"
|
||||
>
|
||||
<div className="flex box-border h-11 rounded-md bg-gray-300 p-2 items-center hover:bg-gray-200 focus:bg-gray-600">
|
||||
<Image
|
||||
src="icons/google_play_logo.svg"
|
||||
alt={""}
|
||||
width={26}
|
||||
height={26}
|
||||
style={{ objectFit: "contain" }}
|
||||
/>
|
||||
<div className="ml-1">
|
||||
<p className="text-[8px]">Download on the</p>
|
||||
<p className="text-[10px] font-bold">Google Play</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(MobileDownload);
|
||||
@ -1,44 +0,0 @@
|
||||
import Image from "next/image";
|
||||
|
||||
const MobileInstallPane: React.FC = () => {
|
||||
return (
|
||||
<div className="p-4 rounded-[8px] border-[1px] border-[#E5E7EB] bg-[#F9FAFB]">
|
||||
<div className="flex flex-col gap-5 items-center">
|
||||
<div className="flex flex-col items-center text-[12px]">
|
||||
<Image src={"icons/app_icon.svg"} width={32} height={32} alt="" />
|
||||
<h2 className="font-bold leading-[12px] text-center">Jan Mobie</h2>
|
||||
<p className="leading-[18px] text-center">
|
||||
Stay up to date and move work forward with Jan on iOS & Android.
|
||||
<br />
|
||||
Download the app today.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-between items-center gap-3">
|
||||
<div className="bg-[#E5E7EB] rounded-[8px] gap-3 p-2 flex items-center">
|
||||
<Image src={"icons/apple.svg"} width={26} height={26} alt="" />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[8px] leading-[12px]">Download on the</span>
|
||||
<h2 className="font-bold text-[12px] leading-[15px]">AppStore</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-[#E5E7EB] rounded-[8px] gap-3 p-2 flex items-center">
|
||||
<Image
|
||||
src={"icons/googleplay.svg"}
|
||||
width={26}
|
||||
height={26}
|
||||
alt=""
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[8px] leading-[12px]">Download on the</span>
|
||||
<h2 className="font-bold text-[12px] leading-[15px]">
|
||||
Google Play
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MobileInstallPane;
|
||||
@ -1,91 +0,0 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
|
||||
const MobileShowcase = () => {
|
||||
return (
|
||||
<div className="md:hidden flex flex-col px-5 mt-10 items-center justify-center w-full gap-10">
|
||||
<Image
|
||||
src="images/mobile.jpg"
|
||||
width={638}
|
||||
height={892}
|
||||
alt="mobile"
|
||||
className="w-full h-full"
|
||||
style={{ objectFit: "contain" }}
|
||||
/>
|
||||
<div className="flex flex-col items-center justify-center mb-20">
|
||||
<Image
|
||||
src="icons/app_icon.svg"
|
||||
width={200}
|
||||
height={200}
|
||||
className="w-[10%]"
|
||||
alt="logo"
|
||||
/>
|
||||
<span className="text-[22px] font-semibold">Download Jan App</span>
|
||||
<p className="text-center text-sm text-gray-500">
|
||||
<span>Stay up to date and move work forward with Jan on iOS</span>
|
||||
<span>& Android. Download the app today.</span>
|
||||
</p>
|
||||
<div className="flex justify-between items-center gap-3 mt-5">
|
||||
<a
|
||||
href={process.env.NEXT_PUBLIC_DOWNLOAD_APP_IOS ?? "#"}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-[48%]"
|
||||
>
|
||||
<div className="flex box-border h-11 rounded-md bg-gray-300 p-2 items-center hover:bg-gray-200 focus:bg-gray-600">
|
||||
<Image
|
||||
src="icons/social_icon_apple.svg"
|
||||
alt={""}
|
||||
width={26}
|
||||
height={26}
|
||||
style={{ objectFit: "contain" }}
|
||||
/>
|
||||
<div className="ml-1">
|
||||
<p className="text-[8px]">Download on the</p>
|
||||
<p className="text-[10px] font-bold">AppStore</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href={process.env.NEXT_PUBLIC_DOWNLOAD_APP_ANDROID ?? "#"}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-[48%]"
|
||||
>
|
||||
<div className="flex box-border h-11 rounded-md bg-gray-300 p-2 items-center hover:bg-gray-200 focus:bg-gray-600">
|
||||
<Image
|
||||
src="icons/google_play_logo.svg"
|
||||
alt={""}
|
||||
width={26}
|
||||
height={26}
|
||||
style={{ objectFit: "contain" }}
|
||||
/>
|
||||
<div className="ml-1">
|
||||
<p className="text-[8px]">Download on the</p>
|
||||
<p className="text-[10px] font-bold">Google Play</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<Link
|
||||
href={process.env.NEXT_PUBLIC_DISCORD_INVITATION_URL ?? "#"}
|
||||
target="_blank_"
|
||||
>
|
||||
<div className="flex flex-row space-x-2 items-center justify-center rounded-[18px] px-2 h-[36px] bg-[#E2E5FF] mt-5">
|
||||
<Image
|
||||
src="icons/discord-icon.svg"
|
||||
width={24}
|
||||
height={24}
|
||||
className=""
|
||||
alt=""
|
||||
/>
|
||||
<span className="text-[#5865F2]">Join our Discord Community</span>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MobileShowcase;
|
||||
@ -1,21 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
type Props = {
|
||||
inferenceTime: string;
|
||||
hardware: string;
|
||||
averageCostPerCall: string;
|
||||
onGetApiKeyClick: () => void;
|
||||
};
|
||||
|
||||
const ModelDetailCost: React.FC<Props> = ({
|
||||
inferenceTime,
|
||||
hardware,
|
||||
averageCostPerCall,
|
||||
onGetApiKeyClick,
|
||||
}) => {
|
||||
return <div>
|
||||
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default ModelDetailCost;
|
||||
@ -1,9 +0,0 @@
|
||||
import OverviewPane from "../OverviewPane";
|
||||
|
||||
const ModelDetailSideBar: React.FC = () => (
|
||||
<div className="flex w-[473px] h-full border-l-[1px] border-[#E5E7EB]">
|
||||
<OverviewPane />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default ModelDetailSideBar;
|
||||
@ -1,56 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useAtomValue } from "jotai";
|
||||
import React from "react";
|
||||
import { currentProductAtom } from "@/_helpers/JotaiWrapper";
|
||||
|
||||
const OverviewPane: React.FC = () => {
|
||||
const product = useAtomValue(currentProductAtom);
|
||||
|
||||
return (
|
||||
<div className="scroll overflow-y-auto">
|
||||
<div className="flex flex-col flex-grow gap-6 m-3">
|
||||
<AboutProductItem
|
||||
title={"About this AI"}
|
||||
value={product?.description ?? ""}
|
||||
/>
|
||||
<SmallItem title={"Model Version"} value={product?.version ?? ""} />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[#6B7280]">Model URL</span>
|
||||
<a
|
||||
className="text-[#1C64F2]"
|
||||
href={product?.modelUrl ?? "#"}
|
||||
target="_blank_"
|
||||
>
|
||||
{product?.modelUrl}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OverviewPane;
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
const AboutProductItem: React.FC<Props> = ({ title, value }) => {
|
||||
return (
|
||||
<div className="flex flex-col items-start">
|
||||
<h2 className="text-black font-bold">{title}</h2>
|
||||
<p className="text-[#6B7280]">{value}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SmallItem: React.FC<Props> = ({ title, value }) => {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[#6B7280] ">{title}</span>
|
||||
<span className="font-semibold">{value}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,10 +0,0 @@
|
||||
import React from "react";
|
||||
import useGetModels from "@/_hooks/useGetModels";
|
||||
|
||||
const ProductOverview: React.FC = () => {
|
||||
const { models } = useGetModels();
|
||||
|
||||
return <div className="bg-gray-100 overflow-y-auto flex-grow scroll"></div>;
|
||||
};
|
||||
|
||||
export default ProductOverview;
|
||||
@ -1,40 +0,0 @@
|
||||
import JanWelcomeTitle from "../JanWelcomeTitle";
|
||||
import { Product } from "@/_models/Product";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { currentPromptAtom } from "@/_helpers/JotaiWrapper";
|
||||
|
||||
type Props = {
|
||||
product: Product;
|
||||
};
|
||||
|
||||
const SampleLlmContainer: React.FC<Props> = ({ product }) => {
|
||||
const setCurrentPrompt = useSetAtom(currentPromptAtom);
|
||||
const { data } = { data: { prompts: [] } };
|
||||
|
||||
return (
|
||||
<div className="flex flex-col max-w-sm flex-shrink-0 gap-9 items-center pt-6 mx-auto">
|
||||
<JanWelcomeTitle
|
||||
title={product.name}
|
||||
description={product.description ?? ""}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<h2 className="font-semibold text-xl leading-6 tracking-[-0.4px] mb-5">
|
||||
Try now
|
||||
</h2>
|
||||
{/* <div className="flex flex-col">
|
||||
{data?.prompts.map((item) => (
|
||||
<button
|
||||
onClick={() => setCurrentPrompt(item.content ?? "")}
|
||||
key={item.slug}
|
||||
className="rounded p-2 hover:bg-[#0000000F] text-xs leading-[18px] text-gray-500 text-left"
|
||||
>
|
||||
<span className="line-clamp-3">{item.content}</span>
|
||||
</button>
|
||||
))}
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SampleLlmContainer;
|
||||
@ -1,34 +0,0 @@
|
||||
import React from "react";
|
||||
import useCreateConversation from "@/_hooks/useCreateConversation";
|
||||
import Image from "next/image";
|
||||
import { Product } from "@/_models/Product";
|
||||
|
||||
type Props = {
|
||||
product: Product;
|
||||
};
|
||||
|
||||
const ShortcutItem: React.FC<Props> = ({ product }) => {
|
||||
const { requestCreateConvo } = useCreateConversation();
|
||||
|
||||
return (
|
||||
<button
|
||||
className="flex items-center gap-2 mx-1 p-2"
|
||||
onClick={() => requestCreateConvo(product)}
|
||||
>
|
||||
{product.avatarUrl && (
|
||||
<Image
|
||||
width={36}
|
||||
height={36}
|
||||
src={product.avatarUrl}
|
||||
className="w-9 aspect-square rounded-full"
|
||||
alt=""
|
||||
/>
|
||||
)}
|
||||
<span className="text-gray-900 dark:text-white font-normal text-sm">
|
||||
{product.name}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ShortcutItem);
|
||||
@ -1,23 +0,0 @@
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
type Props = {
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
const ShowMoreButton: React.FC<Props> = ({ onClick }) => (
|
||||
<button
|
||||
className="flex text-xs leading-[18px] text-gray-800 rounded-lg py-2 px-3"
|
||||
onClick={onClick}
|
||||
>
|
||||
Show more
|
||||
<Image
|
||||
src={"icons/unicorn_angle-down.svg"}
|
||||
width={16}
|
||||
height={16}
|
||||
alt=""
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
|
||||
export default React.memo(ShowMoreButton);
|
||||
@ -1,44 +0,0 @@
|
||||
import useCreateConversation from "@/_hooks/useCreateConversation";
|
||||
import { Product } from "@/_models/Product";
|
||||
import Image from "next/image";
|
||||
|
||||
type Props = {
|
||||
product: Product;
|
||||
};
|
||||
|
||||
const Slide: React.FC<Props> = ({ product }) => {
|
||||
const { name, avatarUrl, description } = product;
|
||||
const { requestCreateConvo } = useCreateConversation();
|
||||
|
||||
return (
|
||||
<div className="w-full embla__slide h-[435px] relative">
|
||||
<Image
|
||||
className="w-full h-auto embla__slide__img"
|
||||
src={avatarUrl}
|
||||
fill
|
||||
priority
|
||||
alt=""
|
||||
/>
|
||||
<div className="absolute bg-[rgba(0,0,0,0.7)] w-full text-white bottom-0 right-0">
|
||||
<div className="flex justify-between p-4">
|
||||
<div className="flex flex-col gap-[2px]">
|
||||
<h2 className="font-semibold text-xl leading-[25px] tracking-[-0.5px]">
|
||||
{name}
|
||||
</h2>
|
||||
<span className="text-gray-300 text-xs leading-[18px]">
|
||||
{description}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => requestCreateConvo(product)}
|
||||
className="flex-none flex w-30 h-12 items-center text-sm justify-center gap-2 px-5 py-[10px] rounded-md bg-white leading-[21px] text-gray-800"
|
||||
>
|
||||
Try now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Slide;
|
||||
@ -1,54 +0,0 @@
|
||||
import { FC, useCallback, useEffect, useState } from "react";
|
||||
import Slide from "../Slide";
|
||||
import useEmblaCarousel, { EmblaCarouselType } from "embla-carousel-react";
|
||||
import { NextButton, PrevButton } from "../ButtonSlider";
|
||||
import { Product } from "@/_models/Product";
|
||||
|
||||
type Props = {
|
||||
products: Product[];
|
||||
};
|
||||
|
||||
const Slider: FC<Props> = ({ products }) => {
|
||||
const [emblaRef, emblaApi] = useEmblaCarousel();
|
||||
const [prevBtnDisabled, setPrevBtnDisabled] = useState(true);
|
||||
const [nextBtnDisabled, setNextBtnDisabled] = useState(true);
|
||||
|
||||
const scrollPrev = useCallback(
|
||||
() => emblaApi && emblaApi.scrollPrev(),
|
||||
[emblaApi]
|
||||
);
|
||||
const scrollNext = useCallback(
|
||||
() => emblaApi && emblaApi.scrollNext(),
|
||||
[emblaApi]
|
||||
);
|
||||
|
||||
const onSelect = useCallback((emblaApi: EmblaCarouselType) => {
|
||||
setPrevBtnDisabled(!emblaApi.canScrollPrev());
|
||||
setNextBtnDisabled(!emblaApi.canScrollNext());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!emblaApi) return;
|
||||
onSelect(emblaApi);
|
||||
emblaApi.on("reInit", onSelect);
|
||||
emblaApi.on("select", onSelect);
|
||||
}, [emblaApi, onSelect]);
|
||||
|
||||
return (
|
||||
<div className="embla rounded-lg overflow-hidden relative mt-6 mx-6">
|
||||
<div className="embla__viewport" ref={emblaRef}>
|
||||
<div className="embla__container">
|
||||
{products.map((product) => (
|
||||
<Slide key={product.slug} product={product} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="embla__buttons">
|
||||
<PrevButton onClick={scrollPrev} disabled={prevBtnDisabled} />
|
||||
<NextButton onClick={scrollNext} disabled={nextBtnDisabled} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Slider;
|
||||
@ -3,7 +3,7 @@ import { displayDate } from "@/_utils/datetime";
|
||||
import { TextCode } from "../TextCode";
|
||||
import { getMessageCode } from "@/_utils/message";
|
||||
import Image from "next/image";
|
||||
import { useAtom } from "jotai";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { currentStreamingMessageAtom } from "@/_helpers/JotaiWrapper";
|
||||
|
||||
type Props = {
|
||||
@ -21,7 +21,7 @@ const StreamTextMessage: React.FC<Props> = ({
|
||||
avatarUrl = "",
|
||||
text = "",
|
||||
}) => {
|
||||
const [message, _] = useAtom(currentStreamingMessageAtom);
|
||||
const message = useAtomValue(currentStreamingMessageAtom);
|
||||
|
||||
return message?.text && message?.text?.length > 0 ? (
|
||||
<div className="flex items-start gap-2 ml-3">
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
type Props = {
|
||||
title: string;
|
||||
};
|
||||
|
||||
export const TitleBlankState: React.FC<Props> = ({ title }) => {
|
||||
return (
|
||||
<h2 className="text-[#6B7280] text-[20px] leading-[25px] tracking-[-0.4px] font-semibold">
|
||||
{title}
|
||||
</h2>
|
||||
);
|
||||
};
|
||||
@ -1,34 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
type Props = {
|
||||
icon: string;
|
||||
title: string;
|
||||
expand: boolean;
|
||||
onTitleClick: () => void;
|
||||
};
|
||||
|
||||
const TogglableHeader: React.FC<Props> = ({
|
||||
icon,
|
||||
title,
|
||||
expand,
|
||||
onTitleClick,
|
||||
}) => (
|
||||
<button className="flex items-center justify-between" onClick={onTitleClick}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Image src={icon} width={24} height={24} alt="" />
|
||||
<span className="text-sm leading-5 font-semibold text-gray-900">
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
<Image
|
||||
className={`${!expand ? "rotate-180" : "rotate-0"}`}
|
||||
src={"icons/unicorn_angle-up.svg"}
|
||||
width={24}
|
||||
height={24}
|
||||
alt=""
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
|
||||
export default React.memo(TogglableHeader);
|
||||
@ -1,102 +0,0 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { FieldValues, UseFormRegister } from "react-hook-form";
|
||||
|
||||
type Props = {
|
||||
register: UseFormRegister<FieldValues>;
|
||||
};
|
||||
|
||||
export const UploadFileImage: React.FC<Props> = ({ register }) => {
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
const [image, setImage] = useState<string | null>(null);
|
||||
const [checked, setChecked] = useState<boolean>(true);
|
||||
const [fileName, setFileName] = useState<string>("No selected file");
|
||||
|
||||
const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
const file = event.dataTransfer.files[0];
|
||||
if (!file || file.type.split("/")[0] !== "image") return;
|
||||
|
||||
setImage(URL.createObjectURL(file));
|
||||
setFileName(file.name);
|
||||
};
|
||||
|
||||
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
ref.current?.click();
|
||||
};
|
||||
|
||||
const onSelectedFile = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = event.target.files;
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const file = files[0];
|
||||
if (file.type.split("/")[0] !== "image") return;
|
||||
|
||||
setImage(URL.createObjectURL(file));
|
||||
setFileName(file.name);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
setImage(null);
|
||||
setFileName("No file selected");
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col gap-[10px] py-3`}
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
>
|
||||
{/* {image ? (
|
||||
<div className="relative group">
|
||||
<Image
|
||||
style={{ width: "100%", height: "107px", objectFit: "cover" }}
|
||||
src={image}
|
||||
width={246}
|
||||
height={104}
|
||||
alt={fileName}
|
||||
/>
|
||||
<div className="hidden justify-center items-center absolute top-0 left-0 w-full h-full group-hover:flex group-hover:bg-[rgba(255, 255, 255, 0.2)]">
|
||||
<button onClick={handleDelete}>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
) : ( */}
|
||||
<div
|
||||
onClick={handleClick}
|
||||
className="flex flex-col justify-center items-center py-5 px-2 gap-2 round-[2px] border border-dashed border-[#C8D0E0] rounded-sm"
|
||||
>
|
||||
{/* <Image src={"icons/ic_plus.svg"} width={14} height={14} alt="" />
|
||||
<span className="text-gray-700 font-normal text-sm">
|
||||
Drag an image here, or click to select
|
||||
</span> */}
|
||||
<input
|
||||
{...register("fileInput", { required: true })}
|
||||
// ref={ref}
|
||||
type="file"
|
||||
onChange={onSelectedFile}
|
||||
accept="image/*"
|
||||
/>
|
||||
</div>
|
||||
){/* } */}
|
||||
<div
|
||||
className="flex gap-2 items-center cursor-pointer"
|
||||
onClick={() => setChecked(!checked)}
|
||||
>
|
||||
<input
|
||||
checked={checked}
|
||||
className="rounded"
|
||||
type="checkbox"
|
||||
onChange={() => setChecked(!checked)}
|
||||
/>
|
||||
<span className="text-sm leading-5 text-[#111928] pointer-events-none">
|
||||
Crop center to fit output resolution
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,19 +1,38 @@
|
||||
import {
|
||||
addNewMessageAtom,
|
||||
chatMessages,
|
||||
currentConversationAtom,
|
||||
currentPromptAtom,
|
||||
currentStreamingMessageAtom,
|
||||
getActiveConvoIdAtom,
|
||||
showingTyping,
|
||||
updateMessageAtom,
|
||||
} from "@/_helpers/JotaiWrapper";
|
||||
|
||||
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
||||
import { selectAtom } from "jotai/utils";
|
||||
import { DataService, InfereceService } from "../../shared/coreService";
|
||||
import { RawMessage, toChatMessage } from "@/_models/ChatMessage";
|
||||
import {
|
||||
MessageSenderType,
|
||||
RawMessage,
|
||||
toChatMessage,
|
||||
} from "@/_models/ChatMessage";
|
||||
import { executeSerial } from "@/_services/pluginService";
|
||||
import { useCallback } from "react";
|
||||
|
||||
export default function useSendChatMessage() {
|
||||
const currentConvo = useAtomValue(currentConversationAtom);
|
||||
|
||||
const updateStreamMessage = useSetAtom(currentStreamingMessageAtom);
|
||||
const addNewMessage = useSetAtom(addNewMessageAtom);
|
||||
const updateMessage = useSetAtom(updateMessageAtom);
|
||||
const activeConversationId = useAtomValue(getActiveConvoIdAtom) ?? "";
|
||||
|
||||
const chatMessagesHistory = useAtomValue(
|
||||
selectAtom(
|
||||
chatMessages,
|
||||
useCallback((v) => v[activeConversationId], [activeConversationId])
|
||||
)
|
||||
);
|
||||
const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom);
|
||||
const [, setIsTyping] = useAtom(showingTyping);
|
||||
const sendChatMessage = async () => {
|
||||
@ -29,18 +48,97 @@ export default function useSendChatMessage() {
|
||||
const id = await executeSerial(DataService.CREATE_MESSAGE, newMessage);
|
||||
newMessage.id = id;
|
||||
|
||||
addNewMessage(await toChatMessage(newMessage));
|
||||
const resp = await executeSerial(InfereceService.PROMPT, prompt);
|
||||
const newChatMessage = await toChatMessage(newMessage);
|
||||
addNewMessage(newChatMessage);
|
||||
|
||||
const recentMessages = [
|
||||
...chatMessagesHistory.sort((a, b) => parseInt(a.id) - parseInt(b.id)),
|
||||
newChatMessage,
|
||||
]
|
||||
.slice(-10)
|
||||
.map((message) => {
|
||||
return {
|
||||
content: message.text,
|
||||
role:
|
||||
message.messageSenderType === MessageSenderType.User
|
||||
? "user"
|
||||
: "assistant",
|
||||
};
|
||||
});
|
||||
const url = await executeSerial(InfereceService.INFERENCE_URL);
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "text/event-stream",
|
||||
"Access-Control-Allow-Origi": "*",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
messages: recentMessages,
|
||||
stream: true,
|
||||
model: "gpt-3.5-turbo",
|
||||
max_tokens: 500,
|
||||
}),
|
||||
});
|
||||
const stream = response.body;
|
||||
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
const reader = stream?.getReader();
|
||||
let answer = "";
|
||||
|
||||
// Cache received response
|
||||
const newResponse: RawMessage = {
|
||||
conversation_id: parseInt(currentConvo?.id ?? "0") ?? 0,
|
||||
message: resp,
|
||||
message: answer,
|
||||
user: "assistant",
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
const respId = await executeSerial(DataService.CREATE_MESSAGE, newResponse);
|
||||
newResponse.id = respId;
|
||||
addNewMessage(await toChatMessage(newResponse));
|
||||
const responseChatMessage = await toChatMessage(newResponse);
|
||||
addNewMessage(responseChatMessage);
|
||||
|
||||
while (true && reader) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
console.log("SSE stream closed");
|
||||
break;
|
||||
}
|
||||
const text = decoder.decode(value);
|
||||
const lines = text.trim().split("\n");
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("data: ") && !line.includes("data: [DONE]")) {
|
||||
setIsTyping(false);
|
||||
const data = JSON.parse(line.replace("data: ", ""));
|
||||
answer += data.choices[0]?.delta?.content ?? "";
|
||||
if (answer.startsWith("assistant: ")) {
|
||||
answer = answer.replace("assistant: ", "");
|
||||
}
|
||||
updateStreamMessage({
|
||||
...responseChatMessage,
|
||||
text: answer,
|
||||
});
|
||||
updateMessage(
|
||||
responseChatMessage.id,
|
||||
responseChatMessage.conversationId,
|
||||
answer
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
updateMessage(
|
||||
responseChatMessage.id,
|
||||
responseChatMessage.conversationId,
|
||||
answer.trimEnd()
|
||||
);
|
||||
await executeSerial(DataService.UPDATE_MESSAGE, {
|
||||
...newResponse,
|
||||
message: answer.trimEnd(),
|
||||
updated_at: new Date()
|
||||
.toISOString()
|
||||
.replace("T", " ")
|
||||
.replace(/\.\d+Z$/, ""),
|
||||
});
|
||||
setIsTyping(false);
|
||||
};
|
||||
return {
|
||||
|
||||
@ -26,6 +26,7 @@ export interface ChatMessage {
|
||||
senderUid: string;
|
||||
senderName: string;
|
||||
senderAvatarUrl: string;
|
||||
htmlText?: string | undefined;
|
||||
text: string | undefined;
|
||||
imageUrls?: string[] | undefined;
|
||||
createdAt: number;
|
||||
@ -45,18 +46,13 @@ export const toChatMessage = async (m: RawMessage): Promise<ChatMessage> => {
|
||||
const createdAt = new Date(m.created_at ?? "").getTime();
|
||||
const imageUrls: string[] = [];
|
||||
const imageUrl = undefined;
|
||||
// m.message_medias.length > 0 ? m.message_medias[0].media_url : null;
|
||||
if (imageUrl) {
|
||||
imageUrls.push(imageUrl);
|
||||
}
|
||||
|
||||
const messageType = MessageType.Text;
|
||||
// m.message_type ? MessageType[m.message_type as keyof typeof MessageType] : MessageType.Text;
|
||||
const messageSenderType =
|
||||
m.user === "user" ? MessageSenderType.User : MessageSenderType.Ai;
|
||||
// m.message_sender_type
|
||||
// ? MessageSenderType[m.message_sender_type as keyof typeof MessageSenderType]
|
||||
// : MessageSenderType.Ai;
|
||||
|
||||
const content = m.message ?? "";
|
||||
const processedContent = await remark().use(html).process(content);
|
||||
@ -68,15 +64,13 @@ export const toChatMessage = async (m: RawMessage): Promise<ChatMessage> => {
|
||||
messageType: messageType,
|
||||
messageSenderType: messageSenderType,
|
||||
senderUid: m.user?.toString() || "0",
|
||||
senderName: m.user === "user" ? "You" : "LLaMA", // m.sender_name ?? "",
|
||||
senderName: m.user === "user" ? "You" : "Assistant",
|
||||
senderAvatarUrl:
|
||||
m.user === "user"
|
||||
? "icons/avatar.svg"
|
||||
: "https://huggingface.co/front/assets/huggingface_logo-noborder.svg", // m.sender_avatar_url ?? "icons/app_icon.svg",
|
||||
text: contentHtml,
|
||||
m.user === "user" ? "icons/avatar.svg" : "icons/app_icon.svg",
|
||||
text: content,
|
||||
htmlText: contentHtml,
|
||||
imageUrls: imageUrls,
|
||||
createdAt: createdAt,
|
||||
status: MessageStatus.Ready,
|
||||
// status: m.status as MessageStatus,
|
||||
};
|
||||
};
|
||||
|
||||
@ -14,7 +14,7 @@ export const isCorePluginInstalled = () => {
|
||||
if (!extensionPoints.get(DataService.GET_CONVERSATIONS)) {
|
||||
return false;
|
||||
}
|
||||
if (!extensionPoints.get(InfereceService.PROMPT)) {
|
||||
if (!extensionPoints.get(InfereceService.INIT_MODEL)) {
|
||||
return false;
|
||||
}
|
||||
if (!extensionPoints.get(ModelManagementService.GET_DOWNLOADED_MODELS)) {
|
||||
@ -33,7 +33,7 @@ export const setupBasePlugins = async () => {
|
||||
|
||||
if (
|
||||
!extensionPoints.get(DataService.GET_CONVERSATIONS) ||
|
||||
!extensionPoints.get(InfereceService.PROMPT) ||
|
||||
!extensionPoints.get(InfereceService.INIT_MODEL) ||
|
||||
!extensionPoints.get(ModelManagementService.GET_DOWNLOADED_MODELS)
|
||||
) {
|
||||
const installed = await plugins.install(basePlugins);
|
||||
|
||||
@ -11,6 +11,7 @@ export enum DataService {
|
||||
CREATE_CONVERSATION = "createConversation",
|
||||
DELETE_CONVERSATION = "deleteConversation",
|
||||
CREATE_MESSAGE = "createMessage",
|
||||
UPDATE_MESSAGE = "updateMessage",
|
||||
GET_CONVERSATION_MESSAGES = "getConversationMessages",
|
||||
|
||||
STORE_MODEL = "storeModel",
|
||||
@ -27,7 +28,7 @@ export enum ModelService {
|
||||
}
|
||||
|
||||
export enum InfereceService {
|
||||
PROMPT = "prompt",
|
||||
INFERENCE_URL = "inferenceUrl",
|
||||
INIT_MODEL = "initModel",
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user