This commit is contained in:
n4ze3m 2024-03-24 12:43:43 +05:30
parent 4055231bbc
commit 9a2adbd859
27 changed files with 725 additions and 3616 deletions

View File

@ -31,12 +31,14 @@
"axios": "^1.6.7", "axios": "^1.6.7",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"html-to-text": "^9.0.5", "html-to-text": "^9.0.5",
"i18next": "^23.10.1",
"i18next-browser-languagedetector": "^7.2.0",
"langchain": "^0.1.9", "langchain": "^0.1.9",
"lucide-react": "^0.350.0", "lucide-react": "^0.350.0",
"plasmo": "0.84.1",
"property-information": "^6.4.1", "property-information": "^6.4.1",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-i18next": "^14.1.0",
"react-markdown": "8.0.0", "react-markdown": "8.0.0",
"react-router-dom": "6.10.0", "react-router-dom": "6.10.0",
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,209 @@
{
"newChat": "New Chat",
"selectAModel": "Select a Model",
"selectAPrompt": "Select a Prompt",
"githubRepository": "GitHub Repository",
"settings": "Settings",
"sidebarTitle": "Chat History",
"error": "Error",
"somethingWentWrong": "Something went wrong",
"validationSelectModel": "Please select a model to continue",
"generalSettings": {
"title": "General Settings",
"heading": "Web UI Settings",
"settings": {
"speechRecognitionLang": {
"label": "Speech Recognition Language",
"placeholder": "Select a language"
},
"darkMode": {
"label": "Change Theme",
"options": {
"light": "Light",
"dark": "Dark"
}
},
"searchMode": {
"label": "Perform Simple Internet Search"
},
"deleteChatHistory": {
"label": "Delete Chat History",
"button": "Delete",
"confirm": "Are you sure you want to delete your chat history? This action cannot be undone."
}
}
},
"manageModels": {
"title": "Manage Models",
"addBtn": "Add New Model",
"columns": {
"name": "Name",
"digest": "Digest",
"modifiedAt": "Modified At",
"size": "Size",
"actions": "Actions"
},
"expandedColumns": {
"parentModel": "Parent Model",
"format": "Format",
"family": "Family",
"parameterSize": "Parameter Size",
"quantizationLevel": "Quantization Level"
},
"tooltip": {
"delete": "Delete Model",
"repull": "Re-Pull Model"
},
"confirm": {
"delete": "Are you sure you want to delete this model?",
"repull": "Are you sure you want to re-pull this model?"
},
"modal": {
"title": "Add New Model",
"placeholder": "Enter Model Name",
"pull": "Pull Model"
},
"notification": {
"pullModel": "Pulling Model",
"pullModelDescription": "Pulling {{modelName}} model. For more details, check the extension icon.",
"success": "Success",
"error": "Error",
"successDescription": "Successfully pulled the model",
"successDeleteDescription": "Successfully deleted the model",
"someError": "Something went wrong. Please try again later"
}
},
"managePrompts": {
"title": "Manage Prompts",
"addBtn": "Add New Prompt",
"columns": {
"title": "Title",
"prompt": "Prompt",
"type": "Prompt Type",
"actions": "Actions"
},
"systemPrompt": "System Prompt",
"quickPrompt": "Quick Prompt",
"tooltip": {
"delete": "Delete Prompt",
"edit": "Edit Prompt"
},
"confirm": {
"delete": "Are you sure you want to delete this prompt? This action cannot be undone."
},
"modal": {
"addTitle": "Add New Prompt",
"editTitle": "Edit Prompt"
},
"form": {
"title": {
"label": "Title",
"placeholder": "My Awesome Prompt",
"required": "Please enter a title"
},
"prompt": {
"label": "Prompt",
"placeholder": "Enter Prompt",
"required": "Please enter a prompt",
"help": "You can use {key} as variable in your prompt."
},
"isSystem": {
"label": "Is System Prompt"
},
"btnSave": {
"saving": "Adding Prompt...",
"save": "Add Prompt"
},
"btnEdit": {
"saving": "Updating Prompt...",
"save": "Update Prompt"
}
},
"notification": {
"addSuccess": "Prompt Added",
"addSuccessDesc": "Prompt has been added successfully",
"error": "Error",
"someError": "Something went wrong. Please try again later",
"updatedSuccess": "Prompt Updated",
"updatedSuccessDesc": "Prompt has been updated successfully",
"deletedSuccess": "Prompt Deleted",
"deletedSuccessDesc": "Prompt has been deleted successfully"
}
},
"manageShare": {
"title": "Manage Share",
"heading": "Configure Page Share URL",
"form": {
"url": {
"label": "Page Share URL",
"placeholder": "Enter Page Share URL",
"required": "Please input your Page Share URL!",
"help": "For privacy reasons, you can self-host the page share and provide the URL here. <anchor>Learn More</anchor>."
}
},
"webshare": {
"heading": "Web Share",
"columns": {
"title": "Title",
"url": "URL",
"actions": "Actions"
},
"tooltip": {
"delete": "Delete Share"
},
"confirm": {
"delete": "Are you sure you want to delete this share? This action cannot be undone."
}
},
"notification": {
"pageShareSuccess": "Page Share URL updated successfully",
"someError": "Something went wrong. Please try again later",
"webShareDeleteSuccess": "Web Share deleted successfully"
}
},
"ollamaSettings": {
"title": "Ollama Settings",
"heading": "Configure Ollama",
"settings": {
"ollamaUrl": {
"label": "Ollama URL",
"placeholder": "Enter Ollama URL"
},
"ragSettings": {
"label": "RAG Settings",
"model": {
"label": "Embedding Model",
"required": "Please select a model",
"help": "Highly recommended to use embedding models like `nomic-embed-text`.",
"placeholder": "Select a model"
},
"chunkSize": {
"label": "Chunk Size",
"placeholder": "Enter Chunk Size",
"required": "Please enter a chunk size"
},
"chunkOverlap": {
"label": "Chunk Overlap",
"placeholder": "Enter Chunk Overlap",
"required": "Please enter a chunk overlap"
}
},
"prompt": {
"label": "Configure RAG Prompt",
"option1": "Normal",
"option2": "Web",
"alert": "Configuring the system prompt here is deprecated. Please use the Manage Prompts section to add or edit prompts. This section will be removed in a future release",
"systemPrompt": "System Prompt",
"systemPromptPlaceholder": "Enter System Prompt",
"webSearchPrompt": "Web Search Prompt",
"webSearchPromptHelp": "Do not remove `{search_results}` from the prompt.",
"webSearchPromptError": "Please enter a web search prompt",
"webSearchPromptPlaceholder": "Enter Web Search Prompt",
"webSearchFollowUpPrompt": "Web Search Follow Up Prompt",
"webSearchFollowUpPromptHelp": "Do not remove `{chat_history}` and `{question}` from the prompt.",
"webSearchFollowUpPromptError": "Please input your Web Search Follow Up Prompt!",
"webSearchFollowUpPromptPlaceholder": "Your Web Search Follow Up Prompt"
}
}
}
}

View File

@ -17,6 +17,7 @@ import {
} from "lucide-react" } from "lucide-react"
import { getAllPrompts } from "~/libs/db" import { getAllPrompts } from "~/libs/db"
import { ShareBtn } from "~/components/Common/ShareBtn" import { ShareBtn } from "~/components/Common/ShareBtn"
import { useTranslation } from "react-i18next"
export default function OptionLayout({ export default function OptionLayout({
children children
@ -24,6 +25,8 @@ export default function OptionLayout({
children: React.ReactNode children: React.ReactNode
}) { }) {
const [sidebarOpen, setSidebarOpen] = useState(false) const [sidebarOpen, setSidebarOpen] = useState(false)
const { t } = useTranslation("option")
const { const {
selectedModel, selectedModel,
setSelectedModel, setSelectedModel,
@ -61,8 +64,8 @@ export default function OptionLayout({
if (prompt?.is_system) { if (prompt?.is_system) {
setSelectedSystemPrompt(prompt.id) setSelectedSystemPrompt(prompt.id)
} else { } else {
setSelectedQuickPrompt(prompt.content) setSelectedQuickPrompt(prompt!.content)
setSelectedSystemPrompt(null) setSelectedSystemPrompt("")
} }
} }
@ -93,7 +96,7 @@ export default function OptionLayout({
onClick={clearChat} onClick={clearChat}
className="inline-flex items-center rounded-lg border dark:border-gray-700 bg-transparent px-3 py-3 text-sm font-medium leading-4 text-gray-800 dark:text-white disabled:opacity-50 "> className="inline-flex items-center rounded-lg border dark:border-gray-700 bg-transparent px-3 py-3 text-sm font-medium leading-4 text-gray-800 dark:text-white disabled:opacity-50 ">
<SquarePen className="h-4 w-4 mr-3" /> <SquarePen className="h-4 w-4 mr-3" />
New Chat {t("newChat")}
</button> </button>
</div> </div>
<span className="text-lg font-thin text-zinc-300 dark:text-zinc-600"> <span className="text-lg font-thin text-zinc-300 dark:text-zinc-600">
@ -106,12 +109,13 @@ export default function OptionLayout({
size="large" size="large"
loading={isModelsLoading || isModelsFetching} loading={isModelsLoading || isModelsFetching}
filterOption={(input, option) => filterOption={(input, option) =>
option.label.toLowerCase().indexOf(input.toLowerCase()) >= option!.label.toLowerCase().indexOf(input.toLowerCase()) >=
0 || 0 ||
option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0 option!.value.toLowerCase().indexOf(input.toLowerCase()) >=
0
} }
showSearch showSearch
placeholder="Select a model" placeholder={t("selectAModel")}
className="w-64 " className="w-64 "
options={models?.map((model) => ({ options={models?.map((model) => ({
label: model.name, label: model.name,
@ -127,12 +131,13 @@ export default function OptionLayout({
size="large" size="large"
loading={isPromptLoading} loading={isPromptLoading}
showSearch showSearch
placeholder="Select a prompt" placeholder={t("selectAPrompt")}
className="w-60" className="w-60"
allowClear allowClear
onChange={handlePromptChange} onChange={handlePromptChange}
value={selectedSystemPrompt} value={selectedSystemPrompt}
filterOption={(input, option) => filterOption={(input, option) =>
//@ts-ignore
option.label.key option.label.key
.toLowerCase() .toLowerCase()
.indexOf(input.toLowerCase()) >= 0 .indexOf(input.toLowerCase()) >= 0
@ -161,7 +166,8 @@ export default function OptionLayout({
{pathname === "/" && messages.length > 0 && !streaming && ( {pathname === "/" && messages.length > 0 && !streaming && (
<ShareBtn messages={messages} /> <ShareBtn messages={messages} />
)} )}
<Tooltip title="Github Repository"> <Tooltip title={t("githubRepository")}
>
<a <a
href="https://github.com/n4ze3m/page-assist" href="https://github.com/n4ze3m/page-assist"
target="_blank" target="_blank"
@ -169,7 +175,8 @@ export default function OptionLayout({
<GithubIcon className="w-6 h-6" /> <GithubIcon className="w-6 h-6" />
</a> </a>
</Tooltip> </Tooltip>
<Tooltip title="Manage Ollama Models"> <Tooltip title={t("settings")}
>
<NavLink <NavLink
to="/settings" to="/settings"
className="!text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"> className="!text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
@ -185,14 +192,12 @@ export default function OptionLayout({
</div> </div>
<Drawer <Drawer
title={"Chat History"} title={t("sidebarTitle")}
placement="left" placement="left"
closeIcon={null} closeIcon={null}
onClose={() => setSidebarOpen(false)} onClose={() => setSidebarOpen(false)}
open={sidebarOpen}> open={sidebarOpen}>
<Sidebar <Sidebar onClose={() => setSidebarOpen(false)} />
onClose={() => setSidebarOpen(false)}
/>
</Drawer> </Drawer>
</div> </div>
) )

View File

@ -5,6 +5,7 @@ import {
Orbit, Orbit,
Share Share
} from "lucide-react" } from "lucide-react"
import { useTranslation } from "react-i18next"
import { Link, useLocation } from "react-router-dom" import { Link, useLocation } from "react-router-dom"
function classNames(...classes: string[]) { function classNames(...classes: string[]) {
@ -44,6 +45,8 @@ const LinkComponent = (item: {
export const SettingsLayout = ({ children }: { children: React.ReactNode }) => { export const SettingsLayout = ({ children }: { children: React.ReactNode }) => {
const location = useLocation() const location = useLocation()
const { t } = useTranslation("option")
return ( return (
<> <>
<div className="mx-auto max-w-7xl lg:flex lg:gap-x-16 lg:px-8"> <div className="mx-auto max-w-7xl lg:flex lg:gap-x-16 lg:px-8">
@ -54,31 +57,31 @@ export const SettingsLayout = ({ children }: { children: React.ReactNode }) => {
className="flex gap-x-3 gap-y-1 whitespace-nowrap lg:flex-col"> className="flex gap-x-3 gap-y-1 whitespace-nowrap lg:flex-col">
<LinkComponent <LinkComponent
href="/settings" href="/settings"
name="General Settings" name={t("generalSettings.title")}
icon={Orbit} icon={Orbit}
current={location.pathname} current={location.pathname}
/> />
<LinkComponent <LinkComponent
href="/settings/ollama" href="/settings/ollama"
name="Ollama Settings" name={t("ollamaSettings.title")}
icon={CircuitBoardIcon} icon={CircuitBoardIcon}
current={location.pathname} current={location.pathname}
/> />
<LinkComponent <LinkComponent
href="/settings/model" href="/settings/model"
name="Manage Model" name={t("manageModels.title")}
current={location.pathname} current={location.pathname}
icon={BrainCircuit} icon={BrainCircuit}
/> />
<LinkComponent <LinkComponent
href="/settings/prompt" href="/settings/prompt"
name="Manage Prompt" name={t("managePrompts.title")}
icon={Book} icon={Book}
current={location.pathname} current={location.pathname}
/> />
<LinkComponent <LinkComponent
href="/settings/share" href="/settings/share"
name="Manage Share" name={t("manageShare.title")}
icon={Share} icon={Share}
current={location.pathname} current={location.pathname}
/> />

View File

@ -7,12 +7,14 @@ import relativeTime from "dayjs/plugin/relativeTime"
import { useState } from "react" import { useState } from "react"
import { useForm } from "@mantine/form" import { useForm } from "@mantine/form"
import { Download, RotateCcw, Trash2 } from "lucide-react" import { Download, RotateCcw, Trash2 } from "lucide-react"
import { useTranslation } from "react-i18next"
dayjs.extend(relativeTime) dayjs.extend(relativeTime)
export const ModelsBody = () => { export const ModelsBody = () => {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const { t } = useTranslation("option")
const form = useForm({ const form = useForm({
initialValues: { initialValues: {
@ -32,22 +34,24 @@ export const ModelsBody = () => {
queryKey: ["fetchAllModels"] queryKey: ["fetchAllModels"]
}) })
notification.success({ notification.success({
message: "Model Deleted", message: t("manageModels.notification.success"),
description: "Model has been deleted successfully" description: t("manageModels.notification.successDeleteDescription")
}) })
}, },
onError: (error) => { onError: (error) => {
notification.error({ notification.error({
message: "Error", message: "Error",
description: error?.message || "Something went wrong" description: error?.message || t("manageModels.notification.someError")
}) })
} }
}) })
const pullModel = async (modelName: string) => { const pullModel = async (modelName: string) => {
notification.info({ notification.info({
message: "Pulling Model", message: t("manageModels.notification.pullModel"),
description: `Pulling ${modelName} model. For more details, check the extension icon.` description: t("manageModels.notification.pullModelDescription", {
modelName
})
}) })
setOpen(false) setOpen(false)
@ -76,7 +80,7 @@ export const ModelsBody = () => {
<button <button
onClick={() => setOpen(true)} onClick={() => setOpen(true)}
className="inline-flex items-center rounded-md border border-transparent bg-black px-2 py-2 text-md font-medium leading-4 text-white shadow-sm hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-white dark:text-gray-800 dark:hover:bg-gray-100 dark:focus:ring-gray-500 dark:focus:ring-offset-gray-100 disabled:opacity-50"> className="inline-flex items-center rounded-md border border-transparent bg-black px-2 py-2 text-md font-medium leading-4 text-white shadow-sm hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-white dark:text-gray-800 dark:hover:bg-gray-100 dark:focus:ring-gray-500 dark:focus:ring-offset-gray-100 disabled:opacity-50">
Add New Model {t("manageModels.addBtn")}
</button> </button>
</div> </div>
</div> </div>
@ -88,12 +92,12 @@ export const ModelsBody = () => {
<Table <Table
columns={[ columns={[
{ {
title: "Name", title: t("manageModels.columns.name"),
dataIndex: "name", dataIndex: "name",
key: "name" key: "name"
}, },
{ {
title: "Digest", title: t("manageModels.columns.digest"),
dataIndex: "digest", dataIndex: "digest",
key: "digest", key: "digest",
render: (text: string) => ( render: (text: string) => (
@ -105,28 +109,26 @@ export const ModelsBody = () => {
) )
}, },
{ {
title: "Modified", title: t("manageModels.columns.modifiedAt"),
dataIndex: "modified_at", dataIndex: "modified_at",
key: "modified_at", key: "modified_at",
render: (text: string) => dayjs(text).fromNow(true) render: (text: string) => dayjs(text).fromNow(true)
}, },
{ {
title: "Size", title: t("manageModels.columns.size"),
dataIndex: "size", dataIndex: "size",
key: "size", key: "size",
render: (text: number) => bytePerSecondFormatter(text) render: (text: number) => bytePerSecondFormatter(text)
}, },
{ {
title: "Action", title: t("manageModels.columns.actions"),
render: (_, record) => ( render: (_, record) => (
<div className="flex gap-4"> <div className="flex gap-4">
<Tooltip title="Delete Model"> <Tooltip title={t("manageModels.tooltip.delete")}>
<button <button
onClick={() => { onClick={() => {
if ( if (
window.confirm( window.confirm(t("manageModels.confirm.delete"))
"Are you sure you want to delete this model?"
)
) { ) {
deleteOllamaModel(record.model) deleteOllamaModel(record.model)
} }
@ -135,13 +137,11 @@ export const ModelsBody = () => {
<Trash2 className="w-5 h-5" /> <Trash2 className="w-5 h-5" />
</button> </button>
</Tooltip> </Tooltip>
<Tooltip title="Re-Pull Model"> <Tooltip title={t("manageModels.tooltip.repull")}>
<button <button
onClick={() => { onClick={() => {
if ( if (
window.confirm( window.confirm(t("manageModels.confirm.repull"))
"Are you sure you want to re-pull this model?"
)
) { ) {
pullOllamaModel(record.model) pullOllamaModel(record.model)
} }
@ -160,27 +160,29 @@ export const ModelsBody = () => {
pagination={false} pagination={false}
columns={[ columns={[
{ {
title: "Parent Model", title: t("manageModels.expandedColumns.parentModel"),
key: "parent_model", key: "parent_model",
dataIndex: "parent_model" dataIndex: "parent_model"
}, },
{ {
title: "Format", title: t("manageModels.expandedColumns.format"),
key: "format", key: "format",
dataIndex: "format" dataIndex: "format"
}, },
{ {
title: "Family", title: t("manageModels.expandedColumns.family"),
key: "family", key: "family",
dataIndex: "family" dataIndex: "family"
}, },
{ {
title: "Parameter Size", title: t("manageModels.expandedColumns.parameterSize"),
key: "parameter_size", key: "parameter_size",
dataIndex: "parameter_size" dataIndex: "parameter_size"
}, },
{ {
title: "Quantization Level", title: t(
"manageModels.expandedColumns.quantizationLevel"
),
key: "quantization_level", key: "quantization_level",
dataIndex: "quantization_level" dataIndex: "quantization_level"
} }
@ -200,13 +202,13 @@ export const ModelsBody = () => {
<Modal <Modal
footer={null} footer={null}
open={open} open={open}
title="Add New Model" title={t("manageModels.modal.title")}
onCancel={() => setOpen(false)}> onCancel={() => setOpen(false)}>
<form <form
onSubmit={form.onSubmit((values) => pullOllamaModel(values.model))}> onSubmit={form.onSubmit((values) => pullOllamaModel(values.model))}>
<Input <Input
{...form.getInputProps("model")} {...form.getInputProps("model")}
placeholder="Enter model name" placeholder={t("manageModels.modal.placeholder")}
size="large" size="large"
/> />
@ -214,7 +216,7 @@ export const ModelsBody = () => {
type="submit" type="submit"
className="inline-flex justify-center w-full text-center mt-4 items-center rounded-md border border-transparent bg-black px-2 py-2 text-sm font-medium leading-4 text-white shadow-sm hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-white dark:text-gray-800 dark:hover:bg-gray-100 dark:focus:ring-gray-500 dark:focus:ring-offset-gray-100 disabled:opacity-50 "> className="inline-flex justify-center w-full text-center mt-4 items-center rounded-md border border-transparent bg-black px-2 py-2 text-sm font-medium leading-4 text-white shadow-sm hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-white dark:text-gray-800 dark:hover:bg-gray-100 dark:focus:ring-gray-500 dark:focus:ring-offset-gray-100 disabled:opacity-50 ">
<Download className="w-5 h-5 mr-3" /> <Download className="w-5 h-5 mr-3" />
Pull Model {t("manageModels.modal.pull")}
</button> </button>
</form> </form>
</Modal> </Modal>

View File

@ -1,8 +1,10 @@
import { PencilIcon } from "lucide-react" import { PencilIcon } from "lucide-react"
import { useMessage } from "../../../hooks/useMessage" import { useMessage } from "../../../hooks/useMessage"
import { useTranslation } from 'react-i18next';
export const PlaygroundNewChat = () => { export const PlaygroundNewChat = () => {
const { setHistory, setMessages, setHistoryId } = useMessage() const { setHistory, setMessages, setHistoryId } = useMessage()
const { t } = useTranslation('optionChat')
const handleClick = () => { const handleClick = () => {
setHistoryId(null) setHistoryId(null)
@ -16,7 +18,7 @@ export const PlaygroundNewChat = () => {
className="flex w-full border bg-transparent hover:bg-gray-200 dark:hover:bg-gray-800 text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 focus:ring-offset-gray-100 rounded-md p-2 dark:border-gray-800"> className="flex w-full border bg-transparent hover:bg-gray-200 dark:hover:bg-gray-800 text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 focus:ring-offset-gray-100 rounded-md p-2 dark:border-gray-800">
<PencilIcon className="mx-3 h-5 w-5" aria-hidden="true" /> <PencilIcon className="mx-3 h-5 w-5" aria-hidden="true" />
<span className="inline-flex font-semibol text-white text-sm"> <span className="inline-flex font-semibol text-white text-sm">
New Chat {t('newChat')}
</span> </span>
</button> </button>
) )

View File

@ -11,6 +11,7 @@ import {
} from "antd" } from "antd"
import { Trash2, Pen, Computer, Zap } from "lucide-react" import { Trash2, Pen, Computer, Zap } from "lucide-react"
import { useState } from "react" import { useState } from "react"
import { useTranslation } from "react-i18next"
import { import {
deletePromptById, deletePromptById,
getAllPrompts, getAllPrompts,
@ -25,6 +26,7 @@ export const PromptBody = () => {
const [editId, setEditId] = useState("") const [editId, setEditId] = useState("")
const [createForm] = Form.useForm() const [createForm] = Form.useForm()
const [editForm] = Form.useForm() const [editForm] = Form.useForm()
const { t } = useTranslation("option")
const { data, status } = useQuery({ const { data, status } = useQuery({
queryKey: ["fetchAllPrompts"], queryKey: ["fetchAllPrompts"],
@ -38,14 +40,14 @@ export const PromptBody = () => {
queryKey: ["fetchAllPrompts"] queryKey: ["fetchAllPrompts"]
}) })
notification.success({ notification.success({
message: "Model Deleted", message: t("managePrompts.notification.deletedSuccess"),
description: "Model has been deleted successfully" description: t("managePrompts.notification.deletedSuccessDesc")
}) })
}, },
onError: (error) => { onError: (error) => {
notification.error({ notification.error({
message: "Error", message: t("managePrompts.notification.error"),
description: error?.message || "Something went wrong" description: error?.message || t("managePrompts.notification.someError")
}) })
} }
}) })
@ -60,14 +62,15 @@ export const PromptBody = () => {
setOpen(false) setOpen(false)
createForm.resetFields() createForm.resetFields()
notification.success({ notification.success({
message: "Prompt Added", message: t("managePrompts.notification.addSuccess"),
description: "Prompt has been added successfully" description: t("managePrompts.notification.addSuccessDesc")
}) })
}, },
onError: (error) => { onError: (error) => {
notification.error({ notification.error({
message: "Error", message: t("managePrompts.notification.error"),
description: error?.message || "Something went wrong" description:
error?.message || t("managePrompts.notification.someError")
}) })
} }
}) })
@ -87,14 +90,15 @@ export const PromptBody = () => {
setOpenEdit(false) setOpenEdit(false)
editForm.resetFields() editForm.resetFields()
notification.success({ notification.success({
message: "Prompt Updated", message: t("managePrompts.notification.updatedSuccess"),
description: "Prompt has been updated successfully" description: t("managePrompts.notification.updatedSuccessDesc")
}) })
}, },
onError: (error) => { onError: (error) => {
notification.error({ notification.error({
message: "Error", message: t("managePrompts.notification.error"),
description: error?.message || "Something went wrong" description:
error?.message || t("managePrompts.notification.someError")
}) })
} }
}) })
@ -108,7 +112,7 @@ export const PromptBody = () => {
<button <button
onClick={() => setOpen(true)} onClick={() => setOpen(true)}
className="inline-flex items-center rounded-md border border-transparent bg-black px-2 py-2 text-md font-medium leading-4 text-white shadow-sm hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-white dark:text-gray-800 dark:hover:bg-gray-100 dark:focus:ring-gray-500 dark:focus:ring-offset-gray-100 disabled:opacity-50"> className="inline-flex items-center rounded-md border border-transparent bg-black px-2 py-2 text-md font-medium leading-4 text-white shadow-sm hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-white dark:text-gray-800 dark:hover:bg-gray-100 dark:focus:ring-gray-500 dark:focus:ring-offset-gray-100 disabled:opacity-50">
Add New Prompt {t("managePrompts.addBtn")}
</button> </button>
</div> </div>
</div> </div>
@ -120,43 +124,41 @@ export const PromptBody = () => {
<Table <Table
columns={[ columns={[
{ {
title: "Title", title: t("managePrompts.columns.title"),
dataIndex: "title", dataIndex: "title",
key: "title" key: "title"
}, },
{ {
title: "Prompt", title: t("managePrompts.columns.prompt"),
dataIndex: "content", dataIndex: "content",
key: "content" key: "content"
}, },
{ {
title: "Prompt Type", title: t("managePrompts.columns.type"),
dataIndex: "is_system", dataIndex: "is_system",
key: "is_system", key: "is_system",
render: (is_system) => render: (is_system) =>
is_system ? ( is_system ? (
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<Computer className="w-5 h-5 " /> <Computer className="w-5 h-5 " />
System Prompt {t("managePrompts.systemPrompt")}
</span> </span>
) : ( ) : (
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<Zap className="w-5 h-5" /> <Zap className="w-5 h-5" />
Quick Prompt {t("managePrompts.quickPrompt")}
</span> </span>
) )
}, },
{ {
title: "Action", title: t("managePrompts.columns.actions"),
render: (_, record) => ( render: (_, record) => (
<div className="flex gap-4"> <div className="flex gap-4">
<Tooltip title="Delete Prompt"> <Tooltip title={t("managePrompts.tooltip.delete")}>
<button <button
onClick={() => { onClick={() => {
if ( if (
window.confirm( window.confirm(t("managePrompts.confirm.delete"))
"Are you sure you want to delete this prompt? This action cannot be undone."
)
) { ) {
deletePrompt(record.id) deletePrompt(record.id)
} }
@ -165,7 +167,7 @@ export const PromptBody = () => {
<Trash2 className="w-5 h-5" /> <Trash2 className="w-5 h-5" />
</button> </button>
</Tooltip> </Tooltip>
<Tooltip title="Edit Prompt"> <Tooltip title={t("managePrompts.tooltip.edit")}>
<button <button
onClick={() => { onClick={() => {
setEditId(record.id) setEditId(record.id)
@ -188,7 +190,7 @@ export const PromptBody = () => {
</div> </div>
<Modal <Modal
title="Add New Prompt" title={t("managePrompts.modal.addTitle")}
open={open} open={open}
onCancel={() => setOpen(false)} onCancel={() => setOpen(false)}
footer={null}> footer={null}>
@ -198,25 +200,35 @@ export const PromptBody = () => {
form={createForm}> form={createForm}>
<Form.Item <Form.Item
name="title" name="title"
label="Title" label={t("managePrompts.form.title.label")}
rules={[{ required: true, message: "Title is required" }]}> rules={[
<Input placeholder="My Awesome Prompt" /> {
required: true,
message: t("managePrompts.form.title.required")
}
]}>
<Input placeholder={t("managePrompts.form.title.placeholder")} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="content" name="content"
label="Prompt" label={t("managePrompts.form.prompt.label")}
rules={[{ required: true, message: "Prompt is required" }]} rules={[
help="You can use {key} as variable in your prompt."> {
required: true,
message: t("managePrompts.form.prompt.required")
}
]}
help={t("managePrompts.form.prompt.help")}>
<Input.TextArea <Input.TextArea
placeholder="Your prompt goes here..." placeholder={t("managePrompts.form.prompt.placeholder")}
autoSize={{ minRows: 3, maxRows: 10 }} autoSize={{ minRows: 3, maxRows: 10 }}
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="is_system" name="is_system"
label="Is System Prompt" label={t("managePrompts.form.isSystem.label")}
valuePropName="checked"> valuePropName="checked">
<Switch /> <Switch />
</Form.Item> </Form.Item>
@ -225,14 +237,16 @@ export const PromptBody = () => {
<button <button
disabled={savePromptLoading} disabled={savePromptLoading}
className="inline-flex justify-center w-full text-center mt-4 items-center rounded-md border border-transparent bg-black px-2 py-2 text-sm font-medium leading-4 text-white shadow-sm hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-white dark:text-gray-800 dark:hover:bg-gray-100 dark:focus:ring-gray-500 dark:focus:ring-offset-gray-100 disabled:opacity-50 "> className="inline-flex justify-center w-full text-center mt-4 items-center rounded-md border border-transparent bg-black px-2 py-2 text-sm font-medium leading-4 text-white shadow-sm hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-white dark:text-gray-800 dark:hover:bg-gray-100 dark:focus:ring-gray-500 dark:focus:ring-offset-gray-100 disabled:opacity-50 ">
{savePromptLoading ? "Adding Prompt..." : "Add Prompt"} {savePromptLoading
? t("managePrompts.form.btnSave.saving")
: t("managePrompts.form.btnSave.save")}
</button> </button>
</Form.Item> </Form.Item>
</Form> </Form>
</Modal> </Modal>
<Modal <Modal
title="Update Prompt" title={t("managePrompts.modal.editTitle")}
open={openEdit} open={openEdit}
onCancel={() => setOpenEdit(false)} onCancel={() => setOpenEdit(false)}
footer={null}> footer={null}>
@ -242,25 +256,35 @@ export const PromptBody = () => {
form={editForm}> form={editForm}>
<Form.Item <Form.Item
name="title" name="title"
label="Title" label={t("managePrompts.form.title.label")}
rules={[{ required: true, message: "Title is required" }]}> rules={[
<Input placeholder="My Awesome Prompt" /> {
required: true,
message: t("managePrompts.form.title.required")
}
]}>
<Input placeholder={t("managePrompts.form.title.placeholder")} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="content" name="content"
label="Prompt" label={t("managePrompts.form.prompt.label")}
rules={[{ required: true, message: "Prompt is required" }]} rules={[
help="You can use {key} as variable in your prompt."> {
required: true,
message: t("managePrompts.form.prompt.required")
}
]}
help={t("managePrompts.form.prompt.help")}>
<Input.TextArea <Input.TextArea
placeholder="Your prompt goes here..." placeholder={t("managePrompts.form.prompt.placeholder")}
autoSize={{ minRows: 3, maxRows: 10 }} autoSize={{ minRows: 3, maxRows: 10 }}
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="is_system" name="is_system"
label="Is System Prompt" label={t("managePrompts.form.isSystem.label")}
valuePropName="checked"> valuePropName="checked">
<Switch /> <Switch />
</Form.Item> </Form.Item>
@ -269,7 +293,9 @@ export const PromptBody = () => {
<button <button
disabled={isUpdatingPrompt} disabled={isUpdatingPrompt}
className="inline-flex justify-center w-full text-center mt-4 items-center rounded-md border border-transparent bg-black px-2 py-2 text-sm font-medium leading-4 text-white shadow-sm hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-white dark:text-gray-800 dark:hover:bg-gray-100 dark:focus:ring-gray-500 dark:focus:ring-offset-gray-100 disabled:opacity-50 "> className="inline-flex justify-center w-full text-center mt-4 items-center rounded-md border border-transparent bg-black px-2 py-2 text-sm font-medium leading-4 text-white shadow-sm hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-white dark:text-gray-800 dark:hover:bg-gray-100 dark:focus:ring-gray-500 dark:focus:ring-offset-gray-100 disabled:opacity-50 ">
{isUpdatingPrompt ? "Updating Prompt..." : "Update Prompt"} {isUpdatingPrompt
? t("managePrompts.form.btnEdit.saving")
: t("managePrompts.form.btnEdit.save")}
</button> </button>
</Form.Item> </Form.Item>
</Form> </Form>

View File

@ -12,9 +12,12 @@ import {
setOllamaURL as saveOllamaURL setOllamaURL as saveOllamaURL
} from "~/services/ollama" } from "~/services/ollama"
import { SettingPrompt } from "./prompt" import { SettingPrompt } from "./prompt"
import { useTranslation } from "react-i18next"
export const SettingsOllama = () => { export const SettingsOllama = () => {
const [ollamaURL, setOllamaURL] = useState<string>("") const [ollamaURL, setOllamaURL] = useState<string>("")
const { t } = useTranslation("option")
const { data: ollamaInfo, status } = useQuery({ const { data: ollamaInfo, status } = useQuery({
queryKey: ["fetchOllamURL"], queryKey: ["fetchOllamURL"],
queryFn: async () => { queryFn: async () => {
@ -54,7 +57,7 @@ export const SettingsOllama = () => {
<div> <div>
<div> <div>
<h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white"> <h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white">
Configure Ollama {t("ollamaSettings.heading")}
</h2> </h2>
<div className="border border-b border-gray-200 dark:border-gray-600 mt-3 mb-6"></div> <div className="border border-b border-gray-200 dark:border-gray-600 mt-3 mb-6"></div>
</div> </div>
@ -62,7 +65,7 @@ export const SettingsOllama = () => {
<label <label
htmlFor="ollamaURL" htmlFor="ollamaURL"
className="text-sm font-medium dark:text-gray-200"> className="text-sm font-medium dark:text-gray-200">
Ollama URL {t("ollamaSettings.settings.ollamaUrl.label")}
</label> </label>
<input <input
type="url" type="url"
@ -71,7 +74,7 @@ export const SettingsOllama = () => {
onChange={(e) => { onChange={(e) => {
setOllamaURL(e.target.value) setOllamaURL(e.target.value)
}} }}
placeholder="Your Ollama URL" placeholder={t("ollamaSettings.settings.ollamaUrl.placeholder")}
className="w-full p-2 border border-gray-300 rounded-md dark:bg-[#262626] dark:text-gray-100" className="w-full p-2 border border-gray-300 rounded-md dark:bg-[#262626] dark:text-gray-100"
/> />
</div> </div>
@ -88,7 +91,7 @@ export const SettingsOllama = () => {
<div> <div>
<div> <div>
<h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white"> <h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white">
Configure RAG {t("ollamaSettings.settings.ragSettings.label")}
</h2> </h2>
<div className="border border-b border-gray-200 dark:border-gray-600 mt-3 mb-6"></div> <div className="border border-b border-gray-200 dark:border-gray-600 mt-3 mb-6"></div>
</div> </div>
@ -108,18 +111,26 @@ export const SettingsOllama = () => {
}}> }}>
<Form.Item <Form.Item
name="defaultEM" name="defaultEM"
label="Embedding Model" label={t("ollamaSettings.settings.ragSettings.model.label")}
help="Highly recommended to use embedding models like `nomic-embed-text`." help={t("ollamaSettings.settings.ragSettings.model.help")}
rules={[{ required: true, message: "Please select a model!" }]}> rules={[
{
required: true,
message: t(
"ollamaSettings.settings.ragSettings.model.required"
)
}
]}>
<Select <Select
size="large" size="large"
filterOption={(input, option) => filterOption={(input, option) =>
option.label.toLowerCase().indexOf(input.toLowerCase()) >= option!.label.toLowerCase().indexOf(input.toLowerCase()) >=
0 || 0 ||
option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0 option!.value.toLowerCase().indexOf(input.toLowerCase()) >=
0
} }
showSearch showSearch
placeholder="Select a model" placeholder={t("ollamaSettings.settings.ragSettings.model.placeholder")}
style={{ width: "100%" }} style={{ width: "100%" }}
className="mt-4" className="mt-4"
options={ollamaInfo.models?.map((model) => ({ options={ollamaInfo.models?.map((model) => ({
@ -131,27 +142,28 @@ export const SettingsOllama = () => {
<Form.Item <Form.Item
name="chunkSize" name="chunkSize"
label="Chunk Size" label={t("ollamaSettings.settings.ragSettings.chunkSize.label")}
rules={[ rules={[
{ required: true, message: "Please input your chunk size!" } { required: true, message: t("ollamaSettings.settings.ragSettings.chunkSize.required")
]}>
<InputNumber
style={{ width: "100%" }}
placeholder="Chunk Size"
/>
</Form.Item>
<Form.Item
name="chunkOverlap"
label="Chunk Overlap"
rules={[
{
required: true,
message: "Please input your chunk overlap!"
} }
]}> ]}>
<InputNumber <InputNumber
style={{ width: "100%" }} style={{ width: "100%" }}
placeholder="Chunk Overlap" placeholder={t("ollamaSettings.settings.ragSettings.chunkSize.placeholder")}
/>
</Form.Item>
<Form.Item
name="chunkOverlap"
label={t("ollamaSettings.settings.ragSettings.chunkOverlap.label")}
rules={[
{
required: true,
message: t("ollamaSettings.settings.ragSettings.chunkOverlap.required")
}
]}>
<InputNumber
style={{ width: "100%" }}
placeholder={t("ollamaSettings.settings.ragSettings.chunkOverlap.placeholder")}
/> />
</Form.Item> </Form.Item>
@ -164,7 +176,7 @@ export const SettingsOllama = () => {
<div> <div>
<div> <div>
<h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white"> <h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white">
Configure RAG Prompt {t("ollamaSettings.settings.prompt.label")}
</h2> </h2>
<div className="border border-b border-gray-200 dark:border-gray-600 mt-3 mb-6"></div> <div className="border border-b border-gray-200 dark:border-gray-600 mt-3 mb-6"></div>
</div> </div>

View File

@ -6,6 +6,7 @@ import { Select } from "antd"
import { SUPPORTED_LANGUAGES } from "~/utils/supporetd-languages" import { SUPPORTED_LANGUAGES } from "~/utils/supporetd-languages"
import { MoonIcon, SunIcon } from "lucide-react" import { MoonIcon, SunIcon } from "lucide-react"
import { SearchModeSettings } from "./search-mode" import { SearchModeSettings } from "./search-mode"
import { useTranslation } from "react-i18next"
export const SettingOther = () => { export const SettingOther = () => {
const { clearChat, speechToTextLanguage, setSpeechToTextLanguage } = const { clearChat, speechToTextLanguage, setSpeechToTextLanguage } =
@ -14,29 +15,31 @@ export const SettingOther = () => {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const { mode, toggleDarkMode } = useDarkMode() const { mode, toggleDarkMode } = useDarkMode()
const { t } = useTranslation("option")
return ( return (
<dl className="flex flex-col space-y-6"> <dl className="flex flex-col space-y-6 text-sm">
<div> <div>
<h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white"> <h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white">
Web UI Settings {t("generalSettings.heading")}
</h2> </h2>
<div className="border border-b border-gray-200 dark:border-gray-600 mt-3"></div> <div className="border border-b border-gray-200 dark:border-gray-600 mt-3"></div>
</div> </div>
<div className="flex flex-row justify-between"> <div className="flex flex-row justify-between">
<span className="text-gray-500 dark:text-neutral-50"> <span className="text-gray-500 dark:text-neutral-50">
Speech Recognition Language {t("generalSettings.settings.speechRecognitionLang.label")}
</span> </span>
<Select <Select
placeholder="Select Language" placeholder={t("generalSettings.settings.speechRecognitionLang.placeholder")}
allowClear allowClear
showSearch showSearch
options={SUPPORTED_LANGUAGES} options={SUPPORTED_LANGUAGES}
value={speechToTextLanguage} value={speechToTextLanguage}
filterOption={(input, option) => filterOption={(input, option) =>
option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 || option!.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 ||
option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0 option!.value.toLowerCase().indexOf(input.toLowerCase()) >= 0
} }
onChange={(value) => { onChange={(value) => {
setSpeechToTextLanguage(value) setSpeechToTextLanguage(value)
@ -44,7 +47,9 @@ export const SettingOther = () => {
/> />
</div> </div>
<div className="flex flex-row justify-between"> <div className="flex flex-row justify-between">
<span className="text-gray-500 dark:text-neutral-50 ">Change Theme</span> <span className="text-gray-500 dark:text-neutral-50 ">
{t("generalSettings.settings.darkMode.label")}
</span>
<button <button
onClick={toggleDarkMode} onClick={toggleDarkMode}
@ -54,19 +59,19 @@ export const SettingOther = () => {
) : ( ) : (
<MoonIcon className="w-4 h-4 mr-2" /> <MoonIcon className="w-4 h-4 mr-2" />
)} )}
{mode === "dark" ? "Light" : "Dark"} {mode === "dark" ? t("generalSettings.settings.darkMode.options.light") : t("generalSettings.settings.darkMode.options.dark")}
</button> </button>
</div> </div>
<SearchModeSettings /> <SearchModeSettings />
<div className="flex flex-row justify-between"> <div className="flex flex-row justify-between">
<span className="text-gray-500 dark:text-neutral-50 "> <span className="text-gray-500 dark:text-neutral-50 ">
Delete Chat History {t("generalSettings.settings.deleteChatHistory.label")}
</span> </span>
<button <button
onClick={async () => { onClick={async () => {
const confirm = window.confirm( const confirm = window.confirm(
"Are you sure you want to delete your chat history? This action cannot be undone." t("generalSettings.settings.deleteChatHistory.confirm")
) )
if (confirm) { if (confirm) {
@ -79,7 +84,7 @@ export const SettingOther = () => {
} }
}} }}
className="bg-red-500 dark:bg-red-600 text-white dark:text-gray-200 px-4 py-2 rounded-md"> className="bg-red-500 dark:bg-red-600 text-white dark:text-gray-200 px-4 py-2 rounded-md">
Delete {t("generalSettings.settings.deleteChatHistory.button")}
</button> </button>
</div> </div>
</dl> </dl>

View File

@ -1,6 +1,7 @@
import { useQuery, useQueryClient } from "@tanstack/react-query" import { useQuery, useQueryClient } from "@tanstack/react-query"
import { Skeleton, Radio, Form, Alert } from "antd" import { Skeleton, Radio, Form, Alert } from "antd"
import React from "react" import React from "react"
import { useTranslation } from "react-i18next"
import { SaveButton } from "~/components/Common/SaveButton" import { SaveButton } from "~/components/Common/SaveButton"
import { import {
getWebSearchPrompt, getWebSearchPrompt,
@ -11,6 +12,8 @@ import {
} from "~/services/ollama" } from "~/services/ollama"
export const SettingPrompt = () => { export const SettingPrompt = () => {
const { t } = useTranslation("option")
const [selectedValue, setSelectedValue] = React.useState<"normal" | "web">( const [selectedValue, setSelectedValue] = React.useState<"normal" | "web">(
"web" "web"
) )
@ -45,8 +48,12 @@ export const SettingPrompt = () => {
<Radio.Group <Radio.Group
defaultValue={selectedValue} defaultValue={selectedValue}
onChange={(e) => setSelectedValue(e.target.value)}> onChange={(e) => setSelectedValue(e.target.value)}>
<Radio.Button value="normal">Normal</Radio.Button> <Radio.Button value="normal">
<Radio.Button value="web">Web</Radio.Button> {t("ollamaSettings.settings.prompt.option1")}
</Radio.Button>
<Radio.Button value="web">
{t("ollamaSettings.settings.prompt.option2")}
</Radio.Button>
</Radio.Group> </Radio.Group>
</div> </div>
@ -64,18 +71,22 @@ export const SettingPrompt = () => {
}}> }}>
<Form.Item> <Form.Item>
<Alert <Alert
message="Configuring the system prompt here is deprecated. Please use the Manage Prompts section to add or edit prompts. This section will be removed in a future release" message={t("ollamaSettings.settings.prompt.alert")}
type="warning" type="warning"
showIcon showIcon
closable closable
/> />
</Form.Item> </Form.Item>
<Form.Item label="System Prompt" name="prompt"> <Form.Item
label={t("ollamaSettings.settings.prompt.systemPrompt")}
name="prompt">
<textarea <textarea
value={data.prompt} value={data.prompt}
rows={5} rows={5}
id="ollamaPrompt" id="ollamaPrompt"
placeholder="Your System Prompt" placeholder={t(
"ollamaSettings.settings.prompt.systemPromptPlaceholder"
)}
className="w-full p-2 border border-gray-300 rounded-md dark:bg-[#262626] dark:text-gray-100" className="w-full p-2 border border-gray-300 rounded-md dark:bg-[#262626] dark:text-gray-100"
/> />
</Form.Item> </Form.Item>
@ -104,38 +115,42 @@ export const SettingPrompt = () => {
webSearchFollowUpPrompt: data.webSearchFollowUpPrompt webSearchFollowUpPrompt: data.webSearchFollowUpPrompt
}}> }}>
<Form.Item <Form.Item
label="Web Search Prompt" label={t("ollamaSettings.settings.prompt.webSearchPrompt")}
name="webSearchPrompt" name="webSearchPrompt"
help="Do not remove `{search_results}` from the prompt." help={t("ollamaSettings.settings.prompt.webSearchPromptHelp")}
rules={[ rules={[
{ {
required: true, required: true,
message: "Please input your Web Search Prompt!" message: t(
"ollamaSettings.settings.prompt.webSearchPromptError"
)
} }
]}> ]}>
<textarea <textarea
value={data.webSearchPrompt} value={data.webSearchPrompt}
rows={5} rows={5}
id="ollamaWebSearchPrompt" id="ollamaWebSearchPrompt"
placeholder="Your Web Search Prompt" placeholder={t(
"ollamaSettings.settings.prompt.webSearchPromptPlaceholder"
)}
className="w-full p-2 border border-gray-300 rounded-md dark:bg-[#262626] dark:text-gray-100" className="w-full p-2 border border-gray-300 rounded-md dark:bg-[#262626] dark:text-gray-100"
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label="Web Search Follow Up Prompt" label={t("ollamaSettings.settings.prompt.webSearchFollowUpPrompt")}
name="webSearchFollowUpPrompt" name="webSearchFollowUpPrompt"
help="Do not remove `{chat_history}` and `{question}` from the prompt." help={t("ollamaSettings.settings.prompt.webSearchFollowUpPromptHelp")}
rules={[ rules={[
{ {
required: true, required: true,
message: "Please input your Web Search Follow Up Prompt!" message: t("ollamaSettings.settings.prompt.webSearchFollowUpPromptError")
} }
]}> ]}>
<textarea <textarea
value={data.webSearchFollowUpPrompt} value={data.webSearchFollowUpPrompt}
rows={5} rows={5}
id="ollamaWebSearchFollowUpPrompt" id="ollamaWebSearchFollowUpPrompt"
placeholder="Your Web Search Follow Up Prompt" placeholder={t("ollamaSettings.settings.prompt.webSearchFollowUpPromptPlaceholder")}
className="w-full p-2 border border-gray-300 rounded-md dark:bg-[#262626] dark:text-gray-100" className="w-full p-2 border border-gray-300 rounded-md dark:bg-[#262626] dark:text-gray-100"
/> />
</Form.Item> </Form.Item>

View File

@ -1,11 +1,14 @@
import { useQuery, useQueryClient } from "@tanstack/react-query" import { useQuery, useQueryClient } from "@tanstack/react-query"
import { Skeleton, Switch } from "antd" import { Skeleton, Switch } from "antd"
import { useTranslation } from "react-i18next"
import { import {
getIsSimpleInternetSearch, getIsSimpleInternetSearch,
setIsSimpleInternetSearch setIsSimpleInternetSearch
} from "~/services/ollama" } from "~/services/ollama"
export const SearchModeSettings = () => { export const SearchModeSettings = () => {
const { t } = useTranslation("option")
const { data, status } = useQuery({ const { data, status } = useQuery({
queryKey: ["fetchIsSimpleInternetSearch"], queryKey: ["fetchIsSimpleInternetSearch"],
queryFn: () => getIsSimpleInternetSearch() queryFn: () => getIsSimpleInternetSearch()
@ -20,7 +23,7 @@ export const SearchModeSettings = () => {
return ( return (
<div className="flex flex-row justify-between"> <div className="flex flex-row justify-between">
<span className="text-gray-500 dark:text-neutral-50 "> <span className="text-gray-500 dark:text-neutral-50 ">
Perform Simple Internet Search {t("generalSettings.settings.searchMode.label")}
</span> </span>
<Switch <Switch

View File

@ -1,6 +1,7 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { Form, Input, Skeleton, Table, Tooltip, message } from "antd" import { Form, Input, Skeleton, Table, Tooltip, message } from "antd"
import { Trash2 } from "lucide-react" import { Trash2 } from "lucide-react"
import { Trans, useTranslation } from "react-i18next"
import { SaveButton } from "~/components/Common/SaveButton" import { SaveButton } from "~/components/Common/SaveButton"
import { deleteWebshare, getAllWebshares, getUserId } from "~/libs/db" import { deleteWebshare, getAllWebshares, getUserId } from "~/libs/db"
import { getPageShareUrl, setPageShareUrl } from "~/services/ollama" import { getPageShareUrl, setPageShareUrl } from "~/services/ollama"
@ -8,6 +9,8 @@ import { verifyPageShareURL } from "~/utils/verify-page-share"
export const OptionShareBody = () => { export const OptionShareBody = () => {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const { t } = useTranslation("option")
const { status, data } = useQuery({ const { status, data } = useQuery({
queryKey: ["fetchShareInfo"], queryKey: ["fetchShareInfo"],
queryFn: async () => { queryFn: async () => {
@ -58,10 +61,10 @@ export const OptionShareBody = () => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ["fetchShareInfo"] queryKey: ["fetchShareInfo"]
}) })
message.success("Page Share URL updated successfully") message.success(t("manageShare.notification.pageShareSuccess"))
}, },
onError: (error) => { onError: (error) => {
message.error(error?.message || "Failed to update Page Share URL") message.error(error?.message || t("manageShare.notification.someError"))
} }
}) })
@ -69,12 +72,16 @@ export const OptionShareBody = () => {
mutationFn: onDelete, mutationFn: onDelete,
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ["fetchShareInfo"] queryKey: ["fetchShareInfo"],
}) })
message.success("Webshare deleted successfully") message.success(
t("manageShare.notification.webShareDeleteSuccess")
)
}, },
onError: (error) => { onError: (error) => {
message.error(error?.message || "Failed to delete Webshare") message.error(
error?.message || t("manageShare.notification.someError")
)
} }
}) })
@ -86,7 +93,7 @@ export const OptionShareBody = () => {
<div> <div>
<div> <div>
<h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white"> <h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white">
Configure Page Share URL {t("manageShare.heading")}
</h2> </h2>
<div className="border border-b border-gray-200 dark:border-gray-600 mt-3 mb-6"></div> <div className="border border-b border-gray-200 dark:border-gray-600 mt-3 mb-6"></div>
</div> </div>
@ -99,25 +106,29 @@ export const OptionShareBody = () => {
<Form.Item <Form.Item
name="url" name="url"
help={ help={
<span> <Trans
For privacy reasons, you can self-host the page share and i18nKey="option:manageShare.form.url.help"
provide the URL here.{" "} components={{
anchor: (
<a <a
href="https://github.com/n4ze3m/page-assist/blob/main/page-share.md" href="https://github.com/n4ze3m/page-assist/blob/main/page-share.md"
target="__blank" target="__blank"
className="text-blue-600 dark:text-blue-400"> className="text-blue-600 dark:text-blue-400"></a>
Learn more )
</a> }}
</span> />
} }
rules={[ rules={[
{ {
required: true, required: true,
message: "Please input your Page Share URL!" message: t("manageShare.form.url.required")
} }
]} ]}
label="Page Share URL"> label={t("manageShare.form.url.label")}>
<Input placeholder="Page Share URL" size="large" /> <Input
placeholder={t("manageShare.form.url.placeholder")}
size="large"
/>
</Form.Item> </Form.Item>
<Form.Item> <Form.Item>
<div className="flex justify-end"> <div className="flex justify-end">
@ -129,7 +140,7 @@ export const OptionShareBody = () => {
<div> <div>
<div> <div>
<h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white"> <h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white">
Webshares {t("manageShare.webshare.heading")}
</h2> </h2>
<div className="border border-b border-gray-200 dark:border-gray-600 mt-3 mb-6"></div> <div className="border border-b border-gray-200 dark:border-gray-600 mt-3 mb-6"></div>
</div> </div>
@ -138,12 +149,12 @@ export const OptionShareBody = () => {
dataSource={data.shares} dataSource={data.shares}
columns={[ columns={[
{ {
title: "Title", title: t("manageShare.webshare.columns.title"),
dataIndex: "title", dataIndex: "title",
key: "title" key: "title"
}, },
{ {
title: "URL", title: t("manageShare.webshare.columns.url"),
dataIndex: "url", dataIndex: "url",
key: "url", key: "url",
render: (url: string) => ( render: (url: string) => (
@ -156,14 +167,14 @@ export const OptionShareBody = () => {
) )
}, },
{ {
title: "Actions", title: t("manageShare.webshare.columns.actions"),
render: (_, render) => ( render: (_, render) => (
<Tooltip title="Delete Share"> <Tooltip title={t("manageShare.webshare.tooltip.delete")}>
<button <button
onClick={() => { onClick={() => {
if ( if (
window.confirm( window.confirm(
"Are you sure you want to delete this webshare?" t("manageShare.webshare.confirm.delete")
) )
) { ) {
deleteMutation({ deleteMutation({

View File

@ -7,6 +7,8 @@ import { ConfigProvider, theme } from "antd"
import { StyleProvider } from "@ant-design/cssinjs" import { StyleProvider } from "@ant-design/cssinjs"
import { useDarkMode } from "~/hooks/useDarkmode" import { useDarkMode } from "~/hooks/useDarkmode"
import { OptionRouting } from "~/routes" import { OptionRouting } from "~/routes"
import "~/i18n"
function IndexOption() { function IndexOption() {
const { mode } = useDarkMode() const { mode } = useDarkMode()
return ( return (

View File

@ -1,7 +1,7 @@
<!doctype html> <!doctype html>
<html> <html>
<head> <head>
<title>Page Assist - Web UI</title> <title>Page Assist - A Web UI for Local AI Models</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="manifest.type" content="browser_action" /> <meta name="manifest.type" content="browser_action" />
<link href="~/assets/tailwind.css" rel="stylesheet" /> <link href="~/assets/tailwind.css" rel="stylesheet" />

View File

@ -7,6 +7,8 @@ const queryClient = new QueryClient()
import { ConfigProvider, theme } from "antd" import { ConfigProvider, theme } from "antd"
import { StyleProvider } from "@ant-design/cssinjs" import { StyleProvider } from "@ant-design/cssinjs"
import { useDarkMode } from "~/hooks/useDarkmode" import { useDarkMode } from "~/hooks/useDarkmode"
import "~/i18n"
function IndexSidepanel() { function IndexSidepanel() {
const { mode } = useDarkMode() const { mode } = useDarkMode()

View File

@ -1,7 +1,7 @@
<!doctype html> <!doctype html>
<html> <html>
<head> <head>
<title>Page Assist - Web UI</title> <title>Page Assist - A Web UI for Local AI Models</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="manifest.type" content="browser_action" /> <meta name="manifest.type" content="browser_action" />
<link href="~/assets/tailwind.css" rel="stylesheet" /> <link href="~/assets/tailwind.css" rel="stylesheet" />

View File

@ -7,12 +7,7 @@ import {
} from "~/services/ollama" } from "~/services/ollama"
import { type ChatHistory, type Message } from "~/store/option" import { type ChatHistory, type Message } from "~/store/option"
import { ChatOllama } from "@langchain/community/chat_models/ollama" import { ChatOllama } from "@langchain/community/chat_models/ollama"
import { import { HumanMessage, SystemMessage } from "@langchain/core/messages"
HumanMessage,
AIMessage,
type MessageContent,
SystemMessage
} from "@langchain/core/messages"
import { useStoreMessageOption } from "~/store/option" import { useStoreMessageOption } from "~/store/option"
import { import {
deleteChatForEdit, deleteChatForEdit,
@ -25,65 +20,8 @@ import {
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { notification } from "antd" import { notification } from "antd"
import { getSystemPromptForWeb } from "~/web/web" import { getSystemPromptForWeb } from "~/web/web"
import { generateHistory } from "@/utils/generate-history"
export type BotResponse = { import { useTranslation } from "react-i18next"
bot: {
text: string
sourceDocuments: any[]
}
history: ChatHistory
history_id: string
}
const generateHistory = (
messages: {
role: "user" | "assistant" | "system"
content: string
image?: string
}[]
) => {
let history = []
for (const message of messages) {
if (message.role === "user") {
let content: MessageContent = [
{
type: "text",
text: message.content
}
]
if (message.image) {
content = [
{
type: "image_url",
image_url: message.image
},
{
type: "text",
text: message.content
}
]
}
history.push(
new HumanMessage({
content: content
})
)
} else if (message.role === "assistant") {
history.push(
new AIMessage({
content: [
{
type: "text",
text: message.content
}
]
})
)
}
}
return history
}
export const useMessageOption = () => { export const useMessageOption = () => {
const { const {
@ -116,7 +54,7 @@ export const useMessageOption = () => {
setSelectedSystemPrompt setSelectedSystemPrompt
} = useStoreMessageOption() } = useStoreMessageOption()
// const { notification } = App.useApp() const { t } = useTranslation("option")
const navigate = useNavigate() const navigate = useNavigate()
const textareaRef = React.useRef<HTMLTextAreaElement>(null) const textareaRef = React.useRef<HTMLTextAreaElement>(null)
@ -150,7 +88,7 @@ export const useMessageOption = () => {
abortControllerRef.current = new AbortController() abortControllerRef.current = new AbortController()
const ollama = new ChatOllama({ const ollama = new ChatOllama({
model: selectedModel, model: selectedModel!,
baseUrl: cleanUrl(url) baseUrl: cleanUrl(url)
}) })
@ -204,7 +142,7 @@ export const useMessageOption = () => {
.replaceAll("{chat_history}", chat_history) .replaceAll("{chat_history}", chat_history)
.replaceAll("{question}", message) .replaceAll("{question}", message)
const questionOllama = new ChatOllama({ const questionOllama = new ChatOllama({
model: selectedModel, model: selectedModel!,
baseUrl: cleanUrl(url) baseUrl: cleanUrl(url)
}) })
const response = await questionOllama.invoke(promptForQuestion) const response = await questionOllama.invoke(promptForQuestion)
@ -308,11 +246,11 @@ export const useMessageOption = () => {
if (historyId) { if (historyId) {
if (!isRegenerate) { if (!isRegenerate) {
await saveMessage(historyId, selectedModel, "user", message, [image]) await saveMessage(historyId, selectedModel!, "user", message, [image])
} }
await saveMessage( await saveMessage(
historyId, historyId,
selectedModel, selectedModel!,
"assistant", "assistant",
newMessage[appendingIndex].message, newMessage[appendingIndex].message,
[], [],
@ -320,12 +258,12 @@ export const useMessageOption = () => {
) )
} else { } else {
const newHistoryId = await saveHistory(message) const newHistoryId = await saveHistory(message)
await saveMessage(newHistoryId.id, selectedModel, "user", message, [ await saveMessage(newHistoryId.id, selectedModel!, "user", message, [
image image
]) ])
await saveMessage( await saveMessage(
newHistoryId.id, newHistoryId.id,
selectedModel, selectedModel!,
"assistant", "assistant",
newMessage[appendingIndex].message, newMessage[appendingIndex].message,
[], [],
@ -337,6 +275,7 @@ export const useMessageOption = () => {
setIsProcessing(false) setIsProcessing(false)
setStreaming(false) setStreaming(false)
} catch (e) { } catch (e) {
//@ts-ignore
if (e?.name === "AbortError") { if (e?.name === "AbortError") {
newMessage[appendingIndex].message = newMessage[ newMessage[appendingIndex].message = newMessage[
appendingIndex appendingIndex
@ -356,22 +295,22 @@ export const useMessageOption = () => {
]) ])
if (historyId) { if (historyId) {
await saveMessage(historyId, selectedModel, "user", message, [image]) await saveMessage(historyId, selectedModel!, "user", message, [image])
await saveMessage( await saveMessage(
historyId, historyId,
selectedModel, selectedModel!,
"assistant", "assistant",
newMessage[appendingIndex].message, newMessage[appendingIndex].message,
[] []
) )
} else { } else {
const newHistoryId = await saveHistory(message) const newHistoryId = await saveHistory(message)
await saveMessage(newHistoryId.id, selectedModel, "user", message, [ await saveMessage(newHistoryId.id, selectedModel!, "user", message, [
image image
]) ])
await saveMessage( await saveMessage(
newHistoryId.id, newHistoryId.id,
selectedModel, selectedModel!,
"assistant", "assistant",
newMessage[appendingIndex].message, newMessage[appendingIndex].message,
[] []
@ -379,9 +318,10 @@ export const useMessageOption = () => {
setHistoryId(newHistoryId.id) setHistoryId(newHistoryId.id)
} }
} else { } else {
//@ts-ignore
notification.error({ notification.error({
message: "Error", message: t("error"),
description: e?.message || "Something went wrong" description: e?.message || t("somethingWentWrong")
}) })
} }
@ -405,7 +345,7 @@ export const useMessageOption = () => {
abortControllerRef.current = new AbortController() abortControllerRef.current = new AbortController()
const ollama = new ChatOllama({ const ollama = new ChatOllama({
model: selectedModel, model: selectedModel!,
baseUrl: cleanUrl(url) baseUrl: cleanUrl(url)
}) })
@ -620,8 +560,8 @@ export const useMessageOption = () => {
} }
} else { } else {
notification.error({ notification.error({
message: "Error", message: t("error"),
description: e?.message || "Something went wrong" description: e?.message || t("somethingWentWrong")
}) })
} }
@ -699,8 +639,8 @@ export const useMessageOption = () => {
const validateBeforeSubmit = () => { const validateBeforeSubmit = () => {
if (!selectedModel || selectedModel?.trim()?.length === 0) { if (!selectedModel || selectedModel?.trim()?.length === 0) {
notification.error({ notification.error({
message: "Error", message: t("error"),
description: "Please select a model to continue" description: t("validationSelectModel")
}) })
return false return false
} }

18
src/i18n/index.ts Normal file
View File

@ -0,0 +1,18 @@
import i18n from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import { initReactI18next } from "react-i18next";
import { en } from "./lang/en";
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
debug: true,
resources: {
en: en
},
fallbackLng: "en",
lng: localStorage.getItem("i18nextLng") || "en",
})
export default i18n;

6
src/i18n/lang/en.ts Normal file
View File

@ -0,0 +1,6 @@
import option from "@/assets/locale/en/option.json";
export const en = {
option
}

View File

@ -0,0 +1,8 @@
{
"extName": {
"message": "Page Assist - A Web UI for Local AI Models"
},
"extDescription": {
"message": "Use your locally running AI models to assist you in your web browsing."
}
}

View File

@ -62,6 +62,7 @@ export const isOllamaRunning = async () => {
} }
export const getAllModels = async ({ returnEmpty = false }: { returnEmpty?: boolean }) => { export const getAllModels = async ({ returnEmpty = false }: { returnEmpty?: boolean }) => {
try {
const baseUrl = await getOllamaURL() const baseUrl = await getOllamaURL()
const response = await fetch(`${cleanUrl(baseUrl)}/api/tags`) const response = await fetch(`${cleanUrl(baseUrl)}/api/tags`)
if (!response.ok) { if (!response.ok) {
@ -87,6 +88,10 @@ export const getAllModels = async ({ returnEmpty = false }: { returnEmpty?: bool
quantization_level: string quantization_level: string
} }
}[] }[]
} catch (e) {
console.error(e)
return []
}
} }
export const deleteModel = async (model: string) => { export const deleteModel = async (model: string) => {

10
src/types/index.ts Normal file
View File

@ -0,0 +1,10 @@
import { ChatHistory } from "@/store"
export type BotResponse = {
bot: {
text: string
sourceDocuments: any[]
}
history: ChatHistory
history_id: string
}

View File

@ -0,0 +1,55 @@
import {
HumanMessage,
AIMessage,
type MessageContent,
} from "@langchain/core/messages"
export const generateHistory = (
messages: {
role: "user" | "assistant" | "system"
content: string
image?: string
}[]
) => {
let history = []
for (const message of messages) {
if (message.role === "user") {
let content: MessageContent = [
{
type: "text",
text: message.content
}
]
if (message.image) {
content = [
{
type: "image_url",
image_url: message.image
},
{
type: "text",
text: message.content
}
]
}
history.push(
new HumanMessage({
content: content
})
)
} else if (message.role === "assistant") {
history.push(
new AIMessage({
content: [
{
type: "text",
text: message.content
}
]
})
)
}
}
return history
}

View File

@ -5,7 +5,8 @@
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"esModuleInterop": true, "esModuleInterop": true,
"jsx": "react-jsx" "jsx": "react-jsx",
"strict": false
}, },
"exclude": [ "exclude": [
"node_modules" "node_modules"

View File

@ -10,10 +10,10 @@ export default defineConfig({
srcDir: "src", srcDir: "src",
outDir: "build", outDir: "build",
manifest: { manifest: {
name: "Page Assist - A Web UI for Local AI Models",
version: "1.1.0", version: "1.1.0",
description: name: '__MSG_extName__',
"Use your locally running AI models to assist you in your web browsing.", description: '__MSG_extDescription__',
default_locale: 'en',
action: {}, action: {},
author: "n4ze3m", author: "n4ze3m",
host_permissions: ["http://*/*", "https://*/*"], host_permissions: ["http://*/*", "https://*/*"],

3450
yarn.lock

File diff suppressed because it is too large Load Diff