merge: branch dev into dev-nlp
This commit is contained in:
commit
aa6b06147d
5
.gitignore
vendored
5
.gitignore
vendored
@ -34,4 +34,7 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
.syncignore
|
||||
.syncignore
|
||||
|
||||
#log
|
||||
app.log
|
82
app/[locale]/tools/base64/page.tsx
Normal file
82
app/[locale]/tools/base64/page.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
"use client";
|
||||
|
||||
import Switcher from "@/components/switcher";
|
||||
import Notice from "@/components/tools/notice";
|
||||
import base64ToHex from "@/lib/base64ToHex";
|
||||
import copyToClipboard from "@/lib/copy";
|
||||
import normalizeHex from "@/lib/normalizeHex";
|
||||
import { validBase64 } from "@/lib/onesearch/baseCheck";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useEffect, useState } from "react";
|
||||
import { utoa, atou } from "unicode-encode";
|
||||
|
||||
export default function Base64() {
|
||||
const t = useTranslations("tools");
|
||||
const [mode, setMode] = useState("Encode");
|
||||
const [message, setMessage] = useState("");
|
||||
const [messageResult, setMessageResult] = useState("");
|
||||
const [isHex, setHex] = useState(false);
|
||||
const [info, setInfo] = useState("");
|
||||
const [type, setType] = useState("");
|
||||
useEffect(() => {
|
||||
setType("");
|
||||
setInfo("");
|
||||
setHex(false);
|
||||
if (mode == "Encode") {
|
||||
setMessageResult(utoa(message));
|
||||
} else {
|
||||
if (validBase64(message)) {
|
||||
try {
|
||||
setMessageResult(atou(message));
|
||||
} catch (e) {
|
||||
setMessageResult(normalizeHex(base64ToHex(message)));
|
||||
setHex(true);
|
||||
setType("info");
|
||||
setInfo("Showing HEX result.");
|
||||
}
|
||||
} else if (message.trim() !== "") {
|
||||
setMessageResult("");
|
||||
setType("warning");
|
||||
setInfo("Invalid Base64.");
|
||||
} else {
|
||||
setMessageResult("");
|
||||
}
|
||||
}
|
||||
}, [mode, message]);
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-3xl font-semibold">{t("base64.title")}</h1>
|
||||
<Switcher items={["Encode", "Decode"]} selected={mode} setSelected={setMode} class="mt-4" />
|
||||
<textarea
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
className="w-full h-80 mt-4 p-4 rounded-lg bg-zinc-100 dark:bg-zinc-800 resize-none outline-none duration-200 transition-colors-opacity border-2 border-transparent focus:border-zinc-600 dark:focus:border-zinc-300"
|
||||
/>
|
||||
<div className="w-full h-12 mt-4">
|
||||
<span className="w-fit text-2xl font-bold leading-10">Result:</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
copyToClipboard(messageResult);
|
||||
setType("info");
|
||||
setInfo("Copied");
|
||||
setTimeout(() => {
|
||||
setInfo("");
|
||||
setType("");
|
||||
}, 3000);
|
||||
}}
|
||||
className="absolute right-0 w-fit h-10 rounded-md leading-10 bg-zinc-100 dark:bg-zinc-800 hover:bg-zinc-300 hover:dark:bg-zinc-700 px-5 z-10 cursor-pointer duration-300"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className={`empty:py-0 mt-6 w-full h-fit rounded-md leading-10 bg-zinc-100 dark:bg-zinc-800 py-2 px-5 z-10 cursor-pointer duration-100 break-all ${
|
||||
isHex ? "font-mono" : ""
|
||||
}`}
|
||||
>
|
||||
{messageResult}
|
||||
</div>
|
||||
<Notice type={type} info={info} class="mt-4" />
|
||||
</div>
|
||||
);
|
||||
}
|
10
app/[locale]/tools/layout.tsx
Normal file
10
app/[locale]/tools/layout.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
export default function ToolsLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="h-screen w-screen overflow-x-hidden bg-white dark:bg-[rgb(23,25,29)]">
|
||||
<main className="relative h-full w-full md:w-3/4 lg:w-1/2 left-0 md:left-[12.5%] lg:left-1/4
|
||||
pt-12">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
8
app/api/error/route.ts
Normal file
8
app/api/error/route.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { logger } from "@/lib/log";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const data = await req.json();
|
||||
logger.warn({type:"client_telemetering",error:data});
|
||||
return new Response();
|
||||
}
|
@ -1,9 +1,14 @@
|
||||
import { completeGoogle } from "search-engine-autocomplete";
|
||||
import { NextRequest } from "next/server"
|
||||
import { suggestionsResponse } from "@/global";
|
||||
import { logger } from "@/lib/log";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const query = request.nextUrl.searchParams.get('q')!;
|
||||
const language = "ja-JP";
|
||||
const t = parseInt(request.nextUrl.searchParams.get('t') || "0") || null;
|
||||
let language = request.nextUrl.searchParams.get('l');
|
||||
if (language === null) language = 'en-US';
|
||||
const data = await completeGoogle(query, language);
|
||||
return new Response(JSON.stringify(data));
|
||||
logger.info({ type: "onesearch_search_autocomplete", query: query, data: data });
|
||||
return new Response(JSON.stringify({...data, time: t} as suggestionsResponse));
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useRecoilState, useSetRecoilState } from "recoil";
|
||||
import { useRecoilValue, useSetRecoilState } from "recoil";
|
||||
import { settingsState } from "./state/settings";
|
||||
import Search from "./search/search";
|
||||
import { bgFocusState } from "./state/background";
|
||||
@ -17,7 +16,7 @@ const Time = dynamic(() => import("./time"), {
|
||||
});
|
||||
|
||||
export default function Homepage() {
|
||||
const [settings, setSettings] = useRecoilState(settingsState);
|
||||
const settings = useRecoilValue(settingsState);
|
||||
const setFocus = useSetRecoilState(bgFocusState);
|
||||
|
||||
return (
|
||||
|
@ -6,12 +6,13 @@ import { useTranslations } from "next-intl";
|
||||
import { useRecoilValue, useSetRecoilState } from "recoil";
|
||||
import { settingsState } from "../state/settings";
|
||||
import { engineTranslation } from "./translatedEngineList";
|
||||
import { settingsType } from "@/global";
|
||||
|
||||
export default function(
|
||||
props: { className: string }
|
||||
) {
|
||||
const t = useTranslations("Search");
|
||||
const settings: settings = useRecoilValue(settingsState);
|
||||
const settings: settingsType = useRecoilValue(settingsState);
|
||||
const items = settings.searchEngines;
|
||||
const currentEngine: string = settings.currentSearchEngine;
|
||||
const displayEngine = getName(currentEngine);
|
||||
@ -20,7 +21,7 @@ export default function(
|
||||
const setSettings = useSetRecoilState(settingsState);
|
||||
|
||||
function setEngine(engine: string) {
|
||||
setSettings((oldSettings) => {
|
||||
setSettings((oldSettings: settingsType) => {
|
||||
return {
|
||||
...oldSettings,
|
||||
currentSearchEngine: engine
|
||||
|
26
components/search/onesearch/handleEnter.ts
Normal file
26
components/search/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();
|
||||
}
|
||||
}
|
30
components/search/onesearch/link.tsx
Normal file
30
components/search/onesearch/link.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { normalizeURL } from "@/lib/normalizeURL";
|
||||
|
||||
export default function (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>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,23 +1,159 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import SuggestionBox from "./suggestionBox";
|
||||
import Suggestion from "./suggestion";
|
||||
import { useRecoilValue } from "recoil";
|
||||
import { useRecoilState, useRecoilValue } from "recoil";
|
||||
import { queryState } from "@/components/state/query";
|
||||
import { useLocale, useTranslations } from "next-intl";
|
||||
import { suggestionItem, suggestionsResponse } from "@/global";
|
||||
import getSearchEngineName from "@/lib/getSearchEngineName";
|
||||
import PlainSearch from "./plainSearch";
|
||||
import { suggestionsState } from "@/components/state/suggestion";
|
||||
import validLink from "@/lib/url/validLink";
|
||||
import Link from "./link";
|
||||
import { selectedSuggestionState } from "@/components/state/suggestionSelection";
|
||||
import { settingsState } from "@/components/state/settings";
|
||||
import { base64NLP } from "@/lib/onesearch/baseCheck";
|
||||
import PlainText from "./plainText";
|
||||
import { sendError } from "@/lib/telemetering/sendError";
|
||||
|
||||
export default function () {
|
||||
const [suggestion, setSuggetsion] = useState([]);
|
||||
const [suggestion, setFinalSuggetsion] = useRecoilState(suggestionsState);
|
||||
const lastRequestTimeRef = useRef(0);
|
||||
const selected = useRecoilValue(selectedSuggestionState);
|
||||
const settings = useRecoilValue(settingsState);
|
||||
const devMode = true;
|
||||
const query = useRecoilValue(queryState);
|
||||
const engineName = getSearchEngineName();
|
||||
const engine = settings.currentSearchEngine;
|
||||
const lang = useLocale();
|
||||
const t = useTranslations("Search");
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`/api/suggestion?q=${query}`)
|
||||
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) => {
|
||||
setSuggetsion(data);
|
||||
.then((data: suggestionsResponse) => {
|
||||
try {
|
||||
let suggestionToUpdate: suggestionItem[] = data.suggestions;
|
||||
if (data.time > lastRequestTimeRef.current) {
|
||||
cleanSuggestion("NAVIGATION", "QUERY");
|
||||
lastRequestTimeRef.current = data.time;
|
||||
updateSuggestion(suggestionToUpdate);
|
||||
}
|
||||
} catch (error: Error | any) {
|
||||
sendError(error);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
// Handle fetch error
|
||||
sendError(error);
|
||||
});
|
||||
}, [query]);
|
||||
|
||||
function updateSuggestion(data: suggestionItem[]) {
|
||||
setFinalSuggetsion((cur: suggestionItem[]) => {
|
||||
const types: string[] = [];
|
||||
for (let sug of data) {
|
||||
if (!types.includes(sug.type)) types.push(sug.type);
|
||||
}
|
||||
for (let 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
]);
|
||||
}
|
||||
const b64 = base64NLP(query);
|
||||
if (b64.suggestion !== null) {
|
||||
updateSuggestion([b64 as suggestionItem]);
|
||||
}
|
||||
}, [query, engineName]);
|
||||
|
||||
return (
|
||||
<SuggestionBox>
|
||||
{suggestion.map((s: string) => {
|
||||
return <Suggestion key={s}>{s}</Suggestion>;
|
||||
{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/search/onesearch/plainSearch.tsx
Normal file
35
components/search/onesearch/plainSearch.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import search from "@/lib/search";
|
||||
import { settingsState } from "@/components/state/settings";
|
||||
import { useRecoilValue } from "recoil";
|
||||
|
||||
export default function (props: { children: React.ReactNode; query: string; selected: boolean }) {
|
||||
const settings = useRecoilValue(settingsState);
|
||||
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/search/onesearch/plainText.tsx
Normal file
21
components/search/onesearch/plainText.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
export default function (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>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
export default function(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className={`w-full h-10 leading-10 bg-slate-200 dark:bg-zinc-800 hover:bg-zinc-700 px-5 z-10 cursor-pointer duration-100`}>
|
||||
<p>{props.children}</p>
|
||||
<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>
|
||||
);
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
export default function(props: { children: React.ReactNode }) {
|
||||
export default function(props: { children?: React.ReactNode }) {
|
||||
return (
|
||||
<div className="relative bg-slate-200 dark:bg-zinc-800 w-11/12 sm:w-[700px] h-auto left-1/2
|
||||
translate-x-[-50%] top-72 z-20 rounded overflow-hidden duration-250">
|
||||
<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>
|
||||
);
|
||||
|
@ -3,28 +3,36 @@
|
||||
import { useRecoilState, useRecoilValue } from "recoil";
|
||||
import { settingsState } from "../state/settings";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState } from "react";
|
||||
import { normalizeURL } from "@/lib/normalizeURL";
|
||||
import validLink from "@/lib/url/validLink";
|
||||
import { queryState } from "../state/query";
|
||||
import { settingsType } from "@/global";
|
||||
import handleEnter from "./onesearch/handleEnter";
|
||||
import { selectedSuggestionState } from "../state/suggestionSelection";
|
||||
import { suggestionsState } from "../state/suggestion";
|
||||
import { KeyboardEvent, useRef } from "react";
|
||||
|
||||
export default function Search(props: { onFocus: () => void }) {
|
||||
const settings: settings = useRecoilValue(settingsState);
|
||||
const settings: settingsType = useRecoilValue(settingsState);
|
||||
const t = useTranslations("Search");
|
||||
const [query, setQuery] = useRecoilState(queryState);
|
||||
const [selectedSuggestion, setSelected] = useRecoilState(selectedSuggestionState);
|
||||
const suggestions = useRecoilValue(suggestionsState);
|
||||
const searchBoxRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
let style = "default";
|
||||
|
||||
function handleKeydown(e: any) {
|
||||
let URL = "";
|
||||
if (validLink(query)) {
|
||||
URL = normalizeURL(query);
|
||||
} else {
|
||||
URL = settings.searchEngines[settings.currentSearchEngine];
|
||||
URL = URL.replace("%s", query);
|
||||
}
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key == "Enter") {
|
||||
location.href = URL;
|
||||
e.preventDefault();
|
||||
handleEnter(selectedSuggestion, suggestions, query, settings, searchBoxRef);
|
||||
return;
|
||||
} else if (e.key == "ArrowUp") {
|
||||
e.preventDefault();
|
||||
const len = suggestions.length;
|
||||
setSelected((selectedSuggestion - 1 + len) % len);
|
||||
} else if (e.key == "ArrowDown") {
|
||||
e.preventDefault();
|
||||
const len = suggestions.length;
|
||||
setSelected((selectedSuggestion + 1) % len);
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,6 +59,7 @@ export default function Search(props: { onFocus: () => void }) {
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
ref={searchBoxRef}
|
||||
value={query}
|
||||
/>
|
||||
</div>
|
||||
@ -74,6 +83,7 @@ export default function Search(props: { onFocus: () => void }) {
|
||||
type="text"
|
||||
placeholder={t("placeholder")}
|
||||
onFocus={props.onFocus}
|
||||
ref={searchBoxRef}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,35 +1,50 @@
|
||||
import { settingsType } from "@/global";
|
||||
import isLocalStorageAvailable from "@/lib/isLocalStorageAvailable";
|
||||
import { atom } from "recoil";
|
||||
|
||||
const defaultSettings: settings = {
|
||||
version: 1,
|
||||
elementBackdrop: true,
|
||||
bgBlur: true,
|
||||
timeShowSecond: false,
|
||||
currentSearchEngine: "google",
|
||||
searchEngines: {
|
||||
google: "https://www.google.com/search?q=%s",
|
||||
bing: "https://www.bing.com/search?q=%s",
|
||||
baidu: "https://www.baidu.com/s?wd=%s",
|
||||
duckduckgo: "https://duckduckgo.com/?q=%s",
|
||||
yandex: "https://yandex.com/search/?text=%s",
|
||||
yahoo: "https://search.yahoo.com/search?p=%s",
|
||||
ecosia: "https://www.ecosia.org/search?q=%s"
|
||||
const defaultSettings: settingsType = {
|
||||
"version": 2,
|
||||
"elementBackdrop": true,
|
||||
"bgBlur": true,
|
||||
"timeShowSecond": false,
|
||||
"currentSearchEngine": "google",
|
||||
"searchInNewTab": true,
|
||||
"searchEngines": {
|
||||
"google": "https://www.google.com/search?q=%s",
|
||||
"bing": "https://www.bing.com/search?q=%s",
|
||||
"baidu": "https://www.baidu.com/s?wd=%s",
|
||||
"duckduckgo": "https://duckduckgo.com/?q=%s",
|
||||
"yandex": "https://yandex.com/search/?text=%s",
|
||||
"yahoo": "https://search.yahoo.com/search?p=%s",
|
||||
"ecosia": "https://www.ecosia.org/search?q=%s"
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const localStorageEffect =
|
||||
(key: any) =>
|
||||
(key: string) =>
|
||||
({ setSelf, onSet }: any) => {
|
||||
if (isLocalStorageAvailable()===false){
|
||||
return;
|
||||
}
|
||||
const savedValue = localStorage.getItem(key);
|
||||
if (savedValue != null) {
|
||||
setSelf(JSON.parse(savedValue));
|
||||
if (localStorage.getItem(key) === null) {
|
||||
localStorage.setItem(key, JSON.stringify(defaultSettings));
|
||||
return;
|
||||
}
|
||||
let settings =JSON.parse(JSON.stringify(defaultSettings));
|
||||
const savedSettings = localStorage.getItem(key)!;
|
||||
const parsedSettings = JSON.parse(savedSettings);
|
||||
|
||||
Object.keys(settings).map((key) => {
|
||||
if (parsedSettings[key] !== undefined && key !== "version"){
|
||||
settings[key] = parsedSettings[key];
|
||||
}
|
||||
})
|
||||
|
||||
onSet((newValue: settings) => {
|
||||
setSelf(settings);
|
||||
localStorage.setItem(key, JSON.stringify(settings));
|
||||
|
||||
onSet((newValue: settingsType) => {
|
||||
localStorage.setItem(key, JSON.stringify(newValue));
|
||||
});
|
||||
};
|
||||
|
11
components/state/suggestion.ts
Normal file
11
components/state/suggestion.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { suggestionItem } from "@/global";
|
||||
import { atom } from "recoil";
|
||||
|
||||
const suggestionsState = atom({
|
||||
key: "oneSearchSuggestions",
|
||||
default: [] as suggestionItem[]
|
||||
});
|
||||
|
||||
export {
|
||||
suggestionsState,
|
||||
}
|
10
components/state/suggestionSelection.ts
Normal file
10
components/state/suggestionSelection.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { atom } from "recoil";
|
||||
|
||||
const selectedSuggestionState = atom({
|
||||
key: "selectedSuggestion",
|
||||
default: 0
|
||||
});
|
||||
|
||||
export {
|
||||
selectedSuggestionState,
|
||||
}
|
31
components/switcher.tsx
Normal file
31
components/switcher.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
|
||||
export default function Switcher(props: { items: string[]; selected: string, setSelected: Function, class?: string }) {
|
||||
const selectedRef = useRef(null);
|
||||
const [selectedCoordinate, setSelectedCoordinate] = useState(0);
|
||||
const [selectedWidth, setSelectedWidth] = useState(0);
|
||||
useEffect(() => {
|
||||
if (selectedRef.current){
|
||||
setSelectedCoordinate((selectedRef.current as HTMLElement)?.offsetLeft);
|
||||
setSelectedWidth((selectedRef.current as HTMLElement)?.getBoundingClientRect().width);
|
||||
}
|
||||
}, [props.selected]);
|
||||
|
||||
return (
|
||||
<div className={`relative w-fit h-12 px-1 flex rounded-lg bg-zinc-100 dark:bg-zinc-800 z-0 ${props.class}`}>
|
||||
{props.items.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="relative mt-[0.375rem] rounded-md w-fit h-9 leading-9 px-4 mx-1 z-20 cursor-pointer duration-100"
|
||||
ref={item == props.selected ? selectedRef : null}
|
||||
onClick={() => props.setSelected(item)}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
<div className="absolute mt-[0.375rem] rounded-md h-9 bg-zinc-300 dark:bg-zinc-600 z-10 duration-250 ease-[cubic-bezier(.15,.16,.2,1.2)]" style={{ left: selectedCoordinate, width: selectedWidth }}></div>
|
||||
</div>
|
||||
);
|
||||
}
|
32
components/tools/notice.tsx
Normal file
32
components/tools/notice.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { Icon } from "@iconify-icon/react";
|
||||
|
||||
const typeToColor: Record<string, string> = {
|
||||
success: "bg-green-500",
|
||||
info: "bg-blue-500",
|
||||
warning: "bg-orange-500",
|
||||
error: "bg-red-500"
|
||||
};
|
||||
|
||||
const typeToIcon: Record<string, string> = {
|
||||
success: "material-symbols:check-circle",
|
||||
info: "material-symbols:info",
|
||||
warning: "material-symbols:warning",
|
||||
error: "material-symbols:error"
|
||||
};
|
||||
|
||||
export default function Notice(props: { type: string; info: string; class?: string }) {
|
||||
if (props.type && props.info)
|
||||
return (
|
||||
<div
|
||||
className={`relative ${props.class} ${
|
||||
typeToColor[props.type]
|
||||
} rounded-md w-full min-h-12 h-fit empty:px-0 px-4 z-20 cursor-pointer duration-100 `}
|
||||
>
|
||||
<Icon className="text-2xl mt-3" icon={typeToIcon[props.type]} />
|
||||
<span className="absolute text-base mt-3 ml-1">{props.info}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
39
global.d.ts
vendored
39
global.d.ts
vendored
@ -1,10 +1,31 @@
|
||||
type settings = {
|
||||
version: number;
|
||||
elementBackdrop: boolean;
|
||||
bgBlur: boolean;
|
||||
timeShowSecond: boolean;
|
||||
currentSearchEngine: string;
|
||||
searchEngines: {
|
||||
[key: string]: string,
|
||||
};
|
||||
import { Suggestion } from "search-engine-autocomplete";
|
||||
|
||||
interface settingsType extends Object{
|
||||
"version": number,
|
||||
"elementBackdrop": boolean,
|
||||
"bgBlur": boolean,
|
||||
"timeShowSecond": boolean,
|
||||
"currentSearchEngine": string,
|
||||
"searchInNewTab": boolean,
|
||||
"searchEngines": {
|
||||
[key: string]: string
|
||||
},
|
||||
};
|
||||
|
||||
interface suggestionsResponse extends Object{
|
||||
suggestions: Suggestion[],
|
||||
query: string,
|
||||
verbatimRelevance: number,
|
||||
time: number
|
||||
}
|
||||
|
||||
type suggestionItem = {
|
||||
suggestion: string,
|
||||
type: string,
|
||||
relativeRelevance?: number,
|
||||
relevance: number,
|
||||
prompt?: string | React.ReactElement,
|
||||
intention?: string | null,
|
||||
probability?: number,
|
||||
confidence?: number,
|
||||
}
|
26
i18n.ts
26
i18n.ts
@ -1,14 +1,14 @@
|
||||
import {notFound} from 'next/navigation';
|
||||
import {getRequestConfig} from 'next-intl/server';
|
||||
|
||||
import { notFound } from "next/navigation";
|
||||
import { getRequestConfig } from "next-intl/server";
|
||||
|
||||
// Can be imported from a shared config
|
||||
const locales = ['en', 'zh'];
|
||||
|
||||
export default getRequestConfig(async ({locale}) => {
|
||||
// Validate that the incoming `locale` parameter is valid
|
||||
if (!locales.includes(locale as any)) notFound();
|
||||
|
||||
return {
|
||||
messages: (await import(`/messages/${locale}.json`)).default
|
||||
};
|
||||
});
|
||||
const locales = ["en-US", "zh-CN"];
|
||||
|
||||
export default getRequestConfig(async ({ locale }) => {
|
||||
// Validate that the incoming `locale` parameter is valid
|
||||
if (!locales.includes(locale as any)) notFound();
|
||||
|
||||
return {
|
||||
messages: (await import(`/messages/${locale}.json`)).default
|
||||
};
|
||||
});
|
||||
|
15
lib/base64ToHex.ts
Normal file
15
lib/base64ToHex.ts
Normal file
@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Converts a base64 string to a hexadecimal string.
|
||||
*
|
||||
* @param {string} base64String - The base64 string to convert.
|
||||
* @return {string} The hexadecimal representation of the base64 string.
|
||||
*/
|
||||
export default function base64ToHex(base64String: string): string {
|
||||
const raw = atob(base64String);
|
||||
let result = "";
|
||||
for (let i = 0; i < raw.length; i++) {
|
||||
const hex = raw.charCodeAt(i).toString(16);
|
||||
result += hex.length === 2 ? hex : "0" + hex;
|
||||
}
|
||||
return result.toUpperCase();
|
||||
}
|
8
lib/copy.ts
Normal file
8
lib/copy.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export default function copyToClipboard(value: string){
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = value;
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(textarea);
|
||||
};
|
17
lib/getSearchEngineName.ts
Normal file
17
lib/getSearchEngineName.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { engineTranslation } from "@/components/search/translatedEngineList";
|
||||
import { settingsState } from "@/components/state/settings";
|
||||
import { settingsType } from "@/global";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useRecoilValue } from "recoil";
|
||||
|
||||
export default function(){
|
||||
const settings: settingsType = useRecoilValue(settingsState);
|
||||
const currentEngine = settings.currentSearchEngine;
|
||||
const displayEngine = getName(currentEngine);
|
||||
return displayEngine;
|
||||
}
|
||||
|
||||
function getName(engineKey: string) {
|
||||
const t = useTranslations("Search");
|
||||
return engineTranslation.includes(engineKey) ? t(`engine.${engineKey}`) : engineKey;
|
||||
}
|
10
lib/log.ts
Normal file
10
lib/log.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import pino from "pino";
|
||||
|
||||
export const logger = pino({
|
||||
level: process.env.PINO_LOG_LEVEL || "info",
|
||||
formatters: {
|
||||
level: (label) => {
|
||||
return { level: label.toUpperCase() };
|
||||
}
|
||||
}
|
||||
},pino.destination(`./app.log`));
|
7
lib/nlp/extract.ts
Normal file
7
lib/nlp/extract.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export default function slotExtract(str: string, keywords: string[]) {
|
||||
let r = str;
|
||||
for (let keyword of keywords) {
|
||||
r = r.replace(keyword, "");
|
||||
}
|
||||
return r.trim();
|
||||
}
|
9
lib/nlp/stopwords.ts
Normal file
9
lib/nlp/stopwords.ts
Normal file
@ -0,0 +1,9 @@
|
||||
const stopwords = ["a","about","above","after","again","against","all","am","an","and","any","are","aren't","as","at","be","because","been","before","being","below","between","both","but","by","can't","cannot","could","couldn't","did","didn't","do","does","doesn't","doing","don't","down","during","each","few","for","from","further","had","hadn't","has","hasn't","have","haven't","having","he","he'd","he'll","he's","her","here","here's","hers","herself","him","himself","his","how","how's","i","i'd","i'll","i'm","i've","if","in","into","is","isn't","it","it's","its","itself","let's","me","more","most","mustn't","my","myself","no","nor","not","of","off","on","once","only","or","other","ought","our","ours ourselves","out","over","own","same","shan't","she","she'd","she'll","she's","should","shouldn't","so","some","such","than","that","that's","the","their","theirs","them","themselves","then","there","there's","these","they","they'd","they'll","they're","they've","this","those","through","to","too","under","until","up","very","was","wasn't","we","we'd","we'll","we're","we've","were","weren't","what","what's","when","when's","where","where's","which","while","who","who's","whom","why","why's","with","won't","would","wouldn't","you","you'd","you'll","you're","you've","your","yours","yourself","yourselves"];
|
||||
|
||||
export default function removeStopwords(str: string, extraStopwords: string[] = [], disableDefault: boolean = false){
|
||||
const list = disableDefault ? extraStopwords : stopwords.concat(extraStopwords);
|
||||
for (let word of list){
|
||||
str = str.replace(new RegExp(`\\b${word}\\b`, 'gi'), '');
|
||||
}
|
||||
return str;
|
||||
}
|
16
lib/normalizeHex.ts
Normal file
16
lib/normalizeHex.ts
Normal file
@ -0,0 +1,16 @@
|
||||
/**
|
||||
* A description of the entire function.
|
||||
*
|
||||
* @param {string} hexString - The input hexadecimal string to normalize.
|
||||
* @return {string} The normalized hexadecimal string.
|
||||
*/
|
||||
export default function normalizeHex(hexString: string): string {
|
||||
const chunkSize = 4;
|
||||
const chunks: string[] = [];
|
||||
|
||||
for (let i = 0; i < hexString.length; i += chunkSize) {
|
||||
chunks.push(hexString.substr(i, chunkSize));
|
||||
}
|
||||
|
||||
return chunks.join(' ');
|
||||
}
|
@ -12,7 +12,6 @@ export function normalizeURL(input: string): string {
|
||||
return urlWithHTTP.href;
|
||||
} catch (error) {
|
||||
// if the URL is still invalid, return the original input
|
||||
console.error("Invalid URL:", input);
|
||||
return input;
|
||||
}
|
||||
}
|
||||
|
17
lib/onesearch/NLPResult.ts
Normal file
17
lib/onesearch/NLPResult.ts
Normal file
@ -0,0 +1,17 @@
|
||||
export class NLPResult {
|
||||
constructor(
|
||||
public suggestion: string | null,
|
||||
public intention: string | null,
|
||||
public probability: number,
|
||||
public confidence: number,
|
||||
public relevanceBase: number = 2000,
|
||||
public confidenceWeight: number = 0.2,
|
||||
public type: string = "text",
|
||||
public prompt?: string | React.ReactElement
|
||||
) {
|
||||
}
|
||||
|
||||
get relevance(): number {
|
||||
return this.relevanceBase * this.probability + this.confidence * this.relevanceBase * this.confidenceWeight;
|
||||
}
|
||||
}
|
90
lib/onesearch/baseCheck.tsx
Normal file
90
lib/onesearch/baseCheck.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
import slotExtract from "../nlp/extract";
|
||||
import removeStopwords from "../nlp/stopwords";
|
||||
import { NLPResult } from "./NLPResult";
|
||||
import { Kbd } from "@nextui-org/react";
|
||||
|
||||
interface KeywordsDict {
|
||||
[key: string]: number;
|
||||
}
|
||||
|
||||
interface IntentionsDict {
|
||||
[key: string]: number;
|
||||
}
|
||||
|
||||
export function validBase64(str: string) {
|
||||
return str.length % 4 == 0 && /^[A-Za-z0-9+/]+[=]{0,2}$/.test(str);
|
||||
}
|
||||
|
||||
export function base64NLP(str: string) {
|
||||
const keywords: KeywordsDict = {
|
||||
base64: 1,
|
||||
b64: 0.95,
|
||||
base: 0.5
|
||||
};
|
||||
let result = new NLPResult(null, null, 0.0, 0.0);
|
||||
for (let keyword of Object.keys(keywords)) {
|
||||
const pos = str.trim().indexOf(keyword);
|
||||
const l = str.length;
|
||||
const w = str.split(" ").length;
|
||||
if (w > 1 && (pos === 0 || pos == l)) {
|
||||
result.probability += keywords[keyword];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const intentions: IntentionsDict = {
|
||||
decode: 0.1,
|
||||
encode: 1
|
||||
};
|
||||
for (let intention of Object.keys(intentions)) {
|
||||
const pos = str.trim().indexOf(intention);
|
||||
const w = str.split(" ").length;
|
||||
if (w > 1 && pos !== -1) {
|
||||
result.confidence += intentions[intention];
|
||||
result.intention = `base64.${intention}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let processedQuery = str;
|
||||
if (result.intention === "base64.encode") {
|
||||
const blacklist = Object.keys(keywords).concat(Object.keys(intentions)).concat(["convert", "turn"]);
|
||||
processedQuery = slotExtract(str, blacklist);
|
||||
} else if (result.intention === "base64.decode") {
|
||||
processedQuery = removeStopwords(str, Object.keys(keywords).concat(Object.keys(intentions))).trim();
|
||||
}
|
||||
if (result.intention === "base64.decode") {
|
||||
if (validBase64(processedQuery)) {
|
||||
result.confidence = 1;
|
||||
} else {
|
||||
result.confidence = 0;
|
||||
}
|
||||
} else if (validBase64(processedQuery) && result.intention !== "base64.encode") {
|
||||
result.intention = "base64.decode";
|
||||
result.confidence += Math.max(1 / Math.log2(1 / processedQuery.length) + 1, 0);
|
||||
result.probability += Math.max(1 / Math.log2(1 / processedQuery.length) + 1, 0);
|
||||
}
|
||||
|
||||
switch (result.intention) {
|
||||
case "base64.encode":
|
||||
result.suggestion = btoa(processedQuery);
|
||||
result.prompt = (
|
||||
<span>
|
||||
Base64 Encode (Hit <Kbd keys={["enter"]}></Kbd> to copy):
|
||||
</span>
|
||||
);
|
||||
break;
|
||||
case "base64.decode":
|
||||
if (result.confidence > 0.1) result.suggestion = atob(processedQuery);
|
||||
result.prompt = (
|
||||
<span>
|
||||
Base64 Decode (Hit <Kbd keys={["enter"]}></Kbd> to copy):
|
||||
</span>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
4
lib/search.ts
Normal file
4
lib/search.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export default function(query: string, engine: string, newTab: boolean = true) {
|
||||
if(newTab) window.open(engine.replace("%s", query));
|
||||
else window.location.href = engine.replace("%s", query);
|
||||
}
|
19
lib/telemetering/sendError.ts
Normal file
19
lib/telemetering/sendError.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { CLIENT_VERSION } from "../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,3 +1,3 @@
|
||||
export const SPARKHOME_VERSION="4.7.2";
|
||||
export const CLIENT_VERSION="4.7.1";
|
||||
export const NEXT_API_VERSION="4.7.1";
|
||||
export const SPARKHOME_VERSION="4.14.3";
|
||||
export const CLIENT_VERSION="4.14.2";
|
||||
export const NEXT_API_VERSION="4.14.3";
|
@ -10,12 +10,18 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
@ -10,9 +10,15 @@
|
||||
"yandex": "Yandex",
|
||||
"yahoo": "雅虎",
|
||||
"ecosia": "Ecosia"
|
||||
}
|
||||
},
|
||||
"search-help-text": "{engine} 搜索"
|
||||
},
|
||||
"404": {
|
||||
"title": "未找到"
|
||||
},
|
||||
"tools": {
|
||||
"base64": {
|
||||
"title": "Base64 工具"
|
||||
}
|
||||
}
|
||||
}
|
@ -2,13 +2,13 @@ import createMiddleware from 'next-intl/middleware';
|
||||
|
||||
export default createMiddleware({
|
||||
// A list of all locales that are supported
|
||||
locales: ['en', 'zh'],
|
||||
locales: ['en-US', 'zh-CN'],
|
||||
|
||||
// Used when no locale matches
|
||||
defaultLocale: 'en'
|
||||
defaultLocale: 'en-US'
|
||||
});
|
||||
|
||||
export const config = {
|
||||
// Match only internationalized pathnames
|
||||
matcher: ['/', '/(zh|en)/:path*']
|
||||
matcher: ['/', '/(zh-CN|en-US)/:path*']
|
||||
};
|
29
package.json
29
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sparkhome",
|
||||
"version": "4.10.0",
|
||||
"version": "4.10.2",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
@ -10,31 +10,34 @@
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nextui-org/react": "^2.2.10",
|
||||
"clsx": "^2.1.0",
|
||||
"framer-motion": "^11.0.25",
|
||||
"@nextui-org/react": "^2.3.6",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^11.1.7",
|
||||
"next": "14.1.4",
|
||||
"next-intl": "^3.11.1",
|
||||
"next-intl": "^3.12.0",
|
||||
"next-themes": "^0.3.0",
|
||||
"pino": "^9.0.0",
|
||||
"punycode": "^2.3.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0",
|
||||
"recoil": "^0.7.7",
|
||||
"search-engine-autocomplete": "^0.4.2",
|
||||
"tailwind-merge": "^2.2.2",
|
||||
"search-engine-autocomplete": "^0.4.3",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"unicode-encode": "^1.4.2",
|
||||
"valid-url": "^1.0.9",
|
||||
"validate-color": "^2.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-icon/react": "^2.1.0",
|
||||
"@jest/globals": "^29.7.0",
|
||||
"@testing-library/jest-dom": "^6.4.2",
|
||||
"@testing-library/react": "^14.3.0",
|
||||
"@testing-library/react": "^14.3.1",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/node": "^20.12.7",
|
||||
"@types/punycode": "^2.1.4",
|
||||
"@types/react": "^18.2.75",
|
||||
"@types/react-dom": "^18.2.24",
|
||||
"@types/react": "^18.3.0",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/valid-url": "^1.0.7",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"jest": "^29.7.0",
|
||||
@ -42,6 +45,6 @@
|
||||
"postcss": "^8.4.38",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"ts-jest": "^29.1.2",
|
||||
"typescript": "^5.4.4"
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
}
|
||||
|
9944
pnpm-lock.yaml
9944
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
import {nextui} from "@nextui-org/react";
|
||||
import { nextui } from "@nextui-org/react";
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
|
11
test/base64.test.ts
Normal file
11
test/base64.test.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { base64NLP } from "@/lib/onesearch/baseCheck";
|
||||
import { describe, expect, test } from "@jest/globals";
|
||||
|
||||
describe("To auto-detect the intention of decoding an base64 string", () => {
|
||||
test("Implicit declaration", () => {
|
||||
expect(base64NLP("base64 encode encode MjM6MjQgQXByIDI1LCAyMDI0").intention).toBe("base64.encode");
|
||||
expect(base64NLP("base64 encode encode MjM6MjQgQXByIDI1LCAyMDI0").suggestion).toBe(
|
||||
"ZW5jb2RlIE1qTTZNalFnUVhCeUlESTFMQ0F5TURJMA=="
|
||||
);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user