Skip to content

Commit

Permalink
search payload accepts record_type, style search page
Browse files Browse the repository at this point in the history
  • Loading branch information
web3nomad committed Mar 25, 2024
1 parent 159b836 commit 03f6668
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 37 deletions.
46 changes: 30 additions & 16 deletions apps/api-server/src/routes/video/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use file_handler::{
};
use prisma_lib::asset_object;
use rspc::{Router, RouterBuilder};
use serde::Serialize;
use serde::{Serialize, Deserialize};
use specta::Type;
use std::sync::Arc;
use tracing::error;
Expand All @@ -15,14 +15,39 @@ where
TCtx: CtxWithLibrary + Clone + Send + Sync + 'static,
{
Router::<TCtx>::new().query("all", |t| {
t(move |ctx: TCtx, input: String| async move {
#[derive(Deserialize, Type)]
#[serde(rename_all = "camelCase")]
pub struct SearchRequestPayload {
pub text: String,
pub record_type: String,
}
#[derive(Serialize, Type)]
#[serde(rename_all = "camelCase")]
pub struct SearchResultPayload {
pub name: String,
pub materialized_path: String,
pub asset_object_id: i32,
pub asset_object_hash: String,
// #[serde(rename = "startTime")]
pub start_time: i32,
}
t(move |ctx: TCtx, input: SearchRequestPayload| async move {
let library = ctx.library()?;

let text = input.text.clone();
let record_type = match input.record_type {
s if s == "Transcript" => SearchRecordType::Transcript,
s if s == "FrameCaption" => SearchRecordType::FrameCaption,
s if s == "Frame" => SearchRecordType::Frame,
_ => return Err(rspc::Error::new(
rspc::ErrorCode::BadRequest,
"invalid record_type".to_string(),
)),
};
let res = file_handler::search::handle_search(
SearchRequest {
text: input,
// record_type: Some(vec![SearchRecordType::Transcript]),
record_type: Some(vec![SearchRecordType::FrameCaption]),
text: text,
record_type: Some(vec![record_type]),
limit: None,
skip: None,
},
Expand Down Expand Up @@ -75,17 +100,6 @@ where
tasks_hash_map.insert(hash, asset_object_data);
});

#[derive(Serialize, Type)]
#[serde(rename_all = "camelCase")]
pub struct SearchResultPayload {
pub name: String,
pub materialized_path: String,
pub asset_object_id: i32,
pub asset_object_hash: String,
// #[serde(rename = "startTime")]
pub start_time: i32,
}

let search_result = search_results
.iter()
.map(
Expand Down
101 changes: 83 additions & 18 deletions apps/web/src/app/search/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
'use client'
import type { SearchResultPayload } from '@/lib/bindings'
import Icon from '@/components/Icon'
import { useCurrentLibrary } from '@/lib/library'
import { rspc } from '@/lib/rspc'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { SearchRequestPayload } from '@/lib/bindings'
import classNames from 'classnames'

const VideoPreview: React.FC<{ item: SearchResultPayload }> = ({ item }) => {
const currentLibrary = useCurrentLibrary()
Expand Down Expand Up @@ -53,7 +56,7 @@ const VideoItem: React.FC<{
}, [item])

return (
<div className="m-4 w-64">
<div className="w-64">
<div
className="relative h-36 w-full cursor-pointer overflow-hidden rounded-md shadow-md"
onClick={() => handleVideoClick(item)}
Expand Down Expand Up @@ -86,12 +89,16 @@ const VideoItem: React.FC<{
}

export default function Search() {
const [searchKeyword, setSearchKeyword] = useState('')
const queryRes = rspc.useQuery(['video.search.all', searchKeyword])
// cosnt { data, isLoading, error } = queryRes;
const [searchPayload, setSearchPayload] = useState<SearchRequestPayload|null>(null)
const queryRes = rspc.useQuery(['video.search.all', searchPayload!], {
enabled: !!searchPayload
})

const searchInputRef = useRef<HTMLInputElement>(null)
const [previewItem, setPreviewItem] = useState<SearchResultPayload | null>(null)

const [keywordTyping, setKeywordTyping] = useState<string>('')

const handleVideoClick = useCallback(
(item: SearchResultPayload) => {
setPreviewItem(item)
Expand All @@ -100,15 +107,16 @@ export default function Search() {
)

const handleSearch = useCallback(
(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
const keyword = searchInputRef.current?.value
if (!keyword) return
setSearchKeyword(keyword)
(text: string, recordType: string = "FrameCaption") => {
if (text && recordType) {
setSearchPayload({ text, recordType })
}
},
[setSearchKeyword],
[setSearchPayload],
)

const [focused, setFocused] = useState(false)

return (
<main className="flex h-full flex-col">
<div className="flex h-12 items-center justify-start border-b border-neutral-100 px-4">
Expand All @@ -117,26 +125,83 @@ export default function Search() {
<div className="px-2 py-1">&gt;</div>
<div className="ml-2 text-sm">搜索</div>
</div>
<div className="w-1/2">
<form onSubmit={handleSearch} className="block">
<div className="w-80 relative">
<form onSubmit={(e) => {
e.preventDefault()
handleSearch(keywordTyping)
if (searchInputRef.current) {
searchInputRef.current.blur()
searchInputRef.current.value = ""
}
}} className="block">
<input
ref={searchInputRef}
type="text"
className="ml-auto mr-auto block w-80 rounded-md bg-neutral-100 px-4
py-2 text-sm text-black"
className="block w-full rounded-md bg-neutral-100 px-4
py-2 text-sm outline-none text-black"
placeholder="搜索"
onInput={(e) => setKeywordTyping(e.currentTarget.value)}
onFocus={(e) => setFocused(true)}
onBlur={(e) => setTimeout(() => setFocused(false), 200)}
/>
{/* <button className="ml-4 px-6 bg-black text-white" type="submit">Search</button> */}
</form>
{focused && keywordTyping ? (
<div className='absolute z-10 top-full w-full text-sm rounded-md p-1 bg-white shadow-md'>
<div className="px-2 py-1 text-neutral-400">搜索类型</div>
<div
className="px-2 py-2 flex items-center justify-start text-neutral-800 hover:bg-neutral-100 rounded-sm"
onClick={() => handleSearch(keywordTyping, "FrameCaption")}
>
<span className="text-neutral-400"><Icon.image className="w-4" /></span>
<span className="mx-2">搜索视频内容</span>
<strong>{keywordTyping}</strong>
</div>
<div
className="px-2 py-2 flex items-center justify-start text-neutral-800 hover:bg-neutral-100 rounded-sm"
onClick={() => handleSearch(keywordTyping, "Transcript")}
>
<span className="text-neutral-400"><Icon.microphone className="w-4" /></span>
<span className="mx-2">搜索视频语音</span>
<strong>{keywordTyping}</strong>
</div>
</div>
) : null}
</div>
</div>
<div className="p-6">
{searchPayload ? (
<div className="px-8 py-2 flex items-center justify-start border-b border-neutral-100">
<div className="border border-neutral-200 flex items-center text-xs rounded-lg overflow-hidden">
<div
className={classNames(
"px-4 py-2",
searchPayload.recordType === "FrameCaption" && "bg-neutral-100"
)}
onClick={() => handleSearch(searchPayload.text, "FrameCaption")}
>视频内容</div>
<div
className={classNames(
"px-4 py-2",
searchPayload.recordType === "Transcript" && "bg-neutral-100"
)}
onClick={() => handleSearch(searchPayload.text, "Transcript")}
>视频语音</div>
</div>
<div className="ml-4 text-sm text-neutral-600">{searchPayload.text}</div>
</div>
) : null}
<div className="p-8">
{queryRes.isLoading ? (
<div className="flex items-center justify-center px-2 py-8 text-sm text-neutral-400">正在搜索...</div>
) : (
<div className="flex flex-wrap">
<div className="flex flex-wrap gap-4">
{queryRes.data?.map((item: SearchResultPayload, index: number) => {
return <VideoItem key={index} item={item} handleVideoClick={handleVideoClick}></VideoItem>
return (
<VideoItem
key={`${item.assetObjectId}-${index}`} item={item}
handleVideoClick={handleVideoClick}
></VideoItem>
)
})}
</div>
)}
Expand Down
17 changes: 14 additions & 3 deletions apps/web/src/components/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ const icons = {
<g style={{ mixBlendMode: 'luminosity' }}>
<path
d="M2.40234 7.39453H5.69141C6.83203 7.39453 7.39453 6.83984 7.39453 5.66797V2.42578C7.39453 1.25391 6.83203 0.699219 5.69141 0.699219H2.40234C1.25391 0.699219 0.699219 1.25391 0.699219 2.42578V5.66797C0.699219 6.83984 1.25391 7.39453 2.40234 7.39453ZM10.3086 7.39453H13.6055C14.7461 7.39453 15.3008 6.83984 15.3008 5.66797V2.42578C15.3008 1.25391 14.7461 0.699219 13.6055 0.699219H10.3086C9.16797 0.699219 8.60547 1.25391 8.60547 2.42578V5.66797C8.60547 6.83984 9.16797 7.39453 10.3086 7.39453ZM2.40234 6.08984C2.12891 6.08984 2.00391 5.94922 2.00391 5.66797V2.41797C2.00391 2.13672 2.12891 1.99609 2.40234 1.99609H5.68359C5.95703 1.99609 6.08984 2.13672 6.08984 2.41797V5.66797C6.08984 5.94922 5.95703 6.08984 5.68359 6.08984H2.40234ZM10.3164 6.08984C10.043 6.08984 9.91016 5.94922 9.91016 5.66797V2.41797C9.91016 2.13672 10.043 1.99609 10.3164 1.99609H13.5977C13.8711 1.99609 14.0039 2.13672 14.0039 2.41797V5.66797C14.0039 5.94922 13.8711 6.08984 13.5977 6.08984H10.3164ZM2.40234 15.3008H5.69141C6.83203 15.3008 7.39453 14.7461 7.39453 13.5742V10.332C7.39453 9.16016 6.83203 8.60547 5.69141 8.60547H2.40234C1.25391 8.60547 0.699219 9.16016 0.699219 10.332V13.5742C0.699219 14.7461 1.25391 15.3008 2.40234 15.3008ZM10.3086 15.3008H13.6055C14.7461 15.3008 15.3008 14.7461 15.3008 13.5742V10.332C15.3008 9.16016 14.7461 8.60547 13.6055 8.60547H10.3086C9.16797 8.60547 8.60547 9.16016 8.60547 10.332V13.5742C8.60547 14.7461 9.16797 15.3008 10.3086 15.3008ZM2.40234 13.9961C2.12891 13.9961 2.00391 13.8633 2.00391 13.582V10.332C2.00391 10.043 2.12891 9.91016 2.40234 9.91016H5.68359C5.95703 9.91016 6.08984 10.043 6.08984 10.332V13.582C6.08984 13.8633 5.95703 13.9961 5.68359 13.9961H2.40234ZM10.3164 13.9961C10.043 13.9961 9.91016 13.8633 9.91016 13.582V10.332C9.91016 10.043 10.043 9.91016 10.3164 9.91016H13.5977C13.8711 9.91016 14.0039 10.043 14.0039 10.332V13.582C14.0039 13.8633 13.8711 13.9961 13.5977 13.9961H10.3164Z"
fill="#676C77"
fill="currentColor"
/>
</g>
</svg>
Expand All @@ -268,7 +268,7 @@ const icons = {
<g style={{ mixBlendMode: 'luminosity' }}>
<path
d="M1.09918 4.33868C1.71069 4.33868 2.19836 3.85101 2.19836 3.2395C2.19836 2.63572 1.71069 2.14032 1.09918 2.14032C0.495404 2.14032 0 2.63572 0 3.2395C0 3.85101 0.495404 4.33868 1.09918 4.33868ZM4.7373 3.9826H15.2492C15.6672 3.9826 16 3.65749 16 3.2395C16 2.8215 15.6749 2.49639 15.2492 2.49639H4.7373C4.32704 2.49639 3.99419 2.8215 3.99419 3.2395C3.99419 3.65749 4.3193 3.9826 4.7373 3.9826ZM1.09918 9.0992C1.71069 9.0992 2.19836 8.61153 2.19836 8.00002C2.19836 7.39625 1.71069 6.90084 1.09918 6.90084C0.495404 6.90084 0 7.39625 0 8.00002C0 8.61153 0.495404 9.0992 1.09918 9.0992ZM4.7373 8.74313H15.2492C15.6672 8.74313 16 8.41802 16 8.00002C16 7.58202 15.6749 7.25691 15.2492 7.25691H4.7373C4.32704 7.25691 3.99419 7.58202 3.99419 8.00002C3.99419 8.41802 4.3193 8.74313 4.7373 8.74313ZM1.09918 13.8597C1.71069 13.8597 2.19836 13.3721 2.19836 12.7605C2.19836 12.1568 1.71069 11.6614 1.09918 11.6614C0.495404 11.6614 0 12.1568 0 12.7605C0 13.3721 0.495404 13.8597 1.09918 13.8597ZM4.7373 13.5036H15.2492C15.6672 13.5036 16 13.1785 16 12.7605C16 12.3425 15.6749 12.0174 15.2492 12.0174H4.7373C4.32704 12.0174 3.99419 12.3425 3.99419 12.7605C3.99419 13.1785 4.3193 13.5036 4.7373 13.5036Z"
fill="#676C77"
fill="currentColor"
/>
</g>
</svg>
Expand All @@ -278,7 +278,7 @@ const icons = {
<g style={{ mixBlendMode: 'luminosity' }}>
<path
d="M2.2211 14.3089H13.7722C15.2371 14.3089 16 13.5528 16 12.108V3.89201C16 2.44728 15.2371 1.69116 13.7722 1.69116H2.2211C0.762869 1.69116 0 2.44053 0 3.89201V12.108C0 13.5528 0.762869 14.3089 2.2211 14.3089ZM2.30886 12.9654C1.68776 12.9654 1.34346 12.6414 1.34346 11.9865V4.01353C1.34346 3.35867 1.68776 3.03462 2.30886 3.03462H4.90127V12.9654H2.30886ZM6.21772 12.9654V3.03462H9.78228V12.9654H6.21772ZM13.6844 3.03462C14.3055 3.03462 14.6498 3.35867 14.6498 4.01353V11.9865C14.6498 12.6414 14.3055 12.9654 13.6844 12.9654H11.092V3.03462H13.6844Z"
fill="#676C77"
fill="currentColor"
/>
</g>
</svg>
Expand All @@ -301,6 +301,17 @@ const icons = {
/>
</svg>
),
microphone: (props: SvgIconProps) => (
<svg {...props} width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.00017 11.7139C9.89889 11.7139 11.4436 10.1691 11.4436 8.27047V3.94353C11.4436 2.04481 9.89889 0.5 8.00017 0.5C6.10145 0.5 4.55664 2.04481 4.55664 3.94353V8.27047C4.55664 10.1692 6.10145 11.7139 8.00017 11.7139Z" fill="currentColor" fillOpacity="0.95"/>
<path d="M12.9537 9.70794C13.0008 9.45341 12.8326 9.20891 12.578 9.16175C12.3232 9.1145 12.0789 9.28288 12.0318 9.5375C11.6903 11.3837 9.9948 12.7237 8.00036 12.7237C6.00583 12.7237 4.31036 11.3837 3.96883 9.5375C3.92177 9.28288 3.67708 9.11507 3.42264 9.16175C3.16802 9.20891 2.99983 9.45341 3.04699 9.70794C3.44336 11.8507 5.29192 13.4436 7.53161 13.6404V14.5626H4.32245C4.06361 14.5626 3.8537 14.7724 3.8537 15.0313C3.8537 15.2903 4.06361 15.5001 4.32245 15.5001H11.6782C11.9371 15.5001 12.1469 15.2903 12.1469 15.0313C12.1469 14.7724 11.9371 14.5626 11.6782 14.5626H8.46911V13.6404C10.7088 13.4436 12.5574 11.8507 12.9537 9.70794Z" fill="currentColor" fillOpacity="0.95"/>
</svg>
),
image: (props: SvgIconProps) => (
<svg {...props} width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M13.875 1.98755H2.125C1.50625 1.98755 1 2.4938 1 3.11255V12.886C1 13.5047 1.50625 14.011 2.125 14.011H13.875C14.4938 14.011 15 13.5047 15 12.886V3.11255C15 2.4938 14.4938 1.98755 13.875 1.98755ZM13.8751 12.8827L13.8735 12.8843H2.12819L2.12663 12.8827V9.55775L5.02663 6.59212L8.42194 10.8093C8.60944 11.0218 8.92507 11.0624 9.15944 10.9031L11.6673 9.20775L13.8751 10.2468V12.8827ZM12.0297 8.07028L13.875 8.83122L13.8734 3.11403L13.8719 3.11247H2.12656L2.125 3.11403V7.9484L4.64375 5.37184C4.75313 5.2609 4.90469 5.1984 5.06094 5.20309C5.21719 5.20778 5.36562 5.27653 5.46875 5.39372L8.93906 9.69372L11.3734 8.05153C11.5734 7.91559 11.8375 7.9234 12.0297 8.07028ZM10.9062 4.26562C10.0359 4.26562 9.32812 4.97344 9.32812 5.84375C9.32812 6.71406 10.0359 7.42188 10.9062 7.42188C11.7766 7.42188 12.4844 6.71406 12.4844 5.84375C12.4844 4.97344 11.7766 4.26562 10.9062 4.26562ZM10.4531 5.84375C10.4531 6.09375 10.6562 6.29688 10.9062 6.29688C11.1562 6.29688 11.3594 6.09375 11.3594 5.84375C11.3594 5.59375 11.1562 5.39062 10.9062 5.39062C10.6562 5.39062 10.4531 5.59375 10.4531 5.84375Z" fill="currentColor"/>
</svg>
)
}

export default icons;

0 comments on commit 03f6668

Please sign in to comment.