Spaces:
Paused
Paused
| import { usePubSub } from "create-pubsub/react"; | |
| import { settingsPubSub } from "../../../../modules/pubSub"; | |
| import { isWebGPUAvailable } from "../../../../modules/webGpu"; | |
| import { match, Pattern } from "ts-pattern"; | |
| import { | |
| NumberInput, | |
| Select, | |
| Slider, | |
| Stack, | |
| Switch, | |
| Textarea, | |
| Text, | |
| TextInput, | |
| Group, | |
| ComboboxData, | |
| Skeleton, | |
| } from "@mantine/core"; | |
| import { useForm } from "@mantine/form"; | |
| import { lazy, Suspense, useEffect, useState } from "react"; | |
| import { defaultSettings, inferenceTypes } from "../../../../modules/settings"; | |
| import { getOpenAiClient } from "../../../../modules/openai"; | |
| import { IconInfoCircle } from "@tabler/icons-react"; | |
| import { addLogEntry } from "../../../../modules/logEntries"; | |
| const WebLlmModelSelect = lazy( | |
| () => import("../../../AiResponse/WebLlmModelSelect"), | |
| ); | |
| const WllamaModelSelect = lazy( | |
| () => import("../../../AiResponse/WllamaModelSelect"), | |
| ); | |
| export default function AISettingsForm() { | |
| const [settings, setSettings] = usePubSub(settingsPubSub); | |
| const [openAiModels, setOpenAiModels] = useState<ComboboxData>([]); | |
| const form = useForm({ | |
| initialValues: settings, | |
| onValuesChange: setSettings, | |
| }); | |
| useEffect(() => { | |
| async function fetchOpenAiModels() { | |
| try { | |
| const openai = getOpenAiClient({ | |
| baseURL: settings.openAiApiBaseUrl, | |
| apiKey: settings.openAiApiKey, | |
| }); | |
| const response = await openai.models.list(); | |
| const models = response.data.map((model) => ({ | |
| label: model.id, | |
| value: model.id, | |
| })); | |
| setOpenAiModels(models); | |
| form.setFieldError("openAiApiModel", null); | |
| if (!form.values.openAiApiModel) { | |
| form.setFieldValue("openAiApiModel", models[0].value); | |
| } | |
| } catch (error) { | |
| const errorMessage = | |
| error instanceof Error ? error.message : String(error); | |
| addLogEntry(`Error fetching OpenAI models: ${errorMessage}`); | |
| setOpenAiModels([]); | |
| form.setFieldError("openAiApiModel", errorMessage); | |
| } | |
| } | |
| if (form.values.inferenceType === "openai") { | |
| fetchOpenAiModels(); | |
| } | |
| }, [ | |
| form.values.inferenceType, | |
| settings.openAiApiBaseUrl, | |
| settings.openAiApiKey, | |
| ]); | |
| const isUsingCustomInstructions = | |
| form.values.systemPrompt !== defaultSettings.systemPrompt; | |
| const handleRestoreDefaultInstructions = () => { | |
| form.setFieldValue("systemPrompt", defaultSettings.systemPrompt); | |
| }; | |
| return ( | |
| <Stack gap="md"> | |
| <Switch | |
| label="AI Response" | |
| {...form.getInputProps("enableAiResponse", { | |
| type: "checkbox", | |
| })} | |
| labelPosition="left" | |
| description="Enable or disable AI-generated responses to your queries. When disabled, you'll only see web search results." | |
| /> | |
| {form.values.enableAiResponse && ( | |
| <> | |
| <Stack gap="xs" mb="md"> | |
| <Text size="sm">Search results to consider</Text> | |
| <Text size="xs" c="dimmed"> | |
| Determines the number of search results to consider when | |
| generating AI responses. A higher value may enhance accuracy, but | |
| it will also increase response time. | |
| </Text> | |
| <Slider | |
| {...form.getInputProps("searchResultsToConsider")} | |
| min={0} | |
| max={6} | |
| marks={Array.from({ length: 7 }, (_, index) => ({ | |
| value: index, | |
| label: index.toString(), | |
| }))} | |
| /> | |
| </Stack> | |
| <Select | |
| {...form.getInputProps("inferenceType")} | |
| label="AI Processing Location" | |
| data={inferenceTypes} | |
| allowDeselect={false} | |
| /> | |
| {form.values.inferenceType === "openai" && ( | |
| <> | |
| <TextInput | |
| {...form.getInputProps("openAiApiBaseUrl")} | |
| label="API Base URL" | |
| placeholder="http://localhost:11434/v1" | |
| required | |
| /> | |
| <Group gap="xs"> | |
| <IconInfoCircle size={16} /> | |
| <Text size="xs" c="dimmed" flex={1}> | |
| You may need to add{" "} | |
| <em> | |
| {`${self.location.protocol}//${self.location.hostname}`} | |
| </em>{" "} | |
| to the list of allowed network origins in your API server | |
| settings. | |
| </Text> | |
| </Group> | |
| <TextInput | |
| {...form.getInputProps("openAiApiKey")} | |
| label="API Key" | |
| type="password" | |
| description="Optional, as local API servers usually do not require it." | |
| /> | |
| <Select | |
| {...form.getInputProps("openAiApiModel")} | |
| label="API Model" | |
| data={openAiModels} | |
| description="Optional, as some API servers don't provide a model list." | |
| allowDeselect={false} | |
| searchable | |
| /> | |
| </> | |
| )} | |
| {form.values.inferenceType === "browser" && ( | |
| <> | |
| {isWebGPUAvailable && ( | |
| <Switch | |
| label="WebGPU" | |
| {...form.getInputProps("enableWebGpu", { | |
| type: "checkbox", | |
| })} | |
| labelPosition="left" | |
| description="Enable or disable WebGPU usage. When disabled, the app will use the CPU instead." | |
| /> | |
| )} | |
| {match([isWebGPUAvailable, form.values.enableWebGpu]) | |
| .with([true, true], () => ( | |
| <Suspense fallback={<Skeleton height={50} />}> | |
| <WebLlmModelSelect | |
| value={form.values.webLlmModelId} | |
| onChange={(value) => | |
| form.setFieldValue("webLlmModelId", value) | |
| } | |
| /> | |
| </Suspense> | |
| )) | |
| .with([false, Pattern.any], [Pattern.any, false], () => ( | |
| <> | |
| <Suspense fallback={<Skeleton height={50} />}> | |
| <WllamaModelSelect | |
| value={form.values.wllamaModelId} | |
| onChange={(value) => | |
| form.setFieldValue("wllamaModelId", value) | |
| } | |
| /> | |
| </Suspense> | |
| <NumberInput | |
| label="CPU threads to use" | |
| description="Number of threads to use for the AI model. Lower values will use less CPU, but may take longer to respond. A too-high value may cause the app to hang." | |
| min={1} | |
| {...form.getInputProps("cpuThreads")} | |
| /> | |
| </> | |
| )) | |
| .otherwise(() => null)} | |
| </> | |
| )} | |
| <Textarea | |
| label="Instructions for AI" | |
| descriptionProps={{ component: "div" }} | |
| description={ | |
| <> | |
| <span> | |
| Customize instructions for the AI to tailor its responses. | |
| </span> | |
| <br /> | |
| <span>For example:</span> | |
| <ul> | |
| <li> | |
| Specify preferences | |
| <ul> | |
| <li> | |
| <em>"use simple language"</em> | |
| </li> | |
| <li> | |
| <em>"provide step-by-step explanations"</em> | |
| </li> | |
| </ul> | |
| </li> | |
| <li> | |
| Set a response style | |
| <ul> | |
| <li> | |
| <em>"answer in a friendly tone"</em> | |
| </li> | |
| <li> | |
| <em>"write your response in Spanish"</em> | |
| </li> | |
| </ul> | |
| </li> | |
| <li> | |
| Provide context about the audience | |
| <ul> | |
| <li> | |
| <em>"you're talking to a high school student"</em> | |
| </li> | |
| <li> | |
| <em> | |
| "consider that your audience is composed of | |
| professionals in the field of graphic design" | |
| </em> | |
| </li> | |
| </ul> | |
| </li> | |
| </ul> | |
| <span> | |
| The special tag <em>{`{{searchResults}}`}</em> will be | |
| replaced with the search results, while{" "} | |
| <em>{`{{dateTime}}`}</em> will be replaced with the current | |
| date and time. | |
| </span> | |
| {isUsingCustomInstructions && ( | |
| <> | |
| <br /> | |
| <br /> | |
| <span> | |
| Currently, you're using custom instructions. If you ever | |
| need to restore the default instructions, you can do so by | |
| clicking | |
| </span>{" "} | |
| <Text | |
| component="span" | |
| size="xs" | |
| c="blue" | |
| style={{ cursor: "pointer" }} | |
| onClick={handleRestoreDefaultInstructions} | |
| > | |
| here | |
| </Text> | |
| <span>.</span> | |
| </> | |
| )} | |
| </> | |
| } | |
| autosize | |
| maxRows={10} | |
| {...form.getInputProps("systemPrompt")} | |
| /> | |
| </> | |
| )} | |
| </Stack> | |
| ); | |
| } | |