jbilcke-hf HF staff commited on
Commit
b8c4528
1 Parent(s): e3cc490

working on AI Tube search engine

Browse files
.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
- Note: AI Tube is still in beta (and this text will be replaced by a search box)</div>
122
- <div className={cn()}>
123
- &nbsp; {/* more buttons? unused for now */}
 
 
 
 
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
+ &nbsp;
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?: "random" | "date"
 
 
 
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: true
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
- if (sortBy === "date") {
57
- allPotentiallyValidVideos.sort(((a, b) => b.updatedAt.localeCompare(a.updatedAt)))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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-neutral-200 bg-white p-4 text-neutral-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-neutral-800 dark:bg-neutral-950 dark:text-neutral-50",
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}