Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
Commit
•
b8c4528
1
Parent(s):
e3cc490
working on AI Tube search engine
Browse files- .env +0 -2
- package-lock.json +39 -0
- package.json +6 -1
- src/app/interface/search-input/index.tsx +144 -0
- src/app/interface/top-header/index.tsx +23 -4
- src/app/server/actions/ai-tube-hf/getVideos.ts +41 -4
- src/app/server/actions/ai-tube-robot/README.md +0 -3
- src/app/server/actions/ai-tube-robot/updateQueue.ts +0 -42
- src/app/server/actions/config.ts +0 -2
- src/app/server/actions/redis.ts +9 -0
- src/app/state/useStore.ts +41 -1
- src/components/ui/popover.tsx +1 -1
.env
CHANGED
@@ -8,8 +8,6 @@ NEXT_PUBLIC_AI_TUBE_OAUTH_CLIENT_ID="35c3efbc-d51f-4763-b5ea-3e149c6158e5"
|
|
8 |
ADMIN_HUGGING_FACE_API_TOKEN=""
|
9 |
ADMIN_HUGGING_FACE_USERNAME=""
|
10 |
|
11 |
-
AI_TUBE_ROBOT_API="https://jbilcke-hf-ai-tube-robot.hf.space"
|
12 |
-
|
13 |
UPSTASH_REDIS_REST_URL=""
|
14 |
UPSTASH_REDIS_REST_TOKEN=""
|
15 |
|
|
|
8 |
ADMIN_HUGGING_FACE_API_TOKEN=""
|
9 |
ADMIN_HUGGING_FACE_USERNAME=""
|
10 |
|
|
|
|
|
11 |
UPSTASH_REDIS_REST_URL=""
|
12 |
UPSTASH_REDIS_REST_TOKEN=""
|
13 |
|
package-lock.json
CHANGED
@@ -10,6 +10,7 @@
|
|
10 |
"dependencies": {
|
11 |
"@huggingface/hub": "0.12.3-oauth",
|
12 |
"@huggingface/inference": "^2.6.4",
|
|
|
13 |
"@photo-sphere-viewer/core": "^5.5.1",
|
14 |
"@photo-sphere-viewer/video-plugin": "^5.5.1",
|
15 |
"@radix-ui/react-accordion": "^1.1.2",
|
@@ -30,6 +31,7 @@
|
|
30 |
"@radix-ui/react-toast": "^1.1.4",
|
31 |
"@radix-ui/react-tooltip": "^1.0.6",
|
32 |
"@react-spring/web": "^9.7.3",
|
|
|
33 |
"@types/node": "20.4.2",
|
34 |
"@types/react": "18.2.15",
|
35 |
"@types/react-dom": "18.2.7",
|
@@ -45,9 +47,12 @@
|
|
45 |
"date-fns": "^2.30.0",
|
46 |
"eslint": "8.45.0",
|
47 |
"eslint-config-next": "13.4.10",
|
|
|
48 |
"hash-wasm": "^4.11.0",
|
|
|
49 |
"lucide-react": "^0.260.0",
|
50 |
"markdown-yaml-metadata-parser": "^3.0.0",
|
|
|
51 |
"next": "^14.1.0",
|
52 |
"photo-sphere-viewer-lensflare-plugin": "^2.0.1",
|
53 |
"pick": "^0.0.1",
|
@@ -982,6 +987,14 @@
|
|
982 |
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
983 |
}
|
984 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
985 |
"node_modules/@jridgewell/gen-mapping": {
|
986 |
"version": "0.3.3",
|
987 |
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
|
@@ -2602,6 +2615,19 @@
|
|
2602 |
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
2603 |
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="
|
2604 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2605 |
"node_modules/@types/node": {
|
2606 |
"version": "20.4.2",
|
2607 |
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.2.tgz",
|
@@ -4860,6 +4886,14 @@
|
|
4860 |
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
|
4861 |
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="
|
4862 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4863 |
"node_modules/fastparse": {
|
4864 |
"version": "1.1.2",
|
4865 |
"resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz",
|
@@ -6087,6 +6121,11 @@
|
|
6087 |
"node": ">=16 || 14 >=14.17"
|
6088 |
}
|
6089 |
},
|
|
|
|
|
|
|
|
|
|
|
6090 |
"node_modules/mkdirp-classic": {
|
6091 |
"version": "0.5.3",
|
6092 |
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
|
|
10 |
"dependencies": {
|
11 |
"@huggingface/hub": "0.12.3-oauth",
|
12 |
"@huggingface/inference": "^2.6.4",
|
13 |
+
"@jcoreio/async-throttle": "^1.6.0",
|
14 |
"@photo-sphere-viewer/core": "^5.5.1",
|
15 |
"@photo-sphere-viewer/video-plugin": "^5.5.1",
|
16 |
"@radix-ui/react-accordion": "^1.1.2",
|
|
|
31 |
"@radix-ui/react-toast": "^1.1.4",
|
32 |
"@radix-ui/react-tooltip": "^1.0.6",
|
33 |
"@react-spring/web": "^9.7.3",
|
34 |
+
"@types/lodash.debounce": "^4.0.9",
|
35 |
"@types/node": "20.4.2",
|
36 |
"@types/react": "18.2.15",
|
37 |
"@types/react-dom": "18.2.7",
|
|
|
47 |
"date-fns": "^2.30.0",
|
48 |
"eslint": "8.45.0",
|
49 |
"eslint-config-next": "13.4.10",
|
50 |
+
"fastest-levenshtein": "^1.0.16",
|
51 |
"hash-wasm": "^4.11.0",
|
52 |
+
"lodash.debounce": "^4.0.8",
|
53 |
"lucide-react": "^0.260.0",
|
54 |
"markdown-yaml-metadata-parser": "^3.0.0",
|
55 |
+
"minisearch": "^6.3.0",
|
56 |
"next": "^14.1.0",
|
57 |
"photo-sphere-viewer-lensflare-plugin": "^2.0.1",
|
58 |
"pick": "^0.0.1",
|
|
|
987 |
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
988 |
}
|
989 |
},
|
990 |
+
"node_modules/@jcoreio/async-throttle": {
|
991 |
+
"version": "1.6.0",
|
992 |
+
"resolved": "https://registry.npmjs.org/@jcoreio/async-throttle/-/async-throttle-1.6.0.tgz",
|
993 |
+
"integrity": "sha512-0efaXmn498OKPti0tG1GGCPdQwnfHecBGyJZ9eJzZf779WEDbAURGAFh4NWgbuTHU53KSMA2fwJcn6WqlOVRJA==",
|
994 |
+
"dependencies": {
|
995 |
+
"@babel/runtime": "^7.12.5"
|
996 |
+
}
|
997 |
+
},
|
998 |
"node_modules/@jridgewell/gen-mapping": {
|
999 |
"version": "0.3.3",
|
1000 |
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
|
|
|
2615 |
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
2616 |
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="
|
2617 |
},
|
2618 |
+
"node_modules/@types/lodash": {
|
2619 |
+
"version": "4.14.202",
|
2620 |
+
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz",
|
2621 |
+
"integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ=="
|
2622 |
+
},
|
2623 |
+
"node_modules/@types/lodash.debounce": {
|
2624 |
+
"version": "4.0.9",
|
2625 |
+
"resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.9.tgz",
|
2626 |
+
"integrity": "sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ==",
|
2627 |
+
"dependencies": {
|
2628 |
+
"@types/lodash": "*"
|
2629 |
+
}
|
2630 |
+
},
|
2631 |
"node_modules/@types/node": {
|
2632 |
"version": "20.4.2",
|
2633 |
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.2.tgz",
|
|
|
4886 |
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
|
4887 |
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="
|
4888 |
},
|
4889 |
+
"node_modules/fastest-levenshtein": {
|
4890 |
+
"version": "1.0.16",
|
4891 |
+
"resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
|
4892 |
+
"integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==",
|
4893 |
+
"engines": {
|
4894 |
+
"node": ">= 4.9.1"
|
4895 |
+
}
|
4896 |
+
},
|
4897 |
"node_modules/fastparse": {
|
4898 |
"version": "1.1.2",
|
4899 |
"resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz",
|
|
|
6121 |
"node": ">=16 || 14 >=14.17"
|
6122 |
}
|
6123 |
},
|
6124 |
+
"node_modules/minisearch": {
|
6125 |
+
"version": "6.3.0",
|
6126 |
+
"resolved": "https://registry.npmjs.org/minisearch/-/minisearch-6.3.0.tgz",
|
6127 |
+
"integrity": "sha512-ihFnidEeU8iXzcVHy74dhkxh/dn8Dc08ERl0xwoMMGqp4+LvRSCgicb+zGqWthVokQKvCSxITlh3P08OzdTYCQ=="
|
6128 |
+
},
|
6129 |
"node_modules/mkdirp-classic": {
|
6130 |
"version": "0.5.3",
|
6131 |
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
package.json
CHANGED
@@ -11,6 +11,7 @@
|
|
11 |
"dependencies": {
|
12 |
"@huggingface/hub": "0.12.3-oauth",
|
13 |
"@huggingface/inference": "^2.6.4",
|
|
|
14 |
"@photo-sphere-viewer/core": "^5.5.1",
|
15 |
"@photo-sphere-viewer/video-plugin": "^5.5.1",
|
16 |
"@radix-ui/react-accordion": "^1.1.2",
|
@@ -31,12 +32,13 @@
|
|
31 |
"@radix-ui/react-toast": "^1.1.4",
|
32 |
"@radix-ui/react-tooltip": "^1.0.6",
|
33 |
"@react-spring/web": "^9.7.3",
|
|
|
34 |
"@types/node": "20.4.2",
|
35 |
"@types/react": "18.2.15",
|
36 |
"@types/react-dom": "18.2.7",
|
37 |
"@types/uuid": "^9.0.2",
|
38 |
-
"@upstash/redis": "^1.28.3",
|
39 |
"@upstash/query": "^0.0.2",
|
|
|
40 |
"alchemy-sdk": "^3.1.2",
|
41 |
"autoprefixer": "10.4.14",
|
42 |
"class-variance-authority": "^0.6.1",
|
@@ -46,9 +48,12 @@
|
|
46 |
"date-fns": "^2.30.0",
|
47 |
"eslint": "8.45.0",
|
48 |
"eslint-config-next": "13.4.10",
|
|
|
49 |
"hash-wasm": "^4.11.0",
|
|
|
50 |
"lucide-react": "^0.260.0",
|
51 |
"markdown-yaml-metadata-parser": "^3.0.0",
|
|
|
52 |
"next": "^14.1.0",
|
53 |
"photo-sphere-viewer-lensflare-plugin": "^2.0.1",
|
54 |
"pick": "^0.0.1",
|
|
|
11 |
"dependencies": {
|
12 |
"@huggingface/hub": "0.12.3-oauth",
|
13 |
"@huggingface/inference": "^2.6.4",
|
14 |
+
"@jcoreio/async-throttle": "^1.6.0",
|
15 |
"@photo-sphere-viewer/core": "^5.5.1",
|
16 |
"@photo-sphere-viewer/video-plugin": "^5.5.1",
|
17 |
"@radix-ui/react-accordion": "^1.1.2",
|
|
|
32 |
"@radix-ui/react-toast": "^1.1.4",
|
33 |
"@radix-ui/react-tooltip": "^1.0.6",
|
34 |
"@react-spring/web": "^9.7.3",
|
35 |
+
"@types/lodash.debounce": "^4.0.9",
|
36 |
"@types/node": "20.4.2",
|
37 |
"@types/react": "18.2.15",
|
38 |
"@types/react-dom": "18.2.7",
|
39 |
"@types/uuid": "^9.0.2",
|
|
|
40 |
"@upstash/query": "^0.0.2",
|
41 |
+
"@upstash/redis": "^1.28.3",
|
42 |
"alchemy-sdk": "^3.1.2",
|
43 |
"autoprefixer": "10.4.14",
|
44 |
"class-variance-authority": "^0.6.1",
|
|
|
48 |
"date-fns": "^2.30.0",
|
49 |
"eslint": "8.45.0",
|
50 |
"eslint-config-next": "13.4.10",
|
51 |
+
"fastest-levenshtein": "^1.0.16",
|
52 |
"hash-wasm": "^4.11.0",
|
53 |
+
"lodash.debounce": "^4.0.8",
|
54 |
"lucide-react": "^0.260.0",
|
55 |
"markdown-yaml-metadata-parser": "^3.0.0",
|
56 |
+
"minisearch": "^6.3.0",
|
57 |
"next": "^14.1.0",
|
58 |
"photo-sphere-viewer-lensflare-plugin": "^2.0.1",
|
59 |
"pick": "^0.0.1",
|
src/app/interface/search-input/index.tsx
ADDED
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useState, useTransition } from "react"
|
2 |
+
import Link from "next/link"
|
3 |
+
// import throttle from "@jcoreio/async-throttle"
|
4 |
+
import debounce from "lodash.debounce"
|
5 |
+
import { GoSearch } from "react-icons/go"
|
6 |
+
|
7 |
+
import { useStore } from "@/app/state/useStore"
|
8 |
+
import { cn } from "@/lib/utils"
|
9 |
+
import { Input } from "@/components/ui/input"
|
10 |
+
import { Button } from "@/components/ui/button"
|
11 |
+
import { getVideos } from "@/app/server/actions/ai-tube-hf/getVideos"
|
12 |
+
|
13 |
+
export function SearchInput() {
|
14 |
+
const [_pending, startTransition] = useTransition()
|
15 |
+
|
16 |
+
const setSearchAutocompleteQuery = useStore(s => s.setSearchAutocompleteQuery)
|
17 |
+
const showAutocompleteBox = useStore(s => s.showAutocompleteBox)
|
18 |
+
const setShowAutocompleteBox = useStore(s => s.setShowAutocompleteBox)
|
19 |
+
|
20 |
+
const searchAutocompleteResults = useStore(s => s.searchAutocompleteResults)
|
21 |
+
const setSearchAutocompleteResults = useStore(s => s.setSearchAutocompleteResults)
|
22 |
+
|
23 |
+
const setSearchQuery = useStore(s => s.setSearchQuery)
|
24 |
+
|
25 |
+
const [searchDraft, setSearchDraft] = useState("")
|
26 |
+
|
27 |
+
// called when pressing enter or clicking on search
|
28 |
+
const debouncedSearch = debounce((query: string) => {
|
29 |
+
startTransition(async () => {
|
30 |
+
console.log(`searching for "${query}"..`)
|
31 |
+
|
32 |
+
const videos = await getVideos({
|
33 |
+
query,
|
34 |
+
sortBy: "match",
|
35 |
+
maxVideos: 8,
|
36 |
+
neverThrow: true,
|
37 |
+
renewCache: false, // bit of optimization
|
38 |
+
})
|
39 |
+
|
40 |
+
console.log(`got ${videos.length} results!`)
|
41 |
+
setSearchAutocompleteResults(videos)
|
42 |
+
|
43 |
+
// TODO: only close the show autocomplete box if we found something
|
44 |
+
// setShowAutocompleteBox(false)
|
45 |
+
})
|
46 |
+
}, 1000)
|
47 |
+
|
48 |
+
// called when pressing enter or clicking on search
|
49 |
+
const handleSearch = () => {
|
50 |
+
setSearchQuery(searchDraft)
|
51 |
+
setShowAutocompleteBox(true)
|
52 |
+
debouncedSearch(searchDraft)
|
53 |
+
}
|
54 |
+
|
55 |
+
return (
|
56 |
+
<div className="flex flex-row flex-grow w-[380px] lg:w-[600px]">
|
57 |
+
|
58 |
+
<div className="flex flex-row w-full"
|
59 |
+
onClick={() => {
|
60 |
+
handleSearch()
|
61 |
+
}}>
|
62 |
+
<Input
|
63 |
+
placeholder="Search"
|
64 |
+
className={cn(
|
65 |
+
`bg-neutral-900 text-neutral-200 dark:bg-neutral-900 dark:text-neutral-200`,
|
66 |
+
`rounded-l-full rounded-r-none`,
|
67 |
+
`border-neutral-700 dark:border-neutral-700 border-r-0`,
|
68 |
+
|
69 |
+
)}
|
70 |
+
// disabled={atLeastOnePanelIsBusy}
|
71 |
+
onFocus={() => {
|
72 |
+
handleSearch()
|
73 |
+
}}
|
74 |
+
onBlur={() => {
|
75 |
+
setShowAutocompleteBox(false)
|
76 |
+
}}
|
77 |
+
onChange={(e) => {
|
78 |
+
setSearchDraft(e.target.value)
|
79 |
+
handleSearch()
|
80 |
+
}}
|
81 |
+
onKeyDown={({ key }) => {
|
82 |
+
if (key === 'Enter') {
|
83 |
+
handleSearch()
|
84 |
+
}
|
85 |
+
}}
|
86 |
+
value={searchDraft}
|
87 |
+
/>
|
88 |
+
<Button
|
89 |
+
className={cn(
|
90 |
+
`rounded-l-none rounded-r-full border border-neutral-700 dark:border-neutral-700`,
|
91 |
+
`cursor-pointer`,
|
92 |
+
`transition-all duration-200 ease-in-out`,
|
93 |
+
`text-neutral-200 dark:text-neutral-200 bg-neutral-800 dark:bg-neutral-800 hover:bg-neutral-700 disabled:bg-neutral-900`
|
94 |
+
)}
|
95 |
+
onClick={() => {
|
96 |
+
handleSearch()
|
97 |
+
// console.log("submit")
|
98 |
+
// setShowAutocompleteBox(false)
|
99 |
+
// setSearchDraft("")
|
100 |
+
}}
|
101 |
+
disabled={false}
|
102 |
+
>
|
103 |
+
<GoSearch className="w-6 h-6" />
|
104 |
+
</Button>
|
105 |
+
</div>
|
106 |
+
<div
|
107 |
+
className={cn(
|
108 |
+
`absolute z-50 ml-1`,
|
109 |
+
|
110 |
+
// please keep this in sync with the parent
|
111 |
+
`w-[320px] lg:w-[540px]`,
|
112 |
+
|
113 |
+
`text-neutral-200 dark:text-neutral-200 bg-neutral-900 dark:bg-neutral-900`,
|
114 |
+
`border border-neutral-800 dark:border-neutral-800`,
|
115 |
+
`rounded-xl shadow-2xl`,
|
116 |
+
`flex flex-col p-2 space-y-1`,
|
117 |
+
|
118 |
+
`transition-all duration-200 ease-in-out`,
|
119 |
+
showAutocompleteBox
|
120 |
+
? `opacity-100 scale-100 mt-11`
|
121 |
+
: `opacity-0 scale-95 mt-6`
|
122 |
+
)}
|
123 |
+
>
|
124 |
+
{searchAutocompleteResults.length === 0 ? <div>No results found.</div> : null}
|
125 |
+
{searchAutocompleteResults.map(media => (
|
126 |
+
<Link key={media.id} href={`/watch?v=${media.id}`}>
|
127 |
+
<div
|
128 |
+
className={cn(
|
129 |
+
`dark:hover:bg-neutral-800 hover:bg-neutral-800`,
|
130 |
+
`text-sm`,
|
131 |
+
`px-3 py-2`,
|
132 |
+
`rounded-xl`
|
133 |
+
)}
|
134 |
+
|
135 |
+
>
|
136 |
+
|
137 |
+
{media.label}
|
138 |
+
</div>
|
139 |
+
</Link>
|
140 |
+
))}
|
141 |
+
</div>
|
142 |
+
</div>
|
143 |
+
)
|
144 |
+
}
|
src/app/interface/top-header/index.tsx
CHANGED
@@ -1,7 +1,8 @@
|
|
1 |
-
import { useEffect, useTransition } from 'react'
|
2 |
|
3 |
import { Pathway_Gothic_One } from 'next/font/google'
|
4 |
import { PiPopcornBold } from "react-icons/pi"
|
|
|
5 |
|
6 |
const pathway = Pathway_Gothic_One({
|
7 |
weight: "400",
|
@@ -14,6 +15,9 @@ import { useStore } from "@/app/state/useStore"
|
|
14 |
import { cn } from "@/lib/utils"
|
15 |
import { getTags } from '@/app/server/actions/ai-tube-hf/getTags'
|
16 |
import Link from 'next/link'
|
|
|
|
|
|
|
17 |
|
18 |
export function TopHeader() {
|
19 |
const [_pending, startTransition] = useTransition()
|
@@ -33,6 +37,17 @@ export function TopHeader() {
|
|
33 |
const currentTags = useStore(s => s.currentTags)
|
34 |
const setCurrentTags = useStore(s => s.setCurrentTags)
|
35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
36 |
const isNormalSize = headerMode === "normal"
|
37 |
|
38 |
|
@@ -118,9 +133,13 @@ export function TopHeader() {
|
|
118 |
`px-4 py-2 w-max-64`,
|
119 |
`text-neutral-400 text-2xs sm:text-xs lg:text-sm italic`
|
120 |
)}>
|
121 |
-
|
122 |
-
|
123 |
-
|
|
|
|
|
|
|
|
|
124 |
</div>
|
125 |
</div>
|
126 |
{
|
|
|
1 |
+
import { useEffect, useState, useTransition } from 'react'
|
2 |
|
3 |
import { Pathway_Gothic_One } from 'next/font/google'
|
4 |
import { PiPopcornBold } from "react-icons/pi"
|
5 |
+
import { GoSearch } from "react-icons/go"
|
6 |
|
7 |
const pathway = Pathway_Gothic_One({
|
8 |
weight: "400",
|
|
|
15 |
import { cn } from "@/lib/utils"
|
16 |
import { getTags } from '@/app/server/actions/ai-tube-hf/getTags'
|
17 |
import Link from 'next/link'
|
18 |
+
import { Input } from '@/components/ui/input'
|
19 |
+
import { Button } from '@/components/ui/button'
|
20 |
+
import { SearchInput } from '../search-input'
|
21 |
|
22 |
export function TopHeader() {
|
23 |
const [_pending, startTransition] = useTransition()
|
|
|
37 |
const currentTags = useStore(s => s.currentTags)
|
38 |
const setCurrentTags = useStore(s => s.setCurrentTags)
|
39 |
|
40 |
+
const setSearchAutocompleteQuery = useStore(s => s.setSearchAutocompleteQuery)
|
41 |
+
const searchAutocompleteResults = useStore(s => s.searchAutocompleteResults)
|
42 |
+
|
43 |
+
const setSearchQuery = useStore(s => s.setSearchQuery)
|
44 |
+
|
45 |
+
const [searchDraft, setSearchDraft] = useState("")
|
46 |
+
useEffect(() => {
|
47 |
+
const searchQuery = searchDraft.trim().toLowerCase()
|
48 |
+
setSearchQuery(searchQuery)
|
49 |
+
}, [searchDraft])
|
50 |
+
|
51 |
const isNormalSize = headerMode === "normal"
|
52 |
|
53 |
|
|
|
133 |
`px-4 py-2 w-max-64`,
|
134 |
`text-neutral-400 text-2xs sm:text-xs lg:text-sm italic`
|
135 |
)}>
|
136 |
+
<SearchInput />
|
137 |
+
</div>
|
138 |
+
<div className={cn("w-32 xl:w-42")}>
|
139 |
+
<span>
|
140 |
+
|
141 |
+
{/* reserved for future use */}
|
142 |
+
</span>
|
143 |
</div>
|
144 |
</div>
|
145 |
{
|
src/app/server/actions/ai-tube-hf/getVideos.ts
CHANGED
@@ -1,5 +1,8 @@
|
|
1 |
"use server"
|
2 |
|
|
|
|
|
|
|
3 |
import { VideoInfo } from "@/types/general"
|
4 |
|
5 |
import { getVideoIndex } from "./getVideoIndex"
|
@@ -11,13 +14,18 @@ const HARD_LIMIT = 100
|
|
11 |
|
12 |
// this just return ALL videos on the platform
|
13 |
export async function getVideos({
|
|
|
14 |
mandatoryTags = [],
|
15 |
niceToHaveTags = [],
|
16 |
sortBy = "date",
|
17 |
ignoreVideoIds = [],
|
18 |
maxVideos = HARD_LIMIT,
|
19 |
neverThrow = false,
|
|
|
20 |
}: {
|
|
|
|
|
|
|
21 |
// the videos MUST include those tags
|
22 |
mandatoryTags?: string[]
|
23 |
|
@@ -25,7 +33,10 @@ export async function getVideos({
|
|
25 |
// but it isn't a hard limit - TODO: use some semantic search here?
|
26 |
niceToHaveTags?: string[]
|
27 |
|
28 |
-
sortBy?:
|
|
|
|
|
|
|
29 |
|
30 |
// ignore some ids - this is used to not show the same videos again
|
31 |
// eg. videos already watched, or disliked etc
|
@@ -34,13 +45,15 @@ export async function getVideos({
|
|
34 |
maxVideos?: number
|
35 |
|
36 |
neverThrow?: boolean
|
|
|
|
|
37 |
}): Promise<VideoInfo[]> {
|
38 |
try {
|
39 |
// the index is gonna grow more and more,
|
40 |
// but in the future we will use some DB eg. Prisma or sqlite
|
41 |
const published = await getVideoIndex({
|
42 |
status: "published",
|
43 |
-
renewCache
|
44 |
})
|
45 |
|
46 |
let allPotentiallyValidVideos = Object.values(published)
|
@@ -53,8 +66,32 @@ export async function getVideos({
|
|
53 |
allPotentiallyValidVideos = allPotentiallyValidVideos.filter(video => !ignoreVideoIds.includes(video.id))
|
54 |
}
|
55 |
|
56 |
-
|
57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
58 |
} else {
|
59 |
allPotentiallyValidVideos.sort(() => Math.random() - 0.5)
|
60 |
}
|
|
|
1 |
"use server"
|
2 |
|
3 |
+
// import { distance } from "fastest-levenshtein"
|
4 |
+
import MiniSearch from "minisearch"
|
5 |
+
|
6 |
import { VideoInfo } from "@/types/general"
|
7 |
|
8 |
import { getVideoIndex } from "./getVideoIndex"
|
|
|
14 |
|
15 |
// this just return ALL videos on the platform
|
16 |
export async function getVideos({
|
17 |
+
query = "",
|
18 |
mandatoryTags = [],
|
19 |
niceToHaveTags = [],
|
20 |
sortBy = "date",
|
21 |
ignoreVideoIds = [],
|
22 |
maxVideos = HARD_LIMIT,
|
23 |
neverThrow = false,
|
24 |
+
renewCache = true,
|
25 |
}: {
|
26 |
+
// optional search query
|
27 |
+
query?: string
|
28 |
+
|
29 |
// the videos MUST include those tags
|
30 |
mandatoryTags?: string[]
|
31 |
|
|
|
33 |
// but it isn't a hard limit - TODO: use some semantic search here?
|
34 |
niceToHaveTags?: string[]
|
35 |
|
36 |
+
sortBy?:
|
37 |
+
| "random" // for the home
|
38 |
+
| "date" // most recent first
|
39 |
+
| "match" // how close we are from the query
|
40 |
|
41 |
// ignore some ids - this is used to not show the same videos again
|
42 |
// eg. videos already watched, or disliked etc
|
|
|
45 |
maxVideos?: number
|
46 |
|
47 |
neverThrow?: boolean
|
48 |
+
|
49 |
+
renewCache?: boolean
|
50 |
}): Promise<VideoInfo[]> {
|
51 |
try {
|
52 |
// the index is gonna grow more and more,
|
53 |
// but in the future we will use some DB eg. Prisma or sqlite
|
54 |
const published = await getVideoIndex({
|
55 |
status: "published",
|
56 |
+
renewCache,
|
57 |
})
|
58 |
|
59 |
let allPotentiallyValidVideos = Object.values(published)
|
|
|
66 |
allPotentiallyValidVideos = allPotentiallyValidVideos.filter(video => !ignoreVideoIds.includes(video.id))
|
67 |
}
|
68 |
|
69 |
+
const q = query.trim().toLowerCase()
|
70 |
+
|
71 |
+
if (sortBy === "match") {
|
72 |
+
// now obviously we are going to migrate to a database search instead,
|
73 |
+
// maybe a bit of vector search too,
|
74 |
+
// but let's say that for now this is good enough
|
75 |
+
let miniSearch = new MiniSearch({
|
76 |
+
fields: ['label', 'description', 'tags'], // fields to index for full-text search
|
77 |
+
storeFields: ['id'] // fields to return with search results
|
78 |
+
})
|
79 |
+
|
80 |
+
miniSearch.addAll(allPotentiallyValidVideos)
|
81 |
+
|
82 |
+
// mini search has plenty of options, see:
|
83 |
+
// https://www.npmjs.com/package/minisearch
|
84 |
+
const results = miniSearch.search(query, {
|
85 |
+
prefix: true, // "moto" will match "motorcycle"
|
86 |
+
fuzzy: 0.2,
|
87 |
+
// to search within a specific category
|
88 |
+
// filter: (result) => result.category === 'fiction'
|
89 |
+
})
|
90 |
+
|
91 |
+
allPotentiallyValidVideos = allPotentiallyValidVideos.filter(v => results.some(r => r.id === v.id))
|
92 |
+
|
93 |
+
} if (sortBy === "date") {
|
94 |
+
allPotentiallyValidVideos.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
|
95 |
} else {
|
96 |
allPotentiallyValidVideos.sort(() => Math.random() - 0.5)
|
97 |
}
|
src/app/server/actions/ai-tube-robot/README.md
DELETED
@@ -1,3 +0,0 @@
|
|
1 |
-
# server/actions/ai-tube-robot
|
2 |
-
|
3 |
-
API client for the AI Tube Robot
|
|
|
|
|
|
|
|
src/app/server/actions/ai-tube-robot/updateQueue.ts
DELETED
@@ -1,42 +0,0 @@
|
|
1 |
-
"use server"
|
2 |
-
|
3 |
-
import { ChannelInfo, UpdateQueueResponse } from "@/types/general"
|
4 |
-
|
5 |
-
import { aiTubeRobotApi } from "../config"
|
6 |
-
|
7 |
-
export async function updateQueue({
|
8 |
-
channel,
|
9 |
-
apiKey,
|
10 |
-
}: {
|
11 |
-
channel?: ChannelInfo
|
12 |
-
apiKey: string
|
13 |
-
}): Promise<number> {
|
14 |
-
if (!apiKey) {
|
15 |
-
throw new Error(`the apiKey is required`)
|
16 |
-
}
|
17 |
-
|
18 |
-
const res = await fetch(`${aiTubeRobotApi}/update-queue`, {
|
19 |
-
method: "POST",
|
20 |
-
headers: {
|
21 |
-
Accept: "application/json",
|
22 |
-
"Content-Type": "application/json",
|
23 |
-
// Authorization: `Bearer ${apiToken}`,
|
24 |
-
},
|
25 |
-
body: JSON.stringify({
|
26 |
-
apiKey,
|
27 |
-
channel
|
28 |
-
}),
|
29 |
-
cache: 'no-store',
|
30 |
-
// we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
|
31 |
-
// next: { revalidate: 1 }
|
32 |
-
})
|
33 |
-
|
34 |
-
if (res.status !== 200) {
|
35 |
-
// This will activate the closest `error.js` Error Boundary
|
36 |
-
throw new Error('Failed to fetch data')
|
37 |
-
}
|
38 |
-
|
39 |
-
const response = (await res.json()) as UpdateQueueResponse
|
40 |
-
// console.log("response:", response)
|
41 |
-
return response.nbUpdated
|
42 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/app/server/actions/config.ts
CHANGED
@@ -6,8 +6,6 @@ export const adminUsername = `${process.env.ADMIN_HUGGING_FACE_USERNAME || ""}`
|
|
6 |
|
7 |
export const adminCredentials: Credentials = { accessToken: adminApiKey }
|
8 |
|
9 |
-
export const aiTubeRobotApi = `${process.env.AI_TUBE_ROBOT_API || ""}`
|
10 |
-
|
11 |
export const redisUrl = `${process.env.UPSTASH_REDIS_REST_URL || ""}`
|
12 |
export const redisToken = `${process.env.UPSTASH_REDIS_REST_TOKEN || ""}`
|
13 |
|
|
|
6 |
|
7 |
export const adminCredentials: Credentials = { accessToken: adminApiKey }
|
8 |
|
|
|
|
|
9 |
export const redisUrl = `${process.env.UPSTASH_REDIS_REST_URL || ""}`
|
10 |
export const redisToken = `${process.env.UPSTASH_REDIS_REST_TOKEN || ""}`
|
11 |
|
src/app/server/actions/redis.ts
CHANGED
@@ -1,4 +1,5 @@
|
|
1 |
import { Redis } from "@upstash/redis"
|
|
|
2 |
|
3 |
import { redisToken, redisUrl } from "./config"
|
4 |
|
@@ -7,3 +8,11 @@ export const redis = new Redis({
|
|
7 |
token: redisToken
|
8 |
})
|
9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import { Redis } from "@upstash/redis"
|
2 |
+
import { Query } from "@upstash/query"
|
3 |
|
4 |
import { redisToken, redisUrl } from "./config"
|
5 |
|
|
|
8 |
token: redisToken
|
9 |
})
|
10 |
|
11 |
+
/*
|
12 |
+
const q = new Redis({
|
13 |
+
url: redisUrl,
|
14 |
+
token: redisToken,
|
15 |
+
automaticDeserialization: false, // redis query needs it to false?
|
16 |
+
})
|
17 |
+
*/
|
18 |
+
|
src/app/state/useStore.ts
CHANGED
@@ -19,6 +19,21 @@ export const useStore = create<{
|
|
19 |
|
20 |
setPathname: (pathname: string) => void
|
21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
22 |
currentUser?: UserInfo
|
23 |
setCurrentUser: (currentUser?: UserInfo) => void
|
24 |
|
@@ -102,12 +117,37 @@ export const useStore = create<{
|
|
102 |
set({ view: routes[pathname] || "not_found" })
|
103 |
},
|
104 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
105 |
currentUser: undefined,
|
106 |
setCurrentUser: (currentUser?: UserInfo) => {
|
107 |
set({ currentUser })
|
108 |
},
|
109 |
|
110 |
-
headerMode: "normal",
|
111 |
setHeaderMode: (headerMode: InterfaceHeaderMode) => {
|
112 |
set({ headerMode })
|
113 |
},
|
|
|
19 |
|
20 |
setPathname: (pathname: string) => void
|
21 |
|
22 |
+
searchQuery: string
|
23 |
+
setSearchQuery: (searchQuery?: string) => void
|
24 |
+
|
25 |
+
showAutocompleteBox: boolean
|
26 |
+
setShowAutocompleteBox: (showAutocompleteBox: boolean) => void
|
27 |
+
|
28 |
+
searchAutocompleteQuery: string
|
29 |
+
setSearchAutocompleteQuery: (searchAutocompleteQuery?: string) => void
|
30 |
+
|
31 |
+
searchAutocompleteResults: VideoInfo[]
|
32 |
+
setSearchAutocompleteResults: (searchAutocompleteResults: VideoInfo[]) => void
|
33 |
+
|
34 |
+
searchResults: VideoInfo[]
|
35 |
+
setSearchResults: (searchResults: VideoInfo[]) => void
|
36 |
+
|
37 |
currentUser?: UserInfo
|
38 |
setCurrentUser: (currentUser?: UserInfo) => void
|
39 |
|
|
|
117 |
set({ view: routes[pathname] || "not_found" })
|
118 |
},
|
119 |
|
120 |
+
searchAutocompleteQuery: "",
|
121 |
+
setSearchAutocompleteQuery: (searchAutocompleteQuery?: string) => {
|
122 |
+
set({ searchAutocompleteQuery })
|
123 |
+
},
|
124 |
+
|
125 |
+
showAutocompleteBox: false,
|
126 |
+
setShowAutocompleteBox: (showAutocompleteBox: boolean) => {
|
127 |
+
set({ showAutocompleteBox })
|
128 |
+
},
|
129 |
+
|
130 |
+
searchAutocompleteResults: [] as VideoInfo[],
|
131 |
+
setSearchAutocompleteResults: (searchAutocompleteResults: VideoInfo[]) => {
|
132 |
+
set({ searchAutocompleteResults })
|
133 |
+
},
|
134 |
+
|
135 |
+
searchQuery: "",
|
136 |
+
setSearchQuery: (searchQuery?: string) => {
|
137 |
+
set({ searchQuery })
|
138 |
+
},
|
139 |
+
|
140 |
+
searchResults: [] as VideoInfo[],
|
141 |
+
setSearchResults: (searchResults: VideoInfo[]) => {
|
142 |
+
set({ searchResults })
|
143 |
+
},
|
144 |
+
|
145 |
currentUser: undefined,
|
146 |
setCurrentUser: (currentUser?: UserInfo) => {
|
147 |
set({ currentUser })
|
148 |
},
|
149 |
|
150 |
+
headerMode: "normal" as InterfaceHeaderMode,
|
151 |
setHeaderMode: (headerMode: InterfaceHeaderMode) => {
|
152 |
set({ headerMode })
|
153 |
},
|
src/components/ui/popover.tsx
CHANGED
@@ -19,7 +19,7 @@ const PopoverContent = React.forwardRef<
|
|
19 |
align={align}
|
20 |
sideOffset={sideOffset}
|
21 |
className={cn(
|
22 |
-
"z-50 w-72 rounded-md border border-
|
23 |
className
|
24 |
)}
|
25 |
{...props}
|
|
|
19 |
align={align}
|
20 |
sideOffset={sideOffset}
|
21 |
className={cn(
|
22 |
+
"z-50 w-72 rounded-md border border-stone-200 bg-white p-4 text-stone-950 shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-stone-800 dark:bg-stone-950 dark:text-stone-50",
|
23 |
className
|
24 |
)}
|
25 |
{...props}
|