feature: backend, and onesearch in frontend
This commit is contained in:
parent
697da861ea
commit
bfc8f08802
@ -1,18 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
env: { browser: true, es2020: true },
|
|
||||||
extends: [
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:@typescript-eslint/recommended',
|
|
||||||
'plugin:react-hooks/recommended',
|
|
||||||
],
|
|
||||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
plugins: ['react-refresh'],
|
|
||||||
rules: {
|
|
||||||
'react-refresh/only-export-components': [
|
|
||||||
'warn',
|
|
||||||
{ allowConstantExport: true },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
77
components/engineSelector.tsx
Normal file
77
components/engineSelector.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { Dropdown, DropdownTrigger, DropdownMenu, DropdownItem, Button } from "@nextui-org/react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { settingsAtom } from "lib/state/settings";
|
||||||
|
import { engineTranslation } from "lib/onesearch/translatedEngineList";
|
||||||
|
import { settingsType } from "global";
|
||||||
|
import { useAtomValue, useSetAtom } from "jotai";
|
||||||
|
|
||||||
|
export default function EngineSelector(
|
||||||
|
props: { className: string }
|
||||||
|
) {
|
||||||
|
const { t } = useTranslation("Search");
|
||||||
|
const settings: settingsType = useAtomValue(settingsAtom);
|
||||||
|
const items = settings.searchEngines;
|
||||||
|
const currentEngine: string = settings.currentSearchEngine;
|
||||||
|
const displayEngine = getName(currentEngine);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const [selectedKeys, setSelectedKeys] = useState(new Set([currentEngine]) as any);
|
||||||
|
const selectedValue = React.useMemo(() => Array.from(selectedKeys).join(", "), [selectedKeys]);
|
||||||
|
const setSettings = useSetAtom(settingsAtom);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function getName(engineKey: string) {
|
||||||
|
return engineTranslation.includes(engineKey) ? t(`engine.${engineKey}`) : engineKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function setEngine(engine: string) {
|
||||||
|
setSettings((oldSettings: settingsType) => {
|
||||||
|
return {
|
||||||
|
...oldSettings,
|
||||||
|
currentSearchEngine: engine
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (selectedValue !== currentEngine) {
|
||||||
|
setEngine(selectedValue);
|
||||||
|
}
|
||||||
|
}, [currentEngine, selectedValue, setSettings]);
|
||||||
|
|
||||||
|
const [isClient, setIsClient] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsClient(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={props.className}>
|
||||||
|
{
|
||||||
|
isClient &&
|
||||||
|
(
|
||||||
|
<Dropdown>
|
||||||
|
<DropdownTrigger>
|
||||||
|
<Button variant="bordered" className="capitalize">
|
||||||
|
{displayEngine}
|
||||||
|
</Button>
|
||||||
|
</DropdownTrigger>
|
||||||
|
<DropdownMenu
|
||||||
|
aria-label={t("engine-aria")}
|
||||||
|
variant="light"
|
||||||
|
disallowEmptySelection
|
||||||
|
selectionMode="single"
|
||||||
|
selectedKeys={selectedKeys}
|
||||||
|
onSelectionChange={setSelectedKeys}
|
||||||
|
>
|
||||||
|
{Object.keys(items).map((item) => (
|
||||||
|
<DropdownItem key={item} suppressHydrationWarning>
|
||||||
|
{getName(item)}
|
||||||
|
</DropdownItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenu>
|
||||||
|
</Dropdown>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
26
components/onesearch/handleEnter.ts
Normal file
26
components/onesearch/handleEnter.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { settingsType, suggestionItem } from "global";
|
||||||
|
import copyToClipboard from "lib/copy";
|
||||||
|
import { normalizeURL } from "lib/normalizeURL";
|
||||||
|
import search from "lib/search";
|
||||||
|
|
||||||
|
export default function (
|
||||||
|
index: number,
|
||||||
|
suggestion: suggestionItem[],
|
||||||
|
query: string,
|
||||||
|
settings: settingsType,
|
||||||
|
searchBoxRef: React.RefObject<HTMLInputElement>
|
||||||
|
) {
|
||||||
|
const selected = suggestion[index];
|
||||||
|
const engine = settings.searchEngines[settings.currentSearchEngine];
|
||||||
|
const newTab = settings.searchInNewTab;
|
||||||
|
let clipboard: any;
|
||||||
|
if (selected.type === "QUERY" || selected.type === "default") {
|
||||||
|
search(selected.suggestion, engine, newTab);
|
||||||
|
} else if (selected.type === "NAVIGATION" || selected.type === "default-link") {
|
||||||
|
window.open(normalizeURL(selected.suggestion));
|
||||||
|
} else if (selected.type === "text") {
|
||||||
|
console.log("????");
|
||||||
|
copyToClipboard(selected.suggestion);
|
||||||
|
searchBoxRef.current?.focus();
|
||||||
|
}
|
||||||
|
}
|
42
components/onesearch/handleNLUResult.ts
Normal file
42
components/onesearch/handleNLUResult.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { suggestionItem } from "global";
|
||||||
|
import { findClosestDateIndex } from "lib/weather/getCurrentWeather";
|
||||||
|
import { getLocationNative } from "lib/weather/getLocation";
|
||||||
|
import { getWeather } from "lib/weather/getWeather";
|
||||||
|
import { WMOCodeTable } from "lib/weather/wmocode";
|
||||||
|
|
||||||
|
type UpdateSuggestionFunction = (data: suggestionItem[]) => void;
|
||||||
|
|
||||||
|
export function handleNLUResult(result: any, updateSuggestion: UpdateSuggestionFunction){
|
||||||
|
if (result.intent == "weather.summary") {
|
||||||
|
getLocationNative((data: GeolocationCoordinates | GeolocationPositionError) => {
|
||||||
|
console.log(data);
|
||||||
|
if (data instanceof GeolocationCoordinates) {
|
||||||
|
getWeather(data.latitude, data.longitude).then((weather) => {
|
||||||
|
console.log(weather["hourly"]);
|
||||||
|
let hourIndex = findClosestDateIndex(
|
||||||
|
weather["hourly"]["time"],
|
||||||
|
weather["utc_offset_seconds"]
|
||||||
|
);
|
||||||
|
let temp = weather["hourly"]["apparent_temperature"][hourIndex];
|
||||||
|
let weatherCode = weather["hourly"]["weather_code"][hourIndex];
|
||||||
|
console.log(temp, weatherCode, hourIndex);
|
||||||
|
updateSuggestion([
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
suggestion: `Weather: ${temp}${weather["hourly_units"]["apparent_temperature"]}, ${WMOCodeTable[weatherCode]["day"].description}`,
|
||||||
|
relevance: 3000 * result.score
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (result.intent !== "None") {
|
||||||
|
updateSuggestion([
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
suggestion: result.intent,
|
||||||
|
relevance: 2200 * result.score
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
30
components/onesearch/link.tsx
Normal file
30
components/onesearch/link.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { normalizeURL } from "lib/normalizeURL";
|
||||||
|
|
||||||
|
export default function Link(props: { children: React.ReactNode; query: string; selected: boolean }) {
|
||||||
|
if (props.selected) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`w-full h-10 leading-10 bg-zinc-300 dark:bg-zinc-700
|
||||||
|
px-5 z-10 cursor-pointer duration-100`}
|
||||||
|
onClick={() => {
|
||||||
|
window.open(normalizeURL(props.query));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`w-full h-10 leading-10 bg-zinc-100 hover:bg-zinc-300
|
||||||
|
dark:bg-zinc-800 hover:dark:bg-zinc-700 px-5 z-10 cursor-pointer duration-100`}
|
||||||
|
onClick={() => {
|
||||||
|
window.open(normalizeURL(props.query));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
178
components/onesearch/onesearch.tsx
Normal file
178
components/onesearch/onesearch.tsx
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import SuggestionBox from "./suggestionBox";
|
||||||
|
import { queryAtom } from "lib/state/query";
|
||||||
|
import { suggestionItem, suggestionsResponse } from "global";
|
||||||
|
import getSearchEngineName from "lib/onesearch/getSearchEngineName";
|
||||||
|
import PlainSearch from "./plainSearch";
|
||||||
|
import { suggestionAtom } from "lib/state/suggestion";
|
||||||
|
import validLink from "lib/url/validLink";
|
||||||
|
import Link from "./link";
|
||||||
|
import { selectedSuggestionAtom } from "lib/state/suggestionSelection";
|
||||||
|
import { settingsAtom } from "lib/state/settings";
|
||||||
|
import PlainText from "./plainText";
|
||||||
|
import { sendError } from "lib/telemetering/sendError";
|
||||||
|
import { NLU } from "lib/nlp/load";
|
||||||
|
import { handleNLUResult } from "./handleNLUResult";
|
||||||
|
import { useAtom, useAtomValue } from "jotai";
|
||||||
|
import i18next from "i18next";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export default function OneSearch() {
|
||||||
|
const [suggestion, setFinalSuggetsion] = useAtom(suggestionAtom);
|
||||||
|
const [manager, setManager] = useState(null);
|
||||||
|
const lastRequestTimeRef = useRef(0);
|
||||||
|
const selected = useAtomValue(selectedSuggestionAtom);
|
||||||
|
const settings = useAtomValue(settingsAtom);
|
||||||
|
const devMode = true;
|
||||||
|
const query = useAtomValue(queryAtom);
|
||||||
|
const engineName = getSearchEngineName();
|
||||||
|
const engine = settings.currentSearchEngine;
|
||||||
|
const { t } = useTranslation("Search");
|
||||||
|
const lang = i18next.language;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const time = new Date().getTime().toString();
|
||||||
|
if (query.trim() === "" || query.length > 120) {
|
||||||
|
cleanSuggestion("QUERY", "NAVIGATION");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetch(`/api/suggestion?q=${query}&l=${lang}&t=${time}&engine=${engine}`)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data: suggestionsResponse) => {
|
||||||
|
try {
|
||||||
|
const suggestionToUpdate: suggestionItem[] = data.suggestions;
|
||||||
|
if (data.time > lastRequestTimeRef.current) {
|
||||||
|
cleanSuggestion("NAVIGATION", "QUERY");
|
||||||
|
lastRequestTimeRef.current = data.time;
|
||||||
|
updateSuggestion(suggestionToUpdate);
|
||||||
|
}
|
||||||
|
} catch (error: Error | unknown) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
sendError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
// Handle fetch error
|
||||||
|
sendError(error);
|
||||||
|
});
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
|
function updateSuggestion(data: suggestionItem[]) {
|
||||||
|
setFinalSuggetsion((cur: suggestionItem[]) => {
|
||||||
|
const types: string[] = [];
|
||||||
|
for (const sug of data) {
|
||||||
|
if (!types.includes(sug.type)) types.push(sug.type);
|
||||||
|
}
|
||||||
|
for (const type of types) {
|
||||||
|
cur = cur.filter((item) => {
|
||||||
|
return item.type !== type;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return cur.concat(data).sort((a, b) => {
|
||||||
|
return b.relevance - a.relevance;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanSuggestion(...types: string[]) {
|
||||||
|
setFinalSuggetsion((suggestion: suggestionItem[]) => {
|
||||||
|
return suggestion.filter((item) => {
|
||||||
|
return !types.includes(item.type);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const NLUModel = new NLU();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
NLUModel.init().then((nlu) => {
|
||||||
|
setManager(nlu.manager);
|
||||||
|
console.log(nlu.manager);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
cleanSuggestion("default-link", "default", "text");
|
||||||
|
if (validLink(query)) {
|
||||||
|
updateSuggestion([
|
||||||
|
{ type: "default-link", suggestion: query, relevance: 3000, prompt: <span>Go to: </span> },
|
||||||
|
{ type: "default", suggestion: query, relevance: 1600 }
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
updateSuggestion([
|
||||||
|
{
|
||||||
|
type: "default",
|
||||||
|
suggestion: query,
|
||||||
|
relevance: 2000
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manager != null) {
|
||||||
|
// @ts-ignore
|
||||||
|
manager.process(query).then((result) => {
|
||||||
|
console.log(result);
|
||||||
|
handleNLUResult(result, updateSuggestion);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [query, engineName]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SuggestionBox>
|
||||||
|
{suggestion.map((s, i) => {
|
||||||
|
if (s.suggestion.trim() === "") return;
|
||||||
|
if (s.type === "default") {
|
||||||
|
return (
|
||||||
|
<PlainSearch key={i} query={s.suggestion} selected={i == selected}>
|
||||||
|
{s.suggestion}
|
||||||
|
<span className="text-zinc-700 dark:text-zinc-400 text-sm">
|
||||||
|
{t("search-help-text", { engine: engineName })}
|
||||||
|
</span>
|
||||||
|
{devMode && (
|
||||||
|
<span className="absolute text-zinc-700 dark:text-zinc-400 text-sm leading-10 h-10 right-2">
|
||||||
|
{s.relevance}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</PlainSearch>
|
||||||
|
);
|
||||||
|
} else if (s.type === "QUERY") {
|
||||||
|
return (
|
||||||
|
<PlainSearch key={i} query={s.suggestion} selected={i == selected}>
|
||||||
|
{s.suggestion}
|
||||||
|
{devMode && (
|
||||||
|
<span className="absolute text-zinc-700 dark:text-zinc-400 text-sm leading-10 h-10 right-2">
|
||||||
|
{s.relevance}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</PlainSearch>
|
||||||
|
);
|
||||||
|
} else if (s.type === "NAVIGATION" || s.type === "default-link") {
|
||||||
|
return (
|
||||||
|
<Link key={i} query={s.suggestion} selected={i == selected}>
|
||||||
|
{s.prompt && <span className="text-zinc-700 dark:text-zinc-400">{s.prompt}</span>}
|
||||||
|
{s.suggestion}
|
||||||
|
{devMode && (
|
||||||
|
<span className="absolute text-zinc-700 dark:text-zinc-400 text-sm leading-10 h-10 right-2">
|
||||||
|
{s.relevance}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
} else if (s.type === "text") {
|
||||||
|
return (
|
||||||
|
<PlainText key={i} selected={i == selected}>
|
||||||
|
{s.prompt && <span className="text-zinc-700 dark:text-zinc-400">{s.prompt}</span>}
|
||||||
|
<p>{s.suggestion}</p>
|
||||||
|
{devMode && (
|
||||||
|
<span className="bottom-0 absolute text-zinc-700 dark:text-zinc-400 text-sm leading-10 h-10 right-2">
|
||||||
|
{s.relevance}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</PlainText>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</SuggestionBox>
|
||||||
|
);
|
||||||
|
}
|
35
components/onesearch/plainSearch.tsx
Normal file
35
components/onesearch/plainSearch.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { useAtomValue } from "jotai";
|
||||||
|
import search from "lib/search";
|
||||||
|
import { settingsAtom } from "lib/state/settings";
|
||||||
|
|
||||||
|
export default function PlainSearch(props: { children: React.ReactNode; query: string; selected: boolean }) {
|
||||||
|
const settings = useAtomValue(settingsAtom);
|
||||||
|
const engine = settings.searchEngines[settings.currentSearchEngine];
|
||||||
|
const newTab = settings.searchInNewTab;
|
||||||
|
if (props.selected) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`relative w-full h-10 leading-10 bg-zinc-300 dark:bg-zinc-700
|
||||||
|
px-5 z-10 cursor-pointer duration-100 truncate`}
|
||||||
|
onClick={() => {
|
||||||
|
search(props.query, engine, newTab);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`relative w-full h-10 leading-10 bg-zinc-100 hover:bg-zinc-300
|
||||||
|
dark:bg-zinc-800 hover:dark:bg-zinc-700 px-5 z-10 cursor-pointer duration-100 truncate`}
|
||||||
|
onClick={() => {
|
||||||
|
search(props.query, engine, newTab);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
21
components/onesearch/plainText.tsx
Normal file
21
components/onesearch/plainText.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
export default function PlainText(props: { children: React.ReactNode; selected: boolean }) {
|
||||||
|
if (props.selected) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`relative w-full h-auto leading-6 break-all py-[0.6rem] bg-zinc-300 dark:bg-zinc-700
|
||||||
|
px-5 z-10 cursor-pointer duration-100`}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`relative w-full h-auto leading-6 break-all py-[0.6rem] bg-zinc-100 hover:bg-zinc-300
|
||||||
|
dark:bg-zinc-800 hover:dark:bg-zinc-700 px-5 z-10 cursor-pointer duration-100`}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
6
components/onesearch/suggestion.tsx
Normal file
6
components/onesearch/suggestion.tsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default function Suggestion(props: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: `<p>${props.children}</p>` as string }} className={`relative w-full h-10 leading-10 bg-zinc-100 hover:bg-zinc-300 dark:bg-zinc-800 hover:dark:bg-zinc-700 px-5 z-10 cursor-pointer duration-100`}>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
9
components/onesearch/suggestionBox.tsx
Normal file
9
components/onesearch/suggestionBox.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export default function SuggestionBox(props: { children?: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className={`relative bg-zinc-100 dark:bg-zinc-800 w-11/12 sm:w-[700px] h-auto max-h-[calc(100vh-20rem)]
|
||||||
|
overflow-y-auto left-1/2 translate-x-[-50%] top-72 z-20 rounded-md overflow-hidden duration-250
|
||||||
|
${props.children ? "opacity-100" : "opacity-0"}`}>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -33,7 +33,7 @@ export default function Time(props: {
|
|||||||
<div
|
<div
|
||||||
className="absolute top-20 lg:top-44 short:top-0 translate-x-[-50%]
|
className="absolute top-20 lg:top-44 short:top-0 translate-x-[-50%]
|
||||||
left-1/2 w-11/12 sm:w-[700px] text:black
|
left-1/2 w-11/12 sm:w-[700px] text:black
|
||||||
dark:text-white text-3xl text-left text-shadow-lg z-10"
|
dark:text-white text-3xl text-left text-shadow-lg"
|
||||||
>
|
>
|
||||||
{formatTime()}{" "}
|
{formatTime()}{" "}
|
||||||
<span className="text-lg leading-9 relative">
|
<span className="text-lg leading-9 relative">
|
||||||
|
31
i18n/en.json
31
i18n/en.json
@ -1,5 +1,32 @@
|
|||||||
{
|
{
|
||||||
"search" : {
|
"Search": {
|
||||||
"placeholder" : "Search or type a URL"
|
"placeholder": "Search or type a URL",
|
||||||
|
"engine-aria": "Switch search engine",
|
||||||
|
"engine": {
|
||||||
|
"google": "Google",
|
||||||
|
"baidu": "Baidu",
|
||||||
|
"bing": "Bing",
|
||||||
|
"duckduckgo": "DuckDuckGo",
|
||||||
|
"yandex": "Yandex",
|
||||||
|
"yahoo": "Yahoo",
|
||||||
|
"ecosia": "Ecosia"
|
||||||
|
},
|
||||||
|
"search-help-text": "Search {engine}"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"title": "Page Not Found"
|
||||||
|
},
|
||||||
|
"About": {
|
||||||
|
"title": "SparkHome"
|
||||||
|
},
|
||||||
|
"tools": {
|
||||||
|
"base64": {
|
||||||
|
"title": "Base64 tools - LuminaraUtils",
|
||||||
|
"decode": "Decode",
|
||||||
|
"encode": "Encode",
|
||||||
|
"result": "Result: ",
|
||||||
|
"copy": "Copy",
|
||||||
|
"copied": "Copied"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
134
lib/nlp/data/en.json
Normal file
134
lib/nlp/data/en.json
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
{
|
||||||
|
"weather.summary": [
|
||||||
|
"how's the weather",
|
||||||
|
"What's going on with the weather?",
|
||||||
|
"Can you give me an update on the weather?",
|
||||||
|
"How's the forecast looking today?",
|
||||||
|
"Give me a summary of the current weather.",
|
||||||
|
"Can you tell me the current weather?",
|
||||||
|
"What is the weather situation at the moment?",
|
||||||
|
"Could you provide a quick weather update?",
|
||||||
|
"Is it raining or sunny outside?",
|
||||||
|
"What's the weather like right now?",
|
||||||
|
"Tell me the current weather conditions.",
|
||||||
|
"How about the weather today?",
|
||||||
|
"Is it a good day to be outside?",
|
||||||
|
"What should I expect in terms of weather today?",
|
||||||
|
"Is there any severe weather to be aware of?",
|
||||||
|
"Can you summarize today's weather forecast?",
|
||||||
|
"What's the weather looking like for the next few hours?",
|
||||||
|
"Is it going to stay this way all day?",
|
||||||
|
"Could you give me a brief overview of the weather?",
|
||||||
|
"What's the general weather situation in our area?",
|
||||||
|
"Is it cloudy or clear outside?",
|
||||||
|
"Any weather alerts I should know about?",
|
||||||
|
"How's the weather looking for outdoor activities?",
|
||||||
|
"What's the forecast saying for today's weather?",
|
||||||
|
"Is it going to be a warm day?",
|
||||||
|
"Are we expecting any storms today?",
|
||||||
|
"What's the weather condition outside my window?",
|
||||||
|
"Is it a typical day for this season in terms of weather?",
|
||||||
|
"how's the weather now?"
|
||||||
|
],
|
||||||
|
|
||||||
|
"weather.temp": [
|
||||||
|
"What's the temperature like right now?",
|
||||||
|
"Can you tell me the current temperature?",
|
||||||
|
"How hot is it outside?",
|
||||||
|
"What's the temperature supposed to be today?",
|
||||||
|
"What is the current temp outside?",
|
||||||
|
"Could you tell me the outdoor temperature?",
|
||||||
|
"Is it cold or warm outside?",
|
||||||
|
"What's the high temperature for today?",
|
||||||
|
"What's the low temperature expected tonight?",
|
||||||
|
"How does the temperature feel outside?",
|
||||||
|
"Is it going to get warmer or cooler today?",
|
||||||
|
"What's the temperature in the shade?",
|
||||||
|
"Can you provide the current temp in Celsius?",
|
||||||
|
"What's the temperature in Fahrenheit right now?",
|
||||||
|
"Is it too hot to be outside?",
|
||||||
|
"What's the temperature like in the morning?",
|
||||||
|
"How about the temperature in the evening?",
|
||||||
|
"Is it warm enough to go swimming?",
|
||||||
|
"What's the temperature in the city center?",
|
||||||
|
"Can you tell me the temp in the nearby area?",
|
||||||
|
"Is it below freezing outside?",
|
||||||
|
"What's the average temperature for today?",
|
||||||
|
"Is the temperature dropping or rising?",
|
||||||
|
"What should I wear considering the temperature?"
|
||||||
|
],
|
||||||
|
|
||||||
|
"base64.encode": [
|
||||||
|
"Please encode this data with base64: %s",
|
||||||
|
"I need to encode the following data in base64: %s",
|
||||||
|
"Could you encode this string using base64? %s",
|
||||||
|
"Convert this data to b64 encoding: %s",
|
||||||
|
"I want to encode this information with base64: %s",
|
||||||
|
"Help me encode this in base64: %s",
|
||||||
|
"Can you encode this data to base64 format? %s",
|
||||||
|
"b64 encode",
|
||||||
|
"base64 encode",
|
||||||
|
"encode base64 %s"
|
||||||
|
],
|
||||||
|
|
||||||
|
"base64.decode": [
|
||||||
|
"Please decode this base64 data: %s",
|
||||||
|
"I have a base64 encoded string that needs decoding: %s",
|
||||||
|
"Could you decode this base64 string for me? %s",
|
||||||
|
"Convert this base64 encoded data back to its original form: %s",
|
||||||
|
"I need to decode this base64 information: %s",
|
||||||
|
"Help me decode this base64 data: %s",
|
||||||
|
"Can you translate this base64 back to normal text? %s",
|
||||||
|
"b64 decode",
|
||||||
|
"base64 decode",
|
||||||
|
"decode base64 %s"
|
||||||
|
],
|
||||||
|
|
||||||
|
"url.encode": [
|
||||||
|
"Please encode this URL: %s",
|
||||||
|
"I need to encode this URL component: %s",
|
||||||
|
"Could you encode this part of the URL? %s",
|
||||||
|
"Convert this URL to its encoded form: %s",
|
||||||
|
"I want to encode this URL for safe transmission: %s",
|
||||||
|
"Help me encode this URL segment: %s",
|
||||||
|
"Can you encode this URL data? %s"
|
||||||
|
],
|
||||||
|
|
||||||
|
"url.decode": [
|
||||||
|
"Please decode this URL: %s",
|
||||||
|
"I have an encoded URL that needs decoding: %s",
|
||||||
|
"Could you decode this URL for me? %s",
|
||||||
|
"Convert this encoded URL back to its original form: %s",
|
||||||
|
"I need to decode this URL component: %s",
|
||||||
|
"Help me decode this URL segment: %s",
|
||||||
|
"Can you translate this encoded URL back to normal? %s"
|
||||||
|
],
|
||||||
|
|
||||||
|
"html.encode": [
|
||||||
|
"Please encode this HTML entity: %s",
|
||||||
|
"I need to encode this text to HTML entity: %s",
|
||||||
|
"Could you encode this as an HTML entity? %s",
|
||||||
|
"Convert this text to HTML entity encoding: %s",
|
||||||
|
"I want to encode this to prevent HTML interpretation: %s",
|
||||||
|
"Help me encode this into HTML entity: %s",
|
||||||
|
"Can you encode this for HTML usage? %s"
|
||||||
|
],
|
||||||
|
|
||||||
|
"html.decode": [
|
||||||
|
"Please decode this HTML entity: %s",
|
||||||
|
"I have an HTML entity that needs decoding: %s",
|
||||||
|
"Could you decode this HTML entity for me? %s",
|
||||||
|
"Convert this HTML entity back to its original text: %s",
|
||||||
|
"I need to decode this HTML entity to plain text: %s",
|
||||||
|
"Help me decode this HTML entity: %s",
|
||||||
|
"Can you translate this HTML entity back to normal text? %s"
|
||||||
|
],
|
||||||
|
|
||||||
|
"None": [
|
||||||
|
"free weather api",
|
||||||
|
"js get timezone",
|
||||||
|
"how",
|
||||||
|
"how's",
|
||||||
|
"how's the"
|
||||||
|
]
|
||||||
|
}
|
124
lib/nlp/data/zh.json
Normal file
124
lib/nlp/data/zh.json
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
{
|
||||||
|
"weather.summary": [
|
||||||
|
"天气如何",
|
||||||
|
"现在的天气",
|
||||||
|
"今天的天气预报",
|
||||||
|
"现在的天气状况",
|
||||||
|
"今天天气怎么样",
|
||||||
|
"目前是什么天气",
|
||||||
|
"今天的天气概述",
|
||||||
|
"当前天气状况如何",
|
||||||
|
"今天会下雨吗",
|
||||||
|
"今天会下雪吗",
|
||||||
|
"今天晴天吗",
|
||||||
|
"今天的天气状况如何",
|
||||||
|
"现在外面是什么天气",
|
||||||
|
"今天天气好么",
|
||||||
|
"今天适合外出吗",
|
||||||
|
"今天的天气适宜做什么",
|
||||||
|
"今天有没有雾霾",
|
||||||
|
"今天的空气质量如何",
|
||||||
|
"今天的紫外线指数是多少",
|
||||||
|
"今天有没有大风",
|
||||||
|
"今天会不会很冷",
|
||||||
|
"今天的天气会变化吗",
|
||||||
|
"今天晚上的天气如何",
|
||||||
|
"今天夜里会下雨吗",
|
||||||
|
"今天的天气对出行有影响吗",
|
||||||
|
"今天的天气对运动有影响吗",
|
||||||
|
"今天的天气对工作有影响吗",
|
||||||
|
"今天的天气对旅游有影响吗",
|
||||||
|
"今天的天气对健康有影响吗"
|
||||||
|
],
|
||||||
|
"weather.temp": [
|
||||||
|
"现在的温度",
|
||||||
|
"现在多少度",
|
||||||
|
"外面有多热",
|
||||||
|
"明天热不热?",
|
||||||
|
"现在的气温是多少",
|
||||||
|
"今天最高温度是多少",
|
||||||
|
"今天最低温度是多少",
|
||||||
|
"现在外面感觉冷吗",
|
||||||
|
"现在需要穿外套吗",
|
||||||
|
"现在适合穿短袖吗",
|
||||||
|
"现在的温度适合外出吗",
|
||||||
|
"现在的温度适合运动吗",
|
||||||
|
"现在的温度适合睡觉吗",
|
||||||
|
"明天会比今天热吗",
|
||||||
|
"明天会比今天冷吗",
|
||||||
|
"今天的温度变化大吗",
|
||||||
|
"现在的温度适合开空调吗",
|
||||||
|
"现在的温度适合开暖气吗",
|
||||||
|
"室外的温度是多少",
|
||||||
|
"室内的温度是多少",
|
||||||
|
"现在的温度适合种植吗",
|
||||||
|
"现在的温度适合养宠物吗",
|
||||||
|
"现在的温度对健康有影响吗",
|
||||||
|
"现在的温度是否舒适",
|
||||||
|
"现在的温度是否适合工作"
|
||||||
|
],
|
||||||
|
"base64.encode": [
|
||||||
|
"请将数据使用base64编码:%s",
|
||||||
|
"需要将以下数据base64编码:%s",
|
||||||
|
"请将此字符串转为base64:%s",
|
||||||
|
"将数据转为base64编码:%s",
|
||||||
|
"信息base64编码:%s",
|
||||||
|
"请帮忙编码base64:%s",
|
||||||
|
"将数据编码为base64:%s"
|
||||||
|
],
|
||||||
|
|
||||||
|
"base64.decode": [
|
||||||
|
"请解码这个base64数据:%s",
|
||||||
|
"有base64编码字符串需要解码:%s",
|
||||||
|
"帮忙解码base64:%s",
|
||||||
|
"将base64编码转回原数据:%s",
|
||||||
|
"解码base64信息:%s",
|
||||||
|
"解码这个base64:%s",
|
||||||
|
"将base64转文本:%s"
|
||||||
|
],
|
||||||
|
|
||||||
|
"url.encode": [
|
||||||
|
"请编码这个URL:%s",
|
||||||
|
"URL部分需要编码:%s",
|
||||||
|
"请将URL部分编码:%s",
|
||||||
|
"URL编码转换:%s",
|
||||||
|
"安全传输需编码URL:%s",
|
||||||
|
"编码URL段:%s",
|
||||||
|
"URL数据编码:%s"
|
||||||
|
],
|
||||||
|
|
||||||
|
"url.decode": [
|
||||||
|
"请解码这个URL:%s",
|
||||||
|
"有URL编码需要解码:%s",
|
||||||
|
"解码这个URL:%s",
|
||||||
|
"URL编码转回原URL:%s",
|
||||||
|
"解码URL部分:%s",
|
||||||
|
"解码URL段:%s",
|
||||||
|
"URL编码转文本:%s"
|
||||||
|
],
|
||||||
|
|
||||||
|
"html.encode": [
|
||||||
|
"请编码HTML实体:%s",
|
||||||
|
"文本转为HTML实体:%s",
|
||||||
|
"编码为HTML实体:%s",
|
||||||
|
"文本HTML实体编码:%s",
|
||||||
|
"预防HTML解析编码:%s",
|
||||||
|
"HTML实体编码:%s",
|
||||||
|
"文本HTML使用编码:%s"
|
||||||
|
],
|
||||||
|
|
||||||
|
"html.decode": [
|
||||||
|
"请解码HTML实体:%s",
|
||||||
|
"HTML实体需要解码:%s",
|
||||||
|
"解码HTML实体:%s",
|
||||||
|
"HTML实体转回文本:%s",
|
||||||
|
"HTML实体解码:%s",
|
||||||
|
"解码HTML实体:%s",
|
||||||
|
"HTML实体转文本:%s"
|
||||||
|
],
|
||||||
|
|
||||||
|
"None": [
|
||||||
|
"你好",
|
||||||
|
"为什么计算机使用二进制"
|
||||||
|
]
|
||||||
|
}
|
55
lib/nlp/load.ts
Normal file
55
lib/nlp/load.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
// @ts-ignore
|
||||||
|
import { containerBootstrap } from "@nlpjs/core";
|
||||||
|
// @ts-ignore
|
||||||
|
import { Nlp } from "@nlpjs/nlp";
|
||||||
|
// @ts-ignore
|
||||||
|
import { NluManager, NluNeural } from "@nlpjs/nlu";
|
||||||
|
// @ts-ignore
|
||||||
|
import { LangEn } from "@nlpjs/lang-en-min";
|
||||||
|
// @ts-ignore
|
||||||
|
import { LangZh } from "@nlpjs/lang-zh";
|
||||||
|
import * as fflate from 'fflate';
|
||||||
|
|
||||||
|
let zh: TrainData = {};
|
||||||
|
let en: TrainData = {};
|
||||||
|
|
||||||
|
type TrainData = {
|
||||||
|
[key: string]: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export class NLU {
|
||||||
|
manager: any;
|
||||||
|
inited: boolean = false;
|
||||||
|
async loadIntentionModel() {
|
||||||
|
const container = await containerBootstrap();
|
||||||
|
container.use(Nlp);
|
||||||
|
container.use(LangEn);
|
||||||
|
container.use(LangZh);
|
||||||
|
container.use(NluNeural);
|
||||||
|
const manager = new NluManager({
|
||||||
|
container,
|
||||||
|
locales: ["en", "zh"],
|
||||||
|
nlu: {
|
||||||
|
useNoneFeature: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const response = await fetch("/model");
|
||||||
|
|
||||||
|
const responseBuf = await response.arrayBuffer();
|
||||||
|
const compressed = new Uint8Array(responseBuf);
|
||||||
|
const decompressed = fflate.decompressSync(compressed);
|
||||||
|
const modelText = fflate.strFromU8(decompressed);
|
||||||
|
manager.fromJSON(JSON.parse(modelText));
|
||||||
|
this.manager = manager;
|
||||||
|
// console.log(this.manager);
|
||||||
|
}
|
||||||
|
async init() {
|
||||||
|
await this.loadIntentionModel();
|
||||||
|
this.inited = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
async process(lang: string, text: string): Promise<any> {
|
||||||
|
const actual = await this.manager.process(lang, text);
|
||||||
|
return actual;
|
||||||
|
}
|
||||||
|
}
|
76
lib/nlp/train.ts
Normal file
76
lib/nlp/train.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
// @ts-ignore
|
||||||
|
import { containerBootstrap } from "@nlpjs/core";
|
||||||
|
// @ts-ignore
|
||||||
|
import { Nlp } from "@nlpjs/nlp";
|
||||||
|
// @ts-ignore
|
||||||
|
import { NluManager, NluNeural } from "@nlpjs/nlu";
|
||||||
|
// @ts-ignore
|
||||||
|
import { LangEn } from "@nlpjs/lang-en-min";
|
||||||
|
// @ts-ignore
|
||||||
|
import { LangZh } from "@nlpjs/lang-zh";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import * as fflate from 'fflate';
|
||||||
|
|
||||||
|
let zh: TrainData = {};
|
||||||
|
let en: TrainData = {};
|
||||||
|
|
||||||
|
type TrainData = {
|
||||||
|
[key: string]: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function trainIntentionModel() {
|
||||||
|
try {
|
||||||
|
const dataZH = fs.readFileSync("./lib/nlp/data/zh.json", "utf8");
|
||||||
|
const dataEN = fs.readFileSync("./lib/nlp/data/en.json", "utf8");
|
||||||
|
zh = JSON.parse(dataZH);
|
||||||
|
en = JSON.parse(dataEN);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = await containerBootstrap();
|
||||||
|
container.use(Nlp);
|
||||||
|
container.use(LangEn);
|
||||||
|
container.use(LangZh);
|
||||||
|
container.use(NluNeural);
|
||||||
|
const manager = new NluManager({
|
||||||
|
container,
|
||||||
|
locales: ["en", "zh"],
|
||||||
|
nlu: {
|
||||||
|
useNoneFeature: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Adds the utterances and intents for the NLP
|
||||||
|
|
||||||
|
for (const key in zh) {
|
||||||
|
for (const value of zh[key]) {
|
||||||
|
manager.add("zh", value, key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in en) {
|
||||||
|
for (const value of en[key]) {
|
||||||
|
manager.add("en", value, key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await manager.train();
|
||||||
|
|
||||||
|
// let actual = await manager.process("en", "base64 decode bilibili");
|
||||||
|
// console.log(actual);
|
||||||
|
// let actualZH = await manager.process("zh", "去除百分号");
|
||||||
|
// console.log(actualZH);
|
||||||
|
|
||||||
|
const resultModel = manager.toJSON();
|
||||||
|
|
||||||
|
const buf = fflate.strToU8(JSON.stringify(resultModel));
|
||||||
|
|
||||||
|
const gzipped = fflate.gzipSync(buf, {
|
||||||
|
filename: 'model.json',
|
||||||
|
mtime: new Date().getTime()
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync("./public/model", Buffer.from(gzipped));
|
||||||
|
}
|
||||||
|
|
||||||
|
trainIntentionModel();
|
17
lib/onesearch/getSearchEngineName.ts
Normal file
17
lib/onesearch/getSearchEngineName.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { engineTranslation } from "lib/onesearch/translatedEngineList";
|
||||||
|
import { settingsAtom } from "lib/state/settings";
|
||||||
|
import { settingsType } from "global";
|
||||||
|
import { useAtomValue } from "jotai";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export default function(){
|
||||||
|
const settings: settingsType = useAtomValue(settingsAtom);
|
||||||
|
const currentEngine = settings.currentSearchEngine;
|
||||||
|
const displayEngine = getName(currentEngine);
|
||||||
|
return displayEngine;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getName(engineKey: string) {
|
||||||
|
const { t } = useTranslation("Search");
|
||||||
|
return engineTranslation.includes(engineKey) ? t(`engine.${engineKey}`) : engineKey;
|
||||||
|
}
|
@ -6,7 +6,7 @@ import search from "lib/search";
|
|||||||
export default function (
|
export default function (
|
||||||
index: number,
|
index: number,
|
||||||
suggestion: suggestionItem[],
|
suggestion: suggestionItem[],
|
||||||
query: string,
|
_query: string,
|
||||||
settings: settingsType,
|
settings: settingsType,
|
||||||
searchBoxRef: React.RefObject<HTMLInputElement>
|
searchBoxRef: React.RefObject<HTMLInputElement>
|
||||||
) {
|
) {
|
||||||
|
1
lib/onesearch/translatedEngineList.ts
Normal file
1
lib/onesearch/translatedEngineList.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const engineTranslation = ["google", "bing", "baidu", "duckduckgo", "yandex", "ecosia", "yahoo"];
|
21
lib/telemetering/sendError.ts
Normal file
21
lib/telemetering/sendError.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import pjson from "package.json"
|
||||||
|
|
||||||
|
const CLIENT_VERSION = pjson.version;
|
||||||
|
|
||||||
|
export function sendError(error: Error) {
|
||||||
|
fetch("/api/error", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
message: error.message,
|
||||||
|
name: error.name,
|
||||||
|
time: new Date().getTime()/1000,
|
||||||
|
version: CLIENT_VERSION,
|
||||||
|
ua: navigator.userAgent,
|
||||||
|
cause: error.cause,
|
||||||
|
stack: error.stack
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
1
lib/url/tldList.ts
Normal file
1
lib/url/tldList.ts
Normal file
File diff suppressed because one or more lines are too long
33
lib/url/validLink.ts
Normal file
33
lib/url/validLink.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import punycode from "punycode/";
|
||||||
|
import { tldList } from "./tldList";
|
||||||
|
|
||||||
|
export default function validLink(link: string) {
|
||||||
|
let finalURL = '';
|
||||||
|
try {
|
||||||
|
const url = new URL(link);
|
||||||
|
finalURL = url.origin;
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
// if the URL is invalid, try to add the protocol
|
||||||
|
try {
|
||||||
|
const urlWithHTTP = new URL("http://" + link);
|
||||||
|
finalURL = urlWithHTTP.origin;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (validTLD(finalURL)) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validTLD(domain: string): boolean {
|
||||||
|
const tld = punycode.toUnicode(domain.split(".").reverse()[0]);
|
||||||
|
if (tldList.includes(tld)) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
39
lib/weather/getCurrentWeather.ts
Normal file
39
lib/weather/getCurrentWeather.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
export function getClosestHourTimestamp(): string {
|
||||||
|
const now = new Date();
|
||||||
|
now.setMinutes(0, 0, 0); // 设置分钟、秒和毫秒为0
|
||||||
|
|
||||||
|
// 获取本地时间的年份、月份、日期、小时
|
||||||
|
const year = now.getFullYear();
|
||||||
|
const month = String(now.getMonth() + 1).padStart(2, '0'); // 月份从0开始
|
||||||
|
const day = String(now.getDate()).padStart(2, '0');
|
||||||
|
const hour = String(now.getHours()).padStart(2, '0');
|
||||||
|
|
||||||
|
// 拼接成所需的格式
|
||||||
|
const localHourTimestamp = `${year}-${month}-${day}T${hour}:00`;
|
||||||
|
|
||||||
|
return localHourTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findClosestDateIndex(dates: string[], utc_offset_seconds: number): number {
|
||||||
|
const now = new Date();
|
||||||
|
const nowTimestamp = now.getTime();
|
||||||
|
const offsetMilliseconds = utc_offset_seconds * 1000;
|
||||||
|
|
||||||
|
let closestIndex = -1;
|
||||||
|
let closestDiff = Infinity;
|
||||||
|
|
||||||
|
for (let i = 0; i < dates.length; i++) {
|
||||||
|
const date = new Date(dates[i]);
|
||||||
|
const adjustedTimestamp = date.getTime();
|
||||||
|
|
||||||
|
if (adjustedTimestamp <= nowTimestamp) {
|
||||||
|
const diff = nowTimestamp - adjustedTimestamp;
|
||||||
|
if (diff < closestDiff) {
|
||||||
|
closestDiff = diff;
|
||||||
|
closestIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return closestIndex;
|
||||||
|
}
|
17
lib/weather/getLocation.ts
Normal file
17
lib/weather/getLocation.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
const options = {
|
||||||
|
enableHighAccuracy: true,
|
||||||
|
timeout: 10000,
|
||||||
|
maximumAge: 3600
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getLocationNative(callback: Function) {
|
||||||
|
navigator.geolocation.getCurrentPosition(
|
||||||
|
(pos: GeolocationPosition) => {
|
||||||
|
callback(pos.coords);
|
||||||
|
},
|
||||||
|
(err: GeolocationPositionError) => {
|
||||||
|
callback(err);
|
||||||
|
},
|
||||||
|
options
|
||||||
|
);
|
||||||
|
}
|
23
lib/weather/getWeather.ts
Normal file
23
lib/weather/getWeather.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
export async function getWeather(lat: number, lon: number) {
|
||||||
|
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
const cacheKey = `weather-cache-${lat.toFixed(2)}-${lon.toFixed(2)}-${timezone}`;
|
||||||
|
const localData = localStorage.getItem(cacheKey);
|
||||||
|
if (localData != null) {
|
||||||
|
console.log('Using cache');
|
||||||
|
const parsedLocalData = JSON.parse(localData);
|
||||||
|
if (parsedLocalData["hourly"]["time"][0] != undefined &&
|
||||||
|
new Date().getTime() - new Date(parsedLocalData["hourly"]["time"][0]).getTime() < 86400 * 1000
|
||||||
|
) {
|
||||||
|
return parsedLocalData;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log('Cache expired');
|
||||||
|
localStorage.removeItem(cacheKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const url = `https://api.open-meteo.com/v1/cma?latitude=${lat.toString()}&longitude=${lon.toString()}&hourly=apparent_temperature,precipitation,weather_code&timezone=${encodeURIComponent(timezone)}&forecast_days=1`;
|
||||||
|
const response = await fetch(url);
|
||||||
|
const responseJson = await response.json();
|
||||||
|
localStorage.setItem(cacheKey, JSON.stringify(responseJson));
|
||||||
|
return responseJson;
|
||||||
|
}
|
294
lib/weather/wmocode.ts
Normal file
294
lib/weather/wmocode.ts
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
type WeatherInfo = {
|
||||||
|
description: string;
|
||||||
|
image: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type WMOCodeTable = {
|
||||||
|
[key: string]: {
|
||||||
|
day: WeatherInfo;
|
||||||
|
night: WeatherInfo;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export let WMOCodeTable: WMOCodeTable = {
|
||||||
|
"0": {
|
||||||
|
day: {
|
||||||
|
description: "Sunny",
|
||||||
|
image: "http://openweathermap.org/img/wn/01d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Clear",
|
||||||
|
image: "http://openweathermap.org/img/wn/01n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"1": {
|
||||||
|
day: {
|
||||||
|
description: "Mainly Sunny",
|
||||||
|
image: "http://openweathermap.org/img/wn/01d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Mainly Clear",
|
||||||
|
image: "http://openweathermap.org/img/wn/01n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"2": {
|
||||||
|
day: {
|
||||||
|
description: "Partly Cloudy",
|
||||||
|
image: "http://openweathermap.org/img/wn/02d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Partly Cloudy",
|
||||||
|
image: "http://openweathermap.org/img/wn/02n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"3": {
|
||||||
|
day: {
|
||||||
|
description: "Cloudy",
|
||||||
|
image: "http://openweathermap.org/img/wn/03d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Cloudy",
|
||||||
|
image: "http://openweathermap.org/img/wn/03n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"45": {
|
||||||
|
day: {
|
||||||
|
description: "Foggy",
|
||||||
|
image: "http://openweathermap.org/img/wn/50d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Foggy",
|
||||||
|
image: "http://openweathermap.org/img/wn/50n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"48": {
|
||||||
|
day: {
|
||||||
|
description: "Rime Fog",
|
||||||
|
image: "http://openweathermap.org/img/wn/50d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Rime Fog",
|
||||||
|
image: "http://openweathermap.org/img/wn/50n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"51": {
|
||||||
|
day: {
|
||||||
|
description: "Light Drizzle",
|
||||||
|
image: "http://openweathermap.org/img/wn/09d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Light Drizzle",
|
||||||
|
image: "http://openweathermap.org/img/wn/09n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"53": {
|
||||||
|
day: {
|
||||||
|
description: "Drizzle",
|
||||||
|
image: "http://openweathermap.org/img/wn/09d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Drizzle",
|
||||||
|
image: "http://openweathermap.org/img/wn/09n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"55": {
|
||||||
|
day: {
|
||||||
|
description: "Heavy Drizzle",
|
||||||
|
image: "http://openweathermap.org/img/wn/09d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Heavy Drizzle",
|
||||||
|
image: "http://openweathermap.org/img/wn/09n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"56": {
|
||||||
|
day: {
|
||||||
|
description: "Light Freezing Drizzle",
|
||||||
|
image: "http://openweathermap.org/img/wn/09d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Light Freezing Drizzle",
|
||||||
|
image: "http://openweathermap.org/img/wn/09n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"57": {
|
||||||
|
day: {
|
||||||
|
description: "Freezing Drizzle",
|
||||||
|
image: "http://openweathermap.org/img/wn/09d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Freezing Drizzle",
|
||||||
|
image: "http://openweathermap.org/img/wn/09n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"61": {
|
||||||
|
day: {
|
||||||
|
description: "Light Rain",
|
||||||
|
image: "http://openweathermap.org/img/wn/10d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Light Rain",
|
||||||
|
image: "http://openweathermap.org/img/wn/10n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"63": {
|
||||||
|
day: {
|
||||||
|
description: "Rain",
|
||||||
|
image: "http://openweathermap.org/img/wn/10d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Rain",
|
||||||
|
image: "http://openweathermap.org/img/wn/10n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"65": {
|
||||||
|
day: {
|
||||||
|
description: "Heavy Rain",
|
||||||
|
image: "http://openweathermap.org/img/wn/10d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Heavy Rain",
|
||||||
|
image: "http://openweathermap.org/img/wn/10n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"66": {
|
||||||
|
day: {
|
||||||
|
description: "Light Freezing Rain",
|
||||||
|
image: "http://openweathermap.org/img/wn/10d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Light Freezing Rain",
|
||||||
|
image: "http://openweathermap.org/img/wn/10n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"67": {
|
||||||
|
day: {
|
||||||
|
description: "Freezing Rain",
|
||||||
|
image: "http://openweathermap.org/img/wn/10d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Freezing Rain",
|
||||||
|
image: "http://openweathermap.org/img/wn/10n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"71": {
|
||||||
|
day: {
|
||||||
|
description: "Light Snow",
|
||||||
|
image: "http://openweathermap.org/img/wn/13d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Light Snow",
|
||||||
|
image: "http://openweathermap.org/img/wn/13n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"73": {
|
||||||
|
day: {
|
||||||
|
description: "Snow",
|
||||||
|
image: "http://openweathermap.org/img/wn/13d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Snow",
|
||||||
|
image: "http://openweathermap.org/img/wn/13n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"75": {
|
||||||
|
day: {
|
||||||
|
description: "Heavy Snow",
|
||||||
|
image: "http://openweathermap.org/img/wn/13d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Heavy Snow",
|
||||||
|
image: "http://openweathermap.org/img/wn/13n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"77": {
|
||||||
|
day: {
|
||||||
|
description: "Snow Grains",
|
||||||
|
image: "http://openweathermap.org/img/wn/13d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Snow Grains",
|
||||||
|
image: "http://openweathermap.org/img/wn/13n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"80": {
|
||||||
|
day: {
|
||||||
|
description: "Light Showers",
|
||||||
|
image: "http://openweathermap.org/img/wn/09d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Light Showers",
|
||||||
|
image: "http://openweathermap.org/img/wn/09n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"81": {
|
||||||
|
day: {
|
||||||
|
description: "Showers",
|
||||||
|
image: "http://openweathermap.org/img/wn/09d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Showers",
|
||||||
|
image: "http://openweathermap.org/img/wn/09n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"82": {
|
||||||
|
day: {
|
||||||
|
description: "Heavy Showers",
|
||||||
|
image: "http://openweathermap.org/img/wn/09d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Heavy Showers",
|
||||||
|
image: "http://openweathermap.org/img/wn/09n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"85": {
|
||||||
|
day: {
|
||||||
|
description: "Light Snow Showers",
|
||||||
|
image: "http://openweathermap.org/img/wn/13d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Light Snow Showers",
|
||||||
|
image: "http://openweathermap.org/img/wn/13n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"86": {
|
||||||
|
day: {
|
||||||
|
description: "Snow Showers",
|
||||||
|
image: "http://openweathermap.org/img/wn/13d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Snow Showers",
|
||||||
|
image: "http://openweathermap.org/img/wn/13n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"95": {
|
||||||
|
day: {
|
||||||
|
description: "Thunderstorm",
|
||||||
|
image: "http://openweathermap.org/img/wn/11d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Thunderstorm",
|
||||||
|
image: "http://openweathermap.org/img/wn/11n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"96": {
|
||||||
|
day: {
|
||||||
|
description: "Light Thunderstorms With Hail",
|
||||||
|
image: "http://openweathermap.org/img/wn/11d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Light Thunderstorms With Hail",
|
||||||
|
image: "http://openweathermap.org/img/wn/11n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"99": {
|
||||||
|
day: {
|
||||||
|
description: "Thunderstorm With Hail",
|
||||||
|
image: "http://openweathermap.org/img/wn/11d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Thunderstorm With Hail",
|
||||||
|
image: "http://openweathermap.org/img/wn/11n@2x.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
15
package.json
15
package.json
@ -4,16 +4,24 @@
|
|||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bunx --bun vite",
|
"dev": "bun server.ts",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
"preview": "vite preview"
|
"preview": "NODE_ENV=production bun server.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@nextui-org/react": "^2.4.2",
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
|
"cac": "^6.7.14",
|
||||||
|
"chalk": "^5.3.0",
|
||||||
|
"express": "^4.19.2",
|
||||||
|
"fflate": "^0.8.2",
|
||||||
|
"framer-motion": "^11.2.12",
|
||||||
"i18next": "^23.11.5",
|
"i18next": "^23.11.5",
|
||||||
"i18next-browser-languagedetector": "^8.0.0",
|
"i18next-browser-languagedetector": "^8.0.0",
|
||||||
"i18next-icu": "^2.3.0",
|
"i18next-icu": "^2.3.0",
|
||||||
"jotai": "^2.8.3",
|
"jotai": "^2.8.3",
|
||||||
|
"node-nlp": "^4.27.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-i18next": "^14.1.2",
|
"react-i18next": "^14.1.2",
|
||||||
@ -21,7 +29,8 @@
|
|||||||
"react-router-dom": "^6.23.1",
|
"react-router-dom": "^6.23.1",
|
||||||
"search-engine-autocomplete": "^0.4.3",
|
"search-engine-autocomplete": "^0.4.3",
|
||||||
"valid-url": "^1.0.9",
|
"valid-url": "^1.0.9",
|
||||||
"validate-color": "^2.2.4"
|
"validate-color": "^2.2.4",
|
||||||
|
"vite-express": "^0.17.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
|
@ -4,16 +4,22 @@ import { useAtomValue, useSetAtom } from "jotai";
|
|||||||
import Search from "components/search";
|
import Search from "components/search";
|
||||||
import { settingsAtom } from "lib/state/settings";
|
import { settingsAtom } from "lib/state/settings";
|
||||||
import { bgFocusAtom } from "lib/state/background";
|
import { bgFocusAtom } from "lib/state/background";
|
||||||
|
import EngineSelector from "components/engineSelector";
|
||||||
|
import OneSearch from "components/onesearch/onesearch";
|
||||||
|
|
||||||
export default function Homepage() {
|
export default function Homepage() {
|
||||||
const settings = useAtomValue(settingsAtom);
|
const settings = useAtomValue(settingsAtom);
|
||||||
const setBgFocus = useSetAtom(bgFocusAtom);
|
const setBgFocus = useSetAtom(bgFocusAtom);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="h-full fixed overflow-hidden w-full bg-black">
|
||||||
<Background />
|
<Background />
|
||||||
|
<EngineSelector className="absolute top-20 lg:top-44 short:top-0 translate-x-[-50%] translate-y-[-0.2rem]
|
||||||
|
left-1/2 w-11/12 sm:w-[700px] text:black text-right
|
||||||
|
dark:text-white text-3xl text-shadow-lg z-10"/>
|
||||||
<Search onFocus={() => setBgFocus(true)} />
|
<Search onFocus={() => setBgFocus(true)} />
|
||||||
<Time showSecond={settings.timeShowSecond} />
|
<Time showSecond={settings.timeShowSecond} />
|
||||||
|
<OneSearch />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
BIN
public/model
Normal file
BIN
public/model
Normal file
Binary file not shown.
87
server.ts
Normal file
87
server.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import chalk from "chalk";
|
||||||
|
import express from "express";
|
||||||
|
import ViteExpress from "vite-express";
|
||||||
|
import pjson from "./package.json";
|
||||||
|
import { networkInterfaces } from "os";
|
||||||
|
import cac from "cac";
|
||||||
|
const start = new Date();
|
||||||
|
|
||||||
|
const cli = cac();
|
||||||
|
const nets = networkInterfaces();
|
||||||
|
const ips: string[] = [];
|
||||||
|
for (const name of Object.keys(nets)) {
|
||||||
|
if (nets[name] === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const net of nets[name]) {
|
||||||
|
// Skip over non-IPv4 and internal (i.e. 127.0.0.1) addresses
|
||||||
|
// 'IPv4' is in Node <= 17, from 18 it's a number 4 or 6
|
||||||
|
const familyV4Value = typeof net.family === "string" ? "IPv4" : 4;
|
||||||
|
if (net.family === familyV4Value && !net.internal) {
|
||||||
|
ips.push(net.address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const port = 3000;
|
||||||
|
let host = "localhost";
|
||||||
|
|
||||||
|
cli.option("--host [host]", "Sepcify host name")
|
||||||
|
cli.help()
|
||||||
|
cli.version(pjson.version);
|
||||||
|
const parsed = cli.parse();
|
||||||
|
if (parsed.options.host!==undefined && typeof parsed.options.host == "boolean" && parsed.options.host) {
|
||||||
|
host = "0.0.0.0";
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get("/message", (_, res) => res.send("Hello from express!"));
|
||||||
|
|
||||||
|
async function helloMessage() {
|
||||||
|
const { base } = await ViteExpress.getViteConfig();
|
||||||
|
//console.clear();
|
||||||
|
const timeCost = new Date().getTime() - start.getTime();
|
||||||
|
console.log("");
|
||||||
|
console.log(
|
||||||
|
" ",
|
||||||
|
chalk.redBright("SparkHome"),
|
||||||
|
chalk.redBright("v" + pjson.version),
|
||||||
|
chalk.whiteBright(" ready in"),
|
||||||
|
`${Math.round(timeCost)} ms`
|
||||||
|
);
|
||||||
|
console.log("");
|
||||||
|
console.log(" ", chalk.redBright("➜ "), "Local:\t", chalk.cyan(`http://${host}:${port}${base}`));
|
||||||
|
if (host !== "localhost") {
|
||||||
|
for (const ip of ips) {
|
||||||
|
console.log(" ", chalk.redBright("➜ "), "Network:\t", chalk.cyan(`http://${ip}:${port}${base}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(" ", chalk.red("➜ "), chalk.whiteBright("press"), "h + enter", chalk.whiteBright("to show help"))
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = app.listen(port, host);
|
||||||
|
|
||||||
|
ViteExpress.bind(app, server, helloMessage);
|
||||||
|
|
||||||
|
async function a() {
|
||||||
|
for await (const line of console) {
|
||||||
|
switch (line) {
|
||||||
|
case "h":
|
||||||
|
console.log(" Shortcuts");
|
||||||
|
console.log(" ", chalk.whiteBright("press"), "c + enter ", chalk.whiteBright("to clear console"));
|
||||||
|
console.log(" ", chalk.whiteBright("press"), "q + enter ", chalk.whiteBright("to quit"));
|
||||||
|
break;
|
||||||
|
case "c":
|
||||||
|
console.clear();
|
||||||
|
break;
|
||||||
|
case "q":
|
||||||
|
server.on("vite:close", ()=>{});
|
||||||
|
server.close();
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a();
|
36
src/app.tsx
36
src/app.tsx
@ -3,19 +3,20 @@ import { useRoutes } from "react-router-dom";
|
|||||||
import routes from "~react-pages";
|
import routes from "~react-pages";
|
||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
import { initReactI18next } from "react-i18next";
|
import { initReactI18next } from "react-i18next";
|
||||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
import LanguageDetector from "i18next-browser-languagedetector";
|
||||||
import ICU from 'i18next-icu';
|
import ICU from "i18next-icu";
|
||||||
import * as en from "i18n/en.json"
|
import * as en from "i18n/en.json";
|
||||||
import * as zh from "i18n/zh.json"
|
import * as zh from "i18n/zh.json";
|
||||||
import * as ja from "i18n/ja.json"
|
import * as ja from "i18n/ja.json";
|
||||||
import * as ar from "i18n/ar.json"
|
import * as ar from "i18n/ar.json";
|
||||||
import * as de from "i18n/de.json"
|
import * as de from "i18n/de.json";
|
||||||
import * as es from "i18n/es.json"
|
import * as es from "i18n/es.json";
|
||||||
import * as fr from "i18n/fr.json"
|
import * as fr from "i18n/fr.json";
|
||||||
import * as it from "i18n/it.json"
|
import * as it from "i18n/it.json";
|
||||||
import * as ko from "i18n/ko.json"
|
import * as ko from "i18n/ko.json";
|
||||||
import * as pt from "i18n/pt.json"
|
import * as pt from "i18n/pt.json";
|
||||||
import * as ru from "i18n/ru.json"
|
import * as ru from "i18n/ru.json";
|
||||||
|
import { NextUIProvider } from "@nextui-org/react";
|
||||||
|
|
||||||
i18n.use(initReactI18next) // passes i18n down to react-i18next
|
i18n.use(initReactI18next) // passes i18n down to react-i18next
|
||||||
.use(LanguageDetector)
|
.use(LanguageDetector)
|
||||||
@ -63,12 +64,15 @@ i18n.use(initReactI18next) // passes i18n down to react-i18next
|
|||||||
},
|
},
|
||||||
|
|
||||||
detection: {
|
detection: {
|
||||||
order: ['navigator'],
|
order: ["navigator"],
|
||||||
caches: []
|
caches: []
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
return <Suspense fallback={<p>Loading...</p>}>{useRoutes(routes)}</Suspense>;
|
return (
|
||||||
|
<NextUIProvider>
|
||||||
|
<Suspense fallback={<p>Loading...</p>}>{useRoutes(routes)}</Suspense>
|
||||||
|
</NextUIProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import type { Config } from "tailwindcss";
|
import type { Config } from "tailwindcss";
|
||||||
|
|
||||||
|
import { nextui } from "@nextui-org/react";
|
||||||
|
|
||||||
const config: Config = {
|
const config: Config = {
|
||||||
content: [
|
content: [
|
||||||
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}",
|
||||||
"./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}"
|
"./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}"
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
@ -13,6 +16,8 @@ const config: Config = {
|
|||||||
"gradient-conic": "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))"
|
"gradient-conic": "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
darkMode: "class",
|
||||||
|
plugins: [nextui()],
|
||||||
};
|
};
|
||||||
export default config;
|
export default config;
|
||||||
|
Loading…
Reference in New Issue
Block a user