diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index d6c9537..0000000 --- a/.eslintrc.cjs +++ /dev/null @@ -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 }, - ], - }, -} diff --git a/bun.lockb b/bun.lockb index 6ab783a..66f3594 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/components/engineSelector.tsx b/components/engineSelector.tsx new file mode 100644 index 0000000..01c95df --- /dev/null +++ b/components/engineSelector.tsx @@ -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 ( +
+ { + isClient && + ( + + + + + + {Object.keys(items).map((item) => ( + + {getName(item)} + + ))} + + + )} +
+ ); +} \ No newline at end of file diff --git a/components/onesearch/handleEnter.ts b/components/onesearch/handleEnter.ts new file mode 100644 index 0000000..f3d6fdd --- /dev/null +++ b/components/onesearch/handleEnter.ts @@ -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 +) { + 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(); + } +} diff --git a/components/onesearch/handleNLUResult.ts b/components/onesearch/handleNLUResult.ts new file mode 100644 index 0000000..6bddeaa --- /dev/null +++ b/components/onesearch/handleNLUResult.ts @@ -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 + } + ]); + } +} \ No newline at end of file diff --git a/components/onesearch/link.tsx b/components/onesearch/link.tsx new file mode 100644 index 0000000..888e98d --- /dev/null +++ b/components/onesearch/link.tsx @@ -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 ( +
{ + window.open(normalizeURL(props.query)); + }} + > + {props.children} +
+ ); + } + else { + return ( +
{ + window.open(normalizeURL(props.query)); + }} + > + {props.children} +
+ ); + } +} diff --git a/components/onesearch/onesearch.tsx b/components/onesearch/onesearch.tsx new file mode 100644 index 0000000..7f686f7 --- /dev/null +++ b/components/onesearch/onesearch.tsx @@ -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: Go to: }, + { 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 ( + + {suggestion.map((s, i) => { + if (s.suggestion.trim() === "") return; + if (s.type === "default") { + return ( + + {s.suggestion}  + + {t("search-help-text", { engine: engineName })} + + {devMode && ( + + {s.relevance} + + )} + + ); + } else if (s.type === "QUERY") { + return ( + + {s.suggestion} + {devMode && ( + + {s.relevance} + + )} + + ); + } else if (s.type === "NAVIGATION" || s.type === "default-link") { + return ( + + {s.prompt && {s.prompt}} + {s.suggestion} + {devMode && ( + + {s.relevance} + + )} + + ); + } else if (s.type === "text") { + return ( + + {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> + ); +} diff --git a/components/onesearch/plainSearch.tsx b/components/onesearch/plainSearch.tsx new file mode 100644 index 0000000..c0e7c8c --- /dev/null +++ b/components/onesearch/plainSearch.tsx @@ -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> + ); + } +} diff --git a/components/onesearch/plainText.tsx b/components/onesearch/plainText.tsx new file mode 100644 index 0000000..71cbe48 --- /dev/null +++ b/components/onesearch/plainText.tsx @@ -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> + ); + } +} diff --git a/components/onesearch/suggestion.tsx b/components/onesearch/suggestion.tsx new file mode 100644 index 0000000..c10b878 --- /dev/null +++ b/components/onesearch/suggestion.tsx @@ -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> + ); +} \ No newline at end of file diff --git a/components/onesearch/suggestionBox.tsx b/components/onesearch/suggestionBox.tsx new file mode 100644 index 0000000..70637a5 --- /dev/null +++ b/components/onesearch/suggestionBox.tsx @@ -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> + ); +} diff --git a/components/time.tsx b/components/time.tsx index c33a6b1..32831c9 100644 --- a/components/time.tsx +++ b/components/time.tsx @@ -33,7 +33,7 @@ export default function Time(props: { <div className="absolute top-20 lg:top-44 short:top-0 translate-x-[-50%] 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()}{" "} <span className="text-lg leading-9 relative"> diff --git a/i18n/en.json b/i18n/en.json index b03c753..35caf64 100755 --- a/i18n/en.json +++ b/i18n/en.json @@ -1,5 +1,32 @@ { - "search" : { - "placeholder" : "Search or type a URL" - } -} \ No newline at end of file + "Search": { + "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" + } + } +} diff --git a/lib/nlp/data/en.json b/lib/nlp/data/en.json new file mode 100644 index 0000000..98e17dc --- /dev/null +++ b/lib/nlp/data/en.json @@ -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" + ] +} diff --git a/lib/nlp/data/zh.json b/lib/nlp/data/zh.json new file mode 100644 index 0000000..09cdf11 --- /dev/null +++ b/lib/nlp/data/zh.json @@ -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": [ + "你好", + "为什么计算机使用二进制" + ] +} diff --git a/lib/nlp/load.ts b/lib/nlp/load.ts new file mode 100644 index 0000000..63d0517 --- /dev/null +++ b/lib/nlp/load.ts @@ -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; + } +} \ No newline at end of file diff --git a/lib/nlp/train.ts b/lib/nlp/train.ts new file mode 100644 index 0000000..21f6972 --- /dev/null +++ b/lib/nlp/train.ts @@ -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(); diff --git a/lib/onesearch/getSearchEngineName.ts b/lib/onesearch/getSearchEngineName.ts new file mode 100644 index 0000000..4d6eac6 --- /dev/null +++ b/lib/onesearch/getSearchEngineName.ts @@ -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; +} \ No newline at end of file diff --git a/lib/onesearch/handleEnter.ts b/lib/onesearch/handleEnter.ts index 7a49360..e604bb5 100644 --- a/lib/onesearch/handleEnter.ts +++ b/lib/onesearch/handleEnter.ts @@ -6,7 +6,7 @@ import search from "lib/search"; export default function ( index: number, suggestion: suggestionItem[], - query: string, + _query: string, settings: settingsType, searchBoxRef: React.RefObject<HTMLInputElement> ) { diff --git a/lib/onesearch/translatedEngineList.ts b/lib/onesearch/translatedEngineList.ts new file mode 100644 index 0000000..d486591 --- /dev/null +++ b/lib/onesearch/translatedEngineList.ts @@ -0,0 +1 @@ +export const engineTranslation = ["google", "bing", "baidu", "duckduckgo", "yandex", "ecosia", "yahoo"]; \ No newline at end of file diff --git a/lib/telemetering/sendError.ts b/lib/telemetering/sendError.ts new file mode 100644 index 0000000..aa558c1 --- /dev/null +++ b/lib/telemetering/sendError.ts @@ -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 + }) + }) +} \ No newline at end of file diff --git a/lib/url/tldList.ts b/lib/url/tldList.ts new file mode 100644 index 0000000..927e0e5 --- /dev/null +++ b/lib/url/tldList.ts @@ -0,0 +1 @@ +export const tldList = ["aaa", "aarp", "abb", "abbott", "abbvie", "abc", "able", "abogado", "abudhabi", "ac", "academy", "accenture", "accountant", "accountants", "aco", "actor", "ad", "ads", "adult", "ae", "aeg", "aero", "aetna", "af", "afl", "africa", "ag", "agakhan", "agency", "ai", "aig", "airbus", "airforce", "airtel", "akdn", "al", "alibaba", "alipay", "allfinanz", "allstate", "ally", "alsace", "alstom", "am", "amazon", "americanexpress", "americanfamily", "amex", "amfam", "amica", "amsterdam", "analytics", "android", "anquan", "anz", "ao", "aol", "apartments", "app", "apple", "aq", "aquarelle", "ar", "arab", "aramco", "archi", "army", "arpa", "art", "arte", "as", "asda", "asia", "associates", "at", "athleta", "attorney", "au", "auction", "audi", "audible", "audio", "auspost", "author", "auto", "autos", "aw", "aws", "ax", "axa", "az", "azure", "ba", "baby", "baidu", "banamex", "band", "bank", "bar", "barcelona", "barclaycard", "barclays", "barefoot", "bargains", "baseball", "basketball", "bauhaus", "bayern", "bb", "bbc", "bbt", "bbva", "bcg", "bcn", "bd", "be", "beats", "beauty", "beer", "bentley", "berlin", "best", "bestbuy", "bet", "bf", "bg", "bh", "bharti", "bi", "bible", "bid", "bike", "bing", "bingo", "bio", "biz", "bj", "black", "blackfriday", "blockbuster", "blog", "bloomberg", "blue", "bm", "bms", "bmw", "bn", "bnpparibas", "bo", "boats", "boehringer", "bofa", "bom", "bond", "boo", "book", "booking", "bosch", "bostik", "boston", "bot", "boutique", "box", "br", "bradesco", "bridgestone", "broadway", "broker", "brother", "brussels", "bs", "bt", "build", "builders", "business", "buy", "buzz", "bv", "bw", "by", "bz", "bzh", "ca", "cab", "cafe", "cal", "call", "calvinklein", "cam", "camera", "camp", "canon", "capetown", "capital", "capitalone", "car", "caravan", "cards", "care", "career", "careers", "cars", "casa", "case", "cash", "casino", "cat", "catering", "catholic", "cba", "cbn", "cbre", "cc", "cd", "center", "ceo", "cern", "cf", "cfa", "cfd", "cg", "ch", "chanel", "channel", "charity", "chase", "chat", "cheap", "chintai", "christmas", "chrome", "church", "ci", "cipriani", "circle", "cisco", "citadel", "citi", "citic", "city", "ck", "cl", "claims", "cleaning", "click", "clinic", "clinique", "clothing", "cloud", "club", "clubmed", "cm", "cn", "co", "coach", "codes", "coffee", "college", "cologne", "com", "commbank", "community", "company", "compare", "computer", "comsec", "condos", "construction", "consulting", "contact", "contractors", "cooking", "cool", "coop", "corsica", "country", "coupon", "coupons", "courses", "cpa", "cr", "credit", "creditcard", "creditunion", "cricket", "crown", "crs", "cruise", "cruises", "cu", "cuisinella", "cv", "cw", "cx", "cy", "cymru", "cyou", "cz", "dabur", "dad", "dance", "data", "date", "dating", "datsun", "day", "dclk", "dds", "de", "deal", "dealer", "deals", "degree", "delivery", "dell", "deloitte", "delta", "democrat", "dental", "dentist", "desi", "design", "dev", "dhl", "diamonds", "diet", "digital", "direct", "directory", "discount", "discover", "dish", "diy", "dj", "dk", "dm", "dnp", "do", "docs", "doctor", "dog", "domains", "dot", "download", "drive", "dtv", "dubai", "dunlop", "dupont", "durban", "dvag", "dvr", "dz", "earth", "eat", "ec", "eco", "edeka", "edu", "education", "ee", "eg", "email", "emerck", "energy", "engineer", "engineering", "enterprises", "epson", "equipment", "er", "ericsson", "erni", "es", "esq", "estate", "et", "eu", "eurovision", "eus", "events", "exchange", "expert", "exposed", "express", "extraspace", "fage", "fail", "fairwinds", "faith", "family", "fan", "fans", "farm", "farmers", "fashion", "fast", "fedex", "feedback", "ferrari", "ferrero", "fi", "fidelity", "fido", "film", "final", "finance", "financial", "fire", "firestone", "firmdale", "fish", "fishing", "fit", "fitness", "fj", "fk", "flickr", "flights", "flir", "florist", "flowers", "fly", "fm", "fo", "foo", "food", "football", "ford", "forex", "forsale", "forum", "foundation", "fox", "fr", "free", "fresenius", "frl", "frogans", "frontier", "ftr", "fujitsu", "fun", "fund", "furniture", "futbol", "fyi", "ga", "gal", "gallery", "gallo", "gallup", "game", "games", "gap", "garden", "gay", "gb", "gbiz", "gd", "gdn", "ge", "gea", "gent", "genting", "george", "gf", "gg", "ggee", "gh", "gi", "gift", "gifts", "gives", "giving", "gl", "glass", "gle", "global", "globo", "gm", "gmail", "gmbh", "gmo", "gmx", "gn", "godaddy", "gold", "goldpoint", "golf", "goo", "goodyear", "goog", "google", "gop", "got", "gov", "gp", "gq", "gr", "grainger", "graphics", "gratis", "green", "gripe", "grocery", "group", "gs", "gt", "gu", "gucci", "guge", "guide", "guitars", "guru", "gw", "gy", "hair", "hamburg", "hangout", "haus", "hbo", "hdfc", "hdfcbank", "health", "healthcare", "help", "helsinki", "here", "hermes", "hiphop", "hisamitsu", "hitachi", "hiv", "hk", "hkt", "hm", "hn", "hockey", "holdings", "holiday", "homedepot", "homegoods", "homes", "homesense", "honda", "horse", "hospital", "host", "hosting", "hot", "hotels", "hotmail", "house", "how", "hr", "hsbc", "ht", "hu", "hughes", "hyatt", "hyundai", "ibm", "icbc", "ice", "icu", "id", "ie", "ieee", "ifm", "ikano", "il", "im", "imamat", "imdb", "immo", "immobilien", "in", "inc", "industries", "infiniti", "info", "ing", "ink", "institute", "insurance", "insure", "int", "international", "intuit", "investments", "io", "ipiranga", "iq", "ir", "irish", "is", "ismaili", "ist", "istanbul", "it", "itau", "itv", "jaguar", "java", "jcb", "je", "jeep", "jetzt", "jewelry", "jio", "jll", "jm", "jmp", "jnj", "jo", "jobs", "joburg", "jot", "joy", "jp", "jpmorgan", "jprs", "juegos", "juniper", "kaufen", "kddi", "ke", "kerryhotels", "kerrylogistics", "kerryproperties", "kfh", "kg", "kh", "ki", "kia", "kids", "kim", "kindle", "kitchen", "kiwi", "km", "kn", "koeln", "komatsu", "kosher", "kp", "kpmg", "kpn", "kr", "krd", "kred", "kuokgroup", "kw", "ky", "kyoto", "kz", "la", "lacaixa", "lamborghini", "lamer", "lancaster", "land", "landrover", "lanxess", "lasalle", "lat", "latino", "latrobe", "law", "lawyer", "lb", "lc", "lds", "lease", "leclerc", "lefrak", "legal", "lego", "lexus", "lgbt", "li", "lidl", "life", "lifeinsurance", "lifestyle", "lighting", "like", "lilly", "limited", "limo", "lincoln", "link", "lipsy", "live", "living", "lk", "llc", "llp", "loan", "loans", "locker", "locus", "lol", "london", "lotte", "lotto", "love", "lpl", "lplfinancial", "lr", "ls", "lt", "ltd", "ltda", "lu", "lundbeck", "luxe", "luxury", "lv", "ly", "ma", "madrid", "maif", "maison", "makeup", "man", "management", "mango", "map", "market", "marketing", "markets", "marriott", "marshalls", "mattel", "mba", "mc", "mckinsey", "md", "me", "med", "media", "meet", "melbourne", "meme", "memorial", "men", "menu", "merckmsd", "mg", "mh", "miami", "microsoft", "mil", "mini", "mint", "mit", "mitsubishi", "mk", "ml", "mlb", "mls", "mm", "mma", "mn", "mo", "mobi", "mobile", "moda", "moe", "moi", "mom", "monash", "money", "monster", "mormon", "mortgage", "moscow", "moto", "motorcycles", "mov", "movie", "mp", "mq", "mr", "ms", "msd", "mt", "mtn", "mtr", "mu", "museum", "music", "mv", "mw", "mx", "my", "mz", "na", "nab", "nagoya", "name", "natura", "navy", "nba", "nc", "ne", "nec", "net", "netbank", "netflix", "network", "neustar", "new", "news", "next", "nextdirect", "nexus", "nf", "nfl", "ng", "ngo", "nhk", "ni", "nico", "nike", "nikon", "ninja", "nissan", "nissay", "nl", "no", "nokia", "norton", "now", "nowruz", "nowtv", "np", "nr", "nra", "nrw", "ntt", "nu", "nyc", "nz", "obi", "observer", "office", "okinawa", "olayan", "olayangroup", "ollo", "om", "omega", "one", "ong", "onl", "online", "ooo", "open", "oracle", "orange", "org", "organic", "origins", "osaka", "otsuka", "ott", "ovh", "pa", "page", "panasonic", "paris", "pars", "partners", "parts", "party", "pay", "pccw", "pe", "pet", "pf", "pfizer", "pg", "ph", "pharmacy", "phd", "philips", "phone", "photo", "photography", "photos", "physio", "pics", "pictet", "pictures", "pid", "pin", "ping", "pink", "pioneer", "pizza", "pk", "pl", "place", "play", "playstation", "plumbing", "plus", "pm", "pn", "pnc", "pohl", "poker", "politie", "porn", "post", "pr", "pramerica", "praxi", "press", "prime", "pro", "prod", "productions", "prof", "progressive", "promo", "properties", "property", "protection", "pru", "prudential", "ps", "pt", "pub", "pw", "pwc", "py", "qa", "qpon", "quebec", "quest", "racing", "radio", "re", "read", "realestate", "realtor", "realty", "recipes", "red", "redstone", "redumbrella", "rehab", "reise", "reisen", "reit", "reliance", "ren", "rent", "rentals", "repair", "report", "republican", "rest", "restaurant", "review", "reviews", "rexroth", "rich", "richardli", "ricoh", "ril", "rio", "rip", "ro", "rocks", "rodeo", "rogers", "room", "rs", "rsvp", "ru", "rugby", "ruhr", "run", "rw", "rwe", "ryukyu", "sa", "saarland", "safe", "safety", "sakura", "sale", "salon", "samsclub", "samsung", "sandvik", "sandvikcoromant", "sanofi", "sap", "sarl", "sas", "save", "saxo", "sb", "sbi", "sbs", "sc", "scb", "schaeffler", "schmidt", "scholarships", "school", "schule", "schwarz", "science", "scot", "sd", "se", "search", "seat", "secure", "security", "seek", "select", "sener", "services", "seven", "sew", "sex", "sexy", "sfr", "sg", "sh", "shangrila", "sharp", "shaw", "shell", "shia", "shiksha", "shoes", "shop", "shopping", "shouji", "show", "si", "silk", "sina", "singles", "site", "sj", "sk", "ski", "skin", "sky", "skype", "sl", "sling", "sm", "smart", "smile", "sn", "sncf", "so", "soccer", "social", "softbank", "software", "sohu", "solar", "solutions", "song", "sony", "soy", "spa", "space", "sport", "spot", "sr", "srl", "ss", "st", "stada", "staples", "star", "statebank", "statefarm", "stc", "stcgroup", "stockholm", "storage", "store", "stream", "studio", "study", "style", "su", "sucks", "supplies", "supply", "support", "surf", "surgery", "suzuki", "sv", "swatch", "swiss", "sx", "sy", "sydney", "systems", "sz", "tab", "taipei", "talk", "taobao", "target", "tatamotors", "tatar", "tattoo", "tax", "taxi", "tc", "tci", "td", "tdk", "team", "tech", "technology", "tel", "temasek", "tennis", "teva", "tf", "tg", "th", "thd", "theater", "theatre", "tiaa", "tickets", "tienda", "tips", "tires", "tirol", "tj", "tjmaxx", "tjx", "tk", "tkmaxx", "tl", "tm", "tmall", "tn", "to", "today", "tokyo", "tools", "top", "toray", "toshiba", "total", "tours", "town", "toyota", "toys", "tr", "trade", "trading", "training", "travel", "travelers", "travelersinsurance", "trust", "trv", "tt", "tube", "tui", "tunes", "tushu", "tv", "tvs", "tw", "tz", "ua", "ubank", "ubs", "ug", "uk", "unicom", "university", "uno", "uol", "ups", "us", "uy", "uz", "va", "vacations", "vana", "vanguard", "vc", "ve", "vegas", "ventures", "verisign", "versicherung", "vet", "vg", "vi", "viajes", "video", "vig", "viking", "villas", "vin", "vip", "virgin", "visa", "vision", "viva", "vivo", "vlaanderen", "vn", "vodka", "volvo", "vote", "voting", "voto", "voyage", "vu", "wales", "walmart", "walter", "wang", "wanggou", "watch", "watches", "weather", "weatherchannel", "webcam", "weber", "website", "wed", "wedding", "weibo", "weir", "wf", "whoswho", "wien", "wiki", "williamhill", "win", "windows", "wine", "winners", "wme", "wolterskluwer", "woodside", "work", "works", "world", "wow", "ws", "wtc", "wtf", "xbox", "xerox", "xihuan", "xin", "कॉम", "セール", "佛山", "ಭಾರತ", "慈善", "集团", "在线", "한국", "ଭାରତ", "点看", "คอม", "ভাৰত", "ভারত", "八卦", "ישראל", "موقع", "বাংলা", "公益", "公司", "香格里拉", "网站", "移动", "我爱你", "москва", "қаз", "католик", "онлайн", "сайт", "联通", "срб", "бг", "бел", "קום", "时尚", "微博", "淡马锡", "ファッション", "орг", "नेट", "ストア", "アマゾン", "삼성", "சிங்கப்பூர்", "商标", "商店", "商城", "дети", "мкд", "ею", "ポイント", "新闻", "家電", "كوم", "中文网", "中信", "中国", "中國", "娱乐", "谷歌", "భారత్", "ලංකා", "電訊盈科", "购物", "クラウド", "ભારત", "通販", "भारतम्", "भारत", "भारोत", "网店", "संगठन", "餐厅", "网络", "ком", "укр", "香港", "亚马逊", "食品", "飞利浦", "台湾", "台灣", "手机", "мон", "الجزائر", "عمان", "ارامكو", "ایران", "العليان", "امارات", "بازار", "موريتانيا", "پاکستان", "الاردن", "بارت", "بھارت", "المغرب", "ابوظبي", "البحرين", "السعودية", "ڀارت", "كاثوليك", "سودان", "همراه", "عراق", "مليسيا", "澳門", "닷컴", "政府", "شبكة", "بيتك", "عرب", "გე", "机构", "组织机构", "健康", "ไทย", "سورية", "招聘", "рус", "рф", "РФ", "تونس", "大拿", "ລາວ", "みんな", "グーグル", "ευ", "ελ", "世界", "書籍", "ഭാരതം", "ਭਾਰਤ", "网址", "닷넷", "コム", "天主教", "游戏", "vermögensberater", "vermögensberatung", "企业", "信息", "嘉里大酒店", "嘉里", "مصر", "قطر", "广东", "இலங்கை", "இந்தியா", "հայ", "新加坡", "فلسطين", "政务", "xxx", "xyz", "yachts", "yahoo", "yamaxun", "yandex", "ye", "yodobashi", "yoga", "yokohama", "you", "youtube", "yt", "yun", "za", "zappos", "zara", "zero", "zip", "zm", "zone", "zuerich", "zw"]; \ No newline at end of file diff --git a/lib/url/validLink.ts b/lib/url/validLink.ts new file mode 100644 index 0000000..04026eb --- /dev/null +++ b/lib/url/validLink.ts @@ -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; + } +} diff --git a/lib/weather/getCurrentWeather.ts b/lib/weather/getCurrentWeather.ts new file mode 100644 index 0000000..b762862 --- /dev/null +++ b/lib/weather/getCurrentWeather.ts @@ -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; +} \ No newline at end of file diff --git a/lib/weather/getLocation.ts b/lib/weather/getLocation.ts new file mode 100644 index 0000000..93774cc --- /dev/null +++ b/lib/weather/getLocation.ts @@ -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 + ); +} diff --git a/lib/weather/getWeather.ts b/lib/weather/getWeather.ts new file mode 100644 index 0000000..b9cc28c --- /dev/null +++ b/lib/weather/getWeather.ts @@ -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; +} \ No newline at end of file diff --git a/lib/weather/wmocode.ts b/lib/weather/wmocode.ts new file mode 100644 index 0000000..d852747 --- /dev/null +++ b/lib/weather/wmocode.ts @@ -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" + } + } +}; diff --git a/package.json b/package.json index 1c563b5..4acdd13 100644 --- a/package.json +++ b/package.json @@ -1,44 +1,53 @@ { - "name": "sparkhome", - "private": false, - "version": "5.2.0", - "type": "module", - "scripts": { - "dev": "bunx --bun vite", - "build": "tsc -b && vite build", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", - "preview": "vite preview" - }, - "dependencies": { - "i18next": "^23.11.5", - "i18next-browser-languagedetector": "^8.0.0", - "i18next-icu": "^2.3.0", - "jotai": "^2.8.3", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-i18next": "^14.1.2", - "react-router": "^6.23.1", - "react-router-dom": "^6.23.1", - "search-engine-autocomplete": "^0.4.3", - "valid-url": "^1.0.9", - "validate-color": "^2.2.4" - }, - "devDependencies": { - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", - "@types/valid-url": "^1.0.7", - "@typescript-eslint/eslint-plugin": "^7.13.1", - "@typescript-eslint/parser": "^7.13.1", - "@vitejs/plugin-react-swc": "^3.5.0", - "autoprefixer": "^10.4.19", - "eslint": "^8.57.0", - "eslint-plugin-react-hooks": "^4.6.2", - "eslint-plugin-react-refresh": "^0.4.7", - "postcss": "^8.4.38", - "tailwindcss": "^3.4.4", - "typescript": "^5.2.2", - "vite": "^5.3.1", - "vite-plugin-pages": "^0.32.2", - "vite-tsconfig-paths": "^4.3.2" - } + "name": "sparkhome", + "private": false, + "version": "5.2.0", + "type": "module", + "scripts": { + "dev": "bun server.ts", + "build": "tsc -b && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "NODE_ENV=production bun server.ts" + }, + "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-browser-languagedetector": "^8.0.0", + "i18next-icu": "^2.3.0", + "jotai": "^2.8.3", + "node-nlp": "^4.27.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-i18next": "^14.1.2", + "react-router": "^6.23.1", + "react-router-dom": "^6.23.1", + "search-engine-autocomplete": "^0.4.3", + "valid-url": "^1.0.9", + "validate-color": "^2.2.4", + "vite-express": "^0.17.0" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@types/valid-url": "^1.0.7", + "@typescript-eslint/eslint-plugin": "^7.13.1", + "@typescript-eslint/parser": "^7.13.1", + "@vitejs/plugin-react-swc": "^3.5.0", + "autoprefixer": "^10.4.19", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.7", + "postcss": "^8.4.38", + "tailwindcss": "^3.4.4", + "typescript": "^5.2.2", + "vite": "^5.3.1", + "vite-plugin-pages": "^0.32.2", + "vite-tsconfig-paths": "^4.3.2" + } } diff --git a/pages/index.tsx b/pages/index.tsx index 060bd98..b57b625 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -4,16 +4,22 @@ import { useAtomValue, useSetAtom } from "jotai"; import Search from "components/search"; import { settingsAtom } from "lib/state/settings"; import { bgFocusAtom } from "lib/state/background"; +import EngineSelector from "components/engineSelector"; +import OneSearch from "components/onesearch/onesearch"; export default function Homepage() { const settings = useAtomValue(settingsAtom); const setBgFocus = useSetAtom(bgFocusAtom); return ( - <div> + <div className="h-full fixed overflow-hidden w-full bg-black"> <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)} /> <Time showSecond={settings.timeShowSecond} /> + <OneSearch /> </div> ); } diff --git a/public/model b/public/model new file mode 100644 index 0000000..5c0b6b7 Binary files /dev/null and b/public/model differ diff --git a/server.ts b/server.ts new file mode 100644 index 0000000..46e9c34 --- /dev/null +++ b/server.ts @@ -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(); \ No newline at end of file diff --git a/src/app.tsx b/src/app.tsx index ce118cb..a432ff6 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -3,19 +3,20 @@ import { useRoutes } from "react-router-dom"; import routes from "~react-pages"; import i18n from "i18next"; import { initReactI18next } from "react-i18next"; -import LanguageDetector from 'i18next-browser-languagedetector'; -import ICU from 'i18next-icu'; -import * as en from "i18n/en.json" -import * as zh from "i18n/zh.json" -import * as ja from "i18n/ja.json" -import * as ar from "i18n/ar.json" -import * as de from "i18n/de.json" -import * as es from "i18n/es.json" -import * as fr from "i18n/fr.json" -import * as it from "i18n/it.json" -import * as ko from "i18n/ko.json" -import * as pt from "i18n/pt.json" -import * as ru from "i18n/ru.json" +import LanguageDetector from "i18next-browser-languagedetector"; +import ICU from "i18next-icu"; +import * as en from "i18n/en.json"; +import * as zh from "i18n/zh.json"; +import * as ja from "i18n/ja.json"; +import * as ar from "i18n/ar.json"; +import * as de from "i18n/de.json"; +import * as es from "i18n/es.json"; +import * as fr from "i18n/fr.json"; +import * as it from "i18n/it.json"; +import * as ko from "i18n/ko.json"; +import * as pt from "i18n/pt.json"; +import * as ru from "i18n/ru.json"; +import { NextUIProvider } from "@nextui-org/react"; i18n.use(initReactI18next) // passes i18n down to react-i18next .use(LanguageDetector) @@ -63,12 +64,15 @@ i18n.use(initReactI18next) // passes i18n down to react-i18next }, detection: { - order: ['navigator'], + order: ["navigator"], caches: [] } }); - export function App() { - return <Suspense fallback={<p>Loading...</p>}>{useRoutes(routes)}</Suspense>; + return ( + <NextUIProvider> + <Suspense fallback={<p>Loading...</p>}>{useRoutes(routes)}</Suspense> + </NextUIProvider> + ); } diff --git a/tailwind.config.ts b/tailwind.config.ts index b52deb2..ca7c3e9 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,9 +1,12 @@ import type { Config } from "tailwindcss"; +import { nextui } from "@nextui-org/react"; + const config: Config = { content: [ "./pages/**/*.{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}" ], theme: { @@ -13,6 +16,8 @@ const config: Config = { "gradient-conic": "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))" } } - } + }, + darkMode: "class", + plugins: [nextui()], }; export default config;