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",
"dayjs": "^1.11.10",
"html-to-text": "^9.0.5",
"i18next": "^23.10.1",
"i18next-browser-languagedetector": "^7.2.0",
"langchain": "^0.1.9",
"lucide-react": "^0.350.0",
"plasmo": "0.84.1",
"property-information": "^6.4.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-i18next": "^14.1.0",
"react-markdown": "8.0.0",
"react-router-dom": "6.10.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"
import { getAllPrompts } from "~/libs/db"
import { ShareBtn } from "~/components/Common/ShareBtn"
import { useTranslation } from "react-i18next"
export default function OptionLayout({
children
@ -24,6 +25,8 @@ export default function OptionLayout({
children: React.ReactNode
}) {
const [sidebarOpen, setSidebarOpen] = useState(false)
const { t } = useTranslation("option")
const {
selectedModel,
setSelectedModel,
@ -61,8 +64,8 @@ export default function OptionLayout({
if (prompt?.is_system) {
setSelectedSystemPrompt(prompt.id)
} else {
setSelectedQuickPrompt(prompt.content)
setSelectedSystemPrompt(null)
setSelectedQuickPrompt(prompt!.content)
setSelectedSystemPrompt("")
}
}
@ -93,7 +96,7 @@ export default function OptionLayout({
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 ">
<SquarePen className="h-4 w-4 mr-3" />
New Chat
{t("newChat")}
</button>
</div>
<span className="text-lg font-thin text-zinc-300 dark:text-zinc-600">
@ -106,12 +109,13 @@ export default function OptionLayout({
size="large"
loading={isModelsLoading || isModelsFetching}
filterOption={(input, option) =>
option.label.toLowerCase().indexOf(input.toLowerCase()) >=
option!.label.toLowerCase().indexOf(input.toLowerCase()) >=
0 ||
option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0
option!.value.toLowerCase().indexOf(input.toLowerCase()) >=
0
}
showSearch
placeholder="Select a model"
placeholder={t("selectAModel")}
className="w-64 "
options={models?.map((model) => ({
label: model.name,
@ -127,12 +131,13 @@ export default function OptionLayout({
size="large"
loading={isPromptLoading}
showSearch
placeholder="Select a prompt"
placeholder={t("selectAPrompt")}
className="w-60"
allowClear
onChange={handlePromptChange}
value={selectedSystemPrompt}
filterOption={(input, option) =>
//@ts-ignore
option.label.key
.toLowerCase()
.indexOf(input.toLowerCase()) >= 0
@ -161,7 +166,8 @@ export default function OptionLayout({
{pathname === "/" && messages.length > 0 && !streaming && (
<ShareBtn messages={messages} />
)}
<Tooltip title="Github Repository">
<Tooltip title={t("githubRepository")}
>
<a
href="https://github.com/n4ze3m/page-assist"
target="_blank"
@ -169,7 +175,8 @@ export default function OptionLayout({
<GithubIcon className="w-6 h-6" />
</a>
</Tooltip>
<Tooltip title="Manage Ollama Models">
<Tooltip title={t("settings")}
>
<NavLink
to="/settings"
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>
<Drawer
title={"Chat History"}
title={t("sidebarTitle")}
placement="left"
closeIcon={null}
onClose={() => setSidebarOpen(false)}
open={sidebarOpen}>
<Sidebar
onClose={() => setSidebarOpen(false)}
/>
<Sidebar onClose={() => setSidebarOpen(false)} />
</Drawer>
</div>
)

View File

@ -5,6 +5,7 @@ import {
Orbit,
Share
} from "lucide-react"
import { useTranslation } from "react-i18next"
import { Link, useLocation } from "react-router-dom"
function classNames(...classes: string[]) {
@ -44,6 +45,8 @@ const LinkComponent = (item: {
export const SettingsLayout = ({ children }: { children: React.ReactNode }) => {
const location = useLocation()
const { t } = useTranslation("option")
return (
<>
<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">
<LinkComponent
href="/settings"
name="General Settings"
name={t("generalSettings.title")}
icon={Orbit}
current={location.pathname}
/>
<LinkComponent
href="/settings/ollama"
name="Ollama Settings"
name={t("ollamaSettings.title")}
icon={CircuitBoardIcon}
current={location.pathname}
/>
<LinkComponent
href="/settings/model"
name="Manage Model"
name={t("manageModels.title")}
current={location.pathname}
icon={BrainCircuit}
/>
<LinkComponent
href="/settings/prompt"
name="Manage Prompt"
name={t("managePrompts.title")}
icon={Book}
current={location.pathname}
/>
<LinkComponent
<LinkComponent
href="/settings/share"
name="Manage Share"
name={t("manageShare.title")}
icon={Share}
current={location.pathname}
/>

View File

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

View File

@ -1,8 +1,10 @@
import { PencilIcon } from "lucide-react"
import { useMessage } from "../../../hooks/useMessage"
import { useTranslation } from 'react-i18next';
export const PlaygroundNewChat = () => {
const { setHistory, setMessages, setHistoryId } = useMessage()
const { t } = useTranslation('optionChat')
const handleClick = () => {
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">
<PencilIcon className="mx-3 h-5 w-5" aria-hidden="true" />
<span className="inline-flex font-semibol text-white text-sm">
New Chat
{t('newChat')}
</span>
</button>
)

View File

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

View File

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

View File

@ -6,6 +6,7 @@ import { Select } from "antd"
import { SUPPORTED_LANGUAGES } from "~/utils/supporetd-languages"
import { MoonIcon, SunIcon } from "lucide-react"
import { SearchModeSettings } from "./search-mode"
import { useTranslation } from "react-i18next"
export const SettingOther = () => {
const { clearChat, speechToTextLanguage, setSpeechToTextLanguage } =
@ -14,29 +15,31 @@ export const SettingOther = () => {
const queryClient = useQueryClient()
const { mode, toggleDarkMode } = useDarkMode()
const { t } = useTranslation("option")
return (
<dl className="flex flex-col space-y-6">
<dl className="flex flex-col space-y-6 text-sm">
<div>
<h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white">
Web UI Settings
{t("generalSettings.heading")}
</h2>
<div className="border border-b border-gray-200 dark:border-gray-600 mt-3"></div>
</div>
<div className="flex flex-row justify-between">
<span className="text-gray-500 dark:text-neutral-50">
Speech Recognition Language
<span className="text-gray-500 dark:text-neutral-50">
{t("generalSettings.settings.speechRecognitionLang.label")}
</span>
<Select
placeholder="Select Language"
placeholder={t("generalSettings.settings.speechRecognitionLang.placeholder")}
allowClear
showSearch
options={SUPPORTED_LANGUAGES}
value={speechToTextLanguage}
filterOption={(input, option) =>
option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 ||
option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0
option!.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 ||
option!.value.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
onChange={(value) => {
setSpeechToTextLanguage(value)
@ -44,7 +47,9 @@ export const SettingOther = () => {
/>
</div>
<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
onClick={toggleDarkMode}
@ -54,19 +59,19 @@ export const SettingOther = () => {
) : (
<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>
</div>
<SearchModeSettings />
<div className="flex flex-row justify-between">
<span className="text-gray-500 dark:text-neutral-50 ">
Delete Chat History
{t("generalSettings.settings.deleteChatHistory.label")}
</span>
<button
onClick={async () => {
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) {
@ -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">
Delete
{t("generalSettings.settings.deleteChatHistory.button")}
</button>
</div>
</dl>

View File

@ -1,6 +1,7 @@
import { useQuery, useQueryClient } from "@tanstack/react-query"
import { Skeleton, Radio, Form, Alert } from "antd"
import React from "react"
import { useTranslation } from "react-i18next"
import { SaveButton } from "~/components/Common/SaveButton"
import {
getWebSearchPrompt,
@ -11,6 +12,8 @@ import {
} from "~/services/ollama"
export const SettingPrompt = () => {
const { t } = useTranslation("option")
const [selectedValue, setSelectedValue] = React.useState<"normal" | "web">(
"web"
)
@ -45,8 +48,12 @@ export const SettingPrompt = () => {
<Radio.Group
defaultValue={selectedValue}
onChange={(e) => setSelectedValue(e.target.value)}>
<Radio.Button value="normal">Normal</Radio.Button>
<Radio.Button value="web">Web</Radio.Button>
<Radio.Button value="normal">
{t("ollamaSettings.settings.prompt.option1")}
</Radio.Button>
<Radio.Button value="web">
{t("ollamaSettings.settings.prompt.option2")}
</Radio.Button>
</Radio.Group>
</div>
@ -64,18 +71,22 @@ export const SettingPrompt = () => {
}}>
<Form.Item>
<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"
showIcon
closable
/>
</Form.Item>
<Form.Item label="System Prompt" name="prompt">
<Form.Item
label={t("ollamaSettings.settings.prompt.systemPrompt")}
name="prompt">
<textarea
value={data.prompt}
rows={5}
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"
/>
</Form.Item>
@ -104,38 +115,42 @@ export const SettingPrompt = () => {
webSearchFollowUpPrompt: data.webSearchFollowUpPrompt
}}>
<Form.Item
label="Web Search Prompt"
label={t("ollamaSettings.settings.prompt.webSearchPrompt")}
name="webSearchPrompt"
help="Do not remove `{search_results}` from the prompt."
help={t("ollamaSettings.settings.prompt.webSearchPromptHelp")}
rules={[
{
required: true,
message: "Please input your Web Search Prompt!"
message: t(
"ollamaSettings.settings.prompt.webSearchPromptError"
)
}
]}>
<textarea
value={data.webSearchPrompt}
rows={5}
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"
/>
</Form.Item>
<Form.Item
label="Web Search Follow Up Prompt"
label={t("ollamaSettings.settings.prompt.webSearchFollowUpPrompt")}
name="webSearchFollowUpPrompt"
help="Do not remove `{chat_history}` and `{question}` from the prompt."
help={t("ollamaSettings.settings.prompt.webSearchFollowUpPromptHelp")}
rules={[
{
required: true,
message: "Please input your Web Search Follow Up Prompt!"
message: t("ollamaSettings.settings.prompt.webSearchFollowUpPromptError")
}
]}>
<textarea
value={data.webSearchFollowUpPrompt}
rows={5}
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"
/>
</Form.Item>

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
<!doctype html>
<html>
<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="manifest.type" content="browser_action" />
<link href="~/assets/tailwind.css" rel="stylesheet" />

View File

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

View File

@ -1,7 +1,7 @@
<!doctype html>
<html>
<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="manifest.type" content="browser_action" />
<link href="~/assets/tailwind.css" rel="stylesheet" />

View File

@ -7,12 +7,7 @@ import {
} from "~/services/ollama"
import { type ChatHistory, type Message } from "~/store/option"
import { ChatOllama } from "@langchain/community/chat_models/ollama"
import {
HumanMessage,
AIMessage,
type MessageContent,
SystemMessage
} from "@langchain/core/messages"
import { HumanMessage, SystemMessage } from "@langchain/core/messages"
import { useStoreMessageOption } from "~/store/option"
import {
deleteChatForEdit,
@ -25,65 +20,8 @@ import {
import { useNavigate } from "react-router-dom"
import { notification } from "antd"
import { getSystemPromptForWeb } from "~/web/web"
export type BotResponse = {
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
}
import { generateHistory } from "@/utils/generate-history"
import { useTranslation } from "react-i18next"
export const useMessageOption = () => {
const {
@ -116,7 +54,7 @@ export const useMessageOption = () => {
setSelectedSystemPrompt
} = useStoreMessageOption()
// const { notification } = App.useApp()
const { t } = useTranslation("option")
const navigate = useNavigate()
const textareaRef = React.useRef<HTMLTextAreaElement>(null)
@ -150,7 +88,7 @@ export const useMessageOption = () => {
abortControllerRef.current = new AbortController()
const ollama = new ChatOllama({
model: selectedModel,
model: selectedModel!,
baseUrl: cleanUrl(url)
})
@ -204,7 +142,7 @@ export const useMessageOption = () => {
.replaceAll("{chat_history}", chat_history)
.replaceAll("{question}", message)
const questionOllama = new ChatOllama({
model: selectedModel,
model: selectedModel!,
baseUrl: cleanUrl(url)
})
const response = await questionOllama.invoke(promptForQuestion)
@ -308,11 +246,11 @@ export const useMessageOption = () => {
if (historyId) {
if (!isRegenerate) {
await saveMessage(historyId, selectedModel, "user", message, [image])
await saveMessage(historyId, selectedModel!, "user", message, [image])
}
await saveMessage(
historyId,
selectedModel,
selectedModel!,
"assistant",
newMessage[appendingIndex].message,
[],
@ -320,12 +258,12 @@ export const useMessageOption = () => {
)
} else {
const newHistoryId = await saveHistory(message)
await saveMessage(newHistoryId.id, selectedModel, "user", message, [
await saveMessage(newHistoryId.id, selectedModel!, "user", message, [
image
])
await saveMessage(
newHistoryId.id,
selectedModel,
selectedModel!,
"assistant",
newMessage[appendingIndex].message,
[],
@ -337,6 +275,7 @@ export const useMessageOption = () => {
setIsProcessing(false)
setStreaming(false)
} catch (e) {
//@ts-ignore
if (e?.name === "AbortError") {
newMessage[appendingIndex].message = newMessage[
appendingIndex
@ -356,22 +295,22 @@ export const useMessageOption = () => {
])
if (historyId) {
await saveMessage(historyId, selectedModel, "user", message, [image])
await saveMessage(historyId, selectedModel!, "user", message, [image])
await saveMessage(
historyId,
selectedModel,
selectedModel!,
"assistant",
newMessage[appendingIndex].message,
[]
)
} else {
const newHistoryId = await saveHistory(message)
await saveMessage(newHistoryId.id, selectedModel, "user", message, [
await saveMessage(newHistoryId.id, selectedModel!, "user", message, [
image
])
await saveMessage(
newHistoryId.id,
selectedModel,
selectedModel!,
"assistant",
newMessage[appendingIndex].message,
[]
@ -379,9 +318,10 @@ export const useMessageOption = () => {
setHistoryId(newHistoryId.id)
}
} else {
//@ts-ignore
notification.error({
message: "Error",
description: e?.message || "Something went wrong"
message: t("error"),
description: e?.message || t("somethingWentWrong")
})
}
@ -405,7 +345,7 @@ export const useMessageOption = () => {
abortControllerRef.current = new AbortController()
const ollama = new ChatOllama({
model: selectedModel,
model: selectedModel!,
baseUrl: cleanUrl(url)
})
@ -620,8 +560,8 @@ export const useMessageOption = () => {
}
} else {
notification.error({
message: "Error",
description: e?.message || "Something went wrong"
message: t("error"),
description: e?.message || t("somethingWentWrong")
})
}
@ -699,8 +639,8 @@ export const useMessageOption = () => {
const validateBeforeSubmit = () => {
if (!selectedModel || selectedModel?.trim()?.length === 0) {
notification.error({
message: "Error",
description: "Please select a model to continue"
message: t("error"),
description: t("validationSelectModel")
})
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,31 +62,36 @@ export const isOllamaRunning = async () => {
}
export const getAllModels = async ({ returnEmpty = false }: { returnEmpty?: boolean }) => {
const baseUrl = await getOllamaURL()
const response = await fetch(`${cleanUrl(baseUrl)}/api/tags`)
if (!response.ok) {
if (returnEmpty) {
return []
try {
const baseUrl = await getOllamaURL()
const response = await fetch(`${cleanUrl(baseUrl)}/api/tags`)
if (!response.ok) {
if (returnEmpty) {
return []
}
throw new Error(response.statusText)
}
throw new Error(response.statusText)
}
const json = await response.json()
const json = await response.json()
return json.models as {
name: string
model: string
modified_at: string
size: number
digest: string
details: {
parent_model: string
format: string
family: string
families: string[]
parameter_size: string
quantization_level: string
}
}[]
return json.models as {
name: string
model: string
modified_at: string
size: number
digest: string
details: {
parent_model: string
format: string
family: string
families: string[]
parameter_size: string
quantization_level: string
}
}[]
} catch (e) {
console.error(e)
return []
}
}
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,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"jsx": "react-jsx"
"jsx": "react-jsx",
"strict": false
},
"exclude": [
"node_modules"

View File

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

3450
yarn.lock

File diff suppressed because it is too large Load Diff